Merge pull request #4869 from hashicorp/txn-checks

Add node/service/check operations to transaction api
This commit is contained in:
Kyle Havlovitz 2019-01-22 11:16:09 -08:00 committed by GitHub
commit 5bdf130767
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 2397 additions and 280 deletions

View File

@ -1435,3 +1435,107 @@ func vetDeregisterWithACL(rule acl.Authorizer, subj *structs.DeregisterRequest,
return nil return nil
} }
// vetNodeTxnOp applies the given ACL policy to a node transaction operation.
func vetNodeTxnOp(op *structs.TxnNodeOp, rule acl.Authorizer) error {
// Fast path if ACLs are not enabled.
if rule == nil {
return nil
}
node := op.Node
n := &api.Node{
Node: node.Node,
ID: string(node.ID),
Address: node.Address,
Datacenter: node.Datacenter,
TaggedAddresses: node.TaggedAddresses,
Meta: node.Meta,
}
// Sentinel doesn't apply to deletes, only creates/updates, so we don't need the scopeFn.
var scope func() map[string]interface{}
if op.Verb != api.NodeDelete && op.Verb != api.NodeDeleteCAS {
scope = func() map[string]interface{} {
return sentinel.ScopeCatalogUpsert(n, nil)
}
}
if rule != nil && !rule.NodeWrite(node.Node, scope) {
return acl.ErrPermissionDenied
}
return nil
}
// vetServiceTxnOp applies the given ACL policy to a service transaction operation.
func vetServiceTxnOp(op *structs.TxnServiceOp, rule acl.Authorizer) error {
// Fast path if ACLs are not enabled.
if rule == nil {
return nil
}
service := op.Service
n := &api.Node{Node: op.Node}
svc := &api.AgentService{
ID: service.ID,
Service: service.Service,
Tags: service.Tags,
Meta: service.Meta,
Address: service.Address,
Port: service.Port,
EnableTagOverride: service.EnableTagOverride,
}
var scope func() map[string]interface{}
if op.Verb != api.ServiceDelete && op.Verb != api.ServiceDeleteCAS {
scope = func() map[string]interface{} {
return sentinel.ScopeCatalogUpsert(n, svc)
}
}
if !rule.ServiceWrite(service.Service, scope) {
return acl.ErrPermissionDenied
}
return nil
}
// vetCheckTxnOp applies the given ACL policy to a check transaction operation.
func vetCheckTxnOp(op *structs.TxnCheckOp, rule acl.Authorizer) error {
// Fast path if ACLs are not enabled.
if rule == nil {
return nil
}
n := &api.Node{Node: op.Check.Node}
svc := &api.AgentService{
ID: op.Check.ServiceID,
Service: op.Check.ServiceID,
Tags: op.Check.ServiceTags,
}
var scope func() map[string]interface{}
if op.Check.ServiceID == "" {
// Node-level check.
if op.Verb == api.CheckDelete || op.Verb == api.CheckDeleteCAS {
scope = func() map[string]interface{} {
return sentinel.ScopeCatalogUpsert(n, svc)
}
}
if !rule.NodeWrite(op.Check.Node, scope) {
return acl.ErrPermissionDenied
}
} else {
// Service-level check.
if op.Verb == api.CheckDelete || op.Verb == api.CheckDeleteCAS {
scope = func() map[string]interface{} {
return sentinel.ScopeCatalogUpsert(n, svc)
}
}
if !rule.ServiceWrite(op.Check.ServiceName, scope) {
return acl.ErrPermissionDenied
}
}
return nil
}

View File

@ -20,54 +20,41 @@ type Catalog struct {
srv *Server srv *Server
} }
// Register is used register that a node is providing a given service. // nodePreApply does the verification of a node before it is applied to Raft.
func (c *Catalog) Register(args *structs.RegisterRequest, reply *struct{}) error { func nodePreApply(nodeName, nodeID string) error {
if done, err := c.srv.forward("Catalog.Register", args, args, reply); done { if nodeName == "" {
return err
}
defer metrics.MeasureSince([]string{"catalog", "register"}, time.Now())
// Verify the args.
if args.Node == "" {
return fmt.Errorf("Must provide node") return fmt.Errorf("Must provide node")
} }
if args.Address == "" && !args.SkipNodeUpdate { if nodeID != "" {
return fmt.Errorf("Must provide address if SkipNodeUpdate is not set") if _, err := uuid.ParseUUID(nodeID); err != nil {
}
if args.ID != "" {
if _, err := uuid.ParseUUID(string(args.ID)); err != nil {
return fmt.Errorf("Bad node ID: %v", err) return fmt.Errorf("Bad node ID: %v", err)
} }
} }
// Fetch the ACL token, if any. return nil
rule, err := c.srv.ResolveToken(args.Token) }
if err != nil {
return err
}
// Handle a service registration. func servicePreApply(service *structs.NodeService, rule acl.Authorizer) error {
if args.Service != nil {
// Validate the service. This is in addition to the below since // Validate the service. This is in addition to the below since
// the above just hasn't been moved over yet. We should move it over // the above just hasn't been moved over yet. We should move it over
// in time. // in time.
if err := args.Service.Validate(); err != nil { if err := service.Validate(); err != nil {
return err return err
} }
// If no service id, but service name, use default // If no service id, but service name, use default
if args.Service.ID == "" && args.Service.Service != "" { if service.ID == "" && service.Service != "" {
args.Service.ID = args.Service.Service service.ID = service.Service
} }
// Verify ServiceName provided if ID. // Verify ServiceName provided if ID.
if args.Service.ID != "" && args.Service.Service == "" { if service.ID != "" && service.Service == "" {
return fmt.Errorf("Must provide service name with ID") return fmt.Errorf("Must provide service name with ID")
} }
// Check the service address here and in the agent endpoint // Check the service address here and in the agent endpoint
// since service registration isn't synchronous. // since service registration isn't synchronous.
if ipaddr.IsAny(args.Service.Address) { if ipaddr.IsAny(service.Address) {
return fmt.Errorf("Invalid service address") return fmt.Errorf("Invalid service address")
} }
@ -76,18 +63,55 @@ func (c *Catalog) Register(args *structs.RegisterRequest, reply *struct{}) error
// is going away after version 0.8). We check this same policy // is going away after version 0.8). We check this same policy
// later if version 0.8 is enabled, so we can eventually just // later if version 0.8 is enabled, so we can eventually just
// delete this and do all the ACL checks down there. // delete this and do all the ACL checks down there.
if args.Service.Service != structs.ConsulServiceName { if service.Service != structs.ConsulServiceName {
if rule != nil && !rule.ServiceWrite(args.Service.Service, nil) { if rule != nil && !rule.ServiceWrite(service.Service, nil) {
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
} }
} }
// Proxies must have write permission on their destination // Proxies must have write permission on their destination
if args.Service.Kind == structs.ServiceKindConnectProxy { if service.Kind == structs.ServiceKindConnectProxy {
if rule != nil && !rule.ServiceWrite(args.Service.Proxy.DestinationServiceName, nil) { if rule != nil && !rule.ServiceWrite(service.Proxy.DestinationServiceName, nil) {
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
} }
} }
return nil
}
// checkPreApply does the verification of a check before it is applied to Raft.
func checkPreApply(check *structs.HealthCheck) {
if check.CheckID == "" && check.Name != "" {
check.CheckID = types.CheckID(check.Name)
}
}
// Register is used register that a node is providing a given service.
func (c *Catalog) Register(args *structs.RegisterRequest, reply *struct{}) error {
if done, err := c.srv.forward("Catalog.Register", args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"catalog", "register"}, time.Now())
// Fetch the ACL token, if any.
rule, err := c.srv.ResolveToken(args.Token)
if err != nil {
return err
}
// Verify the args.
if err := nodePreApply(args.Node, string(args.ID)); err != nil {
return err
}
if args.Address == "" && !args.SkipNodeUpdate {
return fmt.Errorf("Must provide address if SkipNodeUpdate is not set")
}
// Handle a service registration.
if args.Service != nil {
if err := servicePreApply(args.Service, rule); err != nil {
return err
}
} }
// Move the old format single check into the slice, and fixup IDs. // Move the old format single check into the slice, and fixup IDs.
@ -96,12 +120,10 @@ func (c *Catalog) Register(args *structs.RegisterRequest, reply *struct{}) error
args.Check = nil args.Check = nil
} }
for _, check := range args.Checks { for _, check := range args.Checks {
if check.CheckID == "" && check.Name != "" {
check.CheckID = types.CheckID(check.Name)
}
if check.Node == "" { if check.Node == "" {
check.Node = args.Node check.Node = args.Node
} }
checkPreApply(check)
} }
// Check the complete register request against the given ACL policy. // Check the complete register request against the given ACL policy.

View File

