github.com/pulumi/terraform@v1.4.0/pkg/configs/provider_validation.go (about) 1 package configs 2 3 import ( 4 "fmt" 5 "sort" 6 "strings" 7 8 "github.com/hashicorp/hcl/v2" 9 "github.com/pulumi/terraform/pkg/addrs" 10 ) 11 12 // validateProviderConfigs walks the full configuration tree from the root 13 // module outward, static validation rules to the various combinations of 14 // provider configuration, required_providers values, and module call providers 15 // mappings. 16 // 17 // To retain compatibility with previous terraform versions, empty "proxy 18 // provider blocks" are still allowed within modules, though they will 19 // generate warnings when the configuration is loaded. The new validation 20 // however will generate an error if a suitable provider configuration is not 21 // passed in through the module call. 22 // 23 // The call argument is the ModuleCall for the provided Config cfg. The 24 // noProviderConfigRange argument is passed down the call stack, indicating 25 // that the module call, or a parent module call, has used a feature (at the 26 // specified source location) that precludes providers from being configured at 27 // all within the module. 28 func validateProviderConfigs(parentCall *ModuleCall, cfg *Config, noProviderConfigRange *hcl.Range) (diags hcl.Diagnostics) { 29 mod := cfg.Module 30 31 for name, child := range cfg.Children { 32 mc := mod.ModuleCalls[name] 33 childNoProviderConfigRange := noProviderConfigRange 34 // if the module call has any of count, for_each or depends_on, 35 // providers are prohibited from being configured in this module, or 36 // any module beneath this module. 37 switch { 38 case mc.Count != nil: 39 childNoProviderConfigRange = mc.Count.Range().Ptr() 40 case mc.ForEach != nil: 41 childNoProviderConfigRange = mc.ForEach.Range().Ptr() 42 case mc.DependsOn != nil: 43 if len(mc.DependsOn) > 0 { 44 childNoProviderConfigRange = mc.DependsOn[0].SourceRange().Ptr() 45 } else { 46 // Weird! We'll just use the call itself, then. 47 childNoProviderConfigRange = mc.DeclRange.Ptr() 48 } 49 } 50 diags = append(diags, validateProviderConfigs(mc, child, childNoProviderConfigRange)...) 51 } 52 53 // the set of provider configuration names passed into the module, with the 54 // source range of the provider assignment in the module call. 55 passedIn := map[string]PassedProviderConfig{} 56 57 // the set of empty configurations that could be proxy configurations, with 58 // the source range of the empty configuration block. 59 emptyConfigs := map[string]hcl.Range{} 60 61 // the set of provider with a defined configuration, with the source range 62 // of the configuration block declaration. 63 configured := map[string]hcl.Range{} 64 65 // the set of configuration_aliases defined in the required_providers 66 // block, with the fully qualified provider type. 67 configAliases := map[string]addrs.AbsProviderConfig{} 68 69 // the set of provider names defined in the required_providers block, and 70 // their provider types. 71 localNames := map[string]addrs.Provider{} 72 73 for _, pc := range mod.ProviderConfigs { 74 name := providerName(pc.Name, pc.Alias) 75 // Validate the config against an empty schema to see if it's empty. 76 _, pcConfigDiags := pc.Config.Content(&hcl.BodySchema{}) 77 if pcConfigDiags.HasErrors() || pc.Version.Required != nil { 78 configured[name] = pc.DeclRange 79 } else { 80 emptyConfigs[name] = pc.DeclRange 81 } 82 } 83 84 if mod.ProviderRequirements != nil { 85 // Track all known local types too to ensure we don't have duplicated 86 // with different local names. 87 localTypes := map[string]bool{} 88 89 // check for duplicate requirements of the same type 90 for _, req := range mod.ProviderRequirements.RequiredProviders { 91 if localTypes[req.Type.String()] { 92 // find the last declaration to give a better error 93 prevDecl := "" 94 for localName, typ := range localNames { 95 if typ.Equals(req.Type) { 96 prevDecl = localName 97 } 98 } 99 100 diags = append(diags, &hcl.Diagnostic{ 101 Severity: hcl.DiagWarning, 102 Summary: "Duplicate required provider", 103 Detail: fmt.Sprintf( 104 "Provider %s with the local name %q was previously required as %q. A provider can only be required once within required_providers.", 105 req.Type.ForDisplay(), req.Name, prevDecl, 106 ), 107 Subject: &req.DeclRange, 108 }) 109 } else if addrs.IsDefaultProvider(req.Type) { 110 // Now check for possible implied duplicates, where a provider 111 // block uses a default namespaced provider, but that provider 112 // was required via a different name. 113 impliedLocalName := req.Type.Type 114 // We have to search through the configs for a match, since the keys contains any aliases. 115 for _, pc := range mod.ProviderConfigs { 116 if pc.Name == impliedLocalName && req.Name != impliedLocalName { 117 diags = append(diags, &hcl.Diagnostic{ 118 Severity: hcl.DiagWarning, 119 Summary: "Duplicate required provider", 120 Detail: fmt.Sprintf( 121 "Provider %s with the local name %q was implicitly required via a configuration block as %q. The provider configuration block name must match the name used in required_providers.", 122 req.Type.ForDisplay(), req.Name, req.Type.Type, 123 ), 124 Subject: &req.DeclRange, 125 }) 126 break 127 } 128 } 129 } 130 131 localTypes[req.Type.String()] = true 132 133 localNames[req.Name] = req.Type 134 for _, alias := range req.Aliases { 135 addr := addrs.AbsProviderConfig{ 136 Module: cfg.Path, 137 Provider: req.Type, 138 Alias: alias.Alias, 139 } 140 configAliases[providerName(alias.LocalName, alias.Alias)] = addr 141 } 142 } 143 } 144 145 checkImpliedProviderNames := func(resourceConfigs map[string]*Resource) { 146 // Now that we have all the provider configs and requirements validated, 147 // check for any resources which use an implied localname which doesn't 148 // match that of required_providers 149 for _, r := range resourceConfigs { 150 // We're looking for resources with no specific provider reference 151 if r.ProviderConfigRef != nil { 152 continue 153 } 154 155 localName := r.Addr().ImpliedProvider() 156 157 _, err := addrs.ParseProviderPart(localName) 158 if err != nil { 159 diags = append(diags, &hcl.Diagnostic{ 160 Severity: hcl.DiagError, 161 Summary: "Invalid provider local name", 162 Detail: fmt.Sprintf("%q is an invalid implied provider local name: %s", localName, err), 163 Subject: r.DeclRange.Ptr(), 164 }) 165 continue 166 } 167 168 if _, ok := localNames[localName]; ok { 169 // OK, this was listed directly in the required_providers 170 continue 171 } 172 173 defAddr := addrs.ImpliedProviderForUnqualifiedType(localName) 174 175 // Now make sure we don't have the same provider required under a 176 // different name. 177 for prevLocalName, addr := range localNames { 178 if addr.Equals(defAddr) { 179 diags = append(diags, &hcl.Diagnostic{ 180 Severity: hcl.DiagWarning, 181 Summary: "Duplicate required provider", 182 Detail: fmt.Sprintf( 183 "Provider %q was implicitly required via resource %q, but listed in required_providers as %q. Either the local name in required_providers must match the resource name, or the %q provider must be assigned within the resource block.", 184 defAddr, r.Addr(), prevLocalName, prevLocalName, 185 ), 186 Subject: &r.DeclRange, 187 }) 188 } 189 } 190 } 191 } 192 checkImpliedProviderNames(mod.ManagedResources) 193 checkImpliedProviderNames(mod.DataResources) 194 195 // collect providers passed from the parent 196 if parentCall != nil { 197 for _, passed := range parentCall.Providers { 198 name := providerName(passed.InChild.Name, passed.InChild.Alias) 199 passedIn[name] = passed 200 } 201 } 202 203 parentModuleText := "the root module" 204 moduleText := "the root module" 205 if !cfg.Path.IsRoot() { 206 moduleText = cfg.Path.String() 207 if parent := cfg.Path.Parent(); !parent.IsRoot() { 208 // module address are prefixed with `module.` 209 parentModuleText = parent.String() 210 } 211 } 212 213 // Verify that any module calls only refer to named providers, and that 214 // those providers will have a configuration at runtime. This way we can 215 // direct users where to add the missing configuration, because the runtime 216 // error is only "missing provider X". 217 for _, modCall := range mod.ModuleCalls { 218 for _, passed := range modCall.Providers { 219 // aliased providers are handled more strictly, and are never 220 // inherited, so they are validated within modules further down. 221 // Skip these checks to prevent redundant diagnostics. 222 if passed.InParent.Alias != "" { 223 continue 224 } 225 226 name := passed.InParent.String() 227 _, confOK := configured[name] 228 _, localOK := localNames[name] 229 _, passedOK := passedIn[name] 230 231 // This name was not declared somewhere within in the 232 // configuration. We ignore empty configs, because they will 233 // already produce a warning. 234 if !(confOK || localOK) { 235 defAddr := addrs.NewDefaultProvider(name) 236 diags = append(diags, &hcl.Diagnostic{ 237 Severity: hcl.DiagWarning, 238 Summary: "Reference to undefined provider", 239 Detail: fmt.Sprintf( 240 "There is no explicit declaration for local provider name %q in %s, so Terraform is assuming you mean to pass a configuration for provider %q.\n\nTo clarify your intent and silence this warning, add to %s a required_providers entry named %q with source = %q, or a different source address if appropriate.", 241 name, moduleText, defAddr.ForDisplay(), 242 parentModuleText, name, defAddr.ForDisplay(), 243 ), 244 Subject: &passed.InParent.NameRange, 245 }) 246 continue 247 } 248 249 // Now we may have named this provider within the module, but 250 // there won't be a configuration available at runtime if the 251 // parent module did not pass one in. 252 if !cfg.Path.IsRoot() && !(confOK || passedOK) { 253 defAddr := addrs.NewDefaultProvider(name) 254 diags = append(diags, &hcl.Diagnostic{ 255 Severity: hcl.DiagWarning, 256 Summary: "Missing required provider configuration", 257 Detail: fmt.Sprintf( 258 "The configuration for %s expects to inherit a configuration for provider %s with local name %q, but %s doesn't pass a configuration under that name.\n\nTo satisfy this requirement, add an entry for %q to the \"providers\" argument in the module %q block.", 259 moduleText, defAddr.ForDisplay(), name, parentModuleText, 260 name, parentCall.Name, 261 ), 262 Subject: parentCall.DeclRange.Ptr(), 263 }) 264 } 265 } 266 } 267 268 if cfg.Path.IsRoot() { 269 // nothing else to do in the root module 270 return diags 271 } 272 273 // there cannot be any configurations if no provider config is allowed 274 if len(configured) > 0 && noProviderConfigRange != nil { 275 // We report this from the perspective of the use of count, for_each, 276 // or depends_on rather than from inside the module, because the 277 // recipient of this message is more likely to be the author of the 278 // calling module (trying to use an older module that hasn't been 279 // updated yet) than of the called module. 280 diags = append(diags, &hcl.Diagnostic{ 281 Severity: hcl.DiagError, 282 Summary: "Module is incompatible with count, for_each, and depends_on", 283 Detail: fmt.Sprintf( 284 "The module at %s is a legacy module which contains its own local provider configurations, and so calls to it may not use the count, for_each, or depends_on arguments.\n\nIf you also control the module %q, consider updating this module to instead expect provider configurations to be passed by its caller.", 285 cfg.Path, cfg.SourceAddr, 286 ), 287 Subject: noProviderConfigRange, 288 }) 289 } 290 291 // now check that the user is not attempting to override a config 292 for name := range configured { 293 if passed, ok := passedIn[name]; ok { 294 diags = append(diags, &hcl.Diagnostic{ 295 Severity: hcl.DiagError, 296 Summary: "Cannot override provider configuration", 297 Detail: fmt.Sprintf( 298 "The configuration of %s has its own local configuration for %s, and so it cannot accept an overridden configuration provided by %s.", 299 moduleText, name, parentModuleText, 300 ), 301 Subject: &passed.InChild.NameRange, 302 }) 303 } 304 } 305 306 // A declared alias requires either a matching configuration within the 307 // module, or one must be passed in. 308 for name, providerAddr := range configAliases { 309 _, confOk := configured[name] 310 _, passedOk := passedIn[name] 311 312 if confOk || passedOk { 313 continue 314 } 315 316 diags = append(diags, &hcl.Diagnostic{ 317 Severity: hcl.DiagError, 318 Summary: "Missing required provider configuration", 319 Detail: fmt.Sprintf( 320 "The child module requires an additional configuration for provider %s, with the local name %q.\n\nRefer to the module's documentation to understand the intended purpose of this additional provider configuration, and then add an entry for %s in the \"providers\" meta-argument in the module block to choose which provider configuration the module should use for that purpose.", 321 providerAddr.Provider.ForDisplay(), name, 322 name, 323 ), 324 Subject: &parentCall.DeclRange, 325 }) 326 } 327 328 // You cannot pass in a provider that cannot be used 329 for name, passed := range passedIn { 330 childTy := passed.InChild.providerType 331 // get a default type if there was none set 332 if childTy.IsZero() { 333 // This means the child module is only using an inferred 334 // provider type. We allow this but will generate a warning to 335 // declare provider_requirements below. 336 childTy = addrs.NewDefaultProvider(passed.InChild.Name) 337 } 338 339 providerAddr := addrs.AbsProviderConfig{ 340 Module: cfg.Path, 341 Provider: childTy, 342 Alias: passed.InChild.Alias, 343 } 344 345 localAddr, localName := localNames[name] 346 if localName { 347 providerAddr.Provider = localAddr 348 } 349 350 aliasAddr, configAlias := configAliases[name] 351 if configAlias { 352 providerAddr = aliasAddr 353 } 354 355 _, emptyConfig := emptyConfigs[name] 356 357 if !(localName || configAlias || emptyConfig) { 358 359 // we still allow default configs, so switch to a warning if the incoming provider is a default 360 if addrs.IsDefaultProvider(providerAddr.Provider) { 361 diags = append(diags, &hcl.Diagnostic{ 362 Severity: hcl.DiagWarning, 363 Summary: "Reference to undefined provider", 364 Detail: fmt.Sprintf( 365 "There is no explicit declaration for local provider name %q in %s, so Terraform is assuming you mean to pass a configuration for %q.\n\nIf you also control the child module, add a required_providers entry named %q with the source address %q.", 366 name, moduleText, providerAddr.Provider.ForDisplay(), 367 name, providerAddr.Provider.ForDisplay(), 368 ), 369 Subject: &passed.InChild.NameRange, 370 }) 371 } else { 372 diags = append(diags, &hcl.Diagnostic{ 373 Severity: hcl.DiagError, 374 Summary: "Reference to undefined provider", 375 Detail: fmt.Sprintf( 376 "The child module does not declare any provider requirement with the local name %q.\n\nIf you also control the child module, you can add a required_providers entry named %q with the source address %q to accept this provider configuration.", 377 name, name, providerAddr.Provider.ForDisplay(), 378 ), 379 Subject: &passed.InChild.NameRange, 380 }) 381 } 382 } 383 384 // The provider being passed in must also be of the correct type. 385 pTy := passed.InParent.providerType 386 if pTy.IsZero() { 387 // While we would like to ensure required_providers exists here, 388 // implied default configuration is still allowed. 389 pTy = addrs.NewDefaultProvider(passed.InParent.Name) 390 } 391 392 // use the full address for a nice diagnostic output 393 parentAddr := addrs.AbsProviderConfig{ 394 Module: cfg.Parent.Path, 395 Provider: pTy, 396 Alias: passed.InParent.Alias, 397 } 398 399 if cfg.Parent.Module.ProviderRequirements != nil { 400 req, defined := cfg.Parent.Module.ProviderRequirements.RequiredProviders[name] 401 if defined { 402 parentAddr.Provider = req.Type 403 } 404 } 405 406 if !providerAddr.Provider.Equals(parentAddr.Provider) { 407 // If this module declares the same source address for a different 408 // local name then we'll prefer to suggest changing to match 409 // the child module's chosen name, assuming that it was the local 410 // name that was wrong rather than the source address. 411 var otherLocalName string 412 for localName, sourceAddr := range localNames { 413 if sourceAddr.Equals(parentAddr.Provider) { 414 otherLocalName = localName 415 break 416 } 417 } 418 419 const errSummary = "Provider type mismatch" 420 if otherLocalName != "" { 421 diags = append(diags, &hcl.Diagnostic{ 422 Severity: hcl.DiagError, 423 Summary: errSummary, 424 Detail: fmt.Sprintf( 425 "The assigned configuration is for provider %q, but local name %q in %s represents %q.\n\nTo pass this configuration to the child module, use the local name %q instead.", 426 parentAddr.Provider.ForDisplay(), passed.InChild.Name, 427 parentModuleText, providerAddr.Provider.ForDisplay(), 428 otherLocalName, 429 ), 430 Subject: &passed.InChild.NameRange, 431 }) 432 } else { 433 // If there is no declared requirement for the provider the 434 // caller is trying to pass under any name then we'll instead 435 // report it as an unsuitable configuration to pass into the 436 // child module's provider configuration slot. 437 diags = append(diags, &hcl.Diagnostic{ 438 Severity: hcl.DiagError, 439 Summary: errSummary, 440 Detail: fmt.Sprintf( 441 "The local name %q in %s represents provider %q, but %q in %s represents %q.\n\nEach provider has its own distinct configuration schema and provider types, so this module's %q can be assigned only a configuration for %s, which is not required by %s.", 442 passed.InParent, parentModuleText, parentAddr.Provider.ForDisplay(), 443 passed.InChild, moduleText, providerAddr.Provider.ForDisplay(), 444 passed.InChild, providerAddr.Provider.ForDisplay(), 445 moduleText, 446 ), 447 Subject: passed.InParent.NameRange.Ptr(), 448 }) 449 } 450 } 451 } 452 453 // Empty configurations are no longer needed. Since the replacement for 454 // this calls for one entry per provider rather than one entry per 455 // provider _configuration_, we'll first gather them up by provider 456 // and then report a single warning for each, whereby we can show a direct 457 // example of what the replacement should look like. 458 type ProviderReqSuggestion struct { 459 SourceAddr addrs.Provider 460 SourceRanges []hcl.Range 461 RequiredConfigs []string 462 AliasCount int 463 } 464 providerReqSuggestions := make(map[string]*ProviderReqSuggestion) 465 for name, src := range emptyConfigs { 466 providerLocalName := name 467 if idx := strings.IndexByte(providerLocalName, '.'); idx >= 0 { 468 providerLocalName = providerLocalName[:idx] 469 } 470 471 sourceAddr, ok := localNames[name] 472 if !ok { 473 sourceAddr = addrs.NewDefaultProvider(providerLocalName) 474 } 475 476 suggestion := providerReqSuggestions[providerLocalName] 477 if suggestion == nil { 478 providerReqSuggestions[providerLocalName] = &ProviderReqSuggestion{ 479 SourceAddr: sourceAddr, 480 } 481 suggestion = providerReqSuggestions[providerLocalName] 482 } 483 484 if providerLocalName != name { 485 // It's an aliased provider config, then. 486 suggestion.AliasCount++ 487 } 488 489 suggestion.RequiredConfigs = append(suggestion.RequiredConfigs, name) 490 suggestion.SourceRanges = append(suggestion.SourceRanges, src) 491 } 492 for name, suggestion := range providerReqSuggestions { 493 var buf strings.Builder 494 495 fmt.Fprintf( 496 &buf, 497 "Earlier versions of Terraform used empty provider blocks (\"proxy provider configurations\") for child modules to declare their need to be passed a provider configuration by their callers. That approach was ambiguous and is now deprecated.\n\nIf you control this module, you can migrate to the new declaration syntax by removing all of the empty provider %q blocks and then adding or updating an entry like the following to the required_providers block of %s:\n", 498 name, moduleText, 499 ) 500 fmt.Fprintf(&buf, " %s = {\n", name) 501 fmt.Fprintf(&buf, " source = %q\n", suggestion.SourceAddr.ForDisplay()) 502 if suggestion.AliasCount > 0 { 503 // A lexical sort is fine because all of these strings are 504 // guaranteed to start with the same provider local name, and 505 // so we're only really sorting by the alias part. 506 sort.Strings(suggestion.RequiredConfigs) 507 fmt.Fprintln(&buf, " configuration_aliases = [") 508 for _, addrStr := range suggestion.RequiredConfigs { 509 fmt.Fprintf(&buf, " %s,\n", addrStr) 510 } 511 fmt.Fprintln(&buf, " ]") 512 513 } 514 fmt.Fprint(&buf, " }") 515 516 // We're arbitrarily going to just take the one source range that 517 // sorts earliest here. Multiple should be rare, so this is only to 518 // ensure that we produce a deterministic result in the edge case. 519 sort.Slice(suggestion.SourceRanges, func(i, j int) bool { 520 return suggestion.SourceRanges[i].String() < suggestion.SourceRanges[j].String() 521 }) 522 diags = append(diags, &hcl.Diagnostic{ 523 Severity: hcl.DiagWarning, 524 Summary: "Redundant empty provider block", 525 Detail: buf.String(), 526 Subject: suggestion.SourceRanges[0].Ptr(), 527 }) 528 } 529 530 return diags 531 } 532 533 func providerName(name, alias string) string { 534 if alias != "" { 535 name = name + "." + alias 536 } 537 return name 538 }