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 }