github.com/ferranbt/nomad@v0.9.3-0.20190607002617-85c449b7667c/testutil/vault.go (about)

     1  package testutil
     2  
     3  import (
     4  	"fmt"
     5  	"math/rand"
     6  	"os"
     7  	"os/exec"
     8  	"time"
     9  
    10  	"github.com/hashicorp/consul/lib/freeport"
    11  	"github.com/hashicorp/nomad/helper/testlog"
    12  	"github.com/hashicorp/nomad/helper/uuid"
    13  	"github.com/hashicorp/nomad/nomad/structs/config"
    14  	vapi "github.com/hashicorp/vault/api"
    15  	testing "github.com/mitchellh/go-testing-interface"
    16  	"github.com/stretchr/testify/require"
    17  )
    18  
    19  // TestVault is a test helper. It uses a fork/exec model to create a test Vault
    20  // server instance in the background and can be initialized with policies, roles
    21  // and backends mounted. The test Vault instances can be used to run a unit test
    22  // and offers and easy API to tear itself down on test end. The only
    23  // prerequisite is that the Vault binary is on the $PATH.
    24  
    25  // TestVault wraps a test Vault server launched in dev mode, suitable for
    26  // testing.
    27  type TestVault struct {
    28  	cmd    *exec.Cmd
    29  	t      testing.T
    30  	waitCh chan error
    31  
    32  	Addr      string
    33  	HTTPAddr  string
    34  	RootToken string
    35  	Config    *config.VaultConfig
    36  	Client    *vapi.Client
    37  }
    38  
    39  func NewTestVaultFromPath(t testing.T, binary string) *TestVault {
    40  	for i := 10; i >= 0; i-- {
    41  		port := freeport.GetT(t, 1)[0]
    42  		token := uuid.Generate()
    43  		bind := fmt.Sprintf("-dev-listen-address=127.0.0.1:%d", port)
    44  		http := fmt.Sprintf("http://127.0.0.1:%d", port)
    45  		root := fmt.Sprintf("-dev-root-token-id=%s", token)
    46  
    47  		cmd := exec.Command(binary, "server", "-dev", bind, root)
    48  		cmd.Stdout = testlog.NewWriter(t)
    49  		cmd.Stderr = testlog.NewWriter(t)
    50  
    51  		// Build the config
    52  		conf := vapi.DefaultConfig()
    53  		conf.Address = http
    54  
    55  		// Make the client and set the token to the root token
    56  		client, err := vapi.NewClient(conf)
    57  		if err != nil {
    58  			t.Fatalf("failed to build Vault API client: %v", err)
    59  		}
    60  		client.SetToken(token)
    61  
    62  		enable := true
    63  		tv := &TestVault{
    64  			cmd:       cmd,
    65  			t:         t,
    66  			Addr:      bind,
    67  			HTTPAddr:  http,
    68  			RootToken: token,
    69  			Client:    client,
    70  			Config: &config.VaultConfig{
    71  				Enabled: &enable,
    72  				Token:   token,
    73  				Addr:    http,
    74  			},
    75  		}
    76  
    77  		if err := tv.cmd.Start(); err != nil {
    78  			tv.t.Fatalf("failed to start vault: %v", err)
    79  		}
    80  
    81  		// Start the waiter
    82  		tv.waitCh = make(chan error, 1)
    83  		go func() {
    84  			err := tv.cmd.Wait()
    85  			tv.waitCh <- err
    86  		}()
    87  
    88  		// Ensure Vault started
    89  		var startErr error
    90  		select {
    91  		case startErr = <-tv.waitCh:
    92  		case <-time.After(time.Duration(500*TestMultiplier()) * time.Millisecond):
    93  		}
    94  
    95  		if startErr != nil && i == 0 {
    96  			t.Fatalf("failed to start vault: %v", startErr)
    97  		} else if startErr != nil {
    98  			wait := time.Duration(rand.Int31n(2000)) * time.Millisecond
    99  			time.Sleep(wait)
   100  			continue
   101  		}
   102  
   103  		waitErr := tv.waitForAPI()
   104  		if waitErr != nil && i == 0 {
   105  			t.Fatalf("failed to start vault: %v", waitErr)
   106  		} else if waitErr != nil {
   107  			wait := time.Duration(rand.Int31n(2000)) * time.Millisecond
   108  			time.Sleep(wait)
   109  			continue
   110  		}
   111  
   112  		return tv
   113  	}
   114  
   115  	return nil
   116  
   117  }
   118  
   119  // NewTestVault returns a new TestVault instance that has yet to be started
   120  func NewTestVault(t testing.T) *TestVault {
   121  	// Lookup vault from the path
   122  	return NewTestVaultFromPath(t, "vault")
   123  }
   124  
   125  // NewTestVaultDelayed returns a test Vault server that has not been started.
   126  // Start must be called and it is the callers responsibility to deal with any
   127  // port conflicts that may occur and retry accordingly.
   128  func NewTestVaultDelayed(t testing.T) *TestVault {
   129  	port := freeport.GetT(t, 1)[0]
   130  	token := uuid.Generate()
   131  	bind := fmt.Sprintf("-dev-listen-address=127.0.0.1:%d", port)
   132  	http := fmt.Sprintf("http://127.0.0.1:%d", port)
   133  	root := fmt.Sprintf("-dev-root-token-id=%s", token)
   134  
   135  	cmd := exec.Command("vault", "server", "-dev", bind, root)
   136  	cmd.Stdout = os.Stdout
   137  	cmd.Stderr = os.Stderr
   138  
   139  	// Build the config
   140  	conf := vapi.DefaultConfig()
   141  	conf.Address = http
   142  
   143  	// Make the client and set the token to the root token
   144  	client, err := vapi.NewClient(conf)
   145  	if err != nil {
   146  		t.Fatalf("failed to build Vault API client: %v", err)
   147  	}
   148  	client.SetToken(token)
   149  
   150  	enable := true
   151  	tv := &TestVault{
   152  		cmd:       cmd,
   153  		t:         t,
   154  		Addr:      bind,
   155  		HTTPAddr:  http,
   156  		RootToken: token,
   157  		Client:    client,
   158  		Config: &config.VaultConfig{
   159  			Enabled: &enable,
   160  			Token:   token,
   161  			Addr:    http,
   162  		},
   163  	}
   164  
   165  	return tv
   166  }
   167  
   168  // Start starts the test Vault server and waits for it to respond to its HTTP
   169  // API
   170  func (tv *TestVault) Start() error {
   171  	// Start the waiter
   172  	tv.waitCh = make(chan error, 1)
   173  
   174  	go func() {
   175  		// Must call Start and Wait in the same goroutine on Windows #5174
   176  		if err := tv.cmd.Start(); err != nil {
   177  			tv.waitCh <- err
   178  			return
   179  		}
   180  
   181  		err := tv.cmd.Wait()
   182  		tv.waitCh <- err
   183  	}()
   184  
   185  	// Ensure Vault started
   186  	select {
   187  	case err := <-tv.waitCh:
   188  		return err
   189  	case <-time.After(time.Duration(500*TestMultiplier()) * time.Millisecond):
   190  	}
   191  
   192  	return tv.waitForAPI()
   193  }
   194  
   195  // Stop stops the test Vault server
   196  func (tv *TestVault) Stop() {
   197  	if tv.cmd.Process == nil {
   198  		return
   199  	}
   200  
   201  	if err := tv.cmd.Process.Kill(); err != nil {
   202  		tv.t.Errorf("err: %s", err)
   203  	}
   204  	if tv.waitCh != nil {
   205  		select {
   206  		case <-tv.waitCh:
   207  			return
   208  		case <-time.After(1 * time.Second):
   209  			require.Fail(tv.t, "Timed out waiting for vault to terminate")
   210  		}
   211  	}
   212  }
   213  
   214  // waitForAPI waits for the Vault HTTP endpoint to start
   215  // responding. This is an indication that the agent has started.
   216  func (tv *TestVault) waitForAPI() error {
   217  	var waitErr error
   218  	WaitForResult(func() (bool, error) {
   219  		inited, err := tv.Client.Sys().InitStatus()
   220  		if err != nil {
   221  			return false, err
   222  		}
   223  		return inited, nil
   224  	}, func(err error) {
   225  		waitErr = err
   226  	})
   227  	return waitErr
   228  }
   229  
   230  // VaultVersion returns the Vault version as a string or an error if it couldn't
   231  // be determined
   232  func VaultVersion() (string, error) {
   233  	cmd := exec.Command("vault", "version")
   234  	out, err := cmd.Output()
   235  	return string(out), err
   236  }