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