package configs import ( "fmt" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/tfdiags" ) // Provider represents a "provider" block in a module or file. A provider // block is a provider configuration, and there can be zero or more // configurations for each actual provider. type Provider struct { Name string NameRange hcl.Range Alias string AliasRange *hcl.Range // nil if no alias set Version VersionConstraint Config hcl.Body DeclRange hcl.Range // TODO: this may not be set in some cases, so it is not yet suitable for // use outside of this package. We currently only use it for internal // validation, but once we verify that this can be set in all cases, we can // export this so providers don't need to be re-resolved. // This same field is also added to the ProviderConfigRef struct. providerType addrs.Provider } func decodeProviderBlock(block *hcl.Block) (*Provider, hcl.Diagnostics) { var diags hcl.Diagnostics content, config, moreDiags := block.Body.PartialContent(providerBlockSchema) diags = append(diags, moreDiags...) // Provider names must be localized. Produce an error with a message // indicating the action the user can take to fix this message if the local // name is not localized. name := block.Labels[0] nameDiags := checkProviderNameNormalized(name, block.DefRange) diags = append(diags, nameDiags...) if nameDiags.HasErrors() { // If the name is invalid then we mustn't produce a result because // downstreams could try to use it as a provider type and then crash. return nil, diags } provider := &Provider{ Name: name, NameRange: block.LabelRanges[0], Config: config, DeclRange: block.DefRange, } if attr, exists := content.Attributes["alias"]; exists { valDiags := gohcl.DecodeExpression(attr.Expr, nil, &provider.Alias) diags = append(diags, valDiags...) provider.AliasRange = attr.Expr.Range().Ptr() if !hclsyntax.ValidIdentifier(provider.Alias) { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid provider configuration alias", Detail: fmt.Sprintf("An alias must be a valid name. %s", badIdentifierDetail), }) } } if attr, exists := content.Attributes["version"]; exists { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagWarning, Summary: "Version constraints inside provider configuration blocks are deprecated", Detail: "Terraform 0.13 and earlier allowed provider version constraints inside the provider configuration block, but that is now deprecated and will be removed in a future version of Terraform. To silence this warning, move the provider version constraint into the required_providers block.", Subject: attr.Expr.Range().Ptr(), }) var versionDiags hcl.Diagnostics provider.Version, versionDiags = decodeVersionConstraint(attr) diags = append(diags, versionDiags...) } // Reserved attribute names for _, name := range []string{"count", "depends_on", "for_each", "source"} { if attr, exists := content.Attributes[name]; exists { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Reserved argument name in provider block", Detail: fmt.Sprintf("The provider argument name %q is reserved for use by Terraform in a future version.", name), Subject: &attr.NameRange, }) } } var seenEscapeBlock *hcl.Block for _, block := range content.Blocks { switch block.Type { case "_": if seenEscapeBlock != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Duplicate escaping block", Detail: fmt.Sprintf( "The special block type \"_\" can be used to force particular arguments to be interpreted as provider-specific rather than as meta-arguments, but each provider block can have only one such block. The first escaping block was at %s.", seenEscapeBlock.DefRange, ), Subject: &block.DefRange, }) continue } seenEscapeBlock = block // When there's an escaping block its content merges with the // existing config we extracted earlier, so later decoding // will see a blend of both. provider.Config = hcl.MergeBodies([]hcl.Body{provider.Config, block.Body}) default: // All of the other block types in our schema are reserved for // future expansion. diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Reserved block type name in provider block", Detail: fmt.Sprintf("The block type name %q is reserved for use by Terraform in a future version.", block.Type), Subject: &block.TypeRange, }) } } return provider, diags } // Addr returns the address of the receiving provider configuration, relative // to its containing module. func (p *Provider) Addr() addrs.LocalProviderConfig { return addrs.LocalProviderConfig{ LocalName: p.Name, Alias: p.Alias, } } func (p *Provider) moduleUniqueKey() string { if p.Alias != "" { return fmt.Sprintf("%s.%s", p.Name, p.Alias) } return p.Name } // ParseProviderConfigCompact parses the given absolute traversal as a relative // provider address in compact form. The following are examples of traversals // that can be successfully parsed as compact relative provider configuration // addresses: // // aws // aws.foo // // This function will panic if given a relative traversal. // // If the returned diagnostics contains errors then the result value is invalid // and must not be used. func ParseProviderConfigCompact(traversal hcl.Traversal) (addrs.LocalProviderConfig, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics ret := addrs.LocalProviderConfig{ LocalName: traversal.RootName(), } if len(traversal) < 2 { // Just a type name, then. return ret, diags } aliasStep := traversal[1] switch ts := aliasStep.(type) { case hcl.TraverseAttr: ret.Alias = ts.Name return ret, diags default: diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid provider configuration address", Detail: "The provider type name must either stand alone or be followed by an alias name separated with a dot.", Subject: aliasStep.SourceRange().Ptr(), }) } if len(traversal) > 2 { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid provider configuration address", Detail: "Extraneous extra operators after provider configuration address.", Subject: traversal[2:].SourceRange().Ptr(), }) } return ret, diags } // ParseProviderConfigCompactStr is a helper wrapper around ParseProviderConfigCompact // that takes a string and parses it with the HCL native syntax traversal parser // before interpreting it. // // This should be used only in specialized situations since it will cause the // created references to not have any meaningful source location information. // If a reference string is coming from a source that should be identified in // error messages then the caller should instead parse it directly using a // suitable function from the HCL API and pass the traversal itself to // ParseProviderConfigCompact. // // Error diagnostics are returned if either the parsing fails or the analysis // of the traversal fails. There is no way for the caller to distinguish the // two kinds of diagnostics programmatically. If error diagnostics are returned // then the returned address is invalid. func ParseProviderConfigCompactStr(str string) (addrs.LocalProviderConfig, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1}) diags = diags.Append(parseDiags) if parseDiags.HasErrors() { return addrs.LocalProviderConfig{}, diags } addr, addrDiags := ParseProviderConfigCompact(traversal) diags = diags.Append(addrDiags) return addr, diags } var providerBlockSchema = &hcl.BodySchema{ Attributes: []hcl.AttributeSchema{ { Name: "alias", }, { Name: "version", }, // Attribute names reserved for future expansion. {Name: "count"}, {Name: "depends_on"}, {Name: "for_each"}, {Name: "source"}, }, Blocks: []hcl.BlockHeaderSchema{ {Type: "_"}, // meta-argument escaping block // The rest of these are reserved for future expansion. {Type: "lifecycle"}, {Type: "locals"}, }, } // checkProviderNameNormalized verifies that the given string is already // normalized and returns an error if not. func checkProviderNameNormalized(name string, declrange hcl.Range) hcl.Diagnostics { var diags hcl.Diagnostics // verify that the provider local name is normalized normalized, err := addrs.IsProviderPartNormalized(name) if err != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid provider local name", Detail: fmt.Sprintf("%s is an invalid provider local name: %s", name, err), Subject: &declrange, }) return diags } if !normalized { // we would have returned this error already normalizedProvider, _ := addrs.ParseProviderPart(name) diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid provider local name", Detail: fmt.Sprintf("Provider names must be normalized. Replace %q with %q to fix this error.", name, normalizedProvider), Subject: &declrange, }) } return diags }