-
Notifications
You must be signed in to change notification settings - Fork 9.5k
/
provider.go
575 lines (521 loc) · 21.6 KB
/
provider.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
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
package moduletest
import (
"fmt"
"log"
"sync"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/gocty"
ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/repl"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// Provider is an implementation of providers.Interface which we're
// using as a likely-only-temporary vehicle for research on an opinionated
// module testing workflow in Terraform.
//
// We expose this to configuration as "terraform.io/builtin/test", but
// any attempt to configure it will emit a warning that it is experimental
// and likely to change or be removed entirely in future Terraform CLI
// releases.
//
// The testing provider exists to gather up test results during a Terraform
// apply operation. Its "test_results" managed resource type doesn't have any
// user-visible effect on its own, but when used in conjunction with the
// "terraform test" experimental command it is the intermediary that holds
// the test results while the test runs, so that the test command can then
// report them.
//
// For correct behavior of the assertion tracking, the "terraform test"
// command must be sure to use the same instance of Provider for both the
// plan and apply steps, so that the assertions that were planned can still
// be tracked during apply. For other commands that don't explicitly support
// test assertions, the provider will still succeed but the assertions data
// may not be complete if the apply step fails.
type Provider struct {
// components tracks all of the "component" names that have been
// used in test assertions resources so far. Each resource must have
// a unique component name.
components map[string]*Component
// Must lock mutex in order to interact with the components map, because
// test assertions can potentially run concurrently.
mutex sync.RWMutex
}
var _ providers.Interface = (*Provider)(nil)
// NewProvider returns a new instance of the test provider.
func NewProvider() *Provider {
return &Provider{
components: make(map[string]*Component),
}
}
// TestResults returns the current record of test results tracked inside the
// provider.
//
// The result is a direct reference to the internal state of the provider,
// so the caller mustn't modify it nor store it across calls to provider
// operations.
func (p *Provider) TestResults() map[string]*Component {
return p.components
}
// Reset returns the recieving provider back to its original state, with no
// recorded test results.
//
// It additionally detaches the instance from any data structure previously
// returned by method TestResults, freeing the caller from the constraints
// in its documentation about mutability and storage.
//
// For convenience in the presumed common case of resetting as part of
// capturing the results for storage, this method also returns the result
// that method TestResults would've returned if called prior to the call
// to Reset.
func (p *Provider) Reset() map[string]*Component {
p.mutex.Lock()
log.Print("[TRACE] moduletest.Provider: Reset")
ret := p.components
p.components = make(map[string]*Component)
p.mutex.Unlock()
return ret
}
// GetProviderSchema returns the complete schema for the provider.
func (p *Provider) GetProviderSchema() providers.GetProviderSchemaResponse {
return providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_assertions": testAssertionsSchema,
},
}
}
// ValidateProviderConfig validates the provider configuration.
func (p *Provider) ValidateProviderConfig(req providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse {
// This provider has no configurable settings, so nothing to validate.
var res providers.ValidateProviderConfigResponse
return res
}
// ConfigureProvider configures and initializes the provider.
func (p *Provider) ConfigureProvider(providers.ConfigureProviderRequest) providers.ConfigureProviderResponse {
// This provider has no configurable settings, but we use the configure
// request as an opportunity to generate a warning about it being
// experimental.
var res providers.ConfigureProviderResponse
res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
tfdiags.Warning,
"The test provider is experimental",
"The Terraform team is using the test provider (terraform.io/builtin/test) as part of ongoing research about declarative testing of Terraform modules.\n\nThe availability and behavior of this provider is expected to change significantly even in patch releases, so we recommend using this provider only in test configurations and constraining your test configurations to an exact Terraform version.",
nil,
))
return res
}
// ValidateResourceConfig is used to validate configuration values for a resource.
func (p *Provider) ValidateResourceConfig(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse {
log.Print("[TRACE] moduletest.Provider: ValidateResourceConfig")
var res providers.ValidateResourceConfigResponse
if req.TypeName != "test_assertions" { // we only have one resource type
res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported resource type %s", req.TypeName))
return res
}
config := req.Config
if !config.GetAttr("component").IsKnown() {
res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid component expression",
"The component name must be a static value given in the configuration, and may not be derived from a resource type attribute that will only be known during the apply step.",
cty.GetAttrPath("component"),
))
}
if !hclsyntax.ValidIdentifier(config.GetAttr("component").AsString()) {
res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid component name",
"The component name must be a valid identifier, starting with a letter followed by zero or more letters, digits, and underscores.",
cty.GetAttrPath("component"),
))
}
for it := config.GetAttr("equal").ElementIterator(); it.Next(); {
k, obj := it.Element()
if !hclsyntax.ValidIdentifier(k.AsString()) {
res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid assertion name",
"An assertion name must be a valid identifier, starting with a letter followed by zero or more letters, digits, and underscores.",
cty.GetAttrPath("equal").Index(k),
))
}
if !obj.GetAttr("description").IsKnown() {
res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid description expression",
"The description must be a static value given in the configuration, and may not be derived from a resource type attribute that will only be known during the apply step.",
cty.GetAttrPath("equal").Index(k).GetAttr("description"),
))
}
}
for it := config.GetAttr("check").ElementIterator(); it.Next(); {
k, obj := it.Element()
if !hclsyntax.ValidIdentifier(k.AsString()) {
res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid assertion name",
"An assertion name must be a valid identifier, starting with a letter followed by zero or more letters, digits, and underscores.",
cty.GetAttrPath("check").Index(k),
))
}
if !obj.GetAttr("description").IsKnown() {
res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid description expression",
"The description must be a static value given in the configuration, and may not be derived from a resource type attribute that will only be known during the apply step.",
cty.GetAttrPath("equal").Index(k).GetAttr("description"),
))
}
}
return res
}
// ReadResource refreshes a resource and returns its current state.
func (p *Provider) ReadResource(req providers.ReadResourceRequest) providers.ReadResourceResponse {
log.Print("[TRACE] moduletest.Provider: ReadResource")
var res providers.ReadResourceResponse
if req.TypeName != "test_assertions" { // we only have one resource type
res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported resource type %s", req.TypeName))
return res
}
// Test assertions are not a real remote object, so there isn't actually
// anything to refresh here.
res.NewState = req.PriorState
return res
}
// UpgradeResourceState is called to allow the provider to adapt the raw value
// stored in the state in case the schema has changed since it was originally
// written.
func (p *Provider) UpgradeResourceState(req providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse {
log.Print("[TRACE] moduletest.Provider: UpgradeResourceState")
var res providers.UpgradeResourceStateResponse
if req.TypeName != "test_assertions" { // we only have one resource type
res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported resource type %s", req.TypeName))
return res
}
// We assume here that there can never be a flatmap version of this
// resource type's data, because this provider was never included in a
// version of Terraform that used flatmap and this provider's schema
// contains attributes that are not flatmap-compatible anyway.
if len(req.RawStateFlatmap) != 0 {
res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("can't upgrade a flatmap state for %q", req.TypeName))
return res
}
if req.Version != 0 {
res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("the state for this %s was created by a newer version of the provider", req.TypeName))
return res
}
v, err := ctyjson.Unmarshal(req.RawStateJSON, testAssertionsSchema.Block.ImpliedType())
if err != nil {
res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("failed to decode state for %s: %s", req.TypeName, err))
return res
}
res.UpgradedState = v
return res
}
// PlanResourceChange takes the current state and proposed state of a
// resource, and returns the planned final state.
func (p *Provider) PlanResourceChange(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
log.Print("[TRACE] moduletest.Provider: PlanResourceChange")
// this is a destroy plan,
if req.ProposedNewState.IsNull() {
resp.PlannedState = req.ProposedNewState
resp.PlannedPrivate = req.PriorPrivate
return resp
}
var res providers.PlanResourceChangeResponse
if req.TypeName != "test_assertions" { // we only have one resource type
res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported resource type %s", req.TypeName))
return res
}
// During planning, our job is to gather up all of the planned test
// assertions marked as pending, which will then allow us to include
// all of them in test results even if there's a failure during apply
// that prevents the full completion of the graph walk.
//
// In a sense our plan phase is similar to the compile step for a
// test program written in another language. Planning itself can fail,
// which means we won't be able to form a complete test plan at all,
// but if we succeed in planning then subsequent problems can be treated
// as test failures at "runtime", while still keeping a full manifest
// of all of the tests that ought to have run if the apply had run to
// completion.
proposed := req.ProposedNewState
res.PlannedState = proposed
componentName := proposed.GetAttr("component").AsString() // proven known during validate
p.mutex.Lock()
defer p.mutex.Unlock()
// NOTE: Ideally we'd do something here to verify if two assertions
// resources in the configuration attempt to declare the same component,
// but we can't actually do that because Terraform calls PlanResourceChange
// during both plan and apply, and so the second one would always fail.
// Since this is just providing a temporary pseudo-syntax for writing tests
// anyway, we'll live with this for now and aim to solve it with a future
// iteration of testing that's better integrated into the Terraform
// language.
/*
if _, exists := p.components[componentName]; exists {
res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Duplicate test component",
fmt.Sprintf("Another test_assertions resource already declared assertions for the component name %q.", componentName),
cty.GetAttrPath("component"),
))
return res
}
*/
component := Component{
Assertions: make(map[string]*Assertion),
}
for it := proposed.GetAttr("equal").ElementIterator(); it.Next(); {
k, obj := it.Element()
name := k.AsString()
if _, exists := component.Assertions[name]; exists {
// We can't actually get here in practice because so far we've
// only been pulling keys from one map, and so any duplicates
// would've been caught during config decoding, but this is here
// just to make these two blocks symmetrical to avoid mishaps in
// future refactoring/reorganization.
res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Duplicate test assertion",
fmt.Sprintf("Another assertion block in this resource already declared an assertion named %q.", name),
cty.GetAttrPath("equal").Index(k),
))
continue
}
var desc string
descVal := obj.GetAttr("description")
if descVal.IsNull() {
descVal = cty.StringVal("")
}
err := gocty.FromCtyValue(descVal, &desc)
if err != nil {
// We shouldn't get here because we've already validated everything
// that would make FromCtyValue fail above and during validate.
res.Diagnostics = res.Diagnostics.Append(err)
}
component.Assertions[name] = &Assertion{
Outcome: Pending,
Description: desc,
}
}
for it := proposed.GetAttr("check").ElementIterator(); it.Next(); {
k, obj := it.Element()
name := k.AsString()
if _, exists := component.Assertions[name]; exists {
res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Duplicate test assertion",
fmt.Sprintf("Another assertion block in this resource already declared an assertion named %q.", name),
cty.GetAttrPath("check").Index(k),
))
continue
}
var desc string
descVal := obj.GetAttr("description")
if descVal.IsNull() {
descVal = cty.StringVal("")
}
err := gocty.FromCtyValue(descVal, &desc)
if err != nil {
// We shouldn't get here because we've already validated everything
// that would make FromCtyValue fail above and during validate.
res.Diagnostics = res.Diagnostics.Append(err)
}
component.Assertions[name] = &Assertion{
Outcome: Pending,
Description: desc,
}
}
p.components[componentName] = &component
return res
}
// ApplyResourceChange takes the planned state for a resource, which may
// yet contain unknown computed values, and applies the changes returning
// the final state.
func (p *Provider) ApplyResourceChange(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
log.Print("[TRACE] moduletest.Provider: ApplyResourceChange")
var res providers.ApplyResourceChangeResponse
if req.TypeName != "test_assertions" { // we only have one resource type
res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported resource type %s", req.TypeName))
return res
}
// During apply we actually check the assertions and record the results.
// An assertion failure isn't reflected as an error from the apply call
// because if possible we'd like to continue exercising other objects
// downstream in case that allows us to gather more information to report.
// (If something downstream returns an error then that could prevent us
// from completing other assertions, though.)
planned := req.PlannedState
res.NewState = planned
if res.NewState.IsNull() {
// If we're destroying then we'll just quickly return success to
// allow the test process to clean up after itself.
return res
}
componentName := planned.GetAttr("component").AsString() // proven known during validate
p.mutex.Lock()
defer p.mutex.Unlock()
component := p.components[componentName]
if component == nil {
// We might get here when using this provider outside of the
// "terraform test" command, where there won't be any mechanism to
// preserve the test provider instance between the plan and apply
// phases. In that case, we assume that nobody will come looking to
// collect the results anyway, and so we can just silently skip
// checking.
return res
}
for it := planned.GetAttr("equal").ElementIterator(); it.Next(); {
k, obj := it.Element()
name := k.AsString()
var desc string
if plan, exists := component.Assertions[name]; exists {
desc = plan.Description
}
assert := &Assertion{
Outcome: Pending,
Description: desc,
}
gotVal := obj.GetAttr("got")
wantVal := obj.GetAttr("want")
switch {
case wantVal.RawEquals(gotVal):
assert.Outcome = Passed
gotStr := repl.FormatValue(gotVal, 4)
assert.Message = fmt.Sprintf("correct value\n got: %s\n", gotStr)
default:
assert.Outcome = Failed
gotStr := repl.FormatValue(gotVal, 4)
wantStr := repl.FormatValue(wantVal, 4)
assert.Message = fmt.Sprintf("wrong value\n got: %s\n want: %s\n", gotStr, wantStr)
}
component.Assertions[name] = assert
}
for it := planned.GetAttr("check").ElementIterator(); it.Next(); {
k, obj := it.Element()
name := k.AsString()
var desc string
if plan, exists := component.Assertions[name]; exists {
desc = plan.Description
}
assert := &Assertion{
Outcome: Pending,
Description: desc,
}
condVal := obj.GetAttr("condition")
switch {
case condVal.IsNull():
res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid check condition",
"The condition value must be a boolean expression, not null.",
cty.GetAttrPath("check").Index(k).GetAttr("condition"),
))
continue
case condVal.True():
assert.Outcome = Passed
assert.Message = "condition passed"
default:
assert.Outcome = Failed
// For "check" we can't really return a decent error message
// because we've lost all of the context by the time we get here.
// "equal" will be better for most tests for that reason, and also
// this is one reason why in the long run it would be better for
// test assertions to be a first-class language feature rather than
// just a provider-based concept.
assert.Message = "condition failed"
}
component.Assertions[name] = assert
}
return res
}
// ImportResourceState requests that the given resource be imported.
func (p *Provider) ImportResourceState(req providers.ImportResourceStateRequest) providers.ImportResourceStateResponse {
var res providers.ImportResourceStateResponse
res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("%s is not importable", req.TypeName))
return res
}
// ValidateDataResourceConfig is used to to validate the resource configuration values.
func (p *Provider) ValidateDataResourceConfig(req providers.ValidateDataResourceConfigRequest) providers.ValidateDataResourceConfigResponse {
// This provider has no data resouce types at all.
var res providers.ValidateDataResourceConfigResponse
res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported data source %s", req.TypeName))
return res
}
// ReadDataSource returns the data source's current state.
func (p *Provider) ReadDataSource(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
// This provider has no data resouce types at all.
var res providers.ReadDataSourceResponse
res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported data source %s", req.TypeName))
return res
}
// Stop is called when the provider should halt any in-flight actions.
func (p *Provider) Stop() error {
// This provider doesn't do anything that can be cancelled.
return nil
}
// Close is a noop for this provider, since it's run in-process.
func (p *Provider) Close() error {
return nil
}
var testAssertionsSchema = providers.Schema{
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"component": {
Type: cty.String,
Description: "The name of the component being tested. This is just for namespacing assertions in a result report.",
DescriptionKind: configschema.StringPlain,
Required: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"equal": {
Nesting: configschema.NestingMap,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"description": {
Type: cty.String,
Description: "An optional human-readable description of what's being tested by this assertion.",
DescriptionKind: configschema.StringPlain,
Required: true,
},
"got": {
Type: cty.DynamicPseudoType,
Description: "The actual result value generated by the relevant component.",
DescriptionKind: configschema.StringPlain,
Required: true,
},
"want": {
Type: cty.DynamicPseudoType,
Description: "The value that the component is expected to have generated.",
DescriptionKind: configschema.StringPlain,
Required: true,
},
},
},
},
"check": {
Nesting: configschema.NestingMap,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"description": {
Type: cty.String,
Description: "An optional (but strongly recommended) human-readable description of what's being tested by this assertion.",
DescriptionKind: configschema.StringPlain,
Required: true,
},
"condition": {
Type: cty.Bool,
Description: "An expression that must be true in order for the test to pass.",
DescriptionKind: configschema.StringPlain,
Required: true,
},
},
},
},
},
},
}