]>
Commit | Line | Data |
---|---|---|
bae9f6d2 JC |
1 | package terraform |
2 | ||
3 | import ( | |
4 | "fmt" | |
107c1cdb ND |
5 | "log" |
6 | ||
7 | "github.com/hashicorp/hcl2/hcl" | |
8 | "github.com/hashicorp/terraform/addrs" | |
9 | "github.com/hashicorp/terraform/configs" | |
10 | "github.com/hashicorp/terraform/configs/configschema" | |
11 | "github.com/hashicorp/terraform/providers" | |
12 | "github.com/hashicorp/terraform/provisioners" | |
13 | "github.com/hashicorp/terraform/tfdiags" | |
14 | "github.com/zclconf/go-cty/cty" | |
15 | "github.com/zclconf/go-cty/cty/convert" | |
16 | "github.com/zclconf/go-cty/cty/gocty" | |
bae9f6d2 JC |
17 | ) |
18 | ||
bae9f6d2 JC |
19 | // EvalValidateCount is an EvalNode implementation that validates |
20 | // the count of a resource. | |
21 | type EvalValidateCount struct { | |
107c1cdb | 22 | Resource *configs.Resource |
bae9f6d2 JC |
23 | } |
24 | ||
25 | // TODO: test | |
26 | func (n *EvalValidateCount) Eval(ctx EvalContext) (interface{}, error) { | |
107c1cdb | 27 | var diags tfdiags.Diagnostics |
bae9f6d2 | 28 | var count int |
bae9f6d2 | 29 | var err error |
107c1cdb ND |
30 | |
31 | val, valDiags := ctx.EvaluateExpr(n.Resource.Count, cty.Number, nil) | |
32 | diags = diags.Append(valDiags) | |
33 | if valDiags.HasErrors() { | |
bae9f6d2 JC |
34 | goto RETURN |
35 | } | |
107c1cdb ND |
36 | if val.IsNull() || !val.IsKnown() { |
37 | goto RETURN | |
bae9f6d2 | 38 | } |
bae9f6d2 | 39 | |
107c1cdb ND |
40 | err = gocty.FromCtyValue(val, &count) |
41 | if err != nil { | |
42 | // The EvaluateExpr call above already guaranteed us a number value, | |
43 | // so if we end up here then we have something that is out of range | |
44 | // for an int, and the error message will include a description of | |
45 | // the valid range. | |
46 | rawVal := val.AsBigFloat() | |
47 | diags = diags.Append(&hcl.Diagnostic{ | |
48 | Severity: hcl.DiagError, | |
49 | Summary: "Invalid count value", | |
50 | Detail: fmt.Sprintf("The number %s is not a valid count value: %s.", rawVal, err), | |
51 | Subject: n.Resource.Count.Range().Ptr(), | |
52 | }) | |
53 | } else if count < 0 { | |
54 | rawVal := val.AsBigFloat() | |
55 | diags = diags.Append(&hcl.Diagnostic{ | |
56 | Severity: hcl.DiagError, | |
57 | Summary: "Invalid count value", | |
58 | Detail: fmt.Sprintf("The number %s is not a valid count value: count must not be negative.", rawVal), | |
59 | Subject: n.Resource.Count.Range().Ptr(), | |
60 | }) | |
bae9f6d2 JC |
61 | } |
62 | ||
63 | RETURN: | |
107c1cdb | 64 | return nil, diags.NonFatalErr() |
bae9f6d2 JC |
65 | } |
66 | ||
67 | // EvalValidateProvider is an EvalNode implementation that validates | |
107c1cdb | 68 | // a provider configuration. |
bae9f6d2 | 69 | type EvalValidateProvider struct { |
107c1cdb ND |
70 | Addr addrs.ProviderConfig |
71 | Provider *providers.Interface | |
72 | Config *configs.Provider | |
bae9f6d2 JC |
73 | } |
74 | ||
75 | func (n *EvalValidateProvider) Eval(ctx EvalContext) (interface{}, error) { | |
107c1cdb | 76 | var diags tfdiags.Diagnostics |
bae9f6d2 | 77 | provider := *n.Provider |
bae9f6d2 | 78 | |
107c1cdb ND |
79 | configBody := buildProviderConfig(ctx, n.Addr, n.Config) |
80 | ||
81 | resp := provider.GetSchema() | |
82 | diags = diags.Append(resp.Diagnostics) | |
83 | if diags.HasErrors() { | |
84 | return nil, diags.NonFatalErr() | |
bae9f6d2 JC |
85 | } |
86 | ||
107c1cdb ND |
87 | configSchema := resp.Provider.Block |
88 | if configSchema == nil { | |
89 | // Should never happen in real code, but often comes up in tests where | |
90 | // mock schemas are being used that tend to be incomplete. | |
91 | log.Printf("[WARN] EvalValidateProvider: no config schema is available for %s, so using empty schema", n.Addr) | |
92 | configSchema = &configschema.Block{} | |
bae9f6d2 | 93 | } |
107c1cdb ND |
94 | |
95 | configVal, configBody, evalDiags := ctx.EvaluateBlock(configBody, configSchema, nil, EvalDataForNoInstanceKey) | |
96 | diags = diags.Append(evalDiags) | |
97 | if evalDiags.HasErrors() { | |
98 | return nil, diags.NonFatalErr() | |
99 | } | |
100 | ||
101 | req := providers.PrepareProviderConfigRequest{ | |
102 | Config: configVal, | |
103 | } | |
104 | ||
105 | validateResp := provider.PrepareProviderConfig(req) | |
106 | diags = diags.Append(validateResp.Diagnostics) | |
107 | ||
108 | return nil, diags.NonFatalErr() | |
bae9f6d2 JC |
109 | } |
110 | ||
111 | // EvalValidateProvisioner is an EvalNode implementation that validates | |
107c1cdb ND |
112 | // the configuration of a provisioner belonging to a resource. The provisioner |
113 | // config is expected to contain the merged connection configurations. | |
bae9f6d2 | 114 | type EvalValidateProvisioner struct { |
107c1cdb ND |
115 | ResourceAddr addrs.Resource |
116 | Provisioner *provisioners.Interface | |
117 | Schema **configschema.Block | |
118 | Config *configs.Provisioner | |
119 | ResourceHasCount bool | |
bae9f6d2 JC |
120 | } |
121 | ||
122 | func (n *EvalValidateProvisioner) Eval(ctx EvalContext) (interface{}, error) { | |
123 | provisioner := *n.Provisioner | |
124 | config := *n.Config | |
107c1cdb ND |
125 | schema := *n.Schema |
126 | ||
127 | var diags tfdiags.Diagnostics | |
bae9f6d2 JC |
128 | |
129 | { | |
130 | // Validate the provisioner's own config first | |
bae9f6d2 | 131 | |
107c1cdb ND |
132 | configVal, _, configDiags := n.evaluateBlock(ctx, config.Config, schema) |
133 | diags = diags.Append(configDiags) | |
134 | if configDiags.HasErrors() { | |
135 | return nil, diags.Err() | |
136 | } | |
bae9f6d2 | 137 | |
107c1cdb ND |
138 | if configVal == cty.NilVal { |
139 | // Should never happen for a well-behaved EvaluateBlock implementation | |
140 | return nil, fmt.Errorf("EvaluateBlock returned nil value") | |
141 | } | |
142 | ||
143 | req := provisioners.ValidateProvisionerConfigRequest{ | |
144 | Config: configVal, | |
145 | } | |
146 | ||
147 | resp := provisioner.ValidateProvisionerConfig(req) | |
148 | diags = diags.Append(resp.Diagnostics) | |
bae9f6d2 JC |
149 | } |
150 | ||
107c1cdb ND |
151 | { |
152 | // Now validate the connection config, which contains the merged bodies | |
153 | // of the resource and provisioner connection blocks. | |
154 | connDiags := n.validateConnConfig(ctx, config.Connection, n.ResourceAddr) | |
155 | diags = diags.Append(connDiags) | |
bae9f6d2 | 156 | } |
107c1cdb ND |
157 | |
158 | return nil, diags.NonFatalErr() | |
bae9f6d2 JC |
159 | } |
160 | ||
107c1cdb | 161 | func (n *EvalValidateProvisioner) validateConnConfig(ctx EvalContext, config *configs.Connection, self addrs.Referenceable) tfdiags.Diagnostics { |
bae9f6d2 JC |
162 | // We can't comprehensively validate the connection config since its |
163 | // final structure is decided by the communicator and we can't instantiate | |
164 | // that until we have a complete instance state. However, we *can* catch | |
165 | // configuration keys that are not valid for *any* communicator, catching | |
166 | // typos early rather than waiting until we actually try to run one of | |
167 | // the resource's provisioners. | |
168 | ||
107c1cdb | 169 | var diags tfdiags.Diagnostics |
bae9f6d2 | 170 | |
107c1cdb ND |
171 | if config == nil || config.Config == nil { |
172 | // No block to validate | |
173 | return diags | |
174 | } | |
bae9f6d2 | 175 | |
107c1cdb ND |
176 | // We evaluate here just by evaluating the block and returning any |
177 | // diagnostics we get, since evaluation alone is enough to check for | |
178 | // extraneous arguments and incorrectly-typed arguments. | |
179 | _, _, configDiags := n.evaluateBlock(ctx, config.Config, connectionBlockSupersetSchema) | |
180 | diags = diags.Append(configDiags) | |
bae9f6d2 | 181 | |
107c1cdb ND |
182 | return diags |
183 | } | |
bae9f6d2 | 184 | |
107c1cdb ND |
185 | func (n *EvalValidateProvisioner) evaluateBlock(ctx EvalContext, body hcl.Body, schema *configschema.Block) (cty.Value, hcl.Body, tfdiags.Diagnostics) { |
186 | keyData := EvalDataForNoInstanceKey | |
187 | selfAddr := n.ResourceAddr.Instance(addrs.NoKey) | |
bae9f6d2 | 188 | |
107c1cdb ND |
189 | if n.ResourceHasCount { |
190 | // For a resource that has count, we allow count.index but don't | |
191 | // know at this stage what it will return. | |
192 | keyData = InstanceKeyEvalData{ | |
193 | CountIndex: cty.UnknownVal(cty.Number), | |
194 | } | |
bae9f6d2 | 195 | |
107c1cdb ND |
196 | // "self" can't point to an unknown key, but we'll force it to be |
197 | // key 0 here, which should return an unknown value of the | |
198 | // expected type since none of these elements are known at this | |
199 | // point anyway. | |
200 | selfAddr = n.ResourceAddr.Instance(addrs.IntKey(0)) | |
bae9f6d2 | 201 | } |
107c1cdb ND |
202 | |
203 | return ctx.EvaluateBlock(body, schema, selfAddr, keyData) | |
204 | } | |
205 | ||
206 | // connectionBlockSupersetSchema is a schema representing the superset of all | |
207 | // possible arguments for "connection" blocks across all supported connection | |
208 | // types. | |
209 | // | |
210 | // This currently lives here because we've not yet updated our communicator | |
211 | // subsystem to be aware of schema itself. Once that is done, we can remove | |
212 | // this and use a type-specific schema from the communicator to validate | |
213 | // exactly what is expected for a given connection type. | |
214 | var connectionBlockSupersetSchema = &configschema.Block{ | |
215 | Attributes: map[string]*configschema.Attribute{ | |
216 | // NOTE: "type" is not included here because it's treated special | |
217 | // by the config loader and stored away in a separate field. | |
218 | ||
219 | // Common attributes for both connection types | |
220 | "host": { | |
221 | Type: cty.String, | |
222 | Required: true, | |
223 | }, | |
224 | "type": { | |
225 | Type: cty.String, | |
226 | Optional: true, | |
227 | }, | |
228 | "user": { | |
229 | Type: cty.String, | |
230 | Optional: true, | |
231 | }, | |
232 | "password": { | |
233 | Type: cty.String, | |
234 | Optional: true, | |
235 | }, | |
236 | "port": { | |
237 | Type: cty.String, | |
238 | Optional: true, | |
239 | }, | |
240 | "timeout": { | |
241 | Type: cty.String, | |
242 | Optional: true, | |
243 | }, | |
244 | "script_path": { | |
245 | Type: cty.String, | |
246 | Optional: true, | |
247 | }, | |
248 | ||
249 | // For type=ssh only (enforced in ssh communicator) | |
250 | "private_key": { | |
251 | Type: cty.String, | |
252 | Optional: true, | |
253 | }, | |
254 | "certificate": { | |
255 | Type: cty.String, | |
256 | Optional: true, | |
257 | }, | |
258 | "host_key": { | |
259 | Type: cty.String, | |
260 | Optional: true, | |
261 | }, | |
262 | "agent": { | |
263 | Type: cty.Bool, | |
264 | Optional: true, | |
265 | }, | |
266 | "agent_identity": { | |
267 | Type: cty.String, | |
268 | Optional: true, | |
269 | }, | |
270 | "bastion_host": { | |
271 | Type: cty.String, | |
272 | Optional: true, | |
273 | }, | |
274 | "bastion_host_key": { | |
275 | Type: cty.String, | |
276 | Optional: true, | |
277 | }, | |
278 | "bastion_port": { | |
279 | Type: cty.Number, | |
280 | Optional: true, | |
281 | }, | |
282 | "bastion_user": { | |
283 | Type: cty.String, | |
284 | Optional: true, | |
285 | }, | |
286 | "bastion_password": { | |
287 | Type: cty.String, | |
288 | Optional: true, | |
289 | }, | |
290 | "bastion_private_key": { | |
291 | Type: cty.String, | |
292 | Optional: true, | |
293 | }, | |
294 | ||
295 | // For type=winrm only (enforced in winrm communicator) | |
296 | "https": { | |
297 | Type: cty.Bool, | |
298 | Optional: true, | |
299 | }, | |
300 | "insecure": { | |
301 | Type: cty.Bool, | |
302 | Optional: true, | |
303 | }, | |
304 | "cacert": { | |
305 | Type: cty.String, | |
306 | Optional: true, | |
307 | }, | |
308 | "use_ntlm": { | |
309 | Type: cty.Bool, | |
310 | Optional: true, | |
311 | }, | |
312 | }, | |
313 | } | |
314 | ||
315 | // connectionBlockSupersetSchema is a schema representing the superset of all | |
316 | // possible arguments for "connection" blocks across all supported connection | |
317 | // types. | |
318 | // | |
319 | // This currently lives here because we've not yet updated our communicator | |
320 | // subsystem to be aware of schema itself. It's exported only for use in the | |
321 | // configs/configupgrade package and should not be used from anywhere else. | |
322 | // The caller may not modify any part of the returned schema data structure. | |
323 | func ConnectionBlockSupersetSchema() *configschema.Block { | |
324 | return connectionBlockSupersetSchema | |
bae9f6d2 JC |
325 | } |
326 | ||
327 | // EvalValidateResource is an EvalNode implementation that validates | |
328 | // the configuration of a resource. | |
329 | type EvalValidateResource struct { | |
107c1cdb ND |
330 | Addr addrs.Resource |
331 | Provider *providers.Interface | |
332 | ProviderSchema **ProviderSchema | |
333 | Config *configs.Resource | |
bae9f6d2 JC |
334 | |
335 | // IgnoreWarnings means that warnings will not be passed through. This allows | |
336 | // "just-in-time" passes of validation to continue execution through warnings. | |
337 | IgnoreWarnings bool | |
107c1cdb ND |
338 | |
339 | // ConfigVal, if non-nil, will be updated with the value resulting from | |
340 | // evaluating the given configuration body. Since validation is performed | |
341 | // very early, this value is likely to contain lots of unknown values, | |
342 | // but its type will conform to the schema of the resource type associated | |
343 | // with the resource instance being validated. | |
344 | ConfigVal *cty.Value | |
bae9f6d2 JC |
345 | } |
346 | ||
347 | func (n *EvalValidateResource) Eval(ctx EvalContext) (interface{}, error) { | |
107c1cdb ND |
348 | if n.ProviderSchema == nil || *n.ProviderSchema == nil { |
349 | return nil, fmt.Errorf("EvalValidateResource has nil schema for %s", n.Addr) | |
350 | } | |
351 | ||
352 | var diags tfdiags.Diagnostics | |
bae9f6d2 JC |
353 | provider := *n.Provider |
354 | cfg := *n.Config | |
107c1cdb ND |
355 | schema := *n.ProviderSchema |
356 | mode := cfg.Mode | |
357 | ||
358 | keyData := EvalDataForNoInstanceKey | |
359 | if n.Config.Count != nil { | |
360 | // If the config block has count, we'll evaluate with an unknown | |
361 | // number as count.index so we can still type check even though | |
362 | // we won't expand count until the plan phase. | |
363 | keyData = InstanceKeyEvalData{ | |
364 | CountIndex: cty.UnknownVal(cty.Number), | |
365 | } | |
366 | ||
367 | // Basic type-checking of the count argument. More complete validation | |
368 | // of this will happen when we DynamicExpand during the plan walk. | |
369 | countDiags := n.validateCount(ctx, n.Config.Count) | |
370 | diags = diags.Append(countDiags) | |
371 | } | |
372 | ||
373 | for _, traversal := range n.Config.DependsOn { | |
374 | ref, refDiags := addrs.ParseRef(traversal) | |
375 | diags = diags.Append(refDiags) | |
376 | if len(ref.Remaining) != 0 { | |
377 | diags = diags.Append(&hcl.Diagnostic{ | |
378 | Severity: hcl.DiagError, | |
379 | Summary: "Invalid depends_on reference", | |
380 | Detail: "References in depends_on must be to a whole object (resource, etc), not to an attribute of an object.", | |
381 | Subject: ref.Remaining.SourceRange().Ptr(), | |
382 | }) | |
383 | } | |
384 | ||
385 | // The ref must also refer to something that exists. To test that, | |
386 | // we'll just eval it and count on the fact that our evaluator will | |
387 | // detect references to non-existent objects. | |
388 | if !diags.HasErrors() { | |
389 | scope := ctx.EvaluationScope(nil, EvalDataForNoInstanceKey) | |
390 | if scope != nil { // sometimes nil in tests, due to incomplete mocks | |
391 | _, refDiags = scope.EvalReference(ref, cty.DynamicPseudoType) | |
392 | diags = diags.Append(refDiags) | |
393 | } | |
394 | } | |
395 | } | |
396 | ||
bae9f6d2 JC |
397 | // Provider entry point varies depending on resource mode, because |
398 | // managed resources and data resources are two distinct concepts | |
399 | // in the provider abstraction. | |
107c1cdb ND |
400 | switch mode { |
401 | case addrs.ManagedResourceMode: | |
402 | schema, _ := schema.SchemaForResourceType(mode, cfg.Type) | |
403 | if schema == nil { | |
404 | diags = diags.Append(&hcl.Diagnostic{ | |
405 | Severity: hcl.DiagError, | |
406 | Summary: "Invalid resource type", | |
407 | Detail: fmt.Sprintf("The provider %s does not support resource type %q.", cfg.ProviderConfigAddr(), cfg.Type), | |
408 | Subject: &cfg.TypeRange, | |
409 | }) | |
410 | return nil, diags.Err() | |
411 | } | |
412 | ||
413 | configVal, _, valDiags := ctx.EvaluateBlock(cfg.Config, schema, nil, keyData) | |
414 | diags = diags.Append(valDiags) | |
415 | if valDiags.HasErrors() { | |
416 | return nil, diags.Err() | |
417 | } | |
418 | ||
419 | if cfg.Managed != nil { // can be nil only in tests with poorly-configured mocks | |
420 | for _, traversal := range cfg.Managed.IgnoreChanges { | |
421 | moreDiags := schema.StaticValidateTraversal(traversal) | |
422 | diags = diags.Append(moreDiags) | |
423 | } | |
424 | } | |
425 | ||
426 | req := providers.ValidateResourceTypeConfigRequest{ | |
427 | TypeName: cfg.Type, | |
428 | Config: configVal, | |
429 | } | |
430 | ||
431 | resp := provider.ValidateResourceTypeConfig(req) | |
432 | diags = diags.Append(resp.Diagnostics.InConfigBody(cfg.Config)) | |
433 | ||
434 | if n.ConfigVal != nil { | |
435 | *n.ConfigVal = configVal | |
436 | } | |
437 | ||
438 | case addrs.DataResourceMode: | |
439 | schema, _ := schema.SchemaForResourceType(mode, cfg.Type) | |
440 | if schema == nil { | |
441 | diags = diags.Append(&hcl.Diagnostic{ | |
442 | Severity: hcl.DiagError, | |
443 | Summary: "Invalid data source", | |
444 | Detail: fmt.Sprintf("The provider %s does not support data source %q.", cfg.ProviderConfigAddr(), cfg.Type), | |
445 | Subject: &cfg.TypeRange, | |
446 | }) | |
447 | return nil, diags.Err() | |
448 | } | |
449 | ||
450 | configVal, _, valDiags := ctx.EvaluateBlock(cfg.Config, schema, nil, keyData) | |
451 | diags = diags.Append(valDiags) | |
452 | if valDiags.HasErrors() { | |
453 | return nil, diags.Err() | |
454 | } | |
bae9f6d2 | 455 | |
107c1cdb ND |
456 | req := providers.ValidateDataSourceConfigRequest{ |
457 | TypeName: cfg.Type, | |
458 | Config: configVal, | |
459 | } | |
460 | ||
461 | resp := provider.ValidateDataSourceConfig(req) | |
462 | diags = diags.Append(resp.Diagnostics.InConfigBody(cfg.Config)) | |
bae9f6d2 JC |
463 | } |
464 | ||
107c1cdb ND |
465 | if n.IgnoreWarnings { |
466 | // If we _only_ have warnings then we'll return nil. | |
467 | if diags.HasErrors() { | |
468 | return nil, diags.NonFatalErr() | |
469 | } | |
bae9f6d2 | 470 | return nil, nil |
107c1cdb ND |
471 | } else { |
472 | // We'll return an error if there are any diagnostics at all, even if | |
473 | // some of them are warnings. | |
474 | return nil, diags.NonFatalErr() | |
475 | } | |
476 | } | |
477 | ||
478 | func (n *EvalValidateResource) validateCount(ctx EvalContext, expr hcl.Expression) tfdiags.Diagnostics { | |
479 | if expr == nil { | |
480 | return nil | |
481 | } | |
482 | ||
483 | var diags tfdiags.Diagnostics | |
484 | ||
485 | countVal, countDiags := ctx.EvaluateExpr(expr, cty.Number, nil) | |
486 | diags = diags.Append(countDiags) | |
487 | if diags.HasErrors() { | |
488 | return diags | |
489 | } | |
490 | ||
491 | if countVal.IsNull() { | |
492 | diags = diags.Append(&hcl.Diagnostic{ | |
493 | Severity: hcl.DiagError, | |
494 | Summary: "Invalid count argument", | |
495 | Detail: `The given "count" argument value is null. An integer is required.`, | |
496 | Subject: expr.Range().Ptr(), | |
497 | }) | |
498 | return diags | |
bae9f6d2 JC |
499 | } |
500 | ||
107c1cdb ND |
501 | var err error |
502 | countVal, err = convert.Convert(countVal, cty.Number) | |
503 | if err != nil { | |
504 | diags = diags.Append(&hcl.Diagnostic{ | |
505 | Severity: hcl.DiagError, | |
506 | Summary: "Invalid count argument", | |
507 | Detail: fmt.Sprintf(`The given "count" argument value is unsuitable: %s.`, err), | |
508 | Subject: expr.Range().Ptr(), | |
509 | }) | |
510 | return diags | |
bae9f6d2 | 511 | } |
107c1cdb ND |
512 | |
513 | // If the value isn't known then that's the best we can do for now, but | |
514 | // we'll check more thoroughly during the plan walk. | |
515 | if !countVal.IsKnown() { | |
516 | return diags | |
517 | } | |
518 | ||
519 | // If we _do_ know the value, then we can do a few more checks here. | |
520 | var count int | |
521 | err = gocty.FromCtyValue(countVal, &count) | |
522 | if err != nil { | |
523 | // Isn't a whole number, etc. | |
524 | diags = diags.Append(&hcl.Diagnostic{ | |
525 | Severity: hcl.DiagError, | |
526 | Summary: "Invalid count argument", | |
527 | Detail: fmt.Sprintf(`The given "count" argument value is unsuitable: %s.`, err), | |
528 | Subject: expr.Range().Ptr(), | |
529 | }) | |
530 | return diags | |
531 | } | |
532 | ||
533 | if count < 0 { | |
534 | diags = diags.Append(&hcl.Diagnostic{ | |
535 | Severity: hcl.DiagError, | |
536 | Summary: "Invalid count argument", | |
537 | Detail: `The given "count" argument value is unsuitable: count cannot be negative.`, | |
538 | Subject: expr.Range().Ptr(), | |
539 | }) | |
540 | return diags | |
541 | } | |
542 | ||
543 | return diags | |
bae9f6d2 | 544 | } |