1package terraform
2
3import (
4	"fmt"
5	"sort"
6
7	"github.com/hashicorp/hcl2/hcl"
8
9	"github.com/hashicorp/terraform-plugin-sdk/internal/addrs"
10	"github.com/hashicorp/terraform-plugin-sdk/internal/configs"
11	"github.com/hashicorp/terraform-plugin-sdk/internal/helper/didyoumean"
12	"github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags"
13)
14
15// StaticValidateReferences checks the given references against schemas and
16// other statically-checkable rules, producing error diagnostics if any
17// problems are found.
18//
19// If this method returns errors for a particular reference then evaluating
20// that reference is likely to generate a very similar error, so callers should
21// not run this method and then also evaluate the source expression(s) and
22// merge the two sets of diagnostics together, since this will result in
23// confusing redundant errors.
24//
25// This method can find more errors than can be found by evaluating an
26// expression with a partially-populated scope, since it checks the referenced
27// names directly against the schema rather than relying on evaluation errors.
28//
29// The result may include warning diagnostics if, for example, deprecated
30// features are referenced.
31func (d *evaluationStateData) StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable) tfdiags.Diagnostics {
32	var diags tfdiags.Diagnostics
33	for _, ref := range refs {
34		moreDiags := d.staticValidateReference(ref, self)
35		diags = diags.Append(moreDiags)
36	}
37	return diags
38}
39
40func (d *evaluationStateData) staticValidateReference(ref *addrs.Reference, self addrs.Referenceable) tfdiags.Diagnostics {
41	modCfg := d.Evaluator.Config.DescendentForInstance(d.ModulePath)
42	if modCfg == nil {
43		// This is a bug in the caller rather than a problem with the
44		// reference, but rather than crashing out here in an unhelpful way
45		// we'll just ignore it and trust a different layer to catch it.
46		return nil
47	}
48
49	if ref.Subject == addrs.Self {
50		// The "self" address is a special alias for the address given as
51		// our self parameter here, if present.
52		if self == nil {
53			var diags tfdiags.Diagnostics
54			diags = diags.Append(&hcl.Diagnostic{
55				Severity: hcl.DiagError,
56				Summary:  `Invalid "self" reference`,
57				// This detail message mentions some current practice that
58				// this codepath doesn't really "know about". If the "self"
59				// object starts being supported in more contexts later then
60				// we'll need to adjust this message.
61				Detail:  `The "self" object is not available in this context. This object can be used only in resource provisioner and connection blocks.`,
62				Subject: ref.SourceRange.ToHCL().Ptr(),
63			})
64			return diags
65		}
66
67		synthRef := *ref // shallow copy
68		synthRef.Subject = self
69		ref = &synthRef
70	}
71
72	switch addr := ref.Subject.(type) {
73
74	// For static validation we validate both resource and resource instance references the same way.
75	// We mostly disregard the index, though we do some simple validation of
76	// its _presence_ in staticValidateSingleResourceReference and
77	// staticValidateMultiResourceReference respectively.
78	case addrs.Resource:
79		var diags tfdiags.Diagnostics
80		diags = diags.Append(d.staticValidateSingleResourceReference(modCfg, addr, ref.Remaining, ref.SourceRange))
81		diags = diags.Append(d.staticValidateResourceReference(modCfg, addr, ref.Remaining, ref.SourceRange))
82		return diags
83	case addrs.ResourceInstance:
84		var diags tfdiags.Diagnostics
85		diags = diags.Append(d.staticValidateMultiResourceReference(modCfg, addr, ref.Remaining, ref.SourceRange))
86		diags = diags.Append(d.staticValidateResourceReference(modCfg, addr.ContainingResource(), ref.Remaining, ref.SourceRange))
87		return diags
88
89	// We also handle all module call references the same way, disregarding index.
90	case addrs.ModuleCall:
91		return d.staticValidateModuleCallReference(modCfg, addr, ref.Remaining, ref.SourceRange)
92	case addrs.ModuleCallInstance:
93		return d.staticValidateModuleCallReference(modCfg, addr.Call, ref.Remaining, ref.SourceRange)
94	case addrs.ModuleCallOutput:
95		// This one is a funny one because we will take the output name referenced
96		// and use it to fake up a "remaining" that would make sense for the
97		// module call itself, rather than for the specific output, and then
98		// we can just re-use our static module call validation logic.
99		remain := make(hcl.Traversal, len(ref.Remaining)+1)
100		copy(remain[1:], ref.Remaining)
101		remain[0] = hcl.TraverseAttr{
102			Name: addr.Name,
103
104			// Using the whole reference as the source range here doesn't exactly
105			// match how HCL would normally generate an attribute traversal,
106			// but is close enough for our purposes.
107			SrcRange: ref.SourceRange.ToHCL(),
108		}
109		return d.staticValidateModuleCallReference(modCfg, addr.Call.Call, remain, ref.SourceRange)
110
111	default:
112		// Anything else we'll just permit through without any static validation
113		// and let it be caught during dynamic evaluation, in evaluate.go .
114		return nil
115	}
116}
117
118func (d *evaluationStateData) staticValidateSingleResourceReference(modCfg *configs.Config, addr addrs.Resource, remain hcl.Traversal, rng tfdiags.SourceRange) tfdiags.Diagnostics {
119	// If we have at least one step in "remain" and this resource has
120	// "count" set then we know for sure this in invalid because we have
121	// something like:
122	//     aws_instance.foo.bar
123	// ...when we really need
124	//     aws_instance.foo[count.index].bar
125
126	// It is _not_ safe to do this check when remain is empty, because that
127	// would also match aws_instance.foo[count.index].bar due to `count.index`
128	// not being statically-resolvable as part of a reference, and match
129	// direct references to the whole aws_instance.foo tuple.
130	if len(remain) == 0 {
131		return nil
132	}
133
134	var diags tfdiags.Diagnostics
135
136	cfg := modCfg.Module.ResourceByAddr(addr)
137	if cfg == nil {
138		// We'll just bail out here and catch this in our subsequent call to
139		// staticValidateResourceReference, then.
140		return diags
141	}
142
143	if cfg.Count != nil {
144		diags = diags.Append(&hcl.Diagnostic{
145			Severity: hcl.DiagError,
146			Summary:  `Missing resource instance key`,
147			Detail:   fmt.Sprintf("Because %s has \"count\" set, its attributes must be accessed on specific instances.\n\nFor example, to correlate with indices of a referring resource, use:\n    %s[count.index]", addr, addr),
148			Subject:  rng.ToHCL().Ptr(),
149		})
150	}
151	if cfg.ForEach != nil {
152		diags = diags.Append(&hcl.Diagnostic{
153			Severity: hcl.DiagError,
154			Summary:  `Missing resource instance key`,
155			Detail:   fmt.Sprintf("Because %s has \"for_each\" set, its attributes must be accessed on specific instances.\n\nFor example, to correlate with indices of a referring resource, use:\n    %s[each.key]", addr, addr),
156			Subject:  rng.ToHCL().Ptr(),
157		})
158	}
159
160	return diags
161}
162
163func (d *evaluationStateData) staticValidateMultiResourceReference(modCfg *configs.Config, addr addrs.ResourceInstance, remain hcl.Traversal, rng tfdiags.SourceRange) tfdiags.Diagnostics {
164	var diags tfdiags.Diagnostics
165
166	cfg := modCfg.Module.ResourceByAddr(addr.ContainingResource())
167	if cfg == nil {
168		// We'll just bail out here and catch this in our subsequent call to
169		// staticValidateResourceReference, then.
170		return diags
171	}
172
173	if addr.Key == addrs.NoKey {
174		// This is a different path into staticValidateSingleResourceReference
175		return d.staticValidateSingleResourceReference(modCfg, addr.ContainingResource(), remain, rng)
176	} else {
177		if cfg.Count == nil && cfg.ForEach == nil {
178			diags = diags.Append(&hcl.Diagnostic{
179				Severity: hcl.DiagError,
180				Summary:  `Unexpected resource instance key`,
181				Detail:   fmt.Sprintf(`Because %s does not have "count" or "for_each" set, references to it must not include an index key. Remove the bracketed index to refer to the single instance of this resource.`, addr.ContainingResource()),
182				Subject:  rng.ToHCL().Ptr(),
183			})
184		}
185	}
186
187	return diags
188}
189
190func (d *evaluationStateData) staticValidateResourceReference(modCfg *configs.Config, addr addrs.Resource, remain hcl.Traversal, rng tfdiags.SourceRange) tfdiags.Diagnostics {
191	var diags tfdiags.Diagnostics
192
193	var modeAdjective string
194	switch addr.Mode {
195	case addrs.ManagedResourceMode:
196		modeAdjective = "managed"
197	case addrs.DataResourceMode:
198		modeAdjective = "data"
199	default:
200		// should never happen
201		modeAdjective = "<invalid-mode>"
202	}
203
204	cfg := modCfg.Module.ResourceByAddr(addr)
205	if cfg == nil {
206		diags = diags.Append(&hcl.Diagnostic{
207			Severity: hcl.DiagError,
208			Summary:  `Reference to undeclared resource`,
209			Detail:   fmt.Sprintf(`A %s resource %q %q has not been declared in %s.`, modeAdjective, addr.Type, addr.Name, moduleConfigDisplayAddr(modCfg.Path)),
210			Subject:  rng.ToHCL().Ptr(),
211		})
212		return diags
213	}
214
215	// Normally accessing this directly is wrong because it doesn't take into
216	// account provider inheritance, etc but it's okay here because we're only
217	// paying attention to the type anyway.
218	providerType := cfg.ProviderConfigAddr().Type
219	schema, _ := d.Evaluator.Schemas.ResourceTypeConfig(providerType, addr.Mode, addr.Type)
220
221	if schema == nil {
222		// Prior validation should've taken care of a resource block with an
223		// unsupported type, so we should never get here but we'll handle it
224		// here anyway for robustness.
225		diags = diags.Append(&hcl.Diagnostic{
226			Severity: hcl.DiagError,
227			Summary:  `Invalid resource type`,
228			Detail:   fmt.Sprintf(`A %s resource type %q is not supported by provider %q.`, modeAdjective, addr.Type, providerType),
229			Subject:  rng.ToHCL().Ptr(),
230		})
231		return diags
232	}
233
234	// As a special case we'll detect attempts to access an attribute called
235	// "count" and produce a special error for it, since versions of Terraform
236	// prior to v0.12 offered this as a weird special case that we can no
237	// longer support.
238	if len(remain) > 0 {
239		if step, ok := remain[0].(hcl.TraverseAttr); ok && step.Name == "count" {
240			diags = diags.Append(&hcl.Diagnostic{
241				Severity: hcl.DiagError,
242				Summary:  `Invalid resource count attribute`,
243				Detail:   fmt.Sprintf(`The special "count" attribute is no longer supported after Terraform v0.12. Instead, use length(%s) to count resource instances.`, addr),
244				Subject:  rng.ToHCL().Ptr(),
245			})
246			return diags
247		}
248	}
249
250	// If we got this far then we'll try to validate the remaining traversal
251	// steps against our schema.
252	moreDiags := schema.StaticValidateTraversal(remain)
253	diags = diags.Append(moreDiags)
254
255	return diags
256}
257
258func (d *evaluationStateData) staticValidateModuleCallReference(modCfg *configs.Config, addr addrs.ModuleCall, remain hcl.Traversal, rng tfdiags.SourceRange) tfdiags.Diagnostics {
259	var diags tfdiags.Diagnostics
260
261	// For now, our focus here is just in testing that the referenced module
262	// call exists. All other validation is deferred until evaluation time.
263	_, exists := modCfg.Module.ModuleCalls[addr.Name]
264	if !exists {
265		var suggestions []string
266		for name := range modCfg.Module.ModuleCalls {
267			suggestions = append(suggestions, name)
268		}
269		sort.Strings(suggestions)
270		suggestion := didyoumean.NameSuggestion(addr.Name, suggestions)
271		if suggestion != "" {
272			suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
273		}
274
275		diags = diags.Append(&hcl.Diagnostic{
276			Severity: hcl.DiagError,
277			Summary:  `Reference to undeclared module`,
278			Detail:   fmt.Sprintf(`No module call named %q is declared in %s.%s`, addr.Name, moduleConfigDisplayAddr(modCfg.Path), suggestion),
279			Subject:  rng.ToHCL().Ptr(),
280		})
281		return diags
282	}
283
284	return diags
285}
286
287// moduleConfigDisplayAddr returns a string describing the given module
288// address that is appropriate for returning to users in situations where the
289// root module is possible. Specifically, it returns "the root module" if the
290// root module instance is given, or a string representation of the module
291// address otherwise.
292func moduleConfigDisplayAddr(addr addrs.Module) string {
293	switch {
294	case addr.IsRoot():
295		return "the root module"
296	default:
297		return addr.String()
298	}
299}
300