@ -61,8 +61,18 @@ func (t *txnResultsFilter) Len() int {
func (t *txnResultsFilter) Filter(i int) bool { func (t *txnResultsFilter) Filter(i int) bool {
result := t.results[i] result := t.results[i]
if result.KV != nil { switch {
case result.KV != nil:
return !t.authorizer.KeyRead(result.KV.Key) return !t.authorizer.KeyRead(result.KV.Key)
case result.Node != nil:
return !t.authorizer.NodeRead(result.Node.Node)
case result.Service != nil:
return !t.authorizer.ServiceRead(result.Service.Service)
case result.Check != nil:
if result.Check.ServiceName != "" {
return !t.authorizer.ServiceRead(result.Check.ServiceName)
}
return !t.authorizer.NodeRead(result.Check.Node)
} }
return false return false
} }

View File

@ -377,6 +377,35 @@ func (s *Store) ensureNoNodeWithSimilarNameTxn(tx *memdb.Txn, node *structs.Node
return nil return nil
} }
// ensureNodeCASTxn updates a node only if the existing index matches the given index.
// Returns a bool indicating if a write happened and any error.
func (s *Store) ensureNodeCASTxn(tx *memdb.Txn, idx uint64, node *structs.Node) (bool, error) {
// Retrieve the existing entry.
existing, err := getNodeTxn(tx, node.Node)
if err != nil {
return false, err
}
// Check if the we should do the set. A ModifyIndex of 0 means that
// we are doing a set-if-not-exists.
if node.ModifyIndex == 0 && existing != nil {
return false, nil
}
if node.ModifyIndex != 0 && existing == nil {
return false, nil
}
if existing != nil && node.ModifyIndex != 0 && node.ModifyIndex != existing.ModifyIndex {
return false, nil
}
// Perform the update.
if err := s.ensureNodeTxn(tx, idx, node); err != nil {
return false, err
}
return true, nil
}
// ensureNodeTxn is the inner function called to actually create a node // ensureNodeTxn is the inner function called to actually create a node
// registration or modify an existing one in the state store. It allows // registration or modify an existing one in the state store. It allows
// passing in a memdb transaction so it may be part of a larger txn. // passing in a memdb transaction so it may be part of a larger txn.
@ -465,14 +494,22 @@ func (s *Store) GetNode(id string) (uint64, *structs.Node, error) {
idx := maxIndexTxn(tx, "nodes") idx := maxIndexTxn(tx, "nodes")
// Retrieve the node from the state store // Retrieve the node from the state store
node, err := tx.First("nodes", "id", id) node, err := getNodeTxn(tx, id)
if err != nil { if err != nil {
return 0, nil, fmt.Errorf("node lookup failed: %s", err) return 0, nil, fmt.Errorf("node lookup failed: %s", err)
} }
if node != nil { return idx, node, nil
return idx, node.(*structs.Node), nil }
func getNodeTxn(tx *memdb.Txn, nodeName string) (*structs.Node, error) {
node, err := tx.First("nodes", "id", nodeName)
if err != nil {
return nil, fmt.Errorf("node lookup failed: %s", err)
} }
return idx, nil, nil if node != nil {
return node.(*structs.Node), nil
}
return nil, nil
} }
func getNodeIDTxn(tx *memdb.Txn, id types.NodeID) (*structs.Node, error) { func getNodeIDTxn(tx *memdb.Txn, id types.NodeID) (*structs.Node, error) {
@ -573,6 +610,34 @@ func (s *Store) DeleteNode(idx uint64, nodeName string) error {
return nil return nil
} }
// deleteNodeCASTxn is used to try doing a node delete operation with a given
// raft index. If the CAS index specified is not equal to the last observed index for
// the given check, then the call is a noop, otherwise a normal check delete is invoked.
func (s *Store) deleteNodeCASTxn(tx *memdb.Txn, idx, cidx uint64, nodeName string) (bool, error) {
// Look up the node.
node, err := getNodeTxn(tx, nodeName)
if err != nil {
return false, err
}
if node == nil {
return false, nil
}
// If the existing index does not match the provided CAS
// index arg, then we shouldn't update anything and can safely
// return early here.
if node.ModifyIndex != cidx {
return false, nil
}
// Call the actual deletion if the above passed.
if err := s.deleteNodeTxn(tx, idx, nodeName); err != nil {
return false, err
}
return true, nil
}
// deleteNodeTxn is the inner method used for removing a node from // deleteNodeTxn is the inner method used for removing a node from
// the store within a given transaction. // the store within a given transaction.
func (s *Store) deleteNodeTxn(tx *memdb.Txn, idx uint64, nodeName string) error { func (s *Store) deleteNodeTxn(tx *memdb.Txn, idx uint64, nodeName string) error {
@ -680,6 +745,36 @@ func (s *Store) EnsureService(idx uint64, node string, svc *structs.NodeService)
return nil return nil
} }
// ensureServiceCASTxn updates a service only if the existing index matches the given index.
// Returns a bool indicating if a write happened and any error.
func (s *Store) ensureServiceCASTxn(tx *memdb.Txn, idx uint64, node string, svc *structs.NodeService) (bool, error) {
// Retrieve the existing service.
existing, err := tx.First("services", "id", node, svc.ID)
if err != nil {
return false, fmt.Errorf("failed service lookup: %s", err)
}
// Check if the we should do the set. A ModifyIndex of 0 means that
// we are doing a set-if-not-exists.
if svc.ModifyIndex == 0 && existing != nil {
return false, nil
}
if svc.ModifyIndex != 0 && existing == nil {
return false, nil
}
e, ok := existing.(*structs.Node)
if ok && svc.ModifyIndex != 0 && svc.ModifyIndex != e.ModifyIndex {
return false, nil
}
// Perform the update.
if err := s.ensureServiceTxn(tx, idx, node, svc); err != nil {
return false, err
}
return true, nil
}
// ensureServiceTxn is used to upsert a service registration within an // ensureServiceTxn is used to upsert a service registration within an
// existing memdb transaction. // existing memdb transaction.
func (s *Store) ensureServiceTxn(tx *memdb.Txn, idx uint64, node string, svc *structs.NodeService) error { func (s *Store) ensureServiceTxn(tx *memdb.Txn, idx uint64, node string, svc *structs.NodeService) error {
@ -1070,15 +1165,26 @@ func (s *Store) NodeService(nodeName string, serviceID string) (uint64, *structs
idx := maxIndexTxn(tx, "services") idx := maxIndexTxn(tx, "services")
// Query the service // Query the service
service, err := tx.First("services", "id", nodeName, serviceID) service, err := s.getNodeServiceTxn(tx, nodeName, serviceID)
if err != nil { if err != nil {
return 0, nil, fmt.Errorf("failed querying service for node %q: %s", nodeName, err) return 0, nil, fmt.Errorf("failed querying service for node %q: %s", nodeName, err)
} }
if service != nil { return idx, service, nil
return idx, service.(*structs.ServiceNode).ToNodeService(), nil }
func (s *Store) getNodeServiceTxn(tx *memdb.Txn, nodeName, serviceID string) (*structs.NodeService, error) {
// Query the service
service, err := tx.First("services", "id", nodeName, serviceID)
if err != nil {
return nil, fmt.Errorf("failed querying service for node %q: %s", nodeName, err)
} }
return idx, nil, nil
if service != nil {
return service.(*structs.ServiceNode).ToNodeService(), nil
}
return nil, nil
} }
// NodeServices is used to query service registrations by node name or UUID. // NodeServices is used to query service registrations by node name or UUID.
@ -1173,6 +1279,34 @@ func serviceIndexName(name string) string {
return fmt.Sprintf("service.%s", name) return fmt.Sprintf("service.%s", name)
} }
// deleteServiceCASTxn is used to try doing a service delete operation with a given
// raft index. If the CAS index specified is not equal to the last observed index for
// the given service, then the call is a noop, otherwise a normal delete is invoked.
func (s *Store) deleteServiceCASTxn(tx *memdb.Txn, idx, cidx uint64, nodeName, serviceID string) (bool, error) {
// Look up the service.
service, err := s.getNodeServiceTxn(tx, nodeName, serviceID)
if err != nil {
return false, fmt.Errorf("service lookup failed: %s", err)
}
if service == nil {
return false, nil
}
// If the existing index does not match the provided CAS
// index arg, then we shouldn't update anything and can safely
// return early here.
if service.ModifyIndex != cidx {
return false, nil
}
// Call the actual deletion if the above passed.
if err := s.deleteServiceTxn(tx, idx, nodeName, serviceID); err != nil {
return false, err
}
return true, nil
}
// deleteServiceTxn is the inner method called to remove a service // deleteServiceTxn is the inner method called to remove a service
// registration within an existing transaction. // registration within an existing transaction.
func (s *Store) deleteServiceTxn(tx *memdb.Txn, idx uint64, nodeName, serviceID string) error { func (s *Store) deleteServiceTxn(tx *memdb.Txn, idx uint64, nodeName, serviceID string) error {
@ -1273,6 +1407,35 @@ func (s *Store) updateAllServiceIndexesOfNode(tx *memdb.Txn, idx uint64, nodeID
return nil return nil
} }
// ensureCheckCASTxn updates a check only if the existing index matches the given index.
// Returns a bool indicating if a write happened and any error.
func (s *Store) ensureCheckCASTxn(tx *memdb.Txn, idx uint64, hc *structs.HealthCheck) (bool, error) {
// Retrieve the existing entry.
_, existing, err := s.getNodeCheckTxn(tx, hc.Node, hc.CheckID)
if err != nil {
return false, fmt.Errorf("failed health check lookup: %s", err)
}
// Check if the we should do the set. A ModifyIndex of 0 means that
// we are doing a set-if-not-exists.
if hc.ModifyIndex == 0 && existing != nil {
return false, nil
}
if hc.ModifyIndex != 0 && existing == nil {
return false, nil
}
if existing != nil && hc.ModifyIndex != 0 && hc.ModifyIndex != existing.ModifyIndex {
return false, nil
}
// Perform the update.
if err := s.ensureCheckTxn(tx, idx, hc); err != nil {
return false, err
}
return true, nil
}
// ensureCheckTransaction is used as the inner method to handle inserting // ensureCheckTransaction is used as the inner method to handle inserting
// a health check into the state store. It ensures safety against inserting // a health check into the state store. It ensures safety against inserting
// checks with no matching node or service. // checks with no matching node or service.
@ -1389,6 +1552,12 @@ func (s *Store) NodeCheck(nodeName string, checkID types.CheckID) (uint64, *stru
tx := s.db.Txn(false) tx := s.db.Txn(false)
defer tx.Abort() defer tx.Abort()
return s.getNodeCheckTxn(tx, nodeName, checkID)
}
// nodeCheckTxn is used as the inner method to handle reading a health check
// from the state store.
func (s *Store) getNodeCheckTxn(tx *memdb.Txn, nodeName string, checkID types.CheckID) (uint64, *structs.HealthCheck, error) {
// Get the table index. // Get the table index.
idx := maxIndexTxn(tx, "checks") idx := maxIndexTxn(tx, "checks")
@ -1578,6 +1747,34 @@ func (s *Store) DeleteCheck(idx uint64, node string, checkID types.CheckID) erro
return nil return nil
} }
// deleteCheckCASTxn is used to try doing a check delete operation with a given
// raft index. If the CAS index specified is not equal to the last observed index for
// the given check, then the call is a noop, otherwise a normal check delete is invoked.
func (s *Store) deleteCheckCASTxn(tx *memdb.Txn, idx, cidx uint64, node string, checkID types.CheckID) (bool, error) {
// Try to retrieve the existing health check.
_, hc, err := s.getNodeCheckTxn(tx, node, checkID)
if err != nil {
return false, fmt.Errorf("check lookup failed: %s", err)
}
if hc == nil {
return false, nil
}
// If the existing index does not match the provided CAS
// index arg, then we shouldn't update anything and can safely
// return early here.
if hc.ModifyIndex != cidx {
return false, nil
}
// Call the actual deletion if the above passed.
if err := s.deleteCheckTxn(tx, idx, node, checkID); err != nil {
return false, err
}
return true, nil
}
// deleteCheckTxn is the inner method used to call a health // deleteCheckTxn is the inner method used to call a health
// check deletion within an existing transaction. // check deletion within an existing transaction.
func (s *Store) deleteCheckTxn(tx *memdb.Txn, idx uint64, node string, checkID types.CheckID) error { func (s *Store) deleteCheckTxn(tx *memdb.Txn, idx uint64, node string, checkID types.CheckID) error {

View File

@ -118,10 +118,200 @@ func (s *Store) txnIntention(tx *memdb.Txn, idx uint64, op *structs.TxnIntention
case structs.IntentionOpDelete: case structs.IntentionOpDelete:
return s.intentionDeleteTxn(tx, idx, op.Intention.ID) return s.intentionDeleteTxn(tx, idx, op.Intention.ID)
default: default:
return fmt.Errorf("unknown Intention verb %q", op.Op) return fmt.Errorf("unknown Intention op %q", op.Op)
} }
} }
// txnNode handles all Node-related operations.
func (s *Store) txnNode(tx *memdb.Txn, idx uint64, op *structs.TxnNodeOp) (structs.TxnResults, error) {
var entry *structs.Node
var err error
getNode := func() (*structs.Node, error) {
if op.Node.ID != "" {
return getNodeIDTxn(tx, op.Node.ID)
} else {
return getNodeTxn(tx, op.Node.Node)
}
}
switch op.Verb {
case api.NodeGet:
entry, err = getNode()
if entry == nil && err == nil {
err = fmt.Errorf("node %q doesn't exist", op.Node.Node)
}
case api.NodeSet:
err = s.ensureNodeTxn(tx, idx, &op.Node)
if err == nil {
entry, err = getNode()
}
case api.NodeCAS:
var ok bool
ok, err = s.ensureNodeCASTxn(tx, idx, &op.Node)
if !ok && err == nil {
err = fmt.Errorf("failed to set node %q, index is stale", op.Node.Node)
break
}
entry, err = getNode()
case api.NodeDelete:
err = s.deleteNodeTxn(tx, idx, op.Node.Node)
case api.NodeDeleteCAS:
var ok bool
ok, err = s.deleteNodeCASTxn(tx, idx, op.Node.ModifyIndex, op.Node.Node)
if !ok && err == nil {
err = fmt.Errorf("failed to delete node %q, index is stale", op.Node.Node)
}
default:
err = fmt.Errorf("unknown Node verb %q", op.Verb)
}
if err != nil {
return nil, err
}
// For a GET we keep the value, otherwise we clone and blank out the
// value (we have to clone so we don't modify the entry being used by
// the state store).
if entry != nil {
if op.Verb == api.NodeGet {
result := structs.TxnResult{Node: entry}
return structs.TxnResults{&result}, nil
}
clone := *entry
result := structs.TxnResult{Node: &clone}
return structs.TxnResults{&result}, nil
}
return nil, nil
}
// txnService handles all Service-related operations.
func (s *Store) txnService(tx *memdb.Txn, idx uint64, op *structs.TxnServiceOp) (structs.TxnResults, error) {
var entry *structs.NodeService
var err error
switch op.Verb {
case api.ServiceGet:
entry, err = s.getNodeServiceTxn(tx, op.Node, op.Service.ID)
if entry == nil && err == nil {
err = fmt.Errorf("service %q on node %q doesn't exist", op.Service.ID, op.Node)
}
case api.ServiceSet:
err = s.ensureServiceTxn(tx, idx, op.Node, &op.Service)
entry, err = s.getNodeServiceTxn(tx, op.Node, op.Service.ID)
case api.ServiceCAS:
var ok bool
ok, err = s.ensureServiceCASTxn(tx, idx, op.Node, &op.Service)
if !ok && err == nil {
err = fmt.Errorf("failed to set service %q on node %q, index is stale", op.Service.ID, op.Node)
break
}
entry, err = s.getNodeServiceTxn(tx, op.Node, op.Service.ID)
case api.ServiceDelete:
err = s.deleteServiceTxn(tx, idx, op.Node, op.Service.ID)
case api.ServiceDeleteCAS:
var ok bool
ok, err = s.deleteServiceCASTxn(tx, idx, op.Service.ModifyIndex, op.Node, op.Service.ID)
if !ok && err == nil {
err = fmt.Errorf("failed to delete service %q on node %q, index is stale", op.Service.ID, op.Node)
}
default:
err = fmt.Errorf("unknown Service verb %q", op.Verb)
}
if err != nil {
return nil, err
}
// For a GET we keep the value, otherwise we clone and blank out the
// value (we have to clone so we don't modify the entry being used by
// the state store).
if entry != nil {
if op.Verb == api.ServiceGet {
result := structs.TxnResult{Service: entry}
return structs.TxnResults{&result}, nil
}
clone := *entry
result := structs.TxnResult{Service: &clone}
return structs.TxnResults{&result}, nil
}
return nil, nil
}
// txnCheck handles all Check-related operations.
func (s *Store) txnCheck(tx *memdb.Txn, idx uint64, op *structs.TxnCheckOp) (structs.TxnResults, error) {
var entry *structs.HealthCheck
var err error
switch op.Verb {
case api.CheckGet:
_, entry, err = s.getNodeCheckTxn(tx, op.Check.Node, op.Check.CheckID)
if entry == nil && err == nil {
err = fmt.Errorf("check %q on node %q doesn't exist", op.Check.CheckID, op.Check.Node)
}
case api.CheckSet:
err = s.ensureCheckTxn(tx, idx, &op.Check)
if err == nil {
_, entry, err = s.getNodeCheckTxn(tx, op.Check.Node, op.Check.CheckID)
}
case api.CheckCAS:
var ok bool
entry = &op.Check
ok, err = s.ensureCheckCASTxn(tx, idx, entry)
if !ok && err == nil {
err = fmt.Errorf("failed to set check %q on node %q, index is stale", entry.CheckID, entry.Node)
break
}
_, entry, err = s.getNodeCheckTxn(tx, op.Check.Node, op.Check.CheckID)
case api.CheckDelete:
err = s.deleteCheckTxn(tx, idx, op.Check.Node, op.Check.CheckID)
case api.CheckDeleteCAS:
var ok bool
ok, err = s.deleteCheckCASTxn(tx, idx, op.Check.ModifyIndex, op.Check.Node, op.Check.CheckID)
if !ok && err == nil {
err = fmt.Errorf("failed to delete check %q on node %q, index is stale", op.Check.CheckID, op.Check.Node)
}
default:
err = fmt.Errorf("unknown Check verb %q", op.Verb)
}
if err != nil {
return nil, err
}
// For a GET we keep the value, otherwise we clone and blank out the
// value (we have to clone so we don't modify the entry being used by
// the state store).
if entry != nil {
if op.Verb == api.CheckGet {
result := structs.TxnResult{Check: entry}
return structs.TxnResults{&result}, nil
}
clone := entry.Clone()
result := structs.TxnResult{Check: clone}
return structs.TxnResults{&result}, nil
}
return nil, nil
}
// txnDispatch runs the given operations inside the state store transaction. // txnDispatch runs the given operations inside the state store transaction.
func (s *Store) txnDispatch(tx *memdb.Txn, idx uint64, ops structs.TxnOps) (structs.TxnResults, structs.TxnErrors) { func (s *Store) txnDispatch(tx *memdb.Txn, idx uint64, ops structs.TxnOps) (structs.TxnResults, structs.TxnErrors) {
results := make(structs.TxnResults, 0, len(ops)) results := make(structs.TxnResults, 0, len(ops))
@ -136,6 +326,12 @@ func (s *Store) txnDispatch(tx *memdb.Txn, idx uint64, ops structs.TxnOps) (stru
ret, err = s.txnKVS(tx, idx, op.KV) ret, err = s.txnKVS(tx, idx, op.KV)
case op.Intention != nil: case op.Intention != nil:
err = s.txnIntention(tx, idx, op.Intention) err = s.txnIntention(tx, idx, op.Intention)
case op.Node != nil:
ret, err = s.txnNode(tx, idx, op.Node)
case op.Service != nil:
ret, err = s.txnService(tx, idx, op.Service)
case op.Check != nil:
ret, err = s.txnCheck(tx, idx, op.Check)
default: default:
err = fmt.Errorf("no operation specified") err = fmt.Errorf("no operation specified")
} }

View File

@ -1,12 +1,14 @@
package state package state
import ( import (
"fmt"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/types"
"github.com/pascaldekloe/goe/verify" "github.com/pascaldekloe/goe/verify"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -116,6 +118,384 @@ func TestStateStore_Txn_Intention(t *testing.T) {
verify.Values(t, "", actual, intentions) verify.Values(t, "", actual, intentions)
} }
func TestStateStore_Txn_Node(t *testing.T) {
require := require.New(t)
s := testStateStore(t)
// Create some nodes.
var nodes [5]structs.Node
for i := 0; i < len(nodes); i++ {
nodes[i] = structs.Node{
Node: fmt.Sprintf("node%d", i+1),
ID: types.NodeID(testUUID()),
}
// Leave node5 to be created by an operation.
if i < 5 {
s.EnsureNode(uint64(i+1), &nodes[i])
}
}
// Set up a transaction that hits every operation.
ops := structs.TxnOps{
&structs.TxnOp{
Node: &structs.TxnNodeOp{
Verb: api.NodeGet,
Node: nodes[0],
},
},
&structs.TxnOp{
Node: &structs.TxnNodeOp{
Verb: api.NodeSet,
Node: nodes[4],
},
},
&structs.TxnOp{
Node: &structs.TxnNodeOp{
Verb: api.NodeCAS,
Node: structs.Node{
Node: "node2",
ID: nodes[1].ID,
Datacenter: "dc2",
RaftIndex: structs.RaftIndex{ModifyIndex: 2},
},
},
},
&structs.TxnOp{
Node: &structs.TxnNodeOp{
Verb: api.NodeDelete,
Node: structs.Node{Node: "node3"},
},
},
&structs.TxnOp{
Node: &structs.TxnNodeOp{
Verb: api.NodeDeleteCAS,
Node: structs.Node{
Node: "node4",
RaftIndex: structs.RaftIndex{ModifyIndex: 4},
},
},
},
}
results, errors := s.TxnRW(8, ops)
if len(errors) > 0 {
t.Fatalf("err: %v", errors)
}
// Make sure the response looks as expected.
nodes[1].Datacenter = "dc2"
nodes[1].ModifyIndex = 8
expected := structs.TxnResults{
&structs.TxnResult{
Node: &nodes[0],
},
&structs.TxnResult{
Node: &nodes[4],
},
&structs.TxnResult{
Node: &nodes[1],
},
}
verify.Values(t, "", results, expected)
// Pull the resulting state store contents.
idx, actual, err := s.Nodes(nil)
require.NoError(err)
if idx != 8 {
t.Fatalf("bad index: %d", idx)
}
// Make sure it looks as expected.
expectedNodes := structs.Nodes{&nodes[0], &nodes[1], &nodes[4]}
verify.Values(t, "", actual, expectedNodes)
}
func TestStateStore_Txn_Service(t *testing.T) {
require := require.New(t)
s := testStateStore(t)
testRegisterNode(t, s, 1, "node1")
// Create some services.
for i := 1; i <= 4; i++ {
testRegisterService(t, s, uint64(i+1), "node1", fmt.Sprintf("svc%d", i))
}
// Set up a transaction that hits every operation.
ops := structs.TxnOps{
&structs.TxnOp{
Service: &structs.TxnServiceOp{
Verb: api.ServiceGet,
Node: "node1",
Service: structs.NodeService{ID: "svc1"},
},
},
&structs.TxnOp{
Service: &structs.TxnServiceOp{
Verb: api.ServiceSet,
Node: "node1",
Service: structs.NodeService{ID: "svc5"},
},
},
&structs.TxnOp{
Service: &structs.TxnServiceOp{
Verb: api.ServiceCAS,
Node: "node1",
Service: structs.NodeService{
ID: "svc2",
Tags: []string{"modified"},
RaftIndex: structs.RaftIndex{ModifyIndex: 3},
},
},
},
&structs.TxnOp{
Service: &structs.TxnServiceOp{
Verb: api.ServiceDelete,
Node: "node1",
Service: structs.NodeService{ID: "svc3"},
},
},
&structs.TxnOp{
Service: &structs.TxnServiceOp{
Verb: api.ServiceDeleteCAS,
Node: "node1",
Service: structs.NodeService{
ID: "svc4",
RaftIndex: structs.RaftIndex{ModifyIndex: 5},
},
},
},
}
results, errors := s.TxnRW(6, ops)
if len(errors) > 0 {
t.Fatalf("err: %v", errors)
}
// Make sure the response looks as expected.
expected := structs.TxnResults{
&structs.TxnResult{
Service: &structs.NodeService{
ID: "svc1",
Service: "svc1",
Address: "1.1.1.1",
Port: 1111,
Weights: &structs.Weights{Passing: 1, Warning: 1},
RaftIndex: structs.RaftIndex{
CreateIndex: 2,
ModifyIndex: 2,
},
},
},
&structs.TxnResult{
Service: &structs.NodeService{
ID: "svc5",
Weights: &structs.Weights{Passing: 1, Warning: 1},
RaftIndex: structs.RaftIndex{
CreateIndex: 6,
ModifyIndex: 6,
},
},
},
&structs.TxnResult{
Service: &structs.NodeService{
ID: "svc2",
Tags: []string{"modified"},
Weights: &structs.Weights{Passing: 1, Warning: 1},
RaftIndex: structs.RaftIndex{
CreateIndex: 3,
ModifyIndex: 6,
},
},
},
}
verify.Values(t, "", results, expected)
// Pull the resulting state store contents.
idx, actual, err := s.NodeServices(nil, "node1")
require.NoError(err)
if idx != 6 {
t.Fatalf("bad index: %d", idx)
}
// Make sure it looks as expected.
expectedServices := &structs.NodeServices{
Node: &structs.Node{
Node: "node1",
RaftIndex: structs.RaftIndex{
CreateIndex: 1,
ModifyIndex: 1,
},
},
Services: map[string]*structs.NodeService{
"svc1": &structs.NodeService{
ID: "svc1",
Service: "svc1",
Address: "1.1.1.1",
Port: 1111,
RaftIndex: structs.RaftIndex{
CreateIndex: 2,
ModifyIndex: 2,
},
Weights: &structs.Weights{Passing: 1, Warning: 1},
},
"svc5": &structs.NodeService{
ID: "svc5",
RaftIndex: structs.RaftIndex{
CreateIndex: 6,
ModifyIndex: 6,
},
Weights: &structs.Weights{Passing: 1, Warning: 1},
},
"svc2": &structs.NodeService{
ID: "svc2",
Tags: []string{"modified"},
RaftIndex: structs.RaftIndex{
CreateIndex: 3,
ModifyIndex: 6,
},
Weights: &structs.Weights{Passing: 1, Warning: 1},
},
},
}
verify.Values(t, "", actual, expectedServices)
}
func TestStateStore_Txn_Checks(t *testing.T) {
require := require.New(t)
s := testStateStore(t)
testRegisterNode(t, s, 1, "node1")
// Create some checks.
for i := 1; i <= 4; i++ {
testRegisterCheck(t, s, uint64(i+1), "node1", "", types.CheckID(fmt.Sprintf("check%d", i)), "failing")
}
// Set up a transaction that hits every operation.
ops := structs.TxnOps{
&structs.TxnOp{
Check: &structs.TxnCheckOp{
Verb: api.CheckGet,
Check: structs.HealthCheck{Node: "node1", CheckID: "check1"},
},
},
&structs.TxnOp{
Check: &structs.TxnCheckOp{
Verb: api.CheckSet,
Check: structs.HealthCheck{Node: "node1", CheckID: "check5", Status: "passing"},
},
},
&structs.TxnOp{
Check: &structs.TxnCheckOp{
Verb: api.CheckCAS,
Check: structs.HealthCheck{
Node: "node1",
CheckID: "check2",
Status: "warning",
RaftIndex: structs.RaftIndex{ModifyIndex: 3},
},
},
},
&structs.TxnOp{
Check: &structs.TxnCheckOp{
Verb: api.CheckDelete,
Check: structs.HealthCheck{Node: "node1", CheckID: "check3"},
},
},
&structs.TxnOp{
Check: &structs.TxnCheckOp{
Verb: api.CheckDeleteCAS,
Check: structs.HealthCheck{
Node: "node1",
CheckID: "check4",
RaftIndex: structs.RaftIndex{ModifyIndex: 5},
},
},
},
}
results, errors := s.TxnRW(6, ops)
if len(errors) > 0 {
t.Fatalf("err: %v", errors)
}
// Make sure the response looks as expected.
expected := structs.TxnResults{
&structs.TxnResult{
Check: &structs.HealthCheck{
Node: "node1",
CheckID: "check1",
Status: "failing",
RaftIndex: structs.RaftIndex{
CreateIndex: 2,
ModifyIndex: 2,
},
},
},
&structs.TxnResult{
Check: &structs.HealthCheck{
Node: "node1",
CheckID: "check5",
Status: "passing",
RaftIndex: structs.RaftIndex{
CreateIndex: 6,
ModifyIndex: 6,
},
},
},
&structs.TxnResult{
Check: &structs.HealthCheck{
Node: "node1",
CheckID: "check2",
Status: "warning",
RaftIndex: structs.RaftIndex{
CreateIndex: 3,
ModifyIndex: 6,
},
},
},
}
verify.Values(t, "", results, expected)
// Pull the resulting state store contents.
idx, actual, err := s.NodeChecks(nil, "node1")
require.NoError(err)
if idx != 6 {
t.Fatalf("bad index: %d", idx)
}
// Make sure it looks as expected.
expectedChecks := structs.HealthChecks{
&structs.HealthCheck{
Node: "node1",
CheckID: "check1",
Status: "failing",
RaftIndex: structs.RaftIndex{
CreateIndex: 2,
ModifyIndex: 2,
},
},
&structs.HealthCheck{
Node: "node1",
CheckID: "check2",
Status: "warning",
RaftIndex: structs.RaftIndex{
CreateIndex: 3,
ModifyIndex: 6,
},
},
&structs.HealthCheck{
Node: "node1",
CheckID: "check5",
Status: "passing",
RaftIndex: structs.RaftIndex{
CreateIndex: 6,
ModifyIndex: 6,
},
},
}
verify.Values(t, "", actual, expectedChecks)
}
func TestStateStore_Txn_KVS(t *testing.T) { func TestStateStore_Txn_KVS(t *testing.T) {
s := testStateStore(t) s := testStateStore(t)

View File

@ -7,6 +7,7 @@ import (
"github.com/armon/go-metrics" "github.com/armon/go-metrics"
"github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/api"
) )
// Txn endpoint is used to perform multi-object atomic transactions. // Txn endpoint is used to perform multi-object atomic transactions.
@ -21,7 +22,8 @@ func (t *Txn) preCheck(authorizer acl.Authorizer, ops structs.TxnOps) structs.Tx
// Perform the pre-apply checks for any KV operations. // Perform the pre-apply checks for any KV operations.
for i, op := range ops { for i, op := range ops {
if op.KV != nil { switch {
case op.KV != nil:
ok, err := kvsPreApply(t.srv, authorizer, op.KV.Verb, &op.KV.DirEnt) ok, err := kvsPreApply(t.srv, authorizer, op.KV.Verb, &op.KV.DirEnt)
if err != nil { if err != nil {
errors = append(errors, &structs.TxnError{ errors = append(errors, &structs.TxnError{
@ -35,6 +37,65 @@ func (t *Txn) preCheck(authorizer acl.Authorizer, ops structs.TxnOps) structs.Tx
What: err.Error(), What: err.Error(),
}) })
} }
case op.Node != nil:
// Skip the pre-apply checks if this is a GET.
if op.Node.Verb == api.NodeGet {
break
}
node := op.Node.Node
if err := nodePreApply(node.Node, string(node.ID)); err != nil {
errors = append(errors, &structs.TxnError{
OpIndex: i,
What: err.Error(),
})
break
}
// Check that the token has permissions for the given operation.
if err := vetNodeTxnOp(op.Node, authorizer); err != nil {
errors = append(errors, &structs.TxnError{
OpIndex: i,
What: err.Error(),
})
}
case op.Service != nil:
// Skip the pre-apply checks if this is a GET.
if op.Service.Verb == api.ServiceGet {
break
}
service := &op.Service.Service
if err := servicePreApply(service, nil); err != nil {
errors = append(errors, &structs.TxnError{
OpIndex: i,
What: err.Error(),
})
break
}
// Check that the token has permissions for the given operation.
if err := vetServiceTxnOp(op.Service, authorizer); err != nil {
errors = append(errors, &structs.TxnError{
OpIndex: i,
What: err.Error(),
})
}
case op.Check != nil:
// Skip the pre-apply checks if this is a GET.
if op.Check.Verb == api.CheckGet {
break
}
checkPreApply(&op.Check.Check)
// Check that the token has permissions for the given operation.
if err := vetCheckTxnOp(op.Check, authorizer); err != nil {
errors = append(errors, &structs.TxnError{
OpIndex: i,
What: err.Error(),
})
}
} }
} }

View File

@ -12,9 +12,49 @@ import (
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/testrpc" "github.com/hashicorp/consul/testrpc"
"github.com/hashicorp/consul/types"
"github.com/hashicorp/net-rpc-msgpackrpc" "github.com/hashicorp/net-rpc-msgpackrpc"
"github.com/pascaldekloe/goe/verify"
"github.com/stretchr/testify/require"
) )
var testTxnRules = `
key "" {
policy = "deny"
}
key "foo" {
policy = "read"
}
key "test" {
policy = "write"
}
key "test/priv" {
policy = "read"
}
service "" {
policy = "deny"
}
service "foo-svc" {
policy = "read"
}
service "test-svc" {
policy = "write"
}
node "" {
policy = "deny"
}
node "foo-node" {
policy = "read"
}
node "test-node" {
policy = "write"
}
`
var testNodeID = "9749a7df-fac5-46b4-8078-32a3d96c59f3"
func TestTxn_CheckNotExists(t *testing.T) { func TestTxn_CheckNotExists(t *testing.T) {
t.Parallel() t.Parallel()
dir1, s1 := testServer(t) dir1, s1 := testServer(t)
@ -101,12 +141,76 @@ func TestTxn_Apply(t *testing.T) {
}, },
}, },
}, },
&structs.TxnOp{
Node: &structs.TxnNodeOp{
Verb: api.NodeSet,
Node: structs.Node{
ID: types.NodeID(testNodeID),
Node: "foo",
Address: "127.0.0.1",
},
},
},
&structs.TxnOp{
Node: &structs.TxnNodeOp{
Verb: api.NodeGet,
Node: structs.Node{
ID: types.NodeID(testNodeID),
Node: "foo",
},
},
},
&structs.TxnOp{
Service: &structs.TxnServiceOp{
Verb: api.ServiceSet,
Node: "foo",
Service: structs.NodeService{
ID: "svc-foo",
Service: "svc-foo",
Address: "1.1.1.1",
},
},
},
&structs.TxnOp{
Service: &structs.TxnServiceOp{
Verb: api.ServiceGet,
Node: "foo",
Service: structs.NodeService{
ID: "svc-foo",
Service: "svc-foo",
},
},
},
&structs.TxnOp{
Check: &structs.TxnCheckOp{
Verb: api.CheckSet,
Check: structs.HealthCheck{
Node: "foo",
CheckID: types.CheckID("check-foo"),
Name: "test",
Status: "passing",
},
},
},
&structs.TxnOp{
Check: &structs.TxnCheckOp{
Verb: api.CheckGet,
Check: structs.HealthCheck{
Node: "foo",
CheckID: types.CheckID("check-foo"),
Name: "test",
},
},
},
}, },
} }
var out structs.TxnResponse var out structs.TxnResponse
if err := msgpackrpc.CallWithCodec(codec, "Txn.Apply", &arg, &out); err != nil { if err := msgpackrpc.CallWithCodec(codec, "Txn.Apply", &arg, &out); err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
if len(out.Errors) != 0 {
t.Fatalf("errs: %v", out.Errors)
}
// Verify the state store directly. // Verify the state store directly.
state := s1.fsm.State() state := s1.fsm.State()
@ -122,6 +226,30 @@ func TestTxn_Apply(t *testing.T) {
t.Fatalf("bad: %v", d) t.Fatalf("bad: %v", d)
} }
_, n, err := state.GetNode("foo")
if err != nil {
t.Fatalf("err: %v", err)
}
if n.Node != "foo" || n.Address != "127.0.0.1" {
t.Fatalf("bad: %v", err)
}
_, s, err := state.NodeService("foo", "svc-foo")
if err != nil {
t.Fatalf("err: %v", err)
}
if s.ID != "svc-foo" || s.Address != "1.1.1.1" {
t.Fatalf("bad: %v", err)
}
_, c, err := state.NodeCheck("foo", types.CheckID("check-foo"))
if err != nil {
t.Fatalf("err: %v", err)
}
if c.CheckID != "check-foo" || c.Status != "passing" || c.Name != "test" {
t.Fatalf("bad: %v", err)
}
// Verify the transaction's return value. // Verify the transaction's return value.
expected := structs.TxnResponse{ expected := structs.TxnResponse{
Results: structs.TxnResults{ Results: structs.TxnResults{
@ -147,15 +275,34 @@ func TestTxn_Apply(t *testing.T) {
}, },
}, },
}, },
&structs.TxnResult{
Node: n,
},
&structs.TxnResult{
Node: n,
},
&structs.TxnResult{
Service: s,
},
&structs.TxnResult{
Service: s,
},
&structs.TxnResult{
Check: c,
},
&structs.TxnResult{
Check: c,
},
}, },
} }
if !reflect.DeepEqual(out, expected) { verify.Values(t, "", out, expected)
t.Fatalf("bad %v", out)
}
} }
func TestTxn_Apply_ACLDeny(t *testing.T) { func TestTxn_Apply_ACLDeny(t *testing.T) {
t.Parallel() t.Parallel()
require := require.New(t)
dir1, s1 := testServerWithConfig(t, func(c *Config) { dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1" c.ACLDatacenter = "dc1"
c.ACLsEnabled = true c.ACLsEnabled = true
@ -167,15 +314,25 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
testrpc.WaitForLeader(t, s1.RPC, "dc1") testrpc.WaitForLeader(t, s1.RPC, "dc1")
// Put in a key to read back. // Set up some state to read back.
state := s1.fsm.State() state := s1.fsm.State()
d := &structs.DirEntry{ d := &structs.DirEntry{
Key: "nope", Key: "nope",
Value: []byte("hello"), Value: []byte("hello"),
} }
if err := state.KVSSet(1, d); err != nil { require.NoError(state.KVSSet(1, d))
t.Fatalf("err: %v", err)
node := &structs.Node{
ID: types.NodeID(testNodeID),
Node: "nope",
} }
require.NoError(state.EnsureNode(2, node))
svc := structs.NodeService{ID: "nope", Service: "nope", Address: "127.0.0.1"}
require.NoError(state.EnsureService(3, "nope", &svc))
check := structs.HealthCheck{Node: "nope", CheckID: types.CheckID("nope")}
state.EnsureCheck(4, &check)
// Create the ACL. // Create the ACL.
var id string var id string
@ -186,7 +343,7 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
ACL: structs.ACL{ ACL: structs.ACL{
Name: "User token", Name: "User token",
Type: structs.ACLTokenTypeClient, Type: structs.ACLTokenTypeClient,
Rules: testListRules, Rules: testTxnRules,
}, },
WriteRequest: structs.WriteRequest{Token: "root"}, WriteRequest: structs.WriteRequest{Token: "root"},
} }
@ -296,6 +453,101 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
}, },
}, },
}, },
&structs.TxnOp{
Node: &structs.TxnNodeOp{
Verb: api.NodeGet,
Node: structs.Node{ID: node.ID, Node: node.Node},
},
},
&structs.TxnOp{
Node: &structs.TxnNodeOp{
Verb: api.NodeSet,
Node: structs.Node{ID: node.ID, Node: node.Node},
},
},
&structs.TxnOp{
Node: &structs.TxnNodeOp{
Verb: api.NodeCAS,
Node: structs.Node{ID: node.ID, Node: node.Node},
},
},
&structs.TxnOp{
Node: &structs.TxnNodeOp{
Verb: api.NodeDelete,
Node: structs.Node{ID: node.ID, Node: node.Node},
},
},
&structs.TxnOp{
Node: &structs.TxnNodeOp{
Verb: api.NodeDeleteCAS,
Node: structs.Node{ID: node.ID, Node: node.Node},
},
},
&structs.TxnOp{
Service: &structs.TxnServiceOp{
Verb: api.ServiceGet,
Node: "foo-node",
Service: svc,
},
},
&structs.TxnOp{
Service: &structs.TxnServiceOp{
Verb: api.ServiceSet,
Node: "foo-node",
Service: svc,
},
},
&structs.TxnOp{
Service: &structs.TxnServiceOp{
Verb: api.ServiceCAS,
Node: "foo-node",
Service: svc,
},
},
&structs.TxnOp{
Service: &structs.TxnServiceOp{
Verb: api.ServiceDelete,
Node: "foo-node",
Service: svc,
},
},
&structs.TxnOp{
Service: &structs.TxnServiceOp{
Verb: api.ServiceDeleteCAS,
Node: "foo-node",
Service: svc,
},
},
&structs.TxnOp{
Check: &structs.TxnCheckOp{
Verb: api.CheckGet,
Check: check,
},
},
&structs.TxnOp{
Check: &structs.TxnCheckOp{
Verb: api.CheckSet,
Check: check,
},
},
&structs.TxnOp{
Check: &structs.TxnCheckOp{
Verb: api.CheckCAS,
Check: check,
},
},
&structs.TxnOp{
Check: &structs.TxnCheckOp{
Verb: api.CheckDelete,
Check: check,
},
},
&structs.TxnOp{
Check: &structs.TxnCheckOp{
Verb: api.CheckDeleteCAS,
Check: check,
},
},
}, },
WriteRequest: structs.WriteRequest{ WriteRequest: structs.WriteRequest{
Token: id, Token: id,
@ -309,6 +561,8 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
// Verify the transaction's return value. // Verify the transaction's return value.
var expected structs.TxnResponse var expected structs.TxnResponse
for i, op := range arg.Ops { for i, op := range arg.Ops {
switch {
case op.KV != nil:
switch op.KV.Verb { switch op.KV.Verb {
case api.KVGet, api.KVGetTree: case api.KVGet, api.KVGetTree:
// These get filtered but won't result in an error. // These get filtered but won't result in an error.
@ -319,10 +573,43 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
What: acl.ErrPermissionDenied.Error(), What: acl.ErrPermissionDenied.Error(),
}) })
} }
case op.Node != nil:
switch op.Node.Verb {
case api.NodeGet:
// These get filtered but won't result in an error.
default:
expected.Errors = append(expected.Errors, &structs.TxnError{
OpIndex: i,
What: acl.ErrPermissionDenied.Error(),
})
} }
if !reflect.DeepEqual(out, expected) { case op.Service != nil:
t.Fatalf("bad %v", out) switch op.Service.Verb {
case api.ServiceGet:
// These get filtered but won't result in an error.
default:
expected.Errors = append(expected.Errors, &structs.TxnError{
OpIndex: i,
What: acl.ErrPermissionDenied.Error(),
})
} }
case op.Check != nil:
switch op.Check.Verb {
case api.CheckGet:
// These get filtered but won't result in an error.
default:
expected.Errors = append(expected.Errors, &structs.TxnError{
OpIndex: i,
What: acl.ErrPermissionDenied.Error(),
})
}
}
}
verify.Values(t, "", out, expected)
} }
func TestTxn_Apply_LockDelay(t *testing.T) { func TestTxn_Apply_LockDelay(t *testing.T) {
@ -413,6 +700,9 @@ func TestTxn_Apply_LockDelay(t *testing.T) {
func TestTxn_Read(t *testing.T) { func TestTxn_Read(t *testing.T) {
t.Parallel() t.Parallel()
require := require.New(t)
dir1, s1 := testServer(t) dir1, s1 := testServer(t)
defer os.RemoveAll(dir1) defer os.RemoveAll(dir1)
defer s1.Shutdown() defer s1.Shutdown()
@ -431,6 +721,19 @@ func TestTxn_Read(t *testing.T) {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
// Put in a node/check/service to read back.
node := &structs.Node{
ID: types.NodeID(testNodeID),
Node: "foo",
}
require.NoError(state.EnsureNode(2, node))
svc := structs.NodeService{ID: "svc-foo", Service: "svc-foo", Address: "127.0.0.1"}
require.NoError(state.EnsureService(3, "foo", &svc))
check := structs.HealthCheck{Node: "foo", CheckID: types.CheckID("check-foo")}
state.EnsureCheck(4, &check)
// Do a super basic request. The state store test covers the details so // Do a super basic request. The state store test covers the details so
// we just need to be sure that the transaction is sent correctly and // we just need to be sure that the transaction is sent correctly and
// the results are converted appropriately. // the results are converted appropriately.
@ -445,6 +748,25 @@ func TestTxn_Read(t *testing.T) {
}, },
}, },
}, },
&structs.TxnOp{
Node: &structs.TxnNodeOp{
Verb: api.NodeGet,
Node: structs.Node{ID: node.ID, Node: node.Node},
},
},
&structs.TxnOp{
Service: &structs.TxnServiceOp{
Verb: api.ServiceGet,
Node: "foo",
Service: svc,
},
},
&structs.TxnOp{
Check: &structs.TxnCheckOp{
Verb: api.CheckGet,
Check: check,
},
},
}, },
} }
var out structs.TxnReadResponse var out structs.TxnReadResponse
@ -453,6 +775,8 @@ func TestTxn_Read(t *testing.T) {
} }
// Verify the transaction's return value. // Verify the transaction's return value.
svc.Weights = &structs.Weights{Passing: 1, Warning: 1}
svc.RaftIndex = structs.RaftIndex{CreateIndex: 3, ModifyIndex: 3}
expected := structs.TxnReadResponse{ expected := structs.TxnReadResponse{
TxnResponse: structs.TxnResponse{ TxnResponse: structs.TxnResponse{
Results: structs.TxnResults{ Results: structs.TxnResults{
@ -466,19 +790,29 @@ func TestTxn_Read(t *testing.T) {
}, },
}, },
}, },
&structs.TxnResult{
Node: node,
},
&structs.TxnResult{
Service: &svc,
},
&structs.TxnResult{
Check: &check,
},
}, },
}, },
QueryMeta: structs.QueryMeta{ QueryMeta: structs.QueryMeta{
KnownLeader: true, KnownLeader: true,
}, },
} }
if !reflect.DeepEqual(out, expected) { verify.Values(t, "", out, expected)
t.Fatalf("bad %v", out)
}
} }
func TestTxn_Read_ACLDeny(t *testing.T) { func TestTxn_Read_ACLDeny(t *testing.T) {
t.Parallel() t.Parallel()
require := require.New(t)
dir1, s1 := testServerWithConfig(t, func(c *Config) { dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1" c.ACLDatacenter = "dc1"
c.ACLsEnabled = true c.ACLsEnabled = true
@ -502,6 +836,19 @@ func TestTxn_Read_ACLDeny(t *testing.T) {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
// Put in a node/check/service to read back.
node := &structs.Node{
ID: types.NodeID(testNodeID),
Node: "nope",
}
require.NoError(state.EnsureNode(2, node))
svc := structs.NodeService{ID: "nope", Service: "nope", Address: "127.0.0.1"}
require.NoError(state.EnsureService(3, "nope", &svc))
check := structs.HealthCheck{Node: "nope", CheckID: types.CheckID("nope")}
state.EnsureCheck(4, &check)
// Create the ACL. // Create the ACL.
var id string var id string
{ {
@ -511,7 +858,7 @@ func TestTxn_Read_ACLDeny(t *testing.T) {
ACL: structs.ACL{ ACL: structs.ACL{
Name: "User token", Name: "User token",
Type: structs.ACLTokenTypeClient, Type: structs.ACLTokenTypeClient,
Rules: testListRules, Rules: testTxnRules,
}, },
WriteRequest: structs.WriteRequest{Token: "root"}, WriteRequest: structs.WriteRequest{Token: "root"},
} }
@ -557,6 +904,25 @@ func TestTxn_Read_ACLDeny(t *testing.T) {
}, },
}, },
}, },
&structs.TxnOp{
Node: &structs.TxnNodeOp{
Verb: api.NodeGet,
Node: structs.Node{ID: node.ID, Node: node.Node},
},
},
&structs.TxnOp{
Service: &structs.TxnServiceOp{
Verb: api.ServiceGet,
Node: "foo",
Service: svc,
},
},
&structs.TxnOp{
Check: &structs.TxnCheckOp{
Verb: api.CheckGet,
Check: check,
},
},
}, },
QueryOptions: structs.QueryOptions{ QueryOptions: structs.QueryOptions{
Token: id, Token: id,
@ -574,6 +940,8 @@ func TestTxn_Read_ACLDeny(t *testing.T) {
}, },
} }
for i, op := range arg.Ops { for i, op := range arg.Ops {
switch {
case op.KV != nil:
switch op.KV.Verb { switch op.KV.Verb {
case api.KVGet, api.KVGetTree: case api.KVGet, api.KVGetTree:
// These get filtered but won't result in an error. // These get filtered but won't result in an error.
@ -584,6 +952,40 @@ func TestTxn_Read_ACLDeny(t *testing.T) {
What: acl.ErrPermissionDenied.Error(), What: acl.ErrPermissionDenied.Error(),
}) })
} }
case op.Node != nil:
switch op.Node.Verb {
case api.NodeGet:
// These get filtered but won't result in an error.
default:
expected.Errors = append(expected.Errors, &structs.TxnError{
OpIndex: i,
What: acl.ErrPermissionDenied.Error(),
})
}
case op.Service != nil:
switch op.Service.Verb {
case api.ServiceGet:
// These get filtered but won't result in an error.
default:
expected.Errors = append(expected.Errors, &structs.TxnError{
OpIndex: i,
What: acl.ErrPermissionDenied.Error(),
})
}
case op.Check != nil:
switch op.Check.Verb {
case api.CheckGet:
// These get filtered but won't result in an error.
default:
expected.Errors = append(expected.Errors, &structs.TxnError{
OpIndex: i,
What: acl.ErrPermissionDenied.Error(),
})
}
}
} }
if !reflect.DeepEqual(out, expected) { if !reflect.DeepEqual(out, expected) {
t.Fatalf("bad %v", out) t.Fatalf("bad %v", out)

View File

@ -508,7 +508,18 @@ func decodeBody(req *http.Request, out interface{}, cb func(interface{}) error)
return err return err
} }
} }
return mapstructure.Decode(raw, out)
decodeConf := &mapstructure.DecoderConfig{
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
Result: &out,
}
decoder, err := mapstructure.NewDecoder(decodeConf)
if err != nil {
return err
}
return decoder.Decode(raw)
} }
// setTranslateAddr is used to set the address translation header. This is only // setTranslateAddr is used to set the address translation header. This is only

