open-nomad/nomad/state/state_store_secure_variables_test.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

640 lines
20 KiB
Go
Raw Normal View History

package state
import (
"encoding/json"
"fmt"
"sort"
"strings"
"testing"
"time"
memdb "github.com/hashicorp/go-memdb"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/helper/uuid"
"github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/stretchr/testify/require"
)
func TestStateStore_GetSecureVariable(t *testing.T) {
ci.Parallel(t)
testState := testStateStore(t)
ws := memdb.NewWatchSet()
sve, err := testState.GetSecureVariable(ws, "default", "not/a/path")
require.NoError(t, err)
require.Nil(t, sve)
}
func TestStateStore_UpsertSecureVariables(t *testing.T) {
ci.Parallel(t)
testState := testStateStore(t)
ws := memdb.NewWatchSet()
svs, svm := mockSecureVariables(2)
t.Log(printSecureVariables(svs))
insertIndex := uint64(20)
var expectedQuotaSize int
for _, v := range svs {
expectedQuotaSize += len(v.Data)
}
// Ensure new secure variables are inserted as expected with their
// correct indexes, along with an update to the index table.
t.Run("1 create new variables", func(t *testing.T) {
// Perform the initial upsert of secure variables.
err := testState.UpsertSecureVariables(structs.MsgTypeTestSetup, insertIndex, svs)
require.NoError(t, err)
// Check that the index for the table was modified as expected.
initialIndex, err := testState.Index(TableSecureVariables)
require.NoError(t, err)
require.Equal(t, insertIndex, initialIndex)
// List all the secure variables in the table, so we can perform a
// number of tests on the return array.
iter, err := testState.SecureVariables(ws)
require.NoError(t, err)
// Count how many table entries we have, to ensure it is the expected
// number.
var count int
for raw := iter.Next(); raw != nil; raw = iter.Next() {
count++
// Ensure the create and modify indexes are populated correctly.
sv := raw.(*structs.SecureVariableEncrypted)
require.Equal(t, insertIndex, sv.CreateIndex, "incorrect create index", sv.Path)
require.Equal(t, insertIndex, sv.ModifyIndex, "incorrect modify index", sv.Path)
// update the mock element so the test element has the correct create/modify
// indexes and times now that we have validated them
nv := sv.Copy()
svm[sv.Path] = &nv
}
require.Equal(t, len(svs), count, "incorrect number of secure variables found")
quotaUsed, err := testState.SecureVariablesQuotaByNamespace(ws, structs.DefaultNamespace)
require.NoError(t, err)
require.Equal(t, int64(expectedQuotaSize), quotaUsed.Size)
})
svs = svm.List()
t.Log(printSecureVariables(svs))
t.Run("1a fetch variable", func(t *testing.T) {
sve, err := testState.GetSecureVariable(ws, svs[0].Namespace, svs[0].Path)
require.NoError(t, err)
require.NotNil(t, sve)
})
// Upsert the exact same secure variables without any
// modification. In this case, the index table should not be
// updated, indicating no write actually happened due to equality
// checking.
t.Run("2 upsert same", func(t *testing.T) {
reInsertIndex := uint64(30)
require.NoError(t, testState.UpsertSecureVariables(structs.MsgTypeTestSetup, reInsertIndex, svs))
reInsertActualIndex, err := testState.Index(TableSecureVariables)
require.NoError(t, err)
require.Equal(t, insertIndex, reInsertActualIndex, "index should not have changed")
quotaUsed, err := testState.SecureVariablesQuotaByNamespace(ws, structs.DefaultNamespace)
require.NoError(t, err)
require.Equal(t, int64(expectedQuotaSize), quotaUsed.Size)
})
// Modify a single one of the previously inserted secure variables
// and performs an upsert. This ensures the index table is
// modified correctly and that each secure variable is updated, or
// not, as expected.
t.Run("3 modify one", func(t *testing.T) {
sv1Update := svs[0].Copy()
sv1Update.KeyID = "sv1-update"
buf := make([]byte, 1+len(sv1Update.Data))
copy(buf, sv1Update.Data)
buf[len(buf)-1] = 'x'
sv1Update.Data = buf
svs1Update := []*structs.SecureVariableEncrypted{&sv1Update}
update1Index := uint64(40)
require.NoError(t, testState.UpsertSecureVariables(structs.MsgTypeTestSetup, update1Index, svs1Update))
// Check that the index for the table was modified as expected.
updateActualIndex, err := testState.Index(TableSecureVariables)
require.NoError(t, err)
require.Equal(t, update1Index, updateActualIndex, "index should have changed")
// Get the secure variables from the table.
iter, err := testState.SecureVariables(ws)
require.NoError(t, err)
// Iterate all the stored variables and assert they are as expected.
for raw := iter.Next(); raw != nil; raw = iter.Next() {
sv := raw.(*structs.SecureVariableEncrypted)
t.Logf("S " + printSecureVariable(sv))
var expectedModifyIndex uint64
switch sv.Path {
case sv1Update.Path:
expectedModifyIndex = update1Index
case svs[1].Path:
expectedModifyIndex = insertIndex
default:
t.Errorf("unknown secure variable found: %s", sv.Path)
continue
}
require.Equal(t, insertIndex, sv.CreateIndex, "incorrect create index", sv.Path)
require.Equal(t, expectedModifyIndex, sv.ModifyIndex, "incorrect modify index", sv.Path)
// update the mock element so the test element has the correct create/modify
// indexes and times now that we have validated them
nv := sv.Copy()
svm[sv.Path] = &nv
}
quotaUsed, err := testState.SecureVariablesQuotaByNamespace(ws, structs.DefaultNamespace)
require.NoError(t, err)
require.Equal(t, int64(expectedQuotaSize+1), quotaUsed.Size)
})
svs = svm.List()
t.Log(printSecureVariables(svs))
// Modify the second variable but send an upsert request that
// includes this and the already modified variable.
t.Run("4 upsert other", func(t *testing.T) {
update2Index := uint64(50)
sv2 := svs[1].Copy()
sv2.KeyID = "sv2-update"
sv2.ModifyIndex = update2Index
svs2Update := []*structs.SecureVariableEncrypted{svs[0], &sv2}
t.Logf("* " + printSecureVariable(svs[0]))
t.Logf("* " + printSecureVariable(&sv2))
require.NoError(t, testState.UpsertSecureVariables(structs.MsgTypeTestSetup, update2Index, svs2Update))
// Check that the index for the table was modified as expected.
update2ActualIndex, err := testState.Index(TableSecureVariables)
require.NoError(t, err)
require.Equal(t, update2Index, update2ActualIndex, "index should have changed")
// Get the secure variables from the table.
iter, err := testState.SecureVariables(ws)
require.NoError(t, err)
// Iterate all the stored variables and assert they are as expected.
for raw := iter.Next(); raw != nil; raw = iter.Next() {
sv := raw.(*structs.SecureVariableEncrypted)
t.Logf("S " + printSecureVariable(sv))
var (
expectedModifyIndex uint64
expectedSV *structs.SecureVariableEncrypted
)
switch sv.Path {
case sv2.Path:
expectedModifyIndex = update2Index
expectedSV = &sv2
case svs[0].Path:
expectedModifyIndex = svs[0].ModifyIndex
expectedSV = svs[0]
default:
t.Errorf("unknown secure variable found: %s", sv.Path)
continue
}
require.Equal(t, insertIndex, sv.CreateIndex, "%s: incorrect create index", sv.Path)
require.Equal(t, expectedModifyIndex, sv.ModifyIndex, "%s: incorrect modify index", sv.Path)
// update the mock element so the test element has the correct create/modify
// indexes and times now that we have validated them
expectedSV.ModifyTime = sv.ModifyTime
require.True(t, expectedSV.Equals(*sv), "Secure Variables are not equal:\n expected:%s\n received:%s\n", printSecureVariable(expectedSV), printSecureVariable(sv))
}
quotaUsed, err := testState.SecureVariablesQuotaByNamespace(ws, structs.DefaultNamespace)
require.NoError(t, err)
require.Equal(t, int64(expectedQuotaSize+1), quotaUsed.Size)
})
}
func TestStateStore_DeleteSecureVariable(t *testing.T) {
ci.Parallel(t)
testState := testStateStore(t)
// Generate some test secure variables that we will use and modify throughout.
svs, _ := mockSecureVariables(2)
initialIndex := uint64(10)
t.Run("1 delete a secure variable that does not exist", func(t *testing.T) {
err := testState.DeleteSecureVariables(
structs.MsgTypeTestSetup, initialIndex, svs[0].Namespace, []string{svs[0].Path})
require.EqualError(t, err, "secure variable not found")
actualInitialIndex, err := testState.Index(TableSecureVariables)
require.NoError(t, err)
require.Equal(t, uint64(0), actualInitialIndex, "index should not have changed")
quotaUsed, err := testState.SecureVariablesQuotaByNamespace(nil, structs.DefaultNamespace)
require.NoError(t, err)
require.Nil(t, quotaUsed)
})
// Upsert two secure variables, deletes one, then ensure the
// remaining is left as expected.
t.Run("2 upsert variable and delete", func(t *testing.T) {
ns := mock.Namespace()
ns.Name = svs[0].Namespace
require.NoError(t, testState.UpsertNamespaces(initialIndex, []*structs.Namespace{ns}))
initialIndex++
require.NoError(t, testState.UpsertSecureVariables(
structs.MsgTypeTestSetup, initialIndex, svs))
// Perform the delete.
delete1Index := uint64(20)
require.NoError(t, testState.DeleteSecureVariables(
structs.MsgTypeTestSetup, delete1Index, svs[0].Namespace, []string{svs[0].Path}))
// Check that the index for the table was modified as expected.
actualDelete1Index, err := testState.Index(TableSecureVariables)
require.NoError(t, err)
require.Equal(t, delete1Index, actualDelete1Index, "index should have changed")
ws := memdb.NewWatchSet()
// Get the secure variables from the table.
iter, err := testState.SecureVariables(ws)
require.NoError(t, err)
var delete1Count int
var expectedQuotaSize int
// Iterate all the stored variables and assert we have the expected
// number.
for raw := iter.Next(); raw != nil; raw = iter.Next() {
delete1Count++
v := raw.(*structs.SecureVariableEncrypted)
expectedQuotaSize += len(v.Data)
}
require.Equal(t, 1, delete1Count, "unexpected number of variables in table")
quotaUsed, err := testState.SecureVariablesQuotaByNamespace(ws, structs.DefaultNamespace)
require.NoError(t, err)
require.Equal(t, int64(expectedQuotaSize), quotaUsed.Size)
})
t.Run("3 delete remaining variable", func(t *testing.T) {
delete2Index := uint64(30)
require.NoError(t, testState.DeleteSecureVariable(
delete2Index, svs[1].Namespace, svs[1].Path))
// Check that the index for the table was modified as expected.
actualDelete2Index, err := testState.Index(TableSecureVariables)
require.NoError(t, err)
require.Equal(t, delete2Index, actualDelete2Index, "index should have changed")
// Get the secure variables from the table.
ws := memdb.NewWatchSet()
iter, err := testState.SecureVariables(ws)
require.NoError(t, err)
var delete2Count int
// Ensure the table is empty.
for raw := iter.Next(); raw != nil; raw = iter.Next() {
delete2Count++
}
require.Equal(t, 0, delete2Count, "unexpected number of variables in table")
quotaUsed, err := testState.SecureVariablesQuotaByNamespace(ws, structs.DefaultNamespace)
require.NoError(t, err)
require.Equal(t, int64(0), quotaUsed.Size)
})
}
func TestStateStore_GetSecureVariables(t *testing.T) {
ci.Parallel(t)
testState := testStateStore(t)
ns := mock.Namespace()
ns.Name = "~*magical*~"
initialIndex := uint64(10)
require.NoError(t, testState.UpsertNamespaces(initialIndex, []*structs.Namespace{ns}))
// Generate some test secure variables and upsert them.
svs, _ := mockSecureVariables(2)
svs[0].Namespace = "~*magical*~"
initialIndex++
require.NoError(t, testState.UpsertSecureVariables(structs.MsgTypeTestSetup, initialIndex, svs))
// Look up secure variables using the namespace of the first mock variable.
ws := memdb.NewWatchSet()
iter, err := testState.GetSecureVariablesByNamespace(ws, svs[0].Namespace)
require.NoError(t, err)
var count1 int
for raw := iter.Next(); raw != nil; raw = iter.Next() {
count1++
sv := raw.(*structs.SecureVariableEncrypted)
t.Logf("- sv: n=%q p=%q ci=%v mi=%v ed.ki=%q", sv.Namespace, sv.Path, sv.CreateIndex, sv.ModifyIndex, sv.KeyID)
require.Equal(t, initialIndex, sv.CreateIndex, "incorrect create index", sv.Path)
require.Equal(t, initialIndex, sv.ModifyIndex, "incorrect modify index", sv.Path)
require.Equal(t, svs[0].Namespace, sv.Namespace)
}
require.Equal(t, 1, count1)
// Look up variables using the namespace of the second mock variable.
iter, err = testState.GetSecureVariablesByNamespace(ws, svs[1].Namespace)
require.NoError(t, err)
var count2 int
for raw := iter.Next(); raw != nil; raw = iter.Next() {
count2++
sv := raw.(*structs.SecureVariableEncrypted)
t.Logf("- sv: n=%q p=%q ci=%v mi=%v ed.ki=%q", sv.Namespace, sv.Path, sv.CreateIndex, sv.ModifyIndex, sv.KeyID)
require.Equal(t, initialIndex, sv.CreateIndex, "incorrect create index", sv.Path)
require.Equal(t, initialIndex, sv.ModifyIndex, "incorrect modify index", sv.Path)
require.Equal(t, svs[1].Namespace, sv.Namespace)
}
require.Equal(t, 1, count2)
// Look up variables using a namespace that shouldn't contain any
// variables.
iter, err = testState.GetSecureVariablesByNamespace(ws, "pony-club")
require.NoError(t, err)
var count3 int
for raw := iter.Next(); raw != nil; raw = iter.Next() {
count3++
}
require.Equal(t, 0, count3)
}
func TestStateStore_ListSecureVariablesByNamespaceAndPrefix(t *testing.T) {
ci.Parallel(t)
testState := testStateStore(t)
// Generate some test secure variables and upsert them.
svs, _ := mockSecureVariables(6)
svs[0].Path = "a/b"
svs[1].Path = "a/b/c"
svs[2].Path = "unrelated/b/c"
svs[3].Namespace = "other"
svs[3].Path = "a/b/c"
svs[4].Namespace = "other"
svs[4].Path = "a/q/z"
svs[5].Namespace = "other"
svs[5].Path = "a/z/z"
ns := mock.Namespace()
ns.Name = "other"
initialIndex := uint64(10)
require.NoError(t, testState.UpsertNamespaces(initialIndex, []*structs.Namespace{ns}))
initialIndex++
require.NoError(t, testState.UpsertSecureVariables(structs.MsgTypeTestSetup, initialIndex, svs))
t.Run("ByNamespace", func(t *testing.T) {
testCases := []struct {
desc string
namespace string
expectedCount int
}{
{
desc: "default",
namespace: "default",
expectedCount: 2,
},
{
desc: "other",
namespace: "other",
expectedCount: 3,
},
{
desc: "nonexistent",
namespace: "BAD",
expectedCount: 0,
},
}
ws := memdb.NewWatchSet()
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
iter, err := testState.GetSecureVariablesByNamespace(ws, tC.namespace)
require.NoError(t, err)
var count int = 0
for raw := iter.Next(); raw != nil; raw = iter.Next() {
count++
sv := raw.(*structs.SecureVariableEncrypted)
t.Logf("- sv: n=%q p=%q ci=%v mi=%v ed.ki=%q", sv.Namespace, sv.Path, sv.CreateIndex, sv.ModifyIndex, sv.KeyID)
require.Equal(t, initialIndex, sv.CreateIndex, "incorrect create index", sv.Path)
require.Equal(t, initialIndex, sv.ModifyIndex, "incorrect modify index", sv.Path)
require.Equal(t, tC.namespace, sv.Namespace)
}
})
}
})
t.Run("ByNamespaceAndPrefix", func(t *testing.T) {
testCases := []struct {
desc string
namespace string
prefix string
expectedCount int
}{
{
desc: "ns1 with good path",
namespace: "default",
prefix: "a",
expectedCount: 2,
},
{
desc: "ns2 with good path",
namespace: "other",
prefix: "a",
expectedCount: 3,
},
{
desc: "ns1 path valid for ns2",
namespace: "default",
prefix: "a/b/c",
expectedCount: 1,
},
{
desc: "ns2 empty prefix",
namespace: "other",
prefix: "",
expectedCount: 3,
},
{
desc: "nonexistent ns",
namespace: "BAD",
prefix: "",
expectedCount: 0,
},
}
ws := memdb.NewWatchSet()
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
iter, err := testState.GetSecureVariablesByNamespaceAndPrefix(ws, tC.namespace, tC.prefix)
require.NoError(t, err)
var count int = 0
for raw := iter.Next(); raw != nil; raw = iter.Next() {
count++
sv := raw.(*structs.SecureVariableEncrypted)
t.Logf("- sv: n=%q p=%q ci=%v mi=%v ed.ki=%q", sv.Namespace, sv.Path, sv.CreateIndex, sv.ModifyIndex, sv.KeyID)
require.Equal(t, initialIndex, sv.CreateIndex, "incorrect create index", sv.Path)
require.Equal(t, initialIndex, sv.ModifyIndex, "incorrect modify index", sv.Path)
require.Equal(t, tC.namespace, sv.Namespace)
require.True(t, strings.HasPrefix(sv.Path, tC.prefix))
}
require.Equal(t, tC.expectedCount, count)
})
}
})
t.Run("ByPrefix", func(t *testing.T) {
testCases := []struct {
desc string
prefix string
expectedCount int
}{
{
desc: "bad prefix",
prefix: "bad",
expectedCount: 0,
},
{
desc: "multiple ns",
prefix: "a/b/c",
expectedCount: 2,
},
{
desc: "all",
prefix: "",
expectedCount: 6,
},
}
ws := memdb.NewWatchSet()
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
iter, err := testState.GetSecureVariablesByPrefix(ws, tC.prefix)
require.NoError(t, err)
var count int = 0
for raw := iter.Next(); raw != nil; raw = iter.Next() {
count++
sv := raw.(*structs.SecureVariableEncrypted)
t.Logf("- sv: n=%q p=%q ci=%v mi=%v ed.ki=%q", sv.Namespace, sv.Path, sv.CreateIndex, sv.ModifyIndex, sv.KeyID)
require.Equal(t, initialIndex, sv.CreateIndex, "incorrect create index", sv.Path)
require.Equal(t, initialIndex, sv.ModifyIndex, "incorrect modify index", sv.Path)
require.True(t, strings.HasPrefix(sv.Path, tC.prefix))
}
require.Equal(t, tC.expectedCount, count)
})
}
})
}
func TestStateStore_ListSecureVariablesByKeyID(t *testing.T) {
ci.Parallel(t)
testState := testStateStore(t)
// Generate some test secure variables and upsert them.
svs, _ := mockSecureVariables(7)
keyID := uuid.Generate()
expectedForKey := []string{}
for i := 0; i < 5; i++ {
svs[i].KeyID = keyID
expectedForKey = append(expectedForKey, svs[i].Path)
sort.Strings(expectedForKey)
}
expectedOrphaned := []string{svs[5].Path, svs[6].Path}
initialIndex := uint64(10)
require.NoError(t, testState.UpsertSecureVariables(
structs.MsgTypeTestSetup, initialIndex, svs))
ws := memdb.NewWatchSet()
iter, err := testState.GetSecureVariablesByKeyID(ws, keyID)
require.NoError(t, err)
var count int
for raw := iter.Next(); raw != nil; raw = iter.Next() {
sv := raw.(*structs.SecureVariableEncrypted)
require.Equal(t, keyID, sv.KeyID)
require.Equal(t, expectedForKey[count], sv.Path)
require.NotContains(t, expectedOrphaned, sv.Path)
count++
}
require.Equal(t, 5, count)
}
// mockSecureVariables returns a random number of secure variables between min
// and max inclusive.
func mockSecureVariables(count int) (
[]*structs.SecureVariableEncrypted, secureVariableMocks) {
var svm secureVariableMocks = make(map[string]*structs.SecureVariableEncrypted, count)
for i := 0; i < count; i++ {
nv := mock.SecureVariableEncrypted()
// There is an extremely rare chance of path collision because the mock
// secure variables generate their paths randomly. This check will add
// an extra component on conflict to (ideally) disambiguate them.
if _, found := svm[nv.Path]; found {
nv.Path = nv.Path + "/" + fmt.Sprint(time.Now().UnixNano())
}
svm[nv.Path] = nv
}
return svm.List(), svm
}
type secureVariableMocks map[string]*structs.SecureVariableEncrypted
func (svm secureVariableMocks) List() []*structs.SecureVariableEncrypted {
out := make([]*structs.SecureVariableEncrypted, len(svm))
i := 0
for _, v := range svm {
out[i] = v
i++
}
// objects will always come out of state store in namespace, path order.
sort.SliceStable(out, func(i, j int) bool {
if out[i].Namespace != out[j].Namespace {
return out[i].Namespace < out[j].Namespace
}
return out[i].Path < out[j].Path
})
return out
}
func printSecureVariable(tsv *structs.SecureVariableEncrypted) string {
b, _ := json.Marshal(tsv)
return string(b)
}
func printSecureVariables(tsvs []*structs.SecureVariableEncrypted) string {
if len(tsvs) == 0 {
return ""
}
var out strings.Builder
for _, tsv := range tsvs {
out.WriteString(printSecureVariable(tsv) + "\n")
}
return out.String()
}