This repository has been archived by the owner on Apr 27, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 7
/
working_dir.go
394 lines (338 loc) · 11.8 KB
/
working_dir.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
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
package tftest
import (
"bytes"
"context"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/hashicorp/terraform-exec/tfexec"
tfjson "github.com/hashicorp/terraform-json"
)
const (
ConfigFileName = "terraform_plugin_test.tf"
PlanFileName = "tfplan"
)
// WorkingDir represents a distinct working directory that can be used for
// running tests. Each test should construct its own WorkingDir by calling
// NewWorkingDir or RequireNewWorkingDir on its package's singleton
// tftest.Helper.
type WorkingDir struct {
h *Helper
// baseDir is the root of the working directory tree
baseDir string
// baseArgs is arguments that should be appended to all commands
baseArgs []string
// tf is the instance of tfexec.Terraform used for running Terraform commands
tf *tfexec.Terraform
// terraformExec is a path to a terraform binary, inherited from Helper
terraformExec string
// reattachInfo stores the gRPC socket info required for Terraform's
// plugin reattach functionality
reattachInfo tfexec.ReattachInfo
env map[string]string
}
// Close deletes the directories and files created to represent the receiving
// working directory. After this method is called, the working directory object
// is invalid and may no longer be used.
func (wd *WorkingDir) Close() error {
return os.RemoveAll(wd.baseDir)
}
// Setenv sets an environment variable on the WorkingDir.
func (wd *WorkingDir) Setenv(envVar, val string) {
if wd.env == nil {
wd.env = map[string]string{}
}
wd.env[envVar] = val
}
// Unsetenv removes an environment variable from the WorkingDir.
func (wd *WorkingDir) Unsetenv(envVar string) {
delete(wd.env, envVar)
}
func (wd *WorkingDir) SetReattachInfo(reattachInfo tfexec.ReattachInfo) {
wd.reattachInfo = reattachInfo
}
func (wd *WorkingDir) UnsetReattachInfo() {
wd.reattachInfo = nil
}
// GetHelper returns the Helper set on the WorkingDir.
func (wd *WorkingDir) GetHelper() *Helper {
return wd.h
}
// SetConfig sets a new configuration for the working directory.
//
// This must be called at least once before any call to Init, Plan, Apply, or
// Destroy to establish the configuration. Any previously-set configuration is
// discarded and any saved plan is cleared.
func (wd *WorkingDir) SetConfig(cfg string) error {
configFilename := filepath.Join(wd.baseDir, ConfigFileName)
err := ioutil.WriteFile(configFilename, []byte(cfg), 0700)
if err != nil {
return err
}
var mismatch *tfexec.ErrVersionMismatch
err = wd.tf.SetDisablePluginTLS(true)
if err != nil && !errors.As(err, &mismatch) {
return err
}
err = wd.tf.SetSkipProviderVerify(true)
if err != nil && !errors.As(err, &mismatch) {
return err
}
if p := os.Getenv("TF_ACC_LOG_PATH"); p != "" {
wd.tf.SetLogPath(p)
}
// Changing configuration invalidates any saved plan.
err = wd.ClearPlan()
if err != nil {
return err
}
return nil
}
// RequireSetConfig is a variant of SetConfig that will fail the test via the
// given TestControl if the configuration cannot be set.
func (wd *WorkingDir) RequireSetConfig(t TestControl, cfg string) {
t.Helper()
if err := wd.SetConfig(cfg); err != nil {
t := testingT{t}
t.Fatalf("failed to set config: %s", err)
}
}
// ClearState deletes any Terraform state present in the working directory.
//
// Any remote objects tracked by the state are not destroyed first, so this
// will leave them dangling in the remote system.
func (wd *WorkingDir) ClearState() error {
err := os.Remove(filepath.Join(wd.baseDir, "terraform.tfstate"))
if os.IsNotExist(err) {
return nil
}
return err
}
// RequireClearState is a variant of ClearState that will fail the test via the
// given TestControl if the state cannot be cleared.
func (wd *WorkingDir) RequireClearState(t TestControl) {
t.Helper()
if err := wd.ClearState(); err != nil {
t := testingT{t}
t.Fatalf("failed to clear state: %s", err)
}
}
// ClearPlan deletes any saved plan present in the working directory.
func (wd *WorkingDir) ClearPlan() error {
err := os.Remove(wd.planFilename())
if os.IsNotExist(err) {
return nil
}
return err
}
// RequireClearPlan is a variant of ClearPlan that will fail the test via the
// given TestControl if the plan cannot be cleared.
func (wd *WorkingDir) RequireClearPlan(t TestControl) {
t.Helper()
if err := wd.ClearPlan(); err != nil {
t := testingT{t}
t.Fatalf("failed to clear plan: %s", err)
}
}
// Init runs "terraform init" for the given working directory, forcing Terraform
// to use the current version of the plugin under test.
func (wd *WorkingDir) Init() error {
if _, err := os.Stat(wd.configFilename()); err != nil {
return fmt.Errorf("must call SetConfig before Init")
}
return wd.tf.Init(context.Background(), tfexec.Reattach(wd.reattachInfo))
}
func (wd *WorkingDir) configFilename() string {
return filepath.Join(wd.baseDir, ConfigFileName)
}
// RequireInit is a variant of Init that will fail the test via the given
// TestControl if init fails.
func (wd *WorkingDir) RequireInit(t TestControl) {
t.Helper()
if err := wd.Init(); err != nil {
t := testingT{t}
t.Fatalf("init failed: %s", err)
}
}
func (wd *WorkingDir) planFilename() string {
return filepath.Join(wd.baseDir, PlanFileName)
}
// CreatePlan runs "terraform plan" to create a saved plan file, which if successful
// will then be used for the next call to Apply.
func (wd *WorkingDir) CreatePlan() error {
_, err := wd.tf.Plan(context.Background(), tfexec.Reattach(wd.reattachInfo), tfexec.Refresh(false), tfexec.Out(PlanFileName))
return err
}
// RequireCreatePlan is a variant of CreatePlan that will fail the test via
// the given TestControl if plan creation fails.
func (wd *WorkingDir) RequireCreatePlan(t TestControl) {
t.Helper()
if err := wd.CreatePlan(); err != nil {
t := testingT{t}
t.Fatalf("failed to create plan: %s", err)
}
}
// CreateDestroyPlan runs "terraform plan -destroy" to create a saved plan
// file, which if successful will then be used for the next call to Apply.
func (wd *WorkingDir) CreateDestroyPlan() error {
_, err := wd.tf.Plan(context.Background(), tfexec.Reattach(wd.reattachInfo), tfexec.Refresh(false), tfexec.Out(PlanFileName), tfexec.Destroy(true))
return err
}
// Apply runs "terraform apply". If CreatePlan has previously completed
// successfully and the saved plan has not been cleared in the meantime then
// this will apply the saved plan. Otherwise, it will implicitly create a new
// plan and apply it.
func (wd *WorkingDir) Apply() error {
args := []tfexec.ApplyOption{tfexec.Reattach(wd.reattachInfo), tfexec.Refresh(false)}
if wd.HasSavedPlan() {
args = append(args, tfexec.DirOrPlan(PlanFileName))
}
return wd.tf.Apply(context.Background(), args...)
}
// RequireApply is a variant of Apply that will fail the test via
// the given TestControl if the apply operation fails.
func (wd *WorkingDir) RequireApply(t TestControl) {
t.Helper()
if err := wd.Apply(); err != nil {
t := testingT{t}
t.Fatalf("failed to apply: %s", err)
}
}
// Destroy runs "terraform destroy". It does not consider or modify any saved
// plan, and is primarily for cleaning up at the end of a test run.
//
// If destroy fails then remote objects might still exist, and continue to
// exist after a particular test is concluded.
func (wd *WorkingDir) Destroy() error {
return wd.tf.Destroy(context.Background(), tfexec.Reattach(wd.reattachInfo), tfexec.Refresh(false))
}
// RequireDestroy is a variant of Destroy that will fail the test via
// the given TestControl if the destroy operation fails.
//
// If destroy fails then remote objects might still exist, and continue to
// exist after a particular test is concluded.
func (wd *WorkingDir) RequireDestroy(t TestControl) {
t.Helper()
if err := wd.Destroy(); err != nil {
t := testingT{t}
t.Logf("WARNING: destroy failed, so remote objects may still exist and be subject to billing")
t.Fatalf("failed to destroy: %s", err)
}
}
// HasSavedPlan returns true if there is a saved plan in the working directory. If
// so, a subsequent call to Apply will apply that saved plan.
func (wd *WorkingDir) HasSavedPlan() bool {
_, err := os.Stat(wd.planFilename())
return err == nil
}
// SavedPlan returns an object describing the current saved plan file, if any.
//
// If no plan is saved or if the plan file cannot be read, SavedPlan returns
// an error.
func (wd *WorkingDir) SavedPlan() (*tfjson.Plan, error) {
if !wd.HasSavedPlan() {
return nil, fmt.Errorf("there is no current saved plan")
}
return wd.tf.ShowPlanFile(context.Background(), wd.planFilename(), tfexec.Reattach(wd.reattachInfo))
}
// RequireSavedPlan is a variant of SavedPlan that will fail the test via
// the given TestControl if the plan cannot be read.
func (wd *WorkingDir) RequireSavedPlan(t TestControl) *tfjson.Plan {
t.Helper()
ret, err := wd.SavedPlan()
if err != nil {
t := testingT{t}
t.Fatalf("failed to read saved plan: %s", err)
}
return ret
}
// SavedPlanStdout returns a stdout capture of the current saved plan file, if any.
//
// If no plan is saved or if the plan file cannot be read, SavedPlanStdout returns
// an error.
func (wd *WorkingDir) SavedPlanStdout() (string, error) {
if !wd.HasSavedPlan() {
return "", fmt.Errorf("there is no current saved plan")
}
var ret bytes.Buffer
wd.tf.SetStdout(&ret)
defer wd.tf.SetStdout(ioutil.Discard)
_, err := wd.tf.ShowPlanFileRaw(context.Background(), wd.planFilename(), tfexec.Reattach(wd.reattachInfo))
if err != nil {
return "", err
}
return ret.String(), nil
}
// RequireSavedPlanStdout is a variant of SavedPlanStdout that will fail the test via
// the given TestControl if the plan cannot be read.
func (wd *WorkingDir) RequireSavedPlanStdout(t TestControl) string {
t.Helper()
ret, err := wd.SavedPlanStdout()
if err != nil {
t := testingT{t}
t.Fatalf("failed to read saved plan: %s", err)
}
return ret
}
// State returns an object describing the current state.
//
// If the state cannot be read, State returns an error.
func (wd *WorkingDir) State() (*tfjson.State, error) {
return wd.tf.Show(context.Background(), tfexec.Reattach(wd.reattachInfo))
}
// RequireState is a variant of State that will fail the test via
// the given TestControl if the state cannot be read.
func (wd *WorkingDir) RequireState(t TestControl) *tfjson.State {
t.Helper()
ret, err := wd.State()
if err != nil {
t := testingT{t}
t.Fatalf("failed to read state plan: %s", err)
}
return ret
}
// Import runs terraform import
func (wd *WorkingDir) Import(resource, id string) error {
return wd.tf.Import(context.Background(), resource, id, tfexec.Config(wd.baseDir), tfexec.Reattach(wd.reattachInfo))
}
// RequireImport is a variant of Import that will fail the test via
// the given TestControl if the import is non successful.
func (wd *WorkingDir) RequireImport(t TestControl, resource, id string) {
t.Helper()
if err := wd.Import(resource, id); err != nil {
t := testingT{t}
t.Fatalf("failed to import: %s", err)
}
}
// Refresh runs terraform refresh
func (wd *WorkingDir) Refresh() error {
return wd.tf.Refresh(context.Background(), tfexec.Reattach(wd.reattachInfo), tfexec.State(filepath.Join(wd.baseDir, "terraform.tfstate")))
}
// RequireRefresh is a variant of Refresh that will fail the test via
// the given TestControl if the refresh is non successful.
func (wd *WorkingDir) RequireRefresh(t TestControl) {
t.Helper()
if err := wd.Refresh(); err != nil {
t := testingT{t}
t.Fatalf("failed to refresh: %s", err)
}
}
// Schemas returns an object describing the provider schemas.
//
// If the schemas cannot be read, Schemas returns an error.
func (wd *WorkingDir) Schemas() (*tfjson.ProviderSchemas, error) {
return wd.tf.ProvidersSchema(context.Background())
}
// RequireSchemas is a variant of Schemas that will fail the test via
// the given TestControl if the schemas cannot be read.
func (wd *WorkingDir) RequireSchemas(t TestControl) *tfjson.ProviderSchemas {
t.Helper()
ret, err := wd.Schemas()
if err != nil {
t := testingT{t}
t.Fatalf("failed to read schemas: %s", err)
}
return ret
}