View File

@ -2,6 +2,7 @@ package structs
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"math/rand" "math/rand"
"reflect" "reflect"
@ -911,9 +912,67 @@ type HealthCheckDefinition struct {
Header map[string][]string `json:",omitempty"` Header map[string][]string `json:",omitempty"`
Method string `json:",omitempty"` Method string `json:",omitempty"`
TCP string `json:",omitempty"` TCP string `json:",omitempty"`
Interval api.ReadableDuration `json:",omitempty"` Interval time.Duration `json:",omitempty"`
Timeout api.ReadableDuration `json:",omitempty"` Timeout time.Duration `json:",omitempty"`
DeregisterCriticalServiceAfter api.ReadableDuration `json:",omitempty"` DeregisterCriticalServiceAfter time.Duration `json:",omitempty"`
}
func (d *HealthCheckDefinition) MarshalJSON() ([]byte, error) {
type Alias HealthCheckDefinition
exported := &struct {
Interval string
Timeout string
DeregisterCriticalServiceAfter string
*Alias
}{
Interval: d.Interval.String(),
Timeout: d.Timeout.String(),
DeregisterCriticalServiceAfter: d.DeregisterCriticalServiceAfter.String(),
Alias: (*Alias)(d),
}
if d.Interval == 0 {
exported.Interval = ""
}
if d.Timeout == 0 {
exported.Timeout = ""
}
if d.DeregisterCriticalServiceAfter == 0 {
exported.DeregisterCriticalServiceAfter = ""
}
return json.Marshal(exported)
}
func (d *HealthCheckDefinition) UnmarshalJSON(data []byte) error {
type Alias HealthCheckDefinition
aux := &struct {
Interval string
Timeout string
DeregisterCriticalServiceAfter string
*Alias
}{
Alias: (*Alias)(d),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
var err error
if aux.Interval != "" {
if d.Interval, err = time.ParseDuration(aux.Interval); err != nil {
return err
}
}
if aux.Timeout != "" {
if d.Timeout, err = time.ParseDuration(aux.Timeout); err != nil {
return err
}
}
if aux.DeregisterCriticalServiceAfter != "" {
if d.DeregisterCriticalServiceAfter, err = time.ParseDuration(aux.DeregisterCriticalServiceAfter); err != nil {
return err
}
}
return nil
} }
// IsSame checks if one HealthCheck is the same as another, without looking // IsSame checks if one HealthCheck is the same as another, without looking
@ -929,7 +988,8 @@ func (c *HealthCheck) IsSame(other *HealthCheck) bool {
c.Output != other.Output || c.Output != other.Output ||
c.ServiceID != other.ServiceID || c.ServiceID != other.ServiceID ||
c.ServiceName != other.ServiceName || c.ServiceName != other.ServiceName ||
!reflect.DeepEqual(c.ServiceTags, other.ServiceTags) { !reflect.DeepEqual(c.ServiceTags, other.ServiceTags) ||
!reflect.DeepEqual(c.Definition, other.Definition) {
return false return false
} }

View File

@ -9,7 +9,7 @@ import (
) )
// TxnKVOp is used to define a single operation on the KVS inside a // TxnKVOp is used to define a single operation on the KVS inside a
// transaction // transaction.
type TxnKVOp struct { type TxnKVOp struct {
Verb api.KVOp Verb api.KVOp
DirEnt DirEntry DirEnt DirEntry
@ -19,6 +19,40 @@ type TxnKVOp struct {
// inside a transaction. // inside a transaction.
type TxnKVResult *DirEntry type TxnKVResult *DirEntry
// TxnNodeOp is used to define a single operation on a node in the catalog inside
// a transaction.
type TxnNodeOp struct {
Verb api.NodeOp
Node Node
}
// TxnNodeResult is used to define the result of a single operation on a node
// in the catalog inside a transaction.
type TxnNodeResult *Node
// TxnServiceOp is used to define a single operation on a service in the catalog inside
// a transaction.
type TxnServiceOp struct {
Verb api.ServiceOp
Node string
Service NodeService
}
// TxnServiceResult is used to define the result of a single operation on a service
// in the catalog inside a transaction.
type TxnServiceResult *NodeService
// TxnCheckOp is used to define a single operation on a health check inside a
// transaction.
type TxnCheckOp struct {
Verb api.CheckOp
Check HealthCheck
}
// TxnCheckResult is used to define the result of a single operation on a health
// check inside a transaction.
type TxnCheckResult *HealthCheck
// TxnKVOp is used to define a single operation on an Intention inside a // TxnKVOp is used to define a single operation on an Intention inside a
// transaction. // transaction.
type TxnIntentionOp IntentionRequest type TxnIntentionOp IntentionRequest
@ -28,6 +62,9 @@ type TxnIntentionOp IntentionRequest
type TxnOp struct { type TxnOp struct {
KV *TxnKVOp KV *TxnKVOp
Intention *TxnIntentionOp Intention *TxnIntentionOp
Node *TxnNodeOp
Service *TxnServiceOp
Check *TxnCheckOp
} }
// TxnOps is a list of operations within a transaction. // TxnOps is a list of operations within a transaction.
@ -75,7 +112,10 @@ type TxnErrors []*TxnError
// TxnResult is used to define the result of a given operation inside a // TxnResult is used to define the result of a given operation inside a
// transaction. Only one of the types should be filled out per entry. // transaction. Only one of the types should be filled out per entry.
type TxnResult struct { type TxnResult struct {
KV TxnKVResult KV TxnKVResult `json:",omitempty"`
Node TxnNodeResult `json:",omitempty"`
Service TxnServiceResult `json:",omitempty"`
Check TxnCheckResult `json:",omitempty"`
} }
// TxnResults is a list of TxnResult entries. // TxnResults is a list of TxnResult entries.

View File

@ -8,6 +8,7 @@ import (
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/types"
) )
const ( const (
@ -48,9 +49,9 @@ func decodeValue(rawKV interface{}) error {
return nil return nil
} }
// fixupKVOp looks for non-nil KV operations and passes them on for // fixupTxnOp looks for non-nil Txn operations and passes them on for
// value conversion. // value conversion.
func fixupKVOp(rawOp interface{}) error { func fixupTxnOp(rawOp interface{}) error {
rawMap, ok := rawOp.(map[string]interface{}) rawMap, ok := rawOp.(map[string]interface{})
if !ok { if !ok {
return fmt.Errorf("unexpected raw op type: %T", rawOp) return fmt.Errorf("unexpected raw op type: %T", rawOp)
@ -67,15 +68,15 @@ func fixupKVOp(rawOp interface{}) error {
return nil return nil
} }
// fixupKVOps takes the raw decoded JSON and base64 decodes values in KV ops, // fixupTxnOps takes the raw decoded JSON and base64 decodes values in Txn ops,
// replacing them with byte arrays. // replacing them with byte arrays.
func fixupKVOps(raw interface{}) error { func fixupTxnOps(raw interface{}) error {
rawSlice, ok := raw.([]interface{}) rawSlice, ok := raw.([]interface{})
if !ok { if !ok {
return fmt.Errorf("unexpected raw type: %t", raw) return fmt.Errorf("unexpected raw type: %t", raw)
} }
for _, rawOp := range rawSlice { for _, rawOp := range rawSlice {
if err := fixupKVOp(rawOp); err != nil { if err := fixupTxnOp(rawOp); err != nil {
return err return err
} }
} }
@ -100,7 +101,7 @@ func (s *HTTPServer) convertOps(resp http.ResponseWriter, req *http.Request) (st
// decode it, we will return a 400 since we don't have enough context to // decode it, we will return a 400 since we don't have enough context to
// associate the error with a given operation. // associate the error with a given operation.
var ops api.TxnOps var ops api.TxnOps
if err := decodeBody(req, &ops, fixupKVOps); err != nil { if err := decodeBody(req, &ops, fixupTxnOps); err != nil {
resp.WriteHeader(http.StatusBadRequest) resp.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(resp, "Failed to parse body: %v", err) fmt.Fprintf(resp, "Failed to parse body: %v", err)
return nil, 0, false return nil, 0, false
@ -123,7 +124,8 @@ func (s *HTTPServer) convertOps(resp http.ResponseWriter, req *http.Request) (st
var writes int var writes int
var netKVSize int var netKVSize int
for _, in := range ops { for _, in := range ops {
if in.KV != nil { switch {
case in.KV != nil:
size := len(in.KV.Value) size := len(in.KV.Value)
if size > maxKVSize { if size > maxKVSize {
resp.WriteHeader(http.StatusRequestEntityTooLarge) resp.WriteHeader(http.StatusRequestEntityTooLarge)
@ -152,6 +154,102 @@ func (s *HTTPServer) convertOps(resp http.ResponseWriter, req *http.Request) (st
}, },
} }
opsRPC = append(opsRPC, out) opsRPC = append(opsRPC, out)
case in.Node != nil:
if in.Node.Verb != api.NodeGet {
writes++
}
// Setup the default DC if not provided
if in.Node.Node.Datacenter == "" {
in.Node.Node.Datacenter = s.agent.config.Datacenter
}
node := in.Node.Node
out := &structs.TxnOp{
Node: &structs.TxnNodeOp{
Verb: in.Node.Verb,
Node: structs.Node{
ID: types.NodeID(node.ID),
Node: node.Node,
Address: node.Address,
Datacenter: node.Datacenter,
TaggedAddresses: node.TaggedAddresses,
Meta: node.Meta,
RaftIndex: structs.RaftIndex{
ModifyIndex: node.ModifyIndex,
},
},
},
}
opsRPC = append(opsRPC, out)
case in.Service != nil:
if in.Service.Verb != api.ServiceGet {
writes++
}
svc := in.Service.Service
out := &structs.TxnOp{
Service: &structs.TxnServiceOp{
Verb: in.Service.Verb,
Node: in.Service.Node,
Service: structs.NodeService{
ID: svc.ID,
Service: svc.Service,
Tags: svc.Tags,
Address: svc.Address,
Meta: svc.Meta,
Port: svc.Port,
Weights: &structs.Weights{
Passing: svc.Weights.Passing,
Warning: svc.Weights.Warning,
},
EnableTagOverride: svc.EnableTagOverride,
RaftIndex: structs.RaftIndex{
ModifyIndex: svc.ModifyIndex,
},
},
},
}
opsRPC = append(opsRPC, out)
case in.Check != nil:
if in.Check.Verb != api.CheckGet {
writes++
}
check := in.Check.Check
out := &structs.TxnOp{
Check: &structs.TxnCheckOp{
Verb: in.Check.Verb,
Check: structs.HealthCheck{
Node: check.Node,
CheckID: types.CheckID(check.CheckID),
Name: check.Name,
Status: check.Status,
Notes: check.Notes,
Output: check.Output,
ServiceID: check.ServiceID,
ServiceName: check.ServiceName,
ServiceTags: check.ServiceTags,
Definition: structs.HealthCheckDefinition{
HTTP: check.Definition.HTTP,
TLSSkipVerify: check.Definition.TLSSkipVerify,
Header: check.Definition.Header,
Method: check.Definition.Method,
TCP: check.Definition.TCP,
Interval: check.Definition.Interval,
Timeout: check.Definition.Timeout,
DeregisterCriticalServiceAfter: check.Definition.DeregisterCriticalServiceAfter,
},
RaftIndex: structs.RaftIndex{
ModifyIndex: check.ModifyIndex,
},
},
},
}
opsRPC = append(opsRPC, out)
} }
} }

View File

@ -1,8 +1,10 @@
package api package api
import ( import (
"encoding/json"
"fmt" "fmt"
"strings" "strings"
"time"
) )
const ( const (
@ -36,6 +38,9 @@ type HealthCheck struct {
ServiceTags []string ServiceTags []string
Definition HealthCheckDefinition Definition HealthCheckDefinition
CreateIndex uint64
ModifyIndex uint64
} }
// HealthCheckDefinition is used to store the details about // HealthCheckDefinition is used to store the details about
@ -46,9 +51,56 @@ type HealthCheckDefinition struct {
Method string Method string
TLSSkipVerify bool TLSSkipVerify bool
TCP string TCP string
Interval ReadableDuration Interval time.Duration
Timeout ReadableDuration Timeout time.Duration
DeregisterCriticalServiceAfter ReadableDuration DeregisterCriticalServiceAfter time.Duration
}
func (d *HealthCheckDefinition) MarshalJSON() ([]byte, error) {
type Alias HealthCheckDefinition
return json.Marshal(&struct {
Interval string
Timeout string
DeregisterCriticalServiceAfter string
*Alias
}{
Interval: d.Interval.String(),
Timeout: d.Timeout.String(),
DeregisterCriticalServiceAfter: d.DeregisterCriticalServiceAfter.String(),
Alias: (*Alias)(d),
})
}
func (d *HealthCheckDefinition) UnmarshalJSON(data []byte) error {
type Alias HealthCheckDefinition
aux := &struct {
Interval string
Timeout string
DeregisterCriticalServiceAfter string
*Alias
}{
Alias: (*Alias)(d),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
var err error
if aux.Interval != "" {
if d.Interval, err = time.ParseDuration(aux.Interval); err != nil {
return err
}
}
if aux.Timeout != "" {
if d.Timeout, err = time.ParseDuration(aux.Timeout); err != nil {
return err
}
}
if aux.DeregisterCriticalServiceAfter != "" {
if d.DeregisterCriticalServiceAfter, err = time.ParseDuration(aux.DeregisterCriticalServiceAfter); err != nil {
return err
}
}
return nil
} }
// HealthChecks is a collection of HealthCheck structs. // HealthChecks is a collection of HealthCheck structs.

View File

@ -213,9 +213,9 @@ func TestAPI_HealthChecks(t *testing.T) {
if meta.LastIndex == 0 { if meta.LastIndex == 0 {
r.Fatalf("bad: %v", meta) r.Fatalf("bad: %v", meta)
} }
if got, want := out, checks; !verify.Values(t, "checks", got, want) { checks[0].CreateIndex = out[0].CreateIndex
r.Fatal("health.Checks failed") checks[0].ModifyIndex = out[0].ModifyIndex
} verify.Values(r, "checks", out, checks)
}) })
} }

150
api/kv.go
View File

@ -45,44 +45,6 @@ type KVPair struct {
// KVPairs is a list of KVPair objects // KVPairs is a list of KVPair objects
type KVPairs []*KVPair type KVPairs []*KVPair
// KVOp constants give possible operations available in a KVTxn.
type KVOp string
const (
KVSet KVOp = "set"
KVDelete KVOp = "delete"
KVDeleteCAS KVOp = "delete-cas"
KVDeleteTree KVOp = "delete-tree"
KVCAS KVOp = "cas"
KVLock KVOp = "lock"
KVUnlock KVOp = "unlock"
KVGet KVOp = "get"
KVGetTree KVOp = "get-tree"
KVCheckSession KVOp = "check-session"
KVCheckIndex KVOp = "check-index"
KVCheckNotExists KVOp = "check-not-exists"
)
// KVTxnOp defines a single operation inside a transaction.
type KVTxnOp struct {
Verb KVOp
Key string
Value []byte
Flags uint64
Index uint64
Session string
}
// KVTxnOps defines a set of operations to be performed inside a single
// transaction.
type KVTxnOps []*KVTxnOp
// KVTxnResponse has the outcome of a transaction.
type KVTxnResponse struct {
Results []*KVPair
Errors TxnErrors
}
// KV is used to manipulate the K/V API // KV is used to manipulate the K/V API
type KV struct { type KV struct {
c *Client c *Client
@ -300,107 +262,18 @@ func (k *KV) deleteInternal(key string, params map[string]string, q *WriteOption
return res, qm, nil return res, qm, nil
} }
// TxnOp is the internal format we send to Consul. It's not specific to KV, // The Txn function has been deprecated from the KV object; please see the Txn
// though currently only KV operations are supported. // object for more information about Transactions.
type TxnOp struct {
KV *KVTxnOp
}
// TxnOps is a list of transaction operations.
type TxnOps []*TxnOp
// TxnResult is the internal format we receive from Consul.
type TxnResult struct {
KV *KVPair
}
// TxnResults is a list of TxnResult objects.
type TxnResults []*TxnResult
// TxnError is used to return information about an operation in a transaction.
type TxnError struct {
OpIndex int
What string
}
// TxnErrors is a list of TxnError objects.
type TxnErrors []*TxnError
// TxnResponse is the internal format we receive from Consul.
type TxnResponse struct {
Results TxnResults
Errors TxnErrors
}
// Txn is used to apply multiple KV operations in a single, atomic transaction.
//
// Note that Go will perform the required base64 encoding on the values
// automatically because the type is a byte slice. Transactions are defined as a
// list of operations to perform, using the KVOp constants and KVTxnOp structure
// to define operations. If any operation fails, none of the changes are applied
// to the state store. Note that this hides the internal raw transaction interface
// and munges the input and output types into KV-specific ones for ease of use.
// If there are more non-KV operations in the future we may break out a new
// transaction API client, but it will be easy to keep this KV-specific variant
// supported.
//
// Even though this is generally a write operation, we take a QueryOptions input
// and return a QueryMeta output. If the transaction contains only read ops, then
// Consul will fast-path it to a different endpoint internally which supports
// consistency controls, but not blocking. If there are write operations then
// the request will always be routed through raft and any consistency settings
// will be ignored.
//
// Here's an example:
//
// ops := KVTxnOps{
// &KVTxnOp{
// Verb: KVLock,
// Key: "test/lock",
// Session: "adf4238a-882b-9ddc-4a9d-5b6758e4159e",
// Value: []byte("hello"),
// },
// &KVTxnOp{
// Verb: KVGet,
// Key: "another/key",
// },
// }
// ok, response, _, err := kv.Txn(&ops, nil)
//
// If there is a problem making the transaction request then an error will be
// returned. Otherwise, the ok value will be true if the transaction succeeded
// or false if it was rolled back. The response is a structured return value which
// will have the outcome of the transaction. Its Results member will have entries
// for each operation. Deleted keys will have a nil entry in the, and to save
// space, the Value of each key in the Results will be nil unless the operation
// is a KVGet. If the transaction was rolled back, the Errors member will have
// entries referencing the index of the operation that failed along with an error
// message.
func (k *KV) Txn(txn KVTxnOps, q *QueryOptions) (bool, *KVTxnResponse, *QueryMeta, error) { func (k *KV) Txn(txn KVTxnOps, q *QueryOptions) (bool, *KVTxnResponse, *QueryMeta, error) {
r := k.c.newRequest("PUT", "/v1/txn") var ops TxnOps
r.setQueryOptions(q) for _, op := range txn {
ops = append(ops, &TxnOp{KV: op})
// Convert into the internal format since this is an all-KV txn.
ops := make(TxnOps, 0, len(txn))
for _, kvOp := range txn {
ops = append(ops, &TxnOp{KV: kvOp})
} }
r.obj = ops
rtt, resp, err := k.c.doRequest(r) respOk, txnResp, qm, err := k.c.txn(ops, q)
if err != nil { if err != nil {
return false, nil, nil, err return false, nil, nil, err
} }
defer resp.Body.Close()
qm := &QueryMeta{}
parseQueryMeta(resp, qm)
qm.RequestTime = rtt
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusConflict {
var txnResp TxnResponse
if err := decodeBody(resp, &txnResp); err != nil {
return false, nil, nil, err
}
// Convert from the internal format. // Convert from the internal format.
kvResp := KVTxnResponse{ kvResp := KVTxnResponse{
@ -409,12 +282,5 @@ func (k *KV) Txn(txn KVTxnOps, q *QueryOptions) (bool, *KVTxnResponse, *QueryMet
for _, result := range txnResp.Results { for _, result := range txnResp.Results {
kvResp.Results = append(kvResp.Results, result.KV) kvResp.Results = append(kvResp.Results, result.KV)
} }
return resp.StatusCode == http.StatusOK, &kvResp, qm, nil return respOk, &kvResp, qm, nil
}
var buf bytes.Buffer
if _, err := io.Copy(&buf, resp.Body); err != nil {
return false, nil, nil, fmt.Errorf("Failed to read response: %v", err)
}
return false, nil, nil, fmt.Errorf("Failed request: %s", buf.String())
} }

View File

@ -456,7 +456,7 @@ func TestAPI_ClientAcquireRelease(t *testing.T) {
} }
} }
func TestAPI_ClientTxn(t *testing.T) { func TestAPI_KVClientTxn(t *testing.T) {
t.Parallel() t.Parallel()
c, s := makeClient(t) c, s := makeClient(t)
defer s.Stop() defer s.Stop()

230
api/txn.go Normal file
View File

@ -0,0 +1,230 @@
package api
import (
"bytes"
"fmt"
"io"
"net/http"
)
// Txn is used to manipulate the Txn API
type Txn struct {
c *Client
}
// Txn is used to return a handle to the K/V apis
func (c *Client) Txn() *Txn {
return &Txn{c}
}
// TxnOp is the internal format we send to Consul. Currently only K/V and
// check operations are supported.
type TxnOp struct {
KV *KVTxnOp
Node *NodeTxnOp
Service *ServiceTxnOp
Check *CheckTxnOp
}
// TxnOps is a list of transaction operations.
type TxnOps []*TxnOp
// TxnResult is the internal format we receive from Consul.
type TxnResult struct {
KV *KVPair
Node *Node
Service *CatalogService
Check *HealthCheck
}
// TxnResults is a list of TxnResult objects.
type TxnResults []*TxnResult
// TxnError is used to return information about an operation in a transaction.
type TxnError struct {
OpIndex int
What string
}
// TxnErrors is a list of TxnError objects.
type TxnErrors []*TxnError
// TxnResponse is the internal format we receive from Consul.
type TxnResponse struct {
Results TxnResults
Errors TxnErrors
}
// KVOp constants give possible operations available in a transaction.
type KVOp string
const (
KVSet KVOp = "set"
KVDelete KVOp = "delete"
KVDeleteCAS KVOp = "delete-cas"
KVDeleteTree KVOp = "delete-tree"
KVCAS KVOp = "cas"
KVLock KVOp = "lock"
KVUnlock KVOp = "unlock"
KVGet KVOp = "get"
KVGetTree KVOp = "get-tree"
KVCheckSession KVOp = "check-session"
KVCheckIndex KVOp = "check-index"
KVCheckNotExists KVOp = "check-not-exists"
)
// KVTxnOp defines a single operation inside a transaction.
type KVTxnOp struct {
Verb KVOp
Key string
Value []byte
Flags uint64
Index uint64
Session string
}
// KVTxnOps defines a set of operations to be performed inside a single
// transaction.
type KVTxnOps []*KVTxnOp
// KVTxnResponse has the outcome of a transaction.
type KVTxnResponse struct {
Results []*KVPair
Errors TxnErrors
}
// NodeOp constants give possible operations available in a transaction.
type NodeOp string
const (
NodeGet NodeOp = "get"
NodeSet NodeOp = "set"
NodeCAS NodeOp = "cas"
NodeDelete NodeOp = "delete"
NodeDeleteCAS NodeOp = "delete-cas"
)
// NodeTxnOp defines a single operation inside a transaction.
type NodeTxnOp struct {
Verb NodeOp
Node Node
}
// ServiceOp constants give possible operations available in a transaction.
type ServiceOp string
const (
ServiceGet ServiceOp = "get"
ServiceSet ServiceOp = "set"
ServiceCAS ServiceOp = "cas"
ServiceDelete ServiceOp = "delete"
ServiceDeleteCAS ServiceOp = "delete-cas"
)
// ServiceTxnOp defines a single operation inside a transaction.
type ServiceTxnOp struct {
Verb ServiceOp
Node string
Service AgentService
}
// CheckOp constants give possible operations available in a transaction.
type CheckOp string
const (
CheckGet CheckOp = "get"
CheckSet CheckOp = "set"
CheckCAS CheckOp = "cas"
CheckDelete CheckOp = "delete"
CheckDeleteCAS CheckOp = "delete-cas"
)
// CheckTxnOp defines a single operation inside a transaction.
type CheckTxnOp struct {
Verb CheckOp
Check HealthCheck
}
// Txn is used to apply multiple Consul operations in a single, atomic transaction.
//
// Note that Go will perform the required base64 encoding on the values
// automatically because the type is a byte slice. Transactions are defined as a
// list of operations to perform, using the different fields in the TxnOp structure
// to define operations. If any operation fails, none of the changes are applied
// to the state store.
//
// Even though this is generally a write operation, we take a QueryOptions input
// and return a QueryMeta output. If the transaction contains only read ops, then
// Consul will fast-path it to a different endpoint internally which supports
// consistency controls, but not blocking. If there are write operations then
// the request will always be routed through raft and any consistency settings
// will be ignored.
//
// Here's an example:
//
// ops := KVTxnOps{
// &KVTxnOp{
// Verb: KVLock,
// Key: "test/lock",
// Session: "adf4238a-882b-9ddc-4a9d-5b6758e4159e",
// Value: []byte("hello"),
// },
// &KVTxnOp{
// Verb: KVGet,
// Key: "another/key",
// },
// &CheckTxnOp{
// Verb: CheckSet,
// HealthCheck: HealthCheck{
// Node: "foo",
// CheckID: "redis:a",
// Name: "Redis Health Check",
// Status: "passing",
// },
// }
// }
// ok, response, _, err := kv.Txn(&ops, nil)
//
// If there is a problem making the transaction request then an error will be
// returned. Otherwise, the ok value will be true if the transaction succeeded
// or false if it was rolled back. The response is a structured return value which
// will have the outcome of the transaction. Its Results member will have entries
// for each operation. For KV operations, Deleted keys will have a nil entry in the
// results, and to save space, the Value of each key in the Results will be nil
// unless the operation is a KVGet. If the transaction was rolled back, the Errors
// member will have entries referencing the index of the operation that failed
// along with an error message.
func (t *Txn) Txn(txn TxnOps, q *QueryOptions) (bool, *TxnResponse, *QueryMeta, error) {
return t.c.txn(txn, q)
}
func (c *Client) txn(txn TxnOps, q *QueryOptions) (bool, *TxnResponse, *QueryMeta, error) {
r := c.newRequest("PUT", "/v1/txn")
r.setQueryOptions(q)
r.obj = txn
rtt, resp, err := c.doRequest(r)
if err != nil {
return false, nil, nil, err
}
defer resp.Body.Close()
qm := &QueryMeta{}
parseQueryMeta(resp, qm)
qm.RequestTime = rtt
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusConflict {
var txnResp TxnResponse
if err := decodeBody(resp, &txnResp); err != nil {
return false, nil, nil, err
}
return resp.StatusCode == http.StatusOK, &txnResp, qm, nil
}
var buf bytes.Buffer
if _, err := io.Copy(&buf, resp.Body); err != nil {
return false, nil, nil, fmt.Errorf("Failed to read response: %v", err)
}
return false, nil, nil, fmt.Errorf("Failed request: %s", buf.String())
}

247
api/txn_test.go Normal file
View File

@ -0,0 +1,247 @@
package api
import (
"strings"
"testing"
"time"
"github.com/hashicorp/go-uuid"
"github.com/pascaldekloe/goe/verify"
"github.com/stretchr/testify/require"
)
func TestAPI_ClientTxn(t *testing.T) {
t.Parallel()
require := require.New(t)
c, s := makeClient(t)
defer s.Stop()
session := c.Session()
txn := c.Txn()
// Set up a test service and health check.
nodeID, err := uuid.GenerateUUID()
require.NoError(err)
catalog := c.Catalog()
reg := &CatalogRegistration{
ID: nodeID,
Node: "foo",
Address: "2.2.2.2",
Service: &AgentService{
ID: "foo1",
Service: "foo",
},
Check: &AgentCheck{
CheckID: "bar",
Status: "critical",
Definition: HealthCheckDefinition{
TCP: "1.1.1.1",
Interval: 5 * time.Second,
},
},
}
_, err = catalog.Register(reg, nil)
require.NoError(err)
node, _, err := catalog.Node("foo", nil)
require.NoError(err)
require.Equal(nodeID, node.Node.ID)
// Make a session.
id, _, err := session.CreateNoChecks(nil, nil)
if err != nil {
t.Fatalf("err: %v", err)
}
defer session.Destroy(id, nil)
// Acquire and get the key via a transaction, but don't supply a valid
// session.
key := testKey()
value := []byte("test")
ops := TxnOps{
&TxnOp{
KV: &KVTxnOp{
Verb: KVLock,
Key: key,
Value: value,
},
},
&TxnOp{
KV: &KVTxnOp{
Verb: KVGet,
Key: key,
},
},
&TxnOp{
Node: &NodeTxnOp{
Verb: NodeGet,
Node: Node{Node: "foo"},
},
},
&TxnOp{
Service: &ServiceTxnOp{
Verb: ServiceGet,
Node: "foo",
Service: AgentService{ID: "foo1"},
},
},
&TxnOp{
Check: &CheckTxnOp{
Verb: CheckGet,
Check: HealthCheck{Node: "foo", CheckID: "bar"},
},
},
}
ok, ret, _, err := txn.Txn(ops, nil)
if err != nil {
t.Fatalf("err: %v", err)
} else if ok {
t.Fatalf("transaction should have failed")
}
if ret == nil || len(ret.Errors) != 2 || len(ret.Results) != 0 {
t.Fatalf("bad: %v", ret.Errors[2])
}
if ret.Errors[0].OpIndex != 0 ||
!strings.Contains(ret.Errors[0].What, "missing session") ||
!strings.Contains(ret.Errors[1].What, "doesn't exist") {
t.Fatalf("bad: %v", ret.Errors[0])
}
// Now poke in a real session and try again.
ops[0].KV.Session = id
ok, ret, _, err = txn.Txn(ops, nil)
if err != nil {
t.Fatalf("err: %v", err)
} else if !ok {
t.Fatalf("transaction failure")
}
if ret == nil || len(ret.Errors) != 0 || len(ret.Results) != 5 {
t.Fatalf("bad: %v", ret)
}
expected := TxnResults{
&TxnResult{
KV: &KVPair{
Key: key,
Session: id,
LockIndex: 1,
CreateIndex: ret.Results[0].KV.CreateIndex,
ModifyIndex: ret.Results[0].KV.ModifyIndex,
},
},
&TxnResult{
KV: &KVPair{
Key: key,
Session: id,
Value: []byte("test"),
LockIndex: 1,
CreateIndex: ret.Results[1].KV.CreateIndex,
ModifyIndex: ret.Results[1].KV.ModifyIndex,
},
},
&TxnResult{
Node: &Node{
ID: nodeID,
Node: "foo",
Address: "2.2.2.2",
Datacenter: "dc1",
CreateIndex: ret.Results[2].Node.CreateIndex,
ModifyIndex: ret.Results[2].Node.CreateIndex,
},
},
&TxnResult{
Service: &CatalogService{
ID: "foo1",
CreateIndex: ret.Results[3].Service.CreateIndex,
ModifyIndex: ret.Results[3].Service.CreateIndex,
},
},
&TxnResult{
Check: &HealthCheck{
Node: "foo",
CheckID: "bar",
Status: "critical",
Definition: HealthCheckDefinition{
TCP: "1.1.1.1",
Interval: 5 * time.Second,
},
CreateIndex: ret.Results[4].Check.CreateIndex,
ModifyIndex: ret.Results[4].Check.CreateIndex,
},
},
}
verify.Values(t, "", ret.Results, expected)
// Run a read-only transaction.
ops = TxnOps{
&TxnOp{
KV: &KVTxnOp{
Verb: KVGet,
Key: key,
},
},
&TxnOp{
Node: &NodeTxnOp{
Verb: NodeGet,
Node: Node{ID: s.Config.NodeID, Node: s.Config.NodeName},
},
},
}
ok, ret, _, err = txn.Txn(ops, nil)
if err != nil {
t.Fatalf("err: %v", err)
} else if !ok {
t.Fatalf("transaction failure")
}
expected = TxnResults{
&TxnResult{
KV: &KVPair{
Key: key,
Session: id,
Value: []byte("test"),
LockIndex: 1,
CreateIndex: ret.Results[0].KV.CreateIndex,
ModifyIndex: ret.Results[0].KV.ModifyIndex,
},
},
&TxnResult{
Node: &Node{
ID: s.Config.NodeID,
Node: s.Config.NodeName,
Address: "127.0.0.1",
Datacenter: "dc1",
TaggedAddresses: map[string]string{
"lan": s.Config.Bind,
"wan": s.Config.Bind,
},
Meta: map[string]string{"consul-network-segment": ""},
CreateIndex: ret.Results[1].Node.CreateIndex,
ModifyIndex: ret.Results[1].Node.ModifyIndex,
},
},
}
verify.Values(t, "", ret.Results, expected)
// Sanity check using the regular GET API.
kv := c.KV()
pair, meta, err := kv.Get(key, nil)
if err != nil {
t.Fatalf("err: %v", err)
}
if pair == nil {
t.Fatalf("expected value: %#v", pair)
}
if pair.LockIndex != 1 {
t.Fatalf("Expected lock: %v", pair)
}
if pair.Session != id {
t.Fatalf("Expected lock: %v", pair)
}
if meta.LastIndex == 0 {
t.Fatalf("unexpected value: %#v", meta)
}
}

View File

@ -3,19 +3,18 @@ layout: api
page_title: Transaction - HTTP API page_title: Transaction - HTTP API
sidebar_current: api-txn sidebar_current: api-txn
description: |- description: |-
The /txn endpoints manage updates or fetches of multiple keys inside a single, The /txn endpoint manages multiple operations in Consul, including catalog updates and fetches of multiple KV entries inside a single, atomic transaction.
atomic transaction.
--- ---
# Transactions HTTP API # Transactions HTTP API
The `/txn` endpoints manage updates or fetches of multiple keys inside a single, The `/txn` endpoint manages multiple operations in Consul, including catalog
atomic transaction. It is important to note that each datacenter has its own KV updates and fetches of multiple KV entries inside a single, atomic
store, and there is no built-in replication between datacenters. transaction.
## Create Transaction ## Create Transaction
This endpoint permits submitting a list of operations to apply to the KV store This endpoint permits submitting a list of operations to apply to Consul
inside of a transaction. If any operation fails, the transaction is rolled back inside of a transaction. If any operation fails, the transaction is rolled back
and none of the changes are applied. and none of the changes are applied.
@ -43,7 +42,7 @@ The table below shows this endpoint's support for
| Blocking Queries | Consistency Modes | Agent Caching | ACL Required | | Blocking Queries | Consistency Modes | Agent Caching | ACL Required |
| ---------------- | ----------------- | ------------- | ------------ | | ---------------- | ----------------- | ------------- | ------------ |
| `NO` | `all`<sup>1</sup> | `none` | `key:read,key:write`<sup>2</sup> | | `NO` | `all`<sup>1</sup> | `none` | `key:read,key:write`<br>`node:read,node:write`<br>`service:read,service:write`<sup>2</sup>
<sup>1</sup> For read-only transactions <sup>1</sup> For read-only transactions
<br> <br>
@ -55,7 +54,7 @@ The table below shows this endpoint's support for
to the datacenter of the agent being queried. This is specified as part of the to the datacenter of the agent being queried. This is specified as part of the
URL as a query parameter. URL as a query parameter.
- `KV` is the only available operation type, though other types may be added in the future. - `KV` operations have the following fields:
- `Verb` `(string: <required>)` - Specifies the type of operation to perform. - `Verb` `(string: <required>)` - Specifies the type of operation to perform.
Please see the table below for available verbs. Please see the table below for available verbs.
@ -75,6 +74,31 @@ The table below shows this endpoint's support for
- `Session` `(string: "")` - Specifies a session. See the table below for more - `Session` `(string: "")` - Specifies a session. See the table below for more
information. information.
- `Node` operations have the following fields:
- `Verb` `(string: <required>)` - Specifies the type of operation to perform.
- `Node` `(Node: <required>)` - Specifies the node information to use
for the operation. See the [catalog endpoint](/api/catalog.html#parameters) for the fields in this object. Note the only the node can be specified here, not any services or checks - separate service or check operations must be used for those.
- `Service` operations have the following fields:
- `Verb` `(string: <required>)` - Specifies the type of operation to perform.
- `Node` `(string: <required>)` = Specifies the name of the node to use for
this service operation.
- `Service` `(Service: <required>)` - Specifies the service instance information to use
for the operation. See the [catalog endpoint](/api/catalog.html#parameters) for the fields in this object.
- `Check` operations have the following fields:
- `Verb` `(string: <required>)` - Specifies the type of operation to perform.
- `Service` `(Service: <required>)` - Specifies the check to use
for the operation. See the [catalog endpoint](/api/catalog.html#parameters) for the fields in this object.
Please see the table below for available verbs.
### Sample Payload ### Sample Payload
The body of the request should be a list of operations to perform inside the The body of the request should be a list of operations to perform inside the
@ -91,6 +115,48 @@ atomic transaction. Up to 64 operations may be present in a single transaction.
"Index": <index>, "Index": <index>,
"Session": "<session id>" "Session": "<session id>"
} }
},
{
"Node": {
"Verb": "set",
"Node": {
"ID": "67539c9d-b948-ba67-edd4-d07a676d6673",
"Node": "bar",
"Address": "192.168.0.1",
"Datacenter": "dc1",
"Meta": {
"instance_type": "m2.large"
}
}
}
},
{
"Service": {
"Verb": "delete",
"Node": "foo",
"Service": {
"ID": "db1"
}
}
},
{
"Check": {
"Verb": "cas",
"Check": {
"Node": "bar",
"CheckID": "service:web1",
"Name": "Web HTTP Check",
"Status": "critical",
"ServiceID": "web1",
"ServiceName": "web",
"ServiceTags": null,
"Definition": {
"HTTP": "http://localhost:8080",
"Interval": "10s"
},
"ModifyIndex": 22
}
}
} }
] ]
``` ```
@ -123,6 +189,39 @@ look like this:
"CreateIndex": <index>, "CreateIndex": <index>,
"ModifyIndex": <index> "ModifyIndex": <index>
} }
},
{
"Node": {
"ID": "67539c9d-b948-ba67-edd4-d07a676d6673",
"Node": "bar",
"Address": "192.168.0.1",
"Datacenter": "dc1",
"TaggedAddresses": null,
"Meta": {
"instance_type": "m2.large"
},
"CreateIndex": 32,
"ModifyIndex": 32
}
},
{
"Check": {
"Node": "bar",
"CheckID": "service:web1",
"Name": "Web HTTP Check",
"Status": "critical",
"Notes": "",
"Output": "",
"ServiceID": "web1",
"ServiceName": "web",
"ServiceTags": null,
"Definition": {
"HTTP": "http://localhost:8080",
"Interval": "10s"
},
"CreateIndex": 22,
"ModifyIndex": 35
}
} }
], ],
"Errors": [ "Errors": [
@ -130,12 +229,13 @@ look like this:
"OpIndex": <index of failed operation>, "OpIndex": <index of failed operation>,
"What": "<error message for failed operation>" "What": "<error message for failed operation>"
}, },
...
] ]
} }
``` ```
- `Results` has entries for some operations if the transaction was successful. - `Results` has entries for some operations if the transaction was successful.
To save space, the `Value` will be `null` for any `Verb` other than "get" or To save space, the `Value` for KV results will be `null` for any `Verb` other than "get" or
"get-tree". Like the `/v1/kv/<key>` endpoint, `Value` will be Base64-encoded "get-tree". Like the `/v1/kv/<key>` endpoint, `Value` will be Base64-encoded
if it is present. Also, no result entries will be added for verbs that delete if it is present. Also, no result entries will be added for verbs that delete
keys. keys.
@ -145,10 +245,12 @@ look like this:
transaction, and `What` is a string with an error message about why that transaction, and `What` is a string with an error message about why that
operation failed. operation failed.
### Table of Operations ### Tables of Operations
The following table summarizes the available verbs and the fields that apply to #### KV Operations
that operation ("X" means a field is required and "O" means it is optional):
The following tables summarize the available verbs and the fields that apply to
those operations ("X" means a field is required and "O" means it is optional):
| Verb | Operation | Key | Value | Flags | Index | Session | | Verb | Operation | Key | Value | Flags | Index | Session |
| ------------------ | -------------------------------------------- | :--: | :---: | :---: | :---: | :-----: | | ------------------ | -------------------------------------------- | :--: | :---: | :---: | :---: | :-----: |
@ -164,3 +266,42 @@ that operation ("X" means a field is required and "O" means it is optional):
| `delete` | Delete the key | `x` | | | | | | `delete` | Delete the key | `x` | | | | |
| `delete-tree` | Delete all keys with a prefix | `x` | | | | | | `delete-tree` | Delete all keys with a prefix | `x` | | | | |
| `delete-cas` | Delete, but with CAS semantics | `x` | | | `x` | | | `delete-cas` | Delete, but with CAS semantics | `x` | | | `x` | |
#### Node Operations
Node operations act on an individual node and require either a Node ID or name, giving precedence
to the ID if both are set. Delete operations will not return a result on success.
| Verb | Operation |
| ------------------ | -------------------------------------------- |
| `set` | Sets the node to the given state |
| `cas` | Sets, but with CAS semantics using the given ModifyIndex |
| `get` | Get the node, fails if it does not exist |
| `delete` | Delete the node |
| `delete-cas` | Delete, but with CAS semantics |
#### Service Operations
Service operations act on an individual service instance on the given node name. Both a node name
and valid service name are required. Delete operations will not return a result on success.
| Verb | Operation |
| ------------------ | -------------------------------------------- |
| `set` | Sets the service to the given state |
| `cas` | Sets, but with CAS semantics using the given ModifyIndex |
| `get` | Get the service, fails if it does not exist |
| `delete` | Delete the service |
| `delete-cas` | Delete, but with CAS semantics |
#### Check Operations
Check operations act on an individual health check instance on the given node name. Both a node name
and valid check ID are required. Delete operations will not return a result on success.
| Verb | Operation |
| ------------------ | -------------------------------------------- |
| `set` | Sets the health check to the given state |
| `cas` | Sets, but with CAS semantics using the given ModifyIndex |
| `get` | Get the check, fails if it does not exist |
| `delete` | Delete the check |
| `delete-cas` | Delete, but with CAS semantics |