]>
Commit | Line | Data |
---|---|---|
107c1cdb ND |
1 | package terraform |
2 | ||
3 | import ( | |
4 | "fmt" | |
5 | "sort" | |
6 | ||
7 | "github.com/hashicorp/hcl2/hcl" | |
8 | ||
9 | "github.com/hashicorp/terraform/addrs" | |
10 | "github.com/hashicorp/terraform/configs" | |
11 | "github.com/hashicorp/terraform/helper/didyoumean" | |
12 | "github.com/hashicorp/terraform/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. | |
31 | func (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 | ||
40 | func (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 | ||
118 | func (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 | ||
163 | func (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 | ||
190 | func (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 | ||
258 | func (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. | |
292 | func moduleConfigDisplayAddr(addr addrs.Module) string { | |
293 | switch { | |
294 | case addr.IsRoot(): | |
295 | return "the root module" | |
296 | default: | |
297 | return addr.String() | |
298 | } | |
299 | } |