mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-23 23:50:12 -06:00
31349a9c3a
This is part of a general effort to move all of Terraform's non-library package surface under internal in order to reinforce that these are for internal use within Terraform only. If you were previously importing packages under this prefix into an external codebase, you could pin to an earlier release tag as an interim solution until you've make a plan to achieve the same functionality some other way.
313 lines
11 KiB
Go
313 lines
11 KiB
Go
package configs
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
)
|
|
|
|
// validateProviderConfigs walks the full configuration tree from the root
|
|
// module outward, static validation rules to the various combinations of
|
|
// provider configuration, required_providers values, and module call providers
|
|
// mappings.
|
|
//
|
|
// To retain compatibility with previous terraform versions, empty "proxy
|
|
// provider blocks" are still allowed within modules, though they will
|
|
// generate warnings when the configuration is loaded. The new validation
|
|
// however will generate an error if a suitable provider configuration is not
|
|
// passed in through the module call.
|
|
//
|
|
// The call argument is the ModuleCall for the provided Config cfg. The
|
|
// noProviderConfig argument is passed down the call stack, indicating that the
|
|
// module call, or a parent module call, has used a feature that precludes
|
|
// providers from being configured at all within the module.
|
|
func validateProviderConfigs(parentCall *ModuleCall, cfg *Config, noProviderConfig bool) (diags hcl.Diagnostics) {
|
|
mod := cfg.Module
|
|
|
|
for name, child := range cfg.Children {
|
|
mc := mod.ModuleCalls[name]
|
|
|
|
// if the module call has any of count, for_each or depends_on,
|
|
// providers are prohibited from being configured in this module, or
|
|
// any module beneath this module.
|
|
nope := noProviderConfig || mc.Count != nil || mc.ForEach != nil || mc.DependsOn != nil
|
|
diags = append(diags, validateProviderConfigs(mc, child, nope)...)
|
|
}
|
|
|
|
// the set of provider configuration names passed into the module, with the
|
|
// source range of the provider assignment in the module call.
|
|
passedIn := map[string]PassedProviderConfig{}
|
|
|
|
// the set of empty configurations that could be proxy configurations, with
|
|
// the source range of the empty configuration block.
|
|
emptyConfigs := map[string]*hcl.Range{}
|
|
|
|
// the set of provider with a defined configuration, with the source range
|
|
// of the configuration block declaration.
|
|
configured := map[string]*hcl.Range{}
|
|
|
|
// the set of configuration_aliases defined in the required_providers
|
|
// block, with the fully qualified provider type.
|
|
configAliases := map[string]addrs.AbsProviderConfig{}
|
|
|
|
// the set of provider names defined in the required_providers block, and
|
|
// their provider types.
|
|
localNames := map[string]addrs.AbsProviderConfig{}
|
|
|
|
for _, pc := range mod.ProviderConfigs {
|
|
name := providerName(pc.Name, pc.Alias)
|
|
// Validate the config against an empty schema to see if it's empty.
|
|
_, pcConfigDiags := pc.Config.Content(&hcl.BodySchema{})
|
|
if pcConfigDiags.HasErrors() || pc.Version.Required != nil {
|
|
configured[name] = &pc.DeclRange
|
|
} else {
|
|
emptyConfigs[name] = &pc.DeclRange
|
|
}
|
|
}
|
|
|
|
if mod.ProviderRequirements != nil {
|
|
for _, req := range mod.ProviderRequirements.RequiredProviders {
|
|
addr := addrs.AbsProviderConfig{
|
|
Module: cfg.Path,
|
|
Provider: req.Type,
|
|
}
|
|
localNames[req.Name] = addr
|
|
for _, alias := range req.Aliases {
|
|
addr := addrs.AbsProviderConfig{
|
|
Module: cfg.Path,
|
|
Provider: req.Type,
|
|
Alias: alias.Alias,
|
|
}
|
|
configAliases[providerName(alias.LocalName, alias.Alias)] = addr
|
|
}
|
|
}
|
|
}
|
|
|
|
// collect providers passed from the parent
|
|
if parentCall != nil {
|
|
for _, passed := range parentCall.Providers {
|
|
name := providerName(passed.InChild.Name, passed.InChild.Alias)
|
|
passedIn[name] = passed
|
|
}
|
|
}
|
|
|
|
parentModuleText := "the root module"
|
|
moduleText := "the root module"
|
|
if !cfg.Path.IsRoot() {
|
|
moduleText = cfg.Path.String()
|
|
if parent := cfg.Path.Parent(); !parent.IsRoot() {
|
|
// module address are prefixed with `module.`
|
|
parentModuleText = parent.String()
|
|
}
|
|
}
|
|
|
|
// Verify that any module calls only refer to named providers, and that
|
|
// those providers will have a configuration at runtime. This way we can
|
|
// direct users where to add the missing configuration, because the runtime
|
|
// error is only "missing provider X".
|
|
for _, modCall := range mod.ModuleCalls {
|
|
for _, passed := range modCall.Providers {
|
|
// aliased providers are handled more strictly, and are never
|
|
// inherited, so they are validated within modules further down.
|
|
// Skip these checks to prevent redundant diagnostics.
|
|
if passed.InParent.Alias != "" {
|
|
continue
|
|
}
|
|
|
|
name := passed.InParent.String()
|
|
_, confOK := configured[name]
|
|
_, localOK := localNames[name]
|
|
_, passedOK := passedIn[name]
|
|
|
|
// This name was not declared somewhere within in the
|
|
// configuration. We ignore empty configs, because they will
|
|
// already produce a warning.
|
|
if !(confOK || localOK) {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagWarning,
|
|
Summary: fmt.Sprintf("Provider %s is undefined", name),
|
|
Detail: fmt.Sprintf("No provider named %s has been declared in %s.\n", name, moduleText) +
|
|
fmt.Sprintf("If you wish to refer to the %s provider within the module, add a provider configuration, or an entry in the required_providers block.", name),
|
|
Subject: &passed.InParent.NameRange,
|
|
})
|
|
continue
|
|
}
|
|
|
|
// Now we may have named this provider within the module, but
|
|
// there won't be a configuration available at runtime if the
|
|
// parent module did not pass one in.
|
|
if !cfg.Path.IsRoot() && !(confOK || passedOK) {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagWarning,
|
|
Summary: fmt.Sprintf("No configuration passed in for provider %s in %s", name, cfg.Path),
|
|
Detail: fmt.Sprintf("Provider %s is referenced within %s, but no configuration has been supplied.\n", name, moduleText) +
|
|
fmt.Sprintf("Add a provider named %s to the providers map for %s in %s.", name, cfg.Path, parentModuleText),
|
|
Subject: &passed.InParent.NameRange,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
if cfg.Path.IsRoot() {
|
|
// nothing else to do in the root module
|
|
return diags
|
|
}
|
|
|
|
// there cannot be any configurations if no provider config is allowed
|
|
if len(configured) > 0 && noProviderConfig {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: fmt.Sprintf("Module %s contains provider configuration", cfg.Path),
|
|
Detail: "Providers cannot be configured within modules using count, for_each or depends_on.",
|
|
})
|
|
}
|
|
|
|
// now check that the user is not attempting to override a config
|
|
for name := range configured {
|
|
if passed, ok := passedIn[name]; ok {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Cannot override provider configuration",
|
|
Detail: fmt.Sprintf("Provider %s is configured within the module %s and cannot be overridden.", name, cfg.Path),
|
|
Subject: &passed.InChild.NameRange,
|
|
})
|
|
}
|
|
}
|
|
|
|
// A declared alias requires either a matching configuration within the
|
|
// module, or one must be passed in.
|
|
for name, providerAddr := range configAliases {
|
|
_, confOk := configured[name]
|
|
_, passedOk := passedIn[name]
|
|
|
|
if confOk || passedOk {
|
|
continue
|
|
}
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: fmt.Sprintf("No configuration for provider %s", name),
|
|
Detail: fmt.Sprintf("Configuration required for %s.\n", providerAddr) +
|
|
fmt.Sprintf("Add a provider named %s to the providers map for %s in %s.", name, cfg.Path, parentModuleText),
|
|
Subject: &parentCall.DeclRange,
|
|
})
|
|
}
|
|
|
|
// You cannot pass in a provider that cannot be used
|
|
for name, passed := range passedIn {
|
|
childTy := passed.InChild.providerType
|
|
// get a default type if there was none set
|
|
if childTy.IsZero() {
|
|
// This means the child module is only using an inferred
|
|
// provider type. We allow this but will generate a warning to
|
|
// declare provider_requirements below.
|
|
childTy = addrs.NewDefaultProvider(passed.InChild.Name)
|
|
}
|
|
|
|
providerAddr := addrs.AbsProviderConfig{
|
|
Module: cfg.Path,
|
|
Provider: childTy,
|
|
Alias: passed.InChild.Alias,
|
|
}
|
|
|
|
localAddr, localName := localNames[name]
|
|
if localName {
|
|
providerAddr = localAddr
|
|
}
|
|
|
|
aliasAddr, configAlias := configAliases[name]
|
|
if configAlias {
|
|
providerAddr = aliasAddr
|
|
}
|
|
|
|
_, emptyConfig := emptyConfigs[name]
|
|
|
|
if !(localName || configAlias || emptyConfig) {
|
|
severity := hcl.DiagError
|
|
|
|
// we still allow default configs, so switch to a warning if the incoming provider is a default
|
|
if providerAddr.Provider.IsDefault() {
|
|
severity = hcl.DiagWarning
|
|
}
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: severity,
|
|
Summary: fmt.Sprintf("Provider %s is undefined", name),
|
|
Detail: fmt.Sprintf("Module %s does not declare a provider named %s.\n", cfg.Path, name) +
|
|
fmt.Sprintf("If you wish to specify a provider configuration for the module, add an entry for %s in the required_providers block within the module.", name),
|
|
Subject: &passed.InChild.NameRange,
|
|
})
|
|
}
|
|
|
|
// The provider being passed in must also be of the correct type.
|
|
pTy := passed.InParent.providerType
|
|
if pTy.IsZero() {
|
|
// While we would like to ensure required_providers exists here,
|
|
// implied default configuration is still allowed.
|
|
pTy = addrs.NewDefaultProvider(passed.InParent.Name)
|
|
}
|
|
|
|
// use the full address for a nice diagnostic output
|
|
parentAddr := addrs.AbsProviderConfig{
|
|
Module: cfg.Parent.Path,
|
|
Provider: pTy,
|
|
Alias: passed.InParent.Alias,
|
|
}
|
|
|
|
if cfg.Parent.Module.ProviderRequirements != nil {
|
|
req, defined := cfg.Parent.Module.ProviderRequirements.RequiredProviders[name]
|
|
if defined {
|
|
parentAddr.Provider = req.Type
|
|
}
|
|
}
|
|
|
|
if !providerAddr.Provider.Equals(parentAddr.Provider) {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: fmt.Sprintf("Invalid type for provider %s", providerAddr),
|
|
Detail: fmt.Sprintf("Cannot use configuration from %s for %s. ", parentAddr, providerAddr) +
|
|
"The given provider configuration is for a different provider type.",
|
|
Subject: &passed.InChild.NameRange,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Empty configurations are no longer needed
|
|
for name, src := range emptyConfigs {
|
|
detail := fmt.Sprintf("Remove the %s provider block from %s.", name, cfg.Path)
|
|
|
|
isAlias := strings.Contains(name, ".")
|
|
_, isConfigAlias := configAliases[name]
|
|
_, isLocalName := localNames[name]
|
|
|
|
if isAlias && !isConfigAlias {
|
|
localName := strings.Split(name, ".")[0]
|
|
detail = fmt.Sprintf("Remove the %s provider block from %s. Add %s to the list of configuration_aliases for %s in required_providers to define the provider configuration name.", name, cfg.Path, name, localName)
|
|
}
|
|
|
|
if !isAlias && !isLocalName {
|
|
// if there is no local name, add a note to include it in the
|
|
// required_provider block
|
|
detail += fmt.Sprintf("\nTo ensure the correct provider configuration is used, add %s to the required_providers configuration", name)
|
|
}
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagWarning,
|
|
Summary: "Empty provider configuration blocks are not required",
|
|
Detail: detail,
|
|
Subject: src,
|
|
})
|
|
}
|
|
|
|
return diags
|
|
}
|
|
|
|
func providerName(name, alias string) string {
|
|
if alias != "" {
|
|
name = name + "." + alias
|
|
}
|
|
return name
|
|
}
|