open-nomad/testutil/vault.go

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

257 lines
6.1 KiB
Go
Raw Normal View History

2016-08-14 01:33:48 +00:00
package testutil
import (
2022-09-23 18:45:12 +00:00
"errors"
2016-08-14 01:33:48 +00:00
"fmt"
"math/rand"
2016-08-14 01:33:48 +00:00
"os"
"os/exec"
"time"
2016-08-14 01:33:48 +00:00
"github.com/hashicorp/nomad/helper/freeport"
2018-09-19 01:36:19 +00:00
"github.com/hashicorp/nomad/helper/testlog"
"github.com/hashicorp/nomad/helper/uuid"
2016-08-14 01:33:48 +00:00
"github.com/hashicorp/nomad/nomad/structs/config"
vapi "github.com/hashicorp/vault/api"
testing "github.com/mitchellh/go-testing-interface"
2019-01-16 14:06:39 +00:00
"github.com/stretchr/testify/require"
2016-08-14 01:33:48 +00:00
)
// TestVault is a test helper. It uses a fork/exec model to create a test Vault
// server instance in the background and can be initialized with policies, roles
// and backends mounted. The test Vault instances can be used to run a unit test
// and offers and easy API to tear itself down on test end. The only
// prerequisite is that the Vault binary is on the $PATH.
// TestVault wraps a test Vault server launched in dev mode, suitable for
// testing.
type TestVault struct {
cmd *exec.Cmd
t testing.T
waitCh chan error
2016-08-14 01:33:48 +00:00
// ports (if any) that are reserved through freeport that must be returned
// at the end of a test, done when Stop() is called.
ports []int
2016-08-14 01:33:48 +00:00
Addr string
HTTPAddr string
RootToken string
Config *config.VaultConfig
Client *vapi.Client
}
2018-09-19 01:36:19 +00:00
func NewTestVaultFromPath(t testing.T, binary string) *TestVault {
var ports []int
nextPort := func() int {
next := freeport.MustTake(1)
ports = append(ports, next...)
return next[0]
}
2017-07-23 23:21:25 +00:00
for i := 10; i >= 0; i-- {
port := nextPort() // collect every port for cleanup after the test
token := uuid.Generate()
2017-07-23 23:21:25 +00:00
bind := fmt.Sprintf("-dev-listen-address=127.0.0.1:%d", port)
http := fmt.Sprintf("http://127.0.0.1:%d", port)
root := fmt.Sprintf("-dev-root-token-id=%s", token)
2018-09-19 01:36:19 +00:00
cmd := exec.Command(binary, "server", "-dev", bind, root)
cmd.Stdout = testlog.NewWriter(t)
cmd.Stderr = testlog.NewWriter(t)
2017-07-23 23:21:25 +00:00
// Build the config
conf := vapi.DefaultConfig()
conf.Address = http
// Make the client and set the token to the root token
client, err := vapi.NewClient(conf)
if err != nil {
t.Fatalf("failed to build Vault API client: %v", err)
}
client.SetToken(token)
enable := true
tv := &TestVault{
cmd: cmd,
t: t,
ports: ports,
2017-07-23 23:21:25 +00:00
Addr: bind,
HTTPAddr: http,
RootToken: token,
Client: client,
Config: &config.VaultConfig{
Enabled: &enable,
Token: token,
Addr: http,
},
}
if err := tv.cmd.Start(); err != nil {
tv.t.Fatalf("failed to start vault: %v", err)
}
// Start the waiter
tv.waitCh = make(chan error, 1)
go func() {
err := tv.cmd.Wait()
tv.waitCh <- err
}()
// Ensure Vault started
var startErr error
select {
case startErr = <-tv.waitCh:
case <-time.After(time.Duration(500*TestMultiplier()) * time.Millisecond):
}
if startErr != nil && i == 0 {
t.Fatalf("failed to start vault: %v", startErr)
} else if startErr != nil {
wait := time.Duration(rand.Int31n(2000)) * time.Millisecond
time.Sleep(wait)
continue
}
waitErr := tv.waitForAPI()
if waitErr != nil && i == 0 {
t.Fatalf("failed to start vault: %v", waitErr)
} else if waitErr != nil {
wait := time.Duration(rand.Int31n(2000)) * time.Millisecond
time.Sleep(wait)
continue
}
return tv
}
return nil
2018-09-19 01:36:19 +00:00
}
// NewTestVault returns a new TestVault instance that is ready for API calls
2018-09-19 01:36:19 +00:00
func NewTestVault(t testing.T) *TestVault {
// Lookup vault from the path
return NewTestVaultFromPath(t, "vault")
2017-07-23 23:21:25 +00:00
}
// NewTestVaultDelayed returns a test Vault server that has not been started.
// Start must be called and it is the callers responsibility to deal with any
// port conflicts that may occur and retry accordingly.
func NewTestVaultDelayed(t testing.T) *TestVault {
port := freeport.MustTake(1)[0]
token := uuid.Generate()
2016-08-14 01:33:48 +00:00
bind := fmt.Sprintf("-dev-listen-address=127.0.0.1:%d", port)
http := fmt.Sprintf("http://127.0.0.1:%d", port)
root := fmt.Sprintf("-dev-root-token-id=%s", token)
cmd := exec.Command("vault", "server", "-dev", bind, root)
2016-08-14 01:33:48 +00:00
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// Build the config
conf := vapi.DefaultConfig()
conf.Address = http
// Make the client and set the token to the root token
client, err := vapi.NewClient(conf)
if err != nil {
t.Fatalf("failed to build Vault API client: %v", err)
}
2016-08-15 01:56:32 +00:00
client.SetToken(token)
2016-08-14 01:33:48 +00:00
2016-10-11 01:04:39 +00:00
enable := true
2016-08-14 01:33:48 +00:00
tv := &TestVault{
cmd: cmd,
t: t,
Addr: bind,
HTTPAddr: http,
RootToken: token,
2016-08-14 01:33:48 +00:00
Client: client,
Config: &config.VaultConfig{
2016-10-11 01:04:39 +00:00
Enabled: &enable,
2016-08-14 01:33:48 +00:00
Token: token,
Addr: http,
},
}
return tv
}
// Start starts the test Vault server and waits for it to respond to its HTTP
// API
2017-07-23 23:21:25 +00:00
func (tv *TestVault) Start() error {
// Start the waiter
tv.waitCh = make(chan error, 1)
go func() {
// Must call Start and Wait in the same goroutine on Windows #5174
if err := tv.cmd.Start(); err != nil {
tv.waitCh <- err
return
}
err := tv.cmd.Wait()
tv.waitCh <- err
}()
// Ensure Vault started
select {
case err := <-tv.waitCh:
2017-07-23 23:21:25 +00:00
return err
case <-time.After(time.Duration(500*TestMultiplier()) * time.Millisecond):
}
2017-07-23 23:21:25 +00:00
return tv.waitForAPI()
2016-08-14 01:33:48 +00:00
}
// Stop stops the test Vault server
func (tv *TestVault) Stop() {
defer freeport.Return(tv.ports)
2016-08-15 01:56:32 +00:00
if tv.cmd.Process == nil {
return
}
2016-08-14 01:33:48 +00:00
if err := tv.cmd.Process.Kill(); err != nil {
2022-09-23 18:45:12 +00:00
if errors.Is(err, os.ErrProcessDone) {
return
}
2016-08-14 01:33:48 +00:00
tv.t.Errorf("err: %s", err)
}
if tv.waitCh != nil {
select {
case <-tv.waitCh:
return
case <-time.After(1 * time.Second):
2019-01-16 14:06:39 +00:00
require.Fail(tv.t, "Timed out waiting for vault to terminate")
}
}
2016-08-14 01:33:48 +00:00
}
// waitForAPI waits for the Vault HTTP endpoint to start
// responding. This is an indication that the agent has started.
2017-07-23 23:21:25 +00:00
func (tv *TestVault) waitForAPI() error {
var waitErr error
2016-08-14 01:33:48 +00:00
WaitForResult(func() (bool, error) {
inited, err := tv.Client.Sys().InitStatus()
if err != nil {
return false, err
}
return inited, nil
}, func(err error) {
2017-07-23 23:21:25 +00:00
waitErr = err
2016-08-14 01:33:48 +00:00
})
2017-07-23 23:21:25 +00:00
return waitErr
2016-08-14 01:33:48 +00:00
}
2017-01-27 23:06:01 +00:00
// VaultVersion returns the Vault version as a string or an error if it couldn't
// be determined
func VaultVersion() (string, error) {
cmd := exec.Command("vault", "version")
out, err := cmd.Output()
return string(out), err
}