forked from hashicorp/vault-testing-stepwise
-
Notifications
You must be signed in to change notification settings - Fork 0
/
stepwise.go
372 lines (317 loc) · 10.9 KB
/
stepwise.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
// Package stepwise offers types and functions to enable black-box style tests
// that are executed in defined set of steps. Stepwise utilizes "Environments" which
// setup a running instance of Vault and provide a valid API client to execute
// user defined steps against.
package stepwise
import (
"fmt"
"os"
"testing"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/logging"
)
// TestEnvVar must be set to a non-empty value for acceptance tests to run.
const TestEnvVar = "VAULT_ACC"
// Operation defines operations each step could perform. These are
// intentionally redefined from the logical package in the SDK, so users
// consistently use the stepwise package and not a combination of both stepwise
// and logical.
type Operation string
const (
WriteOperation Operation = "create"
UpdateOperation Operation = "update"
ReadOperation Operation = "read"
DeleteOperation Operation = "delete"
ListOperation Operation = "list"
HelpOperation Operation = "help"
)
// Environment is the interface Environments need to implement to be used in
// Case to execute each Step
type Environment interface {
// Setup is responsible for creating the Vault cluster for use in the test
// case.
Setup() error
// Client should return a clone of a configured Vault API client to
// communicate with the Vault cluster created in Setup and managed by this
// Environment.
Client() (*api.Client, error)
// Teardown is responsible for destroying any and all infrastructure created
// during Setup or otherwise over the course of executing test cases.
Teardown() error
// Name returns the name of the environment provider, e.g. Docker, Minikube,
// et.al.
Name() string
// MountPath returns the path the plugin is mounted at
MountPath() string
// RootToken returns the root token of the cluster, used for making requests
// as well as administrative tasks
RootToken() string
}
// PluginType defines the types of plugins supported
// This type re-create constants as a convenience so users don't need to import/use
// the consts package.
type PluginType consts.PluginType
// These are originally defined in sdk/helper/consts/plugin_types.go
const (
PluginTypeUnknown PluginType = iota
PluginTypeCredential
PluginTypeDatabase
PluginTypeSecrets
)
func (p PluginType) String() string {
switch p {
case PluginTypeUnknown:
return "unknown"
case PluginTypeCredential:
return "auth"
case PluginTypeDatabase:
return "database"
case PluginTypeSecrets:
return "secret"
default:
return "unsupported"
}
}
// MountOptions are a collection of options each step driver should
// support
type MountOptions struct {
// MountPathPrefix is an optional prefix to use when mounting the plugin. If
// omitted the mount path will default to the PluginName with a random suffix.
MountPathPrefix string
// Name is used to register the plugin. This can be arbitrary but should be a
// reasonable value. For an example, if the plugin in test is a secret backend
// that generates UUIDs with the name "vault-plugin-secrets-uuid", then "uuid"
// or "test-uuid" would be reasonable. The name is used for lookups in the
// catalog. See "name" in the "Register Plugin" endpoint docs:
// - https://www.vaultproject.io/api-docs/system/plugins-catalog#register-plugin
RegistryName string
// PluginType is the optional type of plugin. See PluginType const defined
// above
PluginType api.PluginType
// PluginName represents the name of the plugin that gets compiled. In the
// standard plugin project file layout, it represents the folder under the
// cmd/ folder. In the below example UUID project, the PluginName would be
// "uuid":
//
// vault-plugin-secrets-uuid/
// - backend.go
// - cmd/
// ----uuid/
// ------main.go
// - path_generate.go
//
PluginName string
}
// Step represents a single step of a test Case
type Step struct {
// Name of the step to be printed in the report
Name string
// Operation defines what action is being taken in this step; write, read,
// delete, et. al.
Operation Operation
// Path is the localized request path. The mount prefix, namespace, and
// optionally "auth" will be automatically added.
Path string
// Arguments to pass in the request. These arguments represent payloads sent
// to the API.
Data map[string]interface{}
// Alternatively get data from a function
GetData func() (map[string]interface{}, error)
// BodyData is the data to pass to a read or delete request
BodyData map[string][]string
// Assert is a function that is called after this step is executed in order to
// test that the step executed successfully. If this is not set, then the next
// step will be called
Assert AssertionFunc
// Unauthenticated will make the request unauthenticated.
Unauthenticated bool
}
// AssertionFunc is the callback used for Assert in Steps.
type AssertionFunc func(*api.Secret, error) error
// Case represents a scenario we want to test which involves a series of
// steps to be followed sequentially, evaluating the results after each step.
type Case struct {
// Environment is used to setup the Vault instance and provide the client that
// will be used to drive the tests
Environment Environment
// Precheck enabls a test case to determine if it should run or not
Precheck func()
// Steps are the set of operations that are run for this test case. During
// execution each step will be logged to output with a 1-based index as it is
// ran, with the first step logged as step '1' and not step '0'.
Steps []Step
// SkipTeardown allows the Environment TeardownFunc to be skipped, leaving any
// infrastructure created after the test exists. This is useful for debugging
// during plugin development to examine the state of the Vault cluster after a
// test runs. Depending on the Environment used this could incur costs the
// user is responsible for.
SkipTeardown bool
}
// Run performs an acceptance test on a backend with the given test case.
//
// Tests are not run unless an environmental variable "VAULT_ACC" is
// set to some non-empty value. This is to avoid test cases surprising
// a user by creating real resources.
//
// Tests will fail unless the verbose flag (`go test -v`, or explicitly
// the "-test.v" flag) is set. Because some acceptance tests take quite
// long, we require the verbose flag so users are able to see progress
// output.
func Run(tt TestT, c Case) {
tt.Helper()
// We only run acceptance tests if an env var is set because they're
// slow and generally require some outside configuration.
checkShouldRun(tt)
if c.Precheck != nil {
c.Precheck()
}
if c.Environment == nil {
tt.Fatal("nil driver in acceptance test")
// return here only used during testing when using mockT type, otherwise
// Fatal will exit
return
}
logger := logging.NewVaultLogger(log.Trace)
if err := c.Environment.Setup(); err != nil {
tt.Fatal(err)
}
defer func() {
if c.SkipTeardown {
logger.Info("driver Teardown skipped")
return
}
if err := c.Environment.Teardown(); err != nil {
logger.Error("error in driver teardown:", "error", err)
}
}()
// retrieve the root client from the Environment. If this returns an error,
// fail immediately
rootClient, err := c.Environment.Client()
if err != nil {
tt.Fatal(err)
}
// Trap the rootToken so that we can preform revocation or other tasks in the
// event any steps remove the token during testing.
rootToken := c.Environment.RootToken()
// Defer revocation of any secrets created. We intentionally enclose the
// responses slice so in the event of a fatal error during test evaluation, we
// are still able to revoke any leases/secrets created
var responses []*api.Secret
defer func() {
// restore root token for admin tasks
rootClient.SetToken(rootToken)
// failedRevokes tracks any errors we get when attempting to revoke a lease
// to log to users at the end of the test.
var failedRevokes []*api.Secret
for _, secret := range responses {
if secret.LeaseID == "" {
continue
}
if err := rootClient.Sys().Revoke(secret.LeaseID); err != nil {
tt.Error(fmt.Errorf("error revoking lease: %w", err))
failedRevokes = append(failedRevokes, secret)
continue
}
}
// If we have any failed revokes, log it.
if len(failedRevokes) > 0 {
for _, s := range failedRevokes {
tt.Error(fmt.Sprintf(
"WARNING: Revoking the following secret failed. It may\n"+
"still exist. Please verify:\n\n%#v",
s))
}
}
}()
stepCount := len(c.Steps)
for i, step := range c.Steps {
if logger.IsWarn() {
// range is zero based, so add 1 for a human friendly output of steps.
progress := fmt.Sprintf("%d/%d", i+1, stepCount)
logger.Info("Executing test step", "step_number", progress, "name", step.Name)
}
// reset token in case it was cleared
client, err := rootClient.Clone()
if err != nil {
tt.Fatal(err)
}
client.SetToken(rootToken)
resp, respErr := makeRequest(tt, c.Environment, step)
if resp != nil {
responses = append(responses, resp)
}
// Run the associated AssertionFunc, if any. If an error was expected it is
// sent to the Assert function to validate.
if step.Assert != nil {
if err := step.Assert(resp, respErr); err != nil {
tt.Error(fmt.Errorf("failed step %d: %w", i+1, err))
}
}
}
}
func makeRequest(tt TestT, env Environment, step Step) (*api.Secret, error) {
tt.Helper()
client, err := env.Client()
if err != nil {
return nil, err
}
if step.Unauthenticated {
token := client.Token()
client.ClearToken()
// restore the client token after this request completes
defer func() {
client.SetToken(token)
}()
}
path := fmt.Sprintf("%s/%s", env.MountPath(), step.Path)
// Step.GetData supersedes Step.Data
data := step.Data
if step.GetData != nil {
data, err = step.GetData()
if err != nil {
return nil, err
}
}
switch step.Operation {
case WriteOperation, UpdateOperation:
return client.Logical().Write(path, data)
case ReadOperation:
if step.BodyData != nil {
return client.Logical().ReadWithData(path, step.BodyData)
}
return client.Logical().Read(path)
case ListOperation:
return client.Logical().List(path)
case DeleteOperation:
if step.BodyData != nil {
return client.Logical().DeleteWithData(path, step.BodyData)
}
return client.Logical().Delete(path)
default:
return nil, fmt.Errorf("invalid operation: %s", step.Operation)
}
}
func checkShouldRun(tt TestT) {
tt.Helper()
if os.Getenv(TestEnvVar) == "" {
tt.Skip(fmt.Sprintf(
"Acceptance tests skipped unless env '%s' set",
TestEnvVar))
return
}
// We require verbose mode so that the user knows what is going on.
if !testing.Verbose() {
tt.Fatal("Acceptance tests must be run with the -v flag on tests")
}
}
// TestT is the interface used to handle the test lifecycle of a test.
//
// Users should just use a *testing.T object, which implements this.
type TestT interface {
Error(args ...interface{})
Fatal(args ...interface{})
Skip(args ...interface{})
Helper()
}