// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package command import ( "bytes" "context" "fmt" "math/rand" "os" "path/filepath" "reflect" "sort" "strings" "sync" "testing" "time" "github.com/go-test/deep" log "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-secure-stdlib/base62" "github.com/hashicorp/vault/command/server" "github.com/hashicorp/vault/sdk/physical" "github.com/hashicorp/vault/vault" ) const trailing_slash_key = "trailing_slash/" func init() { rand.Seed(time.Now().UnixNano()) } func TestMigration(t *testing.T) { t.Run("Default", func(t *testing.T) { data := generateData() fromFactory := physicalBackends["file"] folder := t.TempDir() confFrom := map[string]string{ "path": folder, } from, err := fromFactory(confFrom, nil) if err != nil { t.Fatal(err) } if err := storeData(from, data); err != nil { t.Fatal(err) } toFactory := physicalBackends["inmem"] confTo := map[string]string{} to, err := toFactory(confTo, nil) if err != nil { t.Fatal(err) } cmd := OperatorMigrateCommand{ logger: log.NewNullLogger(), } if err := cmd.migrateAll(context.Background(), from, to, 1); err != nil { t.Fatal(err) } if err := compareStoredData(to, data, ""); err != nil { t.Fatal(err) } }) t.Run("Concurrent migration", func(t *testing.T) { data := generateData() fromFactory := physicalBackends["file"] folder := t.TempDir() confFrom := map[string]string{ "path": folder, } from, err := fromFactory(confFrom, nil) if err != nil { t.Fatal(err) } if err := storeData(from, data); err != nil { t.Fatal(err) } toFactory := physicalBackends["inmem"] confTo := map[string]string{} to, err := toFactory(confTo, nil) if err != nil { t.Fatal(err) } cmd := OperatorMigrateCommand{ logger: log.NewNullLogger(), } if err := cmd.migrateAll(context.Background(), from, to, 10); err != nil { t.Fatal(err) } if err := compareStoredData(to, data, ""); err != nil { t.Fatal(err) } }) t.Run("Start option", func(t *testing.T) { data := generateData() fromFactory := physicalBackends["inmem"] confFrom := map[string]string{} from, err := fromFactory(confFrom, nil) if err != nil { t.Fatal(err) } if err := storeData(from, data); err != nil { t.Fatal(err) } toFactory := physicalBackends["file"] folder := t.TempDir() confTo := map[string]string{ "path": folder, } to, err := toFactory(confTo, nil) if err != nil { t.Fatal(err) } const start = "m" cmd := OperatorMigrateCommand{ logger: log.NewNullLogger(), flagStart: start, } if err := cmd.migrateAll(context.Background(), from, to, 1); err != nil { t.Fatal(err) } if err := compareStoredData(to, data, start); err != nil { t.Fatal(err) } }) t.Run("Start option (parallel)", func(t *testing.T) { data := generateData() fromFactory := physicalBackends["inmem"] confFrom := map[string]string{} from, err := fromFactory(confFrom, nil) if err != nil { t.Fatal(err) } if err := storeData(from, data); err != nil { t.Fatal(err) } toFactory := physicalBackends["file"] folder := t.TempDir() confTo := map[string]string{ "path": folder, } to, err := toFactory(confTo, nil) if err != nil { t.Fatal(err) } const start = "m" cmd := OperatorMigrateCommand{ logger: log.NewNullLogger(), flagStart: start, } if err := cmd.migrateAll(context.Background(), from, to, 10); err != nil { t.Fatal(err) } if err := compareStoredData(to, data, start); err != nil { t.Fatal(err) } }) t.Run("Config parsing", func(t *testing.T) { cmd := new(OperatorMigrateCommand) cfgName := filepath.Join(t.TempDir(), "migrator") os.WriteFile(cfgName, []byte(` storage_source "src_type" { path = "src_path" } storage_destination "dest_type" { path = "dest_path" }`), 0o644) expCfg := &migratorConfig{ StorageSource: &server.Storage{ Type: "src_type", Config: map[string]string{ "path": "src_path", }, }, StorageDestination: &server.Storage{ Type: "dest_type", Config: map[string]string{ "path": "dest_path", }, }, } cfg, err := cmd.loadMigratorConfig(cfgName) if err != nil { t.Fatal(cfg) } if diff := deep.Equal(cfg, expCfg); diff != nil { t.Fatal(diff) } verifyBad := func(cfg string) { os.WriteFile(cfgName, []byte(cfg), 0o644) _, err := cmd.loadMigratorConfig(cfgName) if err == nil { t.Fatalf("expected error but none received from: %v", cfg) } } // missing source verifyBad(` storage_destination "dest_type" { path = "dest_path" }`) // missing destination verifyBad(` storage_source "src_type" { path = "src_path" }`) // duplicate source verifyBad(` storage_source "src_type" { path = "src_path" } storage_source "src_type2" { path = "src_path" } storage_destination "dest_type" { path = "dest_path" }`) // duplicate destination verifyBad(` storage_source "src_type" { path = "src_path" } storage_destination "dest_type" { path = "dest_path" } storage_destination "dest_type2" { path = "dest_path" }`) }) t.Run("DFS Scan", func(t *testing.T) { s, _ := physicalBackends["inmem"](map[string]string{}, nil) data := generateData() data["cc"] = []byte{} data["c/d/e/f"] = []byte{} data["c/d/e/g"] = []byte{} data["c"] = []byte{} storeData(s, data) l := randomLister{s} type SafeAppend struct { out []string lock sync.Mutex } outKeys := SafeAppend{} dfsScan(context.Background(), l, 10, func(ctx context.Context, path string) error { outKeys.lock.Lock() defer outKeys.lock.Unlock() outKeys.out = append(outKeys.out, path) return nil }) delete(data, trailing_slash_key) delete(data, "") var keys []string for key := range data { keys = append(keys, key) } sort.Strings(keys) outKeys.lock.Lock() sort.Strings(outKeys.out) outKeys.lock.Unlock() if !reflect.DeepEqual(keys, outKeys.out) { t.Fatalf("expected equal: %v, %v", keys, outKeys.out) } }) } // randomLister wraps a physical backend, providing a List method // that returns results in a random order. type randomLister struct { b physical.Backend } func (l randomLister) List(ctx context.Context, path string) ([]string, error) { result, err := l.b.List(ctx, path) if err != nil { return nil, err } rand.Shuffle(len(result), func(i, j int) { result[i], result[j] = result[j], result[i] }) return result, err } func (l randomLister) Get(ctx context.Context, path string) (*physical.Entry, error) { return l.b.Get(ctx, path) } func (l randomLister) Put(ctx context.Context, entry *physical.Entry) error { return l.b.Put(ctx, entry) } func (l randomLister) Delete(ctx context.Context, path string) error { return l.b.Delete(ctx, path) } // generateData creates a map of 500 random keys and values func generateData() map[string][]byte { result := make(map[string][]byte) for i := 0; i < 500; i++ { segments := make([]string, rand.Intn(8)+1) for j := 0; j < len(segments); j++ { s, _ := base62.Random(6) segments[j] = s } data := make([]byte, 100) rand.Read(data) result[strings.Join(segments, "/")] = data } // Add special keys that should be excluded from migration result[storageMigrationLock] = []byte{} result[vault.CoreLockPath] = []byte{} // Empty keys are now prevented in Vault, but older data sets // might contain them. result[""] = []byte{} result[trailing_slash_key] = []byte{} return result } func storeData(s physical.Backend, ref map[string][]byte) error { for k, v := range ref { entry := physical.Entry{ Key: k, Value: v, } err := s.Put(context.Background(), &entry) if err != nil { return err } } return nil } func compareStoredData(s physical.Backend, ref map[string][]byte, start string) error { for k, v := range ref { entry, err := s.Get(context.Background(), k) if err != nil { return err } if k == storageMigrationLock || k == vault.CoreLockPath || k == "" || strings.HasSuffix(k, "/") { if entry == nil { continue } return fmt.Errorf("key found that should have been excluded: %s", k) } if k >= start { if entry == nil { return fmt.Errorf("key not found: %s", k) } if !bytes.Equal(v, entry.Value) { return fmt.Errorf("values differ for key: %s", k) } } else { if entry != nil { return fmt.Errorf("found key the should have been skipped by start option: %s", k) } } } return nil }