| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| //go:build foundationdb |
| |
| package foundationdb |
| |
| import ( |
| "bytes" |
| "fmt" |
| "io/ioutil" |
| "os" |
| "testing" |
| "time" |
| |
| log "github.com/hashicorp/go-hclog" |
| uuid "github.com/hashicorp/go-uuid" |
| |
| "github.com/apple/foundationdb/bindings/go/src/fdb" |
| "github.com/apple/foundationdb/bindings/go/src/fdb/directory" |
| |
| "github.com/hashicorp/vault/sdk/helper/logging" |
| "github.com/hashicorp/vault/sdk/physical" |
| |
| dockertest "gopkg.in/ory-am/dockertest.v3" |
| ) |
| |
| func connectToFoundationDB(clusterFile string) (*fdb.Database, error) { |
| if err := fdb.APIVersion(520); err != nil { |
| return nil, fmt.Errorf("failed to set FDB API version: %w", err) |
| } |
| |
| db, err := fdb.Open(clusterFile, []byte("DB")) |
| if err != nil { |
| return nil, fmt.Errorf("failed to open database: %w", err) |
| } |
| |
| return &db, nil |
| } |
| |
| func cleanupTopDir(clusterFile, topDir string) error { |
| db, err := connectToFoundationDB(clusterFile) |
| if err != nil { |
| return fmt.Errorf("could not connect to FDB for cleanup: %w", err) |
| } |
| |
| if _, err := directory.Root().Remove(db, []string{topDir}); err != nil { |
| return fmt.Errorf("could not remove directory: %w", err) |
| } |
| |
| return nil |
| } |
| |
| func TestFoundationDBPathDecoration(t *testing.T) { |
| cases := map[string][]byte{ |
| "foo": []byte("/\x01foo"), |
| "foo/": []byte("/\x01foo/"), |
| "foo/bar": []byte("/\x02foo/\x01bar"), |
| "foo/bar/": []byte("/\x02foo/\x01bar/"), |
| "foo/bar/baz": []byte("/\x02foo/\x02bar/\x01baz"), |
| "foo/bar/baz/": []byte("/\x02foo/\x02bar/\x01baz/"), |
| "foo/bar/baz/quux": []byte("/\x02foo/\x02bar/\x02baz/\x01quux"), |
| } |
| |
| for path, expected := range cases { |
| decorated, err := decoratePath(path) |
| if err != nil { |
| t.Fatalf("path %s error: %s", path, err) |
| } |
| |
| if !bytes.Equal(expected, decorated) { |
| t.Fatalf("path %s expected %v got %v", path, expected, decorated) |
| } |
| |
| undecorated := undecoratePath(decorated) |
| if undecorated != path { |
| t.Fatalf("expected %s got %s", path, undecorated) |
| } |
| } |
| } |
| |
| func TestFoundationDBBackend(t *testing.T) { |
| if testing.Short() { |
| t.Skipf("skipping in short mode") |
| } |
| |
| testUUID, err := uuid.GenerateUUID() |
| if err != nil { |
| t.Fatalf("foundationdb: could not generate UUID to top-level directory: %s", err) |
| } |
| |
| topDir := fmt.Sprintf("vault-test-%s", testUUID) |
| |
| var clusterFile string |
| clusterFile = os.Getenv("FOUNDATIONDB_CLUSTER_FILE") |
| if clusterFile == "" { |
| var cleanup func() |
| cleanup, clusterFile = prepareFoundationDBTestDirectory(t, topDir) |
| defer cleanup() |
| } |
| |
| // Remove the test data once done |
| defer func() { |
| if err := cleanupTopDir(clusterFile, topDir); err != nil { |
| t.Fatalf("foundationdb: could not cleanup test data at end of test: %s", err) |
| } |
| }() |
| |
| // Remove any leftover test data before starting |
| if err := cleanupTopDir(clusterFile, topDir); err != nil { |
| t.Fatalf("foundationdb: could not cleanup test data before starting test: %s", err) |
| } |
| |
| // Run vault tests |
| logger := logging.NewVaultLogger(log.Debug) |
| config := map[string]string{ |
| "path": topDir, |
| "api_version": "520", |
| "cluster_file": clusterFile, |
| } |
| |
| b, err := NewFDBBackend(config, logger) |
| if err != nil { |
| t.Fatalf("foundationdb: failed to create new backend: %s", err) |
| } |
| |
| b2, err := NewFDBBackend(config, logger) |
| if err != nil { |
| t.Fatalf("foundationdb: failed to create new backend: %s", err) |
| } |
| |
| physical.ExerciseBackend(t, b) |
| physical.ExerciseBackend_ListPrefix(t, b) |
| physical.ExerciseTransactionalBackend(t, b) |
| physical.ExerciseHABackend(t, b.(physical.HABackend), b2.(physical.HABackend)) |
| } |
| |
| func prepareFoundationDBTestDirectory(t *testing.T, topDir string) (func(), string) { |
| pool, err := dockertest.NewPool("") |
| if err != nil { |
| t.Fatalf("foundationdb: failed to connect to docker: %s", err) |
| } |
| |
| resource, err := pool.Run("foundationdb", "5.1.7", nil) |
| if err != nil { |
| t.Fatalf("foundationdb: could not start container: %s", err) |
| } |
| |
| tmpFile, err := ioutil.TempFile("", topDir) |
| if err != nil { |
| t.Fatalf("foundationdb: could not create temporary file for cluster file: %s", err) |
| } |
| |
| clusterFile := tmpFile.Name() |
| |
| cleanup := func() { |
| var err error |
| for i := 0; i < 10; i++ { |
| err = pool.Purge(resource) |
| if err == nil { |
| break |
| } |
| time.Sleep(1 * time.Second) |
| } |
| os.Remove(clusterFile) |
| if err != nil { |
| t.Fatalf("Failed to cleanup local container: %s", err) |
| } |
| } |
| |
| setup := func() error { |
| connectString := fmt.Sprintf("foundationdb:foundationdb@127.0.0.1:%s", resource.GetPort("4500/tcp")) |
| |
| if err := tmpFile.Truncate(0); err != nil { |
| return fmt.Errorf("could not truncate cluster file: %w", err) |
| } |
| |
| _, err := tmpFile.WriteAt([]byte(connectString), 0) |
| if err != nil { |
| return fmt.Errorf("could not write cluster file: %w", err) |
| } |
| |
| if _, err := connectToFoundationDB(clusterFile); err != nil { |
| return fmt.Errorf("could not connect to FoundationDB after starting container: %s", err) |
| } |
| |
| return nil |
| } |
| |
| if pool.Retry(setup); err != nil { |
| cleanup() |
| |
| t.Fatalf("foundationdb: could not setup container: %s", err) |
| } |
| |
| tmpFile.Close() |
| |
| return cleanup, clusterFile |
| } |