agent/consul/state: IntentionMatch for performing match resolution
This commit is contained in:
parent
231f7328bd
commit
987b7ce0a2
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue