diff --git a/agent/consul/state/txn.go b/agent/consul/state/txn.go index 9c8fdd5ed..c90f21634 100644 --- a/agent/consul/state/txn.go +++ b/agent/consul/state/txn.go @@ -110,6 +110,18 @@ func (s *Store) txnKVS(tx *memdb.Txn, idx uint64, op *structs.TxnKVOp) (structs. return nil, nil } +// txnIntention handles all Intention-related operations. +func (s *Store) txnIntention(tx *memdb.Txn, idx uint64, op *structs.TxnIntentionOp) error { + switch op.Op { + case structs.IntentionOpCreate, structs.IntentionOpUpdate: + return s.intentionSetTxn(tx, idx, op.Intention) + case structs.IntentionOpDelete: + return s.intentionDeleteTxn(tx, idx, op.Intention.ID) + default: + return fmt.Errorf("unknown Intention verb %q", op.Op) + } +} + // 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) { results := make(structs.TxnResults, 0, len(ops)) @@ -119,9 +131,12 @@ func (s *Store) txnDispatch(tx *memdb.Txn, idx uint64, ops structs.TxnOps) (stru var err error // Dispatch based on the type of operation. - if op.KV != nil { + switch { + case op.KV != nil: ret, err = s.txnKVS(tx, idx, op.KV) - } else { + case op.Intention != nil: + err = s.txnIntention(tx, idx, op.Intention) + default: err = fmt.Errorf("no operation specified") } diff --git a/agent/consul/state/txn_test.go b/agent/consul/state/txn_test.go index 29d0f31bd..5e89e8bad 100644 --- a/agent/consul/state/txn_test.go +++ b/agent/consul/state/txn_test.go @@ -7,8 +7,115 @@ import ( "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/api" + "github.com/pascaldekloe/goe/verify" + "github.com/stretchr/testify/require" ) +func TestStateStore_Txn_Intention(t *testing.T) { + require := require.New(t) + s := testStateStore(t) + + // Create some intentions. + ixn1 := &structs.Intention{ + ID: testUUID(), + SourceNS: "default", + SourceName: "web", + DestinationNS: "default", + DestinationName: "db", + Meta: map[string]string{}, + } + ixn2 := &structs.Intention{ + ID: testUUID(), + SourceNS: "default", + SourceName: "db", + DestinationNS: "default", + DestinationName: "*", + Action: structs.IntentionActionDeny, + Meta: map[string]string{}, + } + ixn3 := &structs.Intention{ + ID: testUUID(), + SourceNS: "default", + SourceName: "foo", + DestinationNS: "default", + DestinationName: "*", + Meta: map[string]string{}, + } + + // Write the first two to the state store, leave the third + // to be created by the transaction operation. + require.NoError(s.IntentionSet(1, ixn1)) + require.NoError(s.IntentionSet(2, ixn2)) + + // Set up a transaction that hits every operation. + ops := structs.TxnOps{ + &structs.TxnOp{ + Intention: &structs.TxnIntentionOp{ + Op: structs.IntentionOpUpdate, + Intention: ixn1, + }, + }, + &structs.TxnOp{ + Intention: &structs.TxnIntentionOp{ + Op: structs.IntentionOpDelete, + Intention: ixn2, + }, + }, + &structs.TxnOp{ + Intention: &structs.TxnIntentionOp{ + Op: structs.IntentionOpCreate, + Intention: ixn3, + }, + }, + } + results, errors := s.TxnRW(3, ops) + if len(errors) > 0 { + t.Fatalf("err: %v", errors) + } + + // Make sure the response looks as expected. + expected := structs.TxnResults{} + verify.Values(t, "", results, expected) + + // Pull the resulting state store contents. + idx, actual, err := s.Intentions(nil) + require.NoError(err) + if idx != 3 { + t.Fatalf("bad index: %d", idx) + } + + // Make sure it looks as expected. + intentions := structs.Intentions{ + &structs.Intention{ + ID: ixn1.ID, + SourceNS: "default", + SourceName: "web", + DestinationNS: "default", + DestinationName: "db", + Meta: map[string]string{}, + Precedence: 9, + RaftIndex: structs.RaftIndex{ + CreateIndex: 1, + ModifyIndex: 3, + }, + }, + &structs.Intention{ + ID: ixn3.ID, + SourceNS: "default", + SourceName: "foo", + DestinationNS: "default", + DestinationName: "*", + Meta: map[string]string{}, + Precedence: 6, + RaftIndex: structs.RaftIndex{ + CreateIndex: 3, + ModifyIndex: 3, + }, + }, + } + verify.Values(t, "", actual, intentions) +} + func TestStateStore_Txn_KVS(t *testing.T) { s := testStateStore(t) diff --git a/agent/structs/intention.go b/agent/structs/intention.go index 77b5f951e..9fec00974 100644 --- a/agent/structs/intention.go +++ b/agent/structs/intention.go @@ -224,6 +224,19 @@ func (x *Intention) String() string { x.ID, x.Precedence) } +// EstimateSize returns an estimate (in bytes) of the size of this structure when encoded. +func (x *Intention) EstimateSize() int { + // 60 = 36 (uuid) + 16 (RaftIndex) + 4 (Precedence) + 4 (DefaultPort) + size := 60 + len(x.Description) + len(x.SourceNS) + len(x.SourceName) + len(x.DestinationNS) + + len(x.DestinationName) + len(x.SourceType) + len(x.Action) + len(x.DefaultAddr) + + for k, v := range x.Meta { + size += len(k) + len(v) + } + + return size +} + // IntentionAction is the action that the intention represents. This // can be "allow" or "deny" to whitelist or blacklist intentions. type IntentionAction string diff --git a/agent/structs/txn.go b/agent/structs/txn.go index c1c0b4056..c3b814b63 100644 --- a/agent/structs/txn.go +++ b/agent/structs/txn.go @@ -1,9 +1,11 @@ package structs import ( + "errors" "fmt" "github.com/hashicorp/consul/api" + multierror "github.com/hashicorp/go-multierror" ) // TxnKVOp is used to define a single operation on the KVS inside a @@ -17,10 +19,15 @@ type TxnKVOp struct { // inside a transaction. type TxnKVResult *DirEntry +// TxnKVOp is used to define a single operation on an Intention inside a +// transaction. +type TxnIntentionOp IntentionRequest + // TxnOp is used to define a single operation inside a transaction. Only one // of the types should be filled out per entry. type TxnOp struct { - KV *TxnKVOp + KV *TxnKVOp + Intention *TxnIntentionOp } // TxnOps is a list of operations within a transaction. @@ -80,6 +87,15 @@ type TxnResponse struct { Errors TxnErrors } +// Error returns an aggregate of all errors in this TxnResponse. +func (r TxnResponse) Error() error { + var errs error + for _, err := range r.Errors { + errs = multierror.Append(errs, errors.New(err.Error())) + } + return errs +} + // TxnReadResponse is the structure returned by a TxnReadRequest. type TxnReadResponse struct { TxnResponse