agent/consul/state: IntentionMatch for performing match resolution
This commit is contained in:
parent
231f7328bd
commit
987b7ce0a2
|
@ -2,6 +2,7 @@ package state
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
"github.com/hashicorp/go-memdb"
|
"github.com/hashicorp/go-memdb"
|
||||||
|
@ -192,3 +193,80 @@ func (s *Store) intentionDeleteTxn(tx *memdb.Txn, idx uint64, queryID string) er
|
||||||
|
|
||||||
return nil
|
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)
|
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 New Issue