/
working_dir.go
340 lines (252 loc) · 10.7 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
package plugintest
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/hashicorp/terraform-exec/tfexec"
tfjson "github.com/hashicorp/terraform-json"
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging"
)
const (
ConfigFileName = "terraform_plugin_test.tf"
ConfigFileNameJSON = ConfigFileName + ".json"
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
// plugintest.Helper.
type WorkingDir struct {
h *Helper
// baseDir is the root of the working directory tree
baseDir string
// configFilename is the full filename where the latest configuration
// was stored; empty until SetConfig is called.
configFilename 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
}
// 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)
}
func (wd *WorkingDir) SetReattachInfo(ctx context.Context, reattachInfo tfexec.ReattachInfo) {
logging.HelperResourceTrace(ctx, "Setting Terraform CLI reattach configuration", map[string]interface{}{"tf_reattach_config": 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(ctx context.Context, cfg string) error {
outFilename := filepath.Join(wd.baseDir, ConfigFileName)
rmFilename := filepath.Join(wd.baseDir, ConfigFileNameJSON)
bCfg := []byte(cfg)
if json.Valid(bCfg) {
outFilename, rmFilename = rmFilename, outFilename
}
if err := os.Remove(rmFilename); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("unable to remove %q: %w", rmFilename, err)
}
err := ioutil.WriteFile(outFilename, bCfg, 0700)
if err != nil {
return err
}
wd.configFilename = outFilename
// Changing configuration invalidates any saved plan.
err = wd.ClearPlan(ctx)
if err != nil {
return err
}
return nil
}
// 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(ctx context.Context) error {
logging.HelperResourceTrace(ctx, "Clearing Terraform state")
err := os.Remove(filepath.Join(wd.baseDir, "terraform.tfstate"))
if os.IsNotExist(err) {
logging.HelperResourceTrace(ctx, "No Terraform state to clear")
return nil
}
if err != nil {
return err
}
logging.HelperResourceTrace(ctx, "Cleared Terraform state")
return nil
}
// ClearPlan deletes any saved plan present in the working directory.
func (wd *WorkingDir) ClearPlan(ctx context.Context) error {
logging.HelperResourceTrace(ctx, "Clearing Terraform plan")
err := os.Remove(wd.planFilename())
if os.IsNotExist(err) {
logging.HelperResourceTrace(ctx, "No Terraform plan to clear")
return nil
}
if err != nil {
return err
}
logging.HelperResourceTrace(ctx, "Cleared Terraform plan")
return nil
}
var errWorkingDirSetConfigNotCalled = fmt.Errorf("must call SetConfig before Init")
// 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(ctx context.Context) error {
if wd.configFilename == "" {
return errWorkingDirSetConfigNotCalled
}
if _, err := os.Stat(wd.configFilename); err != nil {
return errWorkingDirSetConfigNotCalled
}
logging.HelperResourceTrace(ctx, "Calling Terraform CLI init command")
// -upgrade=true is required for per-TestStep provider version changes
// e.g. TestTest_TestStep_ExternalProviders_DifferentVersions
err := wd.tf.Init(context.Background(), tfexec.Reattach(wd.reattachInfo), tfexec.Upgrade(true))
logging.HelperResourceTrace(ctx, "Called Terraform CLI init command")
return 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(ctx context.Context) error {
logging.HelperResourceTrace(ctx, "Calling Terraform CLI plan command")
_, err := wd.tf.Plan(context.Background(), tfexec.Reattach(wd.reattachInfo), tfexec.Refresh(false), tfexec.Out(PlanFileName))
logging.HelperResourceTrace(ctx, "Called Terraform CLI plan command")
return 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(ctx context.Context) error {
logging.HelperResourceTrace(ctx, "Calling Terraform CLI plan -destroy command")
_, err := wd.tf.Plan(context.Background(), tfexec.Reattach(wd.reattachInfo), tfexec.Refresh(false), tfexec.Out(PlanFileName), tfexec.Destroy(true))
logging.HelperResourceTrace(ctx, "Called Terraform CLI plan -destroy command")
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(ctx context.Context) error {
args := []tfexec.ApplyOption{tfexec.Reattach(wd.reattachInfo), tfexec.Refresh(false)}
if wd.HasSavedPlan() {
args = append(args, tfexec.DirOrPlan(PlanFileName))
}
logging.HelperResourceTrace(ctx, "Calling Terraform CLI apply command")
err := wd.tf.Apply(context.Background(), args...)
logging.HelperResourceTrace(ctx, "Called Terraform CLI apply command")
return 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(ctx context.Context) error {
logging.HelperResourceTrace(ctx, "Calling Terraform CLI destroy command")
err := wd.tf.Destroy(context.Background(), tfexec.Reattach(wd.reattachInfo), tfexec.Refresh(false))
logging.HelperResourceTrace(ctx, "Called Terraform CLI destroy command")
return 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(ctx context.Context) (*tfjson.Plan, error) {
if !wd.HasSavedPlan() {
return nil, fmt.Errorf("there is no current saved plan")
}
logging.HelperResourceTrace(ctx, "Calling Terraform CLI apply command")
plan, err := wd.tf.ShowPlanFile(context.Background(), wd.planFilename(), tfexec.Reattach(wd.reattachInfo))
logging.HelperResourceTrace(ctx, "Calling Terraform CLI apply command")
return plan, err
}
// SavedPlanRawStdout returns a human readable stdout capture of the current saved plan file, if any.
//
// If no plan is saved or if the plan file cannot be read, SavedPlanRawStdout returns
// an error.
func (wd *WorkingDir) SavedPlanRawStdout(ctx context.Context) (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)
logging.HelperResourceTrace(ctx, "Calling Terraform CLI show command")
_, err := wd.tf.ShowPlanFileRaw(context.Background(), wd.planFilename(), tfexec.Reattach(wd.reattachInfo))
logging.HelperResourceTrace(ctx, "Called Terraform CLI show command")
if err != nil {
return "", err
}
return ret.String(), nil
}
// State returns an object describing the current state.
//
// If the state cannot be read, State returns an error.
func (wd *WorkingDir) State(ctx context.Context) (*tfjson.State, error) {
logging.HelperResourceTrace(ctx, "Calling Terraform CLI show command")
state, err := wd.tf.Show(context.Background(), tfexec.Reattach(wd.reattachInfo))
logging.HelperResourceTrace(ctx, "Called Terraform CLI show command")
return state, err
}
// Import runs terraform import
func (wd *WorkingDir) Import(ctx context.Context, resource, id string) error {
logging.HelperResourceTrace(ctx, "Calling Terraform CLI import command")
err := wd.tf.Import(context.Background(), resource, id, tfexec.Config(wd.baseDir), tfexec.Reattach(wd.reattachInfo))
logging.HelperResourceTrace(ctx, "Called Terraform CLI import command")
return err
}
// Taint runs terraform taint
func (wd *WorkingDir) Taint(ctx context.Context, address string) error {
logging.HelperResourceTrace(ctx, "Calling Terraform CLI taint command")
err := wd.tf.Taint(context.Background(), address)
logging.HelperResourceTrace(ctx, "Called Terraform CLI import command")
return err
}
// Refresh runs terraform refresh
func (wd *WorkingDir) Refresh(ctx context.Context) error {
logging.HelperResourceTrace(ctx, "Calling Terraform CLI refresh command")
err := wd.tf.Refresh(context.Background(), tfexec.Reattach(wd.reattachInfo))
logging.HelperResourceTrace(ctx, "Called Terraform CLI refresh command")
return err
}
// Schemas returns an object describing the provider schemas.
//
// If the schemas cannot be read, Schemas returns an error.
func (wd *WorkingDir) Schemas(ctx context.Context) (*tfjson.ProviderSchemas, error) {
logging.HelperResourceTrace(ctx, "Calling Terraform CLI providers schema command")
providerSchemas, err := wd.tf.ProvidersSchema(context.Background())
logging.HelperResourceTrace(ctx, "Called Terraform CLI providers schema command")
return providerSchemas, err
}