package configs import ( "fmt" "strings" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/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 }