opentofu/internal/configs/provider_validation.go

313 lines
11 KiB
Go
Raw Normal View History

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
}