From 987b7ce0a291a810b3934cb1bc1e9474cc5b8f2a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 2 Mar 2018 12:56:39 -0800 Subject: [PATCH] agent/consul/state: IntentionMatch for performing match resolution --- agent/consul/state/intention.go | 78 +++++++++++++++ agent/consul/state/intention_test.go | 136 +++++++++++++++++++++++++++ 2 files changed, 214 insertions(+) diff --git a/agent/consul/state/intention.go b/agent/consul/state/intention.go index ea2ee3fd5..51f4e1e3b 100644 --- a/agent/consul/state/intention.go +++ b/agent/consul/state/intention.go @@ -2,6 +2,7 @@ package state import ( "fmt" + "sort" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/go-memdb" @@ -192,3 +193,80 @@ func (s *Store) intentionDeleteTxn(tx *memdb.Txn, idx uint64, queryID string) er return nil } + +// IntentionMatch returns the list of intentions that match the namespace and +// name for either a source or destination. This applies the resolution rules +// so wildcards will match any value. +// +// The returned value is the list of intentions in the same order as the +// entries in args. The intentions themselves are sorted based on the +// intention precedence rules. i.e. result[0][0] is the highest precedent +// rule to match for the first entry. +func (s *Store) IntentionMatch(ws memdb.WatchSet, args *structs.IntentionQueryMatch) (uint64, []structs.Intentions, error) { + tx := s.db.Txn(false) + defer tx.Abort() + + // Get the table index. + idx := maxIndexTxn(tx, intentionsTableName) + + // Make all the calls and accumulate the results + results := make([]structs.Intentions, len(args.Entries)) + for i, entry := range args.Entries { + // Each search entry may require multiple queries to memdb, so this + // returns the arguments for each necessary Get. Note on performance: + // this is not the most optimal set of queries since we repeat some + // many times (such as */*). We can work on improving that in the + // future, the test cases shouldn't have to change for that. + getParams, err := s.intentionMatchGetParams(entry) + if err != nil { + return 0, nil, err + } + + // Perform each call and accumulate the result. + var ixns structs.Intentions + for _, params := range getParams { + iter, err := tx.Get(intentionsTableName, string(args.Type), params...) + if err != nil { + return 0, nil, fmt.Errorf("failed intention lookup: %s", err) + } + + ws.Add(iter.WatchCh()) + + for ixn := iter.Next(); ixn != nil; ixn = iter.Next() { + ixns = append(ixns, ixn.(*structs.Intention)) + } + } + + // TODO: filter for uniques + + // Sort the results by precedence + sort.Sort(structs.IntentionPrecedenceSorter(ixns)) + + // Store the result + results[i] = ixns + } + + return idx, results, nil +} + +// intentionMatchGetParams returns the tx.Get parameters to find all the +// intentions for a certain entry. +func (s *Store) intentionMatchGetParams(entry structs.IntentionMatchEntry) ([][]interface{}, error) { + // We always query for "*/*" so include that. If the namespace is a + // wildcard, then we're actually done. + result := make([][]interface{}, 0, 3) + result = append(result, []interface{}{"*", "*"}) + if entry.Namespace == structs.IntentionWildcard { + return result, nil + } + + // Search for NS/* intentions. If we have a wildcard name, then we're done. + result = append(result, []interface{}{entry.Namespace, "*"}) + if entry.Name == structs.IntentionWildcard { + return result, nil + } + + // Search for the exact NS/N value. + result = append(result, []interface{}{entry.Namespace, entry.Name}) + return result, nil +} diff --git a/agent/consul/state/intention_test.go b/agent/consul/state/intention_test.go index d1494d5e0..2f4fee26b 100644 --- a/agent/consul/state/intention_test.go +++ b/agent/consul/state/intention_test.go @@ -233,3 +233,139 @@ func TestStore_IntentionsList(t *testing.T) { t.Fatalf("bad: %v", actual) } } + +// Test the matrix of match logic. +// +// Note that this doesn't need to test the intention sort logic exhaustively +// since this is tested in their sort implementation in the structs. +func TestStore_IntentionMatch_table(t *testing.T) { + type testCase struct { + Name string + Insert [][]string // List of intentions to insert + Query [][]string // List of intentions to match + Expected [][][]string // List of matches, where each match is a list of intentions + } + + cases := []testCase{ + { + "single exact namespace/name", + [][]string{ + {"foo", "*"}, + {"foo", "bar"}, + {"foo", "baz"}, // shouldn't match + {"bar", "bar"}, // shouldn't match + {"bar", "*"}, // shouldn't match + {"*", "*"}, + }, + [][]string{ + {"foo", "bar"}, + }, + [][][]string{ + { + {"foo", "bar"}, + {"foo", "*"}, + {"*", "*"}, + }, + }, + }, + + { + "multiple exact namespace/name", + [][]string{ + {"foo", "*"}, + {"foo", "bar"}, + {"foo", "baz"}, // shouldn't match + {"bar", "bar"}, + {"bar", "*"}, + }, + [][]string{ + {"foo", "bar"}, + {"bar", "bar"}, + }, + [][][]string{ + { + {"foo", "bar"}, + {"foo", "*"}, + }, + { + {"bar", "bar"}, + {"bar", "*"}, + }, + }, + }, + } + + // testRunner implements the test for a single case, but can be + // parameterized to run for both source and destination so we can + // test both cases. + testRunner := func(t *testing.T, tc testCase, typ structs.IntentionMatchType) { + // Insert the set + s := testStateStore(t) + var idx uint64 = 1 + for _, v := range tc.Insert { + ixn := &structs.Intention{ID: testUUID()} + switch typ { + case structs.IntentionMatchDestination: + ixn.DestinationNS = v[0] + ixn.DestinationName = v[1] + case structs.IntentionMatchSource: + ixn.SourceNS = v[0] + ixn.SourceName = v[1] + } + + err := s.IntentionSet(idx, ixn) + if err != nil { + t.Fatalf("error inserting: %s", err) + } + + idx++ + } + + // Build the arguments + args := &structs.IntentionQueryMatch{Type: typ} + for _, q := range tc.Query { + args.Entries = append(args.Entries, structs.IntentionMatchEntry{ + Namespace: q[0], + Name: q[1], + }) + } + + // Match + _, matches, err := s.IntentionMatch(nil, args) + if err != nil { + t.Fatalf("error matching: %s", err) + } + + // Should have equal lengths + if len(matches) != len(tc.Expected) { + t.Fatalf("bad (got, wanted):\n\n%#v\n\n%#v", tc.Expected, matches) + } + + // Verify matches + for i, expected := range tc.Expected { + var actual [][]string + for _, ixn := range matches[i] { + switch typ { + case structs.IntentionMatchDestination: + actual = append(actual, []string{ixn.DestinationNS, ixn.DestinationName}) + case structs.IntentionMatchSource: + actual = append(actual, []string{ixn.SourceNS, ixn.SourceName}) + } + } + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad (got, wanted):\n\n%#v\n\n%#v", actual, expected) + } + } + } + + for _, tc := range cases { + t.Run(tc.Name+" (destination)", func(t *testing.T) { + testRunner(t, tc, structs.IntentionMatchDestination) + }) + + t.Run(tc.Name+" (source)", func(t *testing.T) { + testRunner(t, tc, structs.IntentionMatchSource) + }) + } +}