562 lines
14 KiB
Go
562 lines
14 KiB
Go
package state
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
"github.com/hashicorp/go-memdb"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestStore_IntentionGet_none(t *testing.T) {
|
|
assert := assert.New(t)
|
|
s := testStateStore(t)
|
|
|
|
// Querying with no results returns nil.
|
|
ws := memdb.NewWatchSet()
|
|
idx, res, err := s.IntentionGet(ws, testUUID())
|
|
assert.Equal(uint64(1), idx)
|
|
assert.Nil(res)
|
|
assert.Nil(err)
|
|
}
|
|
|
|
func TestStore_IntentionSetGet_basic(t *testing.T) {
|
|
assert := assert.New(t)
|
|
s := testStateStore(t)
|
|
|
|
// Call Get to populate the watch set
|
|
ws := memdb.NewWatchSet()
|
|
_, _, err := s.IntentionGet(ws, testUUID())
|
|
assert.Nil(err)
|
|
|
|
// Build a valid intention
|
|
ixn := &structs.Intention{
|
|
ID: testUUID(),
|
|
SourceNS: "default",
|
|
SourceName: "*",
|
|
DestinationNS: "default",
|
|
DestinationName: "web",
|
|
Meta: map[string]string{},
|
|
}
|
|
|
|
// Inserting a with empty ID is disallowed.
|
|
assert.NoError(s.IntentionSet(1, ixn))
|
|
|
|
// Make sure the index got updated.
|
|
assert.Equal(uint64(1), s.maxIndex(intentionsTableName))
|
|
assert.True(watchFired(ws), "watch fired")
|
|
|
|
// Read it back out and verify it.
|
|
expected := &structs.Intention{
|
|
ID: ixn.ID,
|
|
SourceNS: "default",
|
|
SourceName: "*",
|
|
DestinationNS: "default",
|
|
DestinationName: "web",
|
|
Meta: map[string]string{},
|
|
RaftIndex: structs.RaftIndex{
|
|
CreateIndex: 1,
|
|
ModifyIndex: 1,
|
|
},
|
|
}
|
|
expected.UpdatePrecedence()
|
|
|
|
ws = memdb.NewWatchSet()
|
|
idx, actual, err := s.IntentionGet(ws, ixn.ID)
|
|
assert.NoError(err)
|
|
assert.Equal(expected.CreateIndex, idx)
|
|
assert.Equal(expected, actual)
|
|
|
|
// Change a value and test updating
|
|
ixn.SourceNS = "foo"
|
|
assert.NoError(s.IntentionSet(2, ixn))
|
|
|
|
// Change a value that isn't in the unique 4 tuple and check we don't
|
|
// incorrectly consider this a duplicate when updating.
|
|
ixn.Action = structs.IntentionActionDeny
|
|
assert.NoError(s.IntentionSet(2, ixn))
|
|
|
|
// Make sure the index got updated.
|
|
assert.Equal(uint64(2), s.maxIndex(intentionsTableName))
|
|
assert.True(watchFired(ws), "watch fired")
|
|
|
|
// Read it back and verify the data was updated
|
|
expected.SourceNS = ixn.SourceNS
|
|
expected.Action = structs.IntentionActionDeny
|
|
expected.ModifyIndex = 2
|
|
ws = memdb.NewWatchSet()
|
|
idx, actual, err = s.IntentionGet(ws, ixn.ID)
|
|
assert.NoError(err)
|
|
assert.Equal(expected.ModifyIndex, idx)
|
|
assert.Equal(expected, actual)
|
|
|
|
// Attempt to insert another intention with duplicate 4-tuple
|
|
ixn = &structs.Intention{
|
|
ID: testUUID(),
|
|
SourceNS: "default",
|
|
SourceName: "*",
|
|
DestinationNS: "default",
|
|
DestinationName: "web",
|
|
Meta: map[string]string{},
|
|
}
|
|
|
|
// Duplicate 4-tuple should cause an error
|
|
ws = memdb.NewWatchSet()
|
|
assert.Error(s.IntentionSet(3, ixn))
|
|
|
|
// Make sure the index did NOT get updated.
|
|
assert.Equal(uint64(2), s.maxIndex(intentionsTableName))
|
|
assert.False(watchFired(ws), "watch not fired")
|
|
}
|
|
|
|
func TestStore_IntentionSet_emptyId(t *testing.T) {
|
|
assert := assert.New(t)
|
|
s := testStateStore(t)
|
|
|
|
ws := memdb.NewWatchSet()
|
|
_, _, err := s.IntentionGet(ws, testUUID())
|
|
assert.NoError(err)
|
|
|
|
// Inserting a with empty ID is disallowed.
|
|
err = s.IntentionSet(1, &structs.Intention{})
|
|
assert.Error(err)
|
|
assert.Contains(err.Error(), ErrMissingIntentionID.Error())
|
|
|
|
// Index is not updated if nothing is saved.
|
|
assert.Equal(s.maxIndex(intentionsTableName), uint64(0))
|
|
assert.False(watchFired(ws), "watch fired")
|
|
}
|
|
|
|
func TestStore_IntentionSet_updateCreatedAt(t *testing.T) {
|
|
assert := assert.New(t)
|
|
s := testStateStore(t)
|
|
|
|
// Build a valid intention
|
|
now := time.Now()
|
|
ixn := structs.Intention{
|
|
ID: testUUID(),
|
|
CreatedAt: now,
|
|
}
|
|
|
|
// Insert
|
|
assert.NoError(s.IntentionSet(1, &ixn))
|
|
|
|
// Change a value and test updating
|
|
ixnUpdate := ixn
|
|
ixnUpdate.CreatedAt = now.Add(10 * time.Second)
|
|
assert.NoError(s.IntentionSet(2, &ixnUpdate))
|
|
|
|
// Read it back and verify
|
|
_, actual, err := s.IntentionGet(nil, ixn.ID)
|
|
assert.NoError(err)
|
|
assert.Equal(now, actual.CreatedAt)
|
|
}
|
|
|
|
func TestStore_IntentionSet_metaNil(t *testing.T) {
|
|
assert := assert.New(t)
|
|
s := testStateStore(t)
|
|
|
|
// Build a valid intention
|
|
ixn := structs.Intention{
|
|
ID: testUUID(),
|
|
}
|
|
|
|
// Insert
|
|
assert.NoError(s.IntentionSet(1, &ixn))
|
|
|
|
// Read it back and verify
|
|
_, actual, err := s.IntentionGet(nil, ixn.ID)
|
|
assert.NoError(err)
|
|
assert.NotNil(actual.Meta)
|
|
}
|
|
|
|
func TestStore_IntentionSet_metaSet(t *testing.T) {
|
|
assert := assert.New(t)
|
|
s := testStateStore(t)
|
|
|
|
// Build a valid intention
|
|
ixn := structs.Intention{
|
|
ID: testUUID(),
|
|
Meta: map[string]string{"foo": "bar"},
|
|
}
|
|
|
|
// Insert
|
|
assert.NoError(s.IntentionSet(1, &ixn))
|
|
|
|
// Read it back and verify
|
|
_, actual, err := s.IntentionGet(nil, ixn.ID)
|
|
assert.NoError(err)
|
|
assert.Equal(ixn.Meta, actual.Meta)
|
|
}
|
|
|
|
func TestStore_IntentionDelete(t *testing.T) {
|
|
assert := assert.New(t)
|
|
s := testStateStore(t)
|
|
|
|
// Call Get to populate the watch set
|
|
ws := memdb.NewWatchSet()
|
|
_, _, err := s.IntentionGet(ws, testUUID())
|
|
assert.NoError(err)
|
|
|
|
// Create
|
|
ixn := &structs.Intention{ID: testUUID()}
|
|
assert.NoError(s.IntentionSet(1, ixn))
|
|
|
|
// Make sure the index got updated.
|
|
assert.Equal(s.maxIndex(intentionsTableName), uint64(1))
|
|
assert.True(watchFired(ws), "watch fired")
|
|
|
|
// Delete
|
|
assert.NoError(s.IntentionDelete(2, ixn.ID))
|
|
|
|
// Make sure the index got updated.
|
|
assert.Equal(s.maxIndex(intentionsTableName), uint64(2))
|
|
assert.True(watchFired(ws), "watch fired")
|
|
|
|
// Sanity check to make sure it's not there.
|
|
idx, actual, err := s.IntentionGet(nil, ixn.ID)
|
|
assert.NoError(err)
|
|
assert.Equal(idx, uint64(2))
|
|
assert.Nil(actual)
|
|
}
|
|
|
|
func TestStore_IntentionsList(t *testing.T) {
|
|
s := testStateStore(t)
|
|
|
|
entMeta := structs.WildcardEnterpriseMeta()
|
|
|
|
// Querying with no results returns nil.
|
|
ws := memdb.NewWatchSet()
|
|
idx, res, err := s.Intentions(ws, entMeta)
|
|
require.NoError(t, err)
|
|
require.Nil(t, res)
|
|
require.Equal(t, uint64(1), idx)
|
|
|
|
testIntention := func(srcNS, src, dstNS, dst string) *structs.Intention {
|
|
id := testUUID()
|
|
return &structs.Intention{
|
|
ID: id,
|
|
SourceNS: srcNS,
|
|
SourceName: src,
|
|
DestinationNS: dstNS,
|
|
DestinationName: dst,
|
|
Meta: map[string]string{},
|
|
}
|
|
}
|
|
|
|
cmpIntention := func(ixn *structs.Intention, id string, index uint64) *structs.Intention {
|
|
ixn.ID = id
|
|
ixn.CreateIndex = index
|
|
ixn.ModifyIndex = index
|
|
ixn.UpdatePrecedence() // to match what is returned...
|
|
return ixn
|
|
}
|
|
|
|
// Create some intentions
|
|
ixns := structs.Intentions{
|
|
testIntention("default", "foo", "default", "bar"),
|
|
testIntention("default", "foo", "default", "*"),
|
|
testIntention("*", "*", "default", "*"),
|
|
testIntention("default", "*", "*", "*"),
|
|
testIntention("*", "*", "*", "*"),
|
|
}
|
|
|
|
// Create
|
|
for i, ixn := range ixns {
|
|
require.NoError(t, s.IntentionSet(uint64(1+i), ixn))
|
|
}
|
|
require.True(t, watchFired(ws), "watch fired")
|
|
|
|
// Read it back and verify.
|
|
expected := structs.Intentions{
|
|
cmpIntention(testIntention("default", "foo", "default", "bar"), ixns[0].ID, 1),
|
|
cmpIntention(testIntention("default", "foo", "default", "*"), ixns[1].ID, 2),
|
|
cmpIntention(testIntention("*", "*", "default", "*"), ixns[2].ID, 3),
|
|
cmpIntention(testIntention("default", "*", "*", "*"), ixns[3].ID, 4),
|
|
cmpIntention(testIntention("*", "*", "*", "*"), ixns[4].ID, 5),
|
|
}
|
|
|
|
idx, actual, err := s.Intentions(nil, entMeta)
|
|
require.NoError(t, err)
|
|
require.Equal(t, idx, uint64(5))
|
|
require.ElementsMatch(t, expected, 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", "*"},
|
|
},
|
|
},
|
|
},
|
|
|
|
{
|
|
"single exact namespace/name with duplicate destinations",
|
|
[][]string{
|
|
// 4-tuple specifies src and destination to test duplicate destinations
|
|
// with different sources. We flip them around to test in both
|
|
// directions. The first pair are the ones searched on in both cases so
|
|
// the duplicates need to be there.
|
|
{"foo", "bar", "foo", "*"},
|
|
{"foo", "bar", "bar", "*"},
|
|
{"*", "*", "*", "*"},
|
|
},
|
|
[][]string{
|
|
{"foo", "bar"},
|
|
},
|
|
[][][]string{
|
|
{
|
|
// Note the first two have the same precedence so we rely on arbitrary
|
|
// lexicographical tie-break behavior.
|
|
{"foo", "bar", "bar", "*"},
|
|
{"foo", "bar", "foo", "*"},
|
|
{"*", "*", "*", "*"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// 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
|
|
assert := assert.New(t)
|
|
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]
|
|
if len(v) == 4 {
|
|
ixn.SourceNS = v[2]
|
|
ixn.SourceName = v[3]
|
|
}
|
|
case structs.IntentionMatchSource:
|
|
ixn.SourceNS = v[0]
|
|
ixn.SourceName = v[1]
|
|
if len(v) == 4 {
|
|
ixn.DestinationNS = v[2]
|
|
ixn.DestinationName = v[3]
|
|
}
|
|
}
|
|
|
|
assert.NoError(s.IntentionSet(idx, ixn))
|
|
|
|
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)
|
|
assert.NoError(err)
|
|
|
|
// Should have equal lengths
|
|
require.Len(t, matches, len(tc.Expected))
|
|
|
|
// Verify matches
|
|
for i, expected := range tc.Expected {
|
|
var actual [][]string
|
|
for _, ixn := range matches[i] {
|
|
switch typ {
|
|
case structs.IntentionMatchDestination:
|
|
if len(expected) > 1 && len(expected[0]) == 4 {
|
|
actual = append(actual, []string{
|
|
ixn.DestinationNS,
|
|
ixn.DestinationName,
|
|
ixn.SourceNS,
|
|
ixn.SourceName,
|
|
})
|
|
} else {
|
|
actual = append(actual, []string{ixn.DestinationNS, ixn.DestinationName})
|
|
}
|
|
case structs.IntentionMatchSource:
|
|
if len(expected) > 1 && len(expected[0]) == 4 {
|
|
actual = append(actual, []string{
|
|
ixn.SourceNS,
|
|
ixn.SourceName,
|
|
ixn.DestinationNS,
|
|
ixn.DestinationName,
|
|
})
|
|
} else {
|
|
actual = append(actual, []string{ixn.SourceNS, ixn.SourceName})
|
|
}
|
|
}
|
|
}
|
|
|
|
assert.Equal(expected, actual)
|
|
}
|
|
}
|
|
|
|
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)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStore_Intention_Snapshot_Restore(t *testing.T) {
|
|
assert := assert.New(t)
|
|
s := testStateStore(t)
|
|
|
|
// Create some intentions.
|
|
ixns := structs.Intentions{
|
|
&structs.Intention{
|
|
DestinationName: "foo",
|
|
},
|
|
&structs.Intention{
|
|
DestinationName: "bar",
|
|
},
|
|
&structs.Intention{
|
|
DestinationName: "baz",
|
|
},
|
|
}
|
|
|
|
// Force the sort order of the UUIDs before we create them so the
|
|
// order is deterministic.
|
|
id := testUUID()
|
|
ixns[0].ID = "a" + id[1:]
|
|
ixns[1].ID = "b" + id[1:]
|
|
ixns[2].ID = "c" + id[1:]
|
|
|
|
// Now create
|
|
for i, ixn := range ixns {
|
|
assert.NoError(s.IntentionSet(uint64(4+i), ixn))
|
|
}
|
|
|
|
// Snapshot the queries.
|
|
snap := s.Snapshot()
|
|
defer snap.Close()
|
|
|
|
// Alter the real state store.
|
|
assert.NoError(s.IntentionDelete(7, ixns[0].ID))
|
|
|
|
// Verify the snapshot.
|
|
assert.Equal(snap.LastIndex(), uint64(6))
|
|
|
|
// Expect them sorted in insertion order
|
|
expected := structs.Intentions{
|
|
&structs.Intention{
|
|
ID: ixns[0].ID,
|
|
DestinationName: "foo",
|
|
Meta: map[string]string{},
|
|
RaftIndex: structs.RaftIndex{
|
|
CreateIndex: 4,
|
|
ModifyIndex: 4,
|
|
},
|
|
},
|
|
&structs.Intention{
|
|
ID: ixns[1].ID,
|
|
DestinationName: "bar",
|
|
Meta: map[string]string{},
|
|
RaftIndex: structs.RaftIndex{
|
|
CreateIndex: 5,
|
|
ModifyIndex: 5,
|
|
},
|
|
},
|
|
&structs.Intention{
|
|
ID: ixns[2].ID,
|
|
DestinationName: "baz",
|
|
Meta: map[string]string{},
|
|
RaftIndex: structs.RaftIndex{
|
|
CreateIndex: 6,
|
|
ModifyIndex: 6,
|
|
},
|
|
},
|
|
}
|
|
for i := range expected {
|
|
expected[i].UpdatePrecedence() // to match what is returned...
|
|
}
|
|
dump, err := snap.Intentions()
|
|
assert.NoError(err)
|
|
assert.Equal(expected, dump)
|
|
|
|
// Restore the values into a new state store.
|
|
func() {
|
|
s := testStateStore(t)
|
|
restore := s.Restore()
|
|
for _, ixn := range dump {
|
|
assert.NoError(restore.Intention(ixn))
|
|
}
|
|
restore.Commit()
|
|
|
|
// Read the restored values back out and verify that they match. Note that
|
|
// Intentions are returned precedence sorted unlike the snapshot so we need
|
|
// to rearrange the expected slice some.
|
|
expected[0], expected[1], expected[2] = expected[1], expected[2], expected[0]
|
|
entMeta := structs.WildcardEnterpriseMeta()
|
|
idx, actual, err := s.Intentions(nil, entMeta)
|
|
assert.NoError(err)
|
|
assert.Equal(idx, uint64(6))
|
|
assert.Equal(expected, actual)
|
|
}()
|
|
}
|