Files
nomad/client/state/upgrade_test.go
Tim Gross 7bcf0afd81 CSI: allow for concurrent plugin allocations (#12078)
The dynamic plugin registry assumes that plugins are singletons, which
matches the behavior of other Nomad plugins. But because dynamic
plugins like CSI are implemented by allocations, we need to handle the
possibility of multiple allocations for a given plugin type + ID, as
well as behaviors around interleaved allocation starts and stops.

Update the data structure for the dynamic registry so that more recent
allocations take over as the instance manager singleton, but we still
preserve the previous running allocations so that restores work
without racing.

Multiple allocations can run on a client for the same plugin, even if
only during updates. Provide each plugin task a unique path for the
control socket so that the tasks don't interfere with each other.
2022-02-23 15:23:07 -05:00

194 lines
4.8 KiB
Go

package state
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/boltdb/bolt"
"github.com/hashicorp/nomad/helper/boltdd"
"github.com/hashicorp/nomad/helper/testlog"
"github.com/hashicorp/nomad/helper/uuid"
"github.com/stretchr/testify/require"
)
func setupBoltDB(t *testing.T) (*bolt.DB, func()) {
dir, err := ioutil.TempDir("", "nomadtest")
require.NoError(t, err)
db, err := bolt.Open(filepath.Join(dir, "state.db"), 0666, nil)
if err != nil {
os.RemoveAll(dir)
require.NoError(t, err)
}
return db, func() {
require.NoError(t, db.Close())
require.NoError(t, os.RemoveAll(dir))
}
}
// TestUpgrade_NeedsUpgrade_New asserts new state dbs do not need upgrading.
func TestUpgrade_NeedsUpgrade_New(t *testing.T) {
t.Parallel()
// Setting up a new StateDB should initialize it at the latest version.
db, cleanup := setupBoltStateDB(t)
defer cleanup()
to09, to12, err := NeedsUpgrade(db.DB().BoltDB())
require.NoError(t, err)
require.False(t, to09)
require.False(t, to12)
}
// TestUpgrade_NeedsUpgrade_Old asserts state dbs with just the alloctions
// bucket *do* need upgrading.
func TestUpgrade_NeedsUpgrade_Old(t *testing.T) {
t.Parallel()
db, cleanup := setupBoltDB(t)
defer cleanup()
// Create the allocations bucket which exists in both the old and 0.9
// schemas
require.NoError(t, db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucket(allocationsBucketName)
return err
}))
to09, to12, err := NeedsUpgrade(db)
require.NoError(t, err)
require.True(t, to09)
require.True(t, to12)
// Adding meta should mark it as upgraded
require.NoError(t, db.Update(addMeta))
to09, to12, err = NeedsUpgrade(db)
require.NoError(t, err)
require.False(t, to09)
require.False(t, to12)
}
// TestUpgrade_NeedsUpgrade_Error asserts that an error is returned from
// NeedsUpgrade if an invalid db version is found. This is a safety measure to
// prevent invalid and unintentional upgrades when downgrading Nomad.
func TestUpgrade_NeedsUpgrade_Error(t *testing.T) {
t.Parallel()
cases := [][]byte{
{'"', '2', '"'}, // wrong type
{'1'}, // wrong version (never existed)
{'4'}, // wrong version (future)
}
for _, tc := range cases {
tc := tc
t.Run(fmt.Sprintf("%v", tc), func(t *testing.T) {
db, cleanup := setupBoltDB(t)
defer cleanup()
require.NoError(t, db.Update(func(tx *bolt.Tx) error {
bkt, err := tx.CreateBucketIfNotExists(metaBucketName)
require.NoError(t, err)
return bkt.Put(metaVersionKey, tc)
}))
_, _, err := NeedsUpgrade(db)
require.Error(t, err)
})
}
}
// TestUpgrade_DeleteInvalidAllocs asserts invalid allocations are deleted
// during state upgades instead of failing the entire agent.
func TestUpgrade_DeleteInvalidAllocs_NoAlloc(t *testing.T) {
t.Parallel()
bdb, cleanup := setupBoltDB(t)
defer cleanup()
db := boltdd.New(bdb)
allocID := []byte(uuid.Generate())
// Create an allocation bucket with no `alloc` key. This is an observed
// pre-0.9 state corruption that should result in the allocation being
// dropped while allowing the upgrade to continue.
require.NoError(t, db.Update(func(tx *boltdd.Tx) error {
parentBkt, err := tx.CreateBucket(allocationsBucketName)
if err != nil {
return err
}
_, err = parentBkt.CreateBucket(allocID)
return err
}))
// Perform the Upgrade
require.NoError(t, db.Update(func(tx *boltdd.Tx) error {
return UpgradeAllocs(testlog.HCLogger(t), tx)
}))
// Assert invalid allocation bucket was removed
require.NoError(t, db.View(func(tx *boltdd.Tx) error {
parentBkt := tx.Bucket(allocationsBucketName)
if parentBkt == nil {
return fmt.Errorf("parent allocations bucket should not have been removed")
}
if parentBkt.Bucket(allocID) != nil {
return fmt.Errorf("invalid alloc bucket should have been deleted")
}
return nil
}))
}
// TestUpgrade_DeleteInvalidTaskEntries asserts invalid entries under a task
// bucket are deleted.
func TestUpgrade_upgradeTaskBucket_InvalidEntries(t *testing.T) {
t.Parallel()
db, cleanup := setupBoltDB(t)
defer cleanup()
taskName := []byte("fake-task")
// Insert unexpected bucket, unexpected key, and missing simple-all
require.NoError(t, db.Update(func(tx *bolt.Tx) error {
bkt, err := tx.CreateBucket(taskName)
if err != nil {
return err
}
_, err = bkt.CreateBucket([]byte("unexpectedBucket"))
if err != nil {
return err
}
return bkt.Put([]byte("unexepectedKey"), []byte{'x'})
}))
require.NoError(t, db.Update(func(tx *bolt.Tx) error {
bkt := tx.Bucket(taskName)
// upgradeTaskBucket should fail
state, err := upgradeTaskBucket(testlog.HCLogger(t), bkt)
require.Nil(t, state)
require.Error(t, err)
// Invalid entries should have been deleted
cur := bkt.Cursor()
for k, v := cur.First(); k != nil; k, v = cur.Next() {
t.Errorf("unexpected entry found: key=%q value=%q", k, v)
}
return nil
}))
}