1package terraform
2
3import (
4	"fmt"
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"
17)
18
19// EvalValidateCount is an EvalNode implementation that validates
20// the count of a resource.
21type EvalValidateCount struct {
22	Resource *configs.Resource
23}
24
25// TODO: test
26func (n *EvalValidateCount) Eval(ctx EvalContext) (interface{}, error) {
27	var diags tfdiags.Diagnostics
28	var count int
29	var err error
30
31	val, valDiags := ctx.EvaluateExpr(n.Resource.Count, cty.Number, nil)
32	diags = diags.Append(valDiags)
33	if valDiags.HasErrors() {
34		goto RETURN
35	}
36	if val.IsNull() || !val.IsKnown() {
37		goto RETURN
38	}
39
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		})
61	}
62
63RETURN:
64	return nil, diags.NonFatalErr()
65}
66
67// EvalValidateProvider is an EvalNode implementation that validates
68// a provider configuration.
69type EvalValidateProvider struct {
70	Addr     addrs.ProviderConfig
71	Provider *providers.Interface
72	Config   *configs.Provider
73}
74
75func (n *EvalValidateProvider) Eval(ctx EvalContext) (interface{}, error) {
76	var diags tfdiags.Diagnostics
77	provider := *n.Provider
78
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()
85	}
86
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{}
93	}
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()
109}
110
111// EvalValidateProvisioner is an EvalNode implementation that validates
112// the configuration of a provisioner belonging to a resource. The provisioner
113// config is expected to contain the merged connection configurations.
114type EvalValidateProvisioner struct {
115	ResourceAddr     addrs.Resource
116	Provisioner      *provisioners.Interface
117	Schema           **configschema.Block
118	Config           *configs.Provisioner
119	ResourceHasCount bool
120}
121
122func (n *EvalValidateProvisioner) Eval(ctx EvalContext) (interface{}, error) {
123	provisioner := *n.Provisioner
124	config := *n.Config
125	schema := *n.Schema
126
127	var diags tfdiags.Diagnostics
128
129	{
130		// Validate the provisioner's own config first
131
132		configVal, _, configDiags := n.evaluateBlock(ctx, config.Config, schema)
133		diags = diags.Append(configDiags)
134		if configDiags.HasErrors() {
135			return nil, diags.Err()
136		}
137
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)
149	}
150
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)
156	}
157
158	return nil, diags.NonFatalErr()
159}
160
161func (n *EvalValidateProvisioner) validateConnConfig(ctx EvalContext, config *configs.Connection, self addrs.Referenceable) tfdiags.Diagnostics {
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
169	var diags tfdiags.Diagnostics
170
171	if config == nil || config.Config == nil {
172		// No block to validate
173		return diags
174	}
175
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)
181
182	return diags
183}
184
185func (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)
188
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		}
195
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))
201	}
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.
214var 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.
323func ConnectionBlockSupersetSchema() *configschema.Block {
324	return connectionBlockSupersetSchema
325}
326
327// EvalValidateResource is an EvalNode implementation that validates
328// the configuration of a resource.
329type EvalValidateResource struct {
330	Addr           addrs.Resource
331	Provider       *providers.Interface
332	ProviderSchema **ProviderSchema
333	Config         *configs.Resource
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
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
345}
346
347func (n *EvalValidateResource) Eval(ctx EvalContext) (interface{}, error) {
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
353	provider := *n.Provider
354	cfg := *n.Config
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
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.
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		}
455
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))
463	}
464
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		}
470		return nil, nil
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
478func (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
499	}
500
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
511	}
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
544}
545