github.com/hashicorp/terraform-plugin-sdk@v1.17.2/terraform/evaluate_valid.go (about) 1 package terraform 2 3 import ( 4 "fmt" 5 "sort" 6 7 "github.com/hashicorp/hcl/v2" 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. 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 }