| 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/getmodules" |
| ) |
| |
| // ModuleCall represents a "module" block in a module or file. |
| type ModuleCall struct { |
| Name string |
| |
| SourceAddr addrs.ModuleSource |
| SourceAddrRaw string |
| SourceAddrRange hcl.Range |
| SourceSet bool |
| |
| Config hcl.Body |
| |
| Version VersionConstraint |
| |
| Count hcl.Expression |
| ForEach hcl.Expression |
| |
| Providers []PassedProviderConfig |
| |
| DependsOn []hcl.Traversal |
| |
| DeclRange hcl.Range |
| } |
| |
| func decodeModuleBlock(block *hcl.Block, override bool) (*ModuleCall, hcl.Diagnostics) { |
| var diags hcl.Diagnostics |
| |
| mc := &ModuleCall{ |
| Name: block.Labels[0], |
| DeclRange: block.DefRange, |
| } |
| |
| schema := moduleBlockSchema |
| if override { |
| schema = schemaForOverrides(schema) |
| } |
| |
| content, remain, moreDiags := block.Body.PartialContent(schema) |
| diags = append(diags, moreDiags...) |
| mc.Config = remain |
| |
| if !hclsyntax.ValidIdentifier(mc.Name) { |
| diags = append(diags, &hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid module instance name", |
| Detail: badIdentifierDetail, |
| Subject: &block.LabelRanges[0], |
| }) |
| } |
| |
| haveVersionArg := false |
| if attr, exists := content.Attributes["version"]; exists { |
| var versionDiags hcl.Diagnostics |
| mc.Version, versionDiags = decodeVersionConstraint(attr) |
| diags = append(diags, versionDiags...) |
| haveVersionArg = true |
| } |
| |
| if attr, exists := content.Attributes["source"]; exists { |
| mc.SourceSet = true |
| mc.SourceAddrRange = attr.Expr.Range() |
| valDiags := gohcl.DecodeExpression(attr.Expr, nil, &mc.SourceAddrRaw) |
| diags = append(diags, valDiags...) |
| if !valDiags.HasErrors() { |
| var addr addrs.ModuleSource |
| var err error |
| if haveVersionArg { |
| addr, err = addrs.ParseModuleSourceRegistry(mc.SourceAddrRaw) |
| } else { |
| addr, err = addrs.ParseModuleSource(mc.SourceAddrRaw) |
| } |
| mc.SourceAddr = addr |
| if err != nil { |
| // NOTE: We leave mc.SourceAddr as nil for any situation where the |
| // source attribute is invalid, so any code which tries to carefully |
| // use the partial result of a failed config decode must be |
| // resilient to that. |
| mc.SourceAddr = nil |
| |
| // NOTE: In practice it's actually very unlikely to end up here, |
| // because our source address parser can turn just about any string |
| // into some sort of remote package address, and so for most errors |
| // we'll detect them only during module installation. There are |
| // still a _few_ purely-syntax errors we can catch at parsing time, |
| // though, mostly related to remote package sub-paths and local |
| // paths. |
| switch err := err.(type) { |
| case *getmodules.MaybeRelativePathErr: |
| diags = append(diags, &hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid module source address", |
| Detail: fmt.Sprintf( |
| "Terraform failed to determine your intended installation method for remote module package %q.\n\nIf you intended this as a path relative to the current module, use \"./%s\" instead. The \"./\" prefix indicates that the address is a relative filesystem path.", |
| err.Addr, err.Addr, |
| ), |
| Subject: mc.SourceAddrRange.Ptr(), |
| }) |
| default: |
| if haveVersionArg { |
| // In this case we'll include some extra context that |
| // we assumed a registry source address due to the |
| // version argument. |
| diags = append(diags, &hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid registry module source address", |
| Detail: fmt.Sprintf("Failed to parse module registry address: %s.\n\nTerraform assumed that you intended a module registry source address because you also set the argument \"version\", which applies only to registry modules.", err), |
| Subject: mc.SourceAddrRange.Ptr(), |
| }) |
| } else { |
| diags = append(diags, &hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid module source address", |
| Detail: fmt.Sprintf("Failed to parse module source address: %s.", err), |
| Subject: mc.SourceAddrRange.Ptr(), |
| }) |
| } |
| } |
| } |
| } |
| } |
| |
| if attr, exists := content.Attributes["count"]; exists { |
| mc.Count = attr.Expr |
| } |
| |
| if attr, exists := content.Attributes["for_each"]; exists { |
| if mc.Count != nil { |
| diags = append(diags, &hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: `Invalid combination of "count" and "for_each"`, |
| Detail: `The "count" and "for_each" meta-arguments are mutually-exclusive, only one should be used to be explicit about the number of resources to be created.`, |
| Subject: &attr.NameRange, |
| }) |
| } |
| |
| mc.ForEach = attr.Expr |
| } |
| |
| if attr, exists := content.Attributes["depends_on"]; exists { |
| deps, depsDiags := decodeDependsOn(attr) |
| diags = append(diags, depsDiags...) |
| mc.DependsOn = append(mc.DependsOn, deps...) |
| } |
| |
| if attr, exists := content.Attributes["providers"]; exists { |
| seen := make(map[string]hcl.Range) |
| pairs, pDiags := hcl.ExprMap(attr.Expr) |
| diags = append(diags, pDiags...) |
| for _, pair := range pairs { |
| key, keyDiags := decodeProviderConfigRef(pair.Key, "providers") |
| diags = append(diags, keyDiags...) |
| value, valueDiags := decodeProviderConfigRef(pair.Value, "providers") |
| diags = append(diags, valueDiags...) |
| if keyDiags.HasErrors() || valueDiags.HasErrors() { |
| continue |
| } |
| |
| matchKey := key.String() |
| if prev, exists := seen[matchKey]; exists { |
| diags = append(diags, &hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Duplicate provider address", |
| Detail: fmt.Sprintf("A provider configuration was already passed to %s at %s. Each child provider configuration can be assigned only once.", matchKey, prev), |
| Subject: pair.Value.Range().Ptr(), |
| }) |
| continue |
| } |
| |
| rng := hcl.RangeBetween(pair.Key.Range(), pair.Value.Range()) |
| seen[matchKey] = rng |
| mc.Providers = append(mc.Providers, PassedProviderConfig{ |
| InChild: key, |
| InParent: value, |
| }) |
| } |
| } |
| |
| 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 module input variables rather than as meta-arguments, but each module 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. |
| mc.Config = hcl.MergeBodies([]hcl.Body{mc.Config, block.Body}) |
| |
| default: |
| // All of the other block types in our schema are reserved. |
| diags = append(diags, &hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Reserved block type name in module 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 mc, diags |
| } |
| |
| // EntersNewPackage returns true if this call is to an external module, either |
| // directly via a remote source address or indirectly via a registry source |
| // address. |
| // |
| // Other behaviors in Terraform may treat package crossings as a special |
| // situation, because that indicates that the caller and callee can change |
| // independently of one another and thus we should disallow using any features |
| // where the caller assumes anything about the callee other than its input |
| // variables, required provider configurations, and output values. |
| func (mc *ModuleCall) EntersNewPackage() bool { |
| return moduleSourceAddrEntersNewPackage(mc.SourceAddr) |
| } |
| |
| // PassedProviderConfig represents a provider config explicitly passed down to |
| // a child module, possibly giving it a new local address in the process. |
| type PassedProviderConfig struct { |
| InChild *ProviderConfigRef |
| InParent *ProviderConfigRef |
| } |
| |
| var moduleBlockSchema = &hcl.BodySchema{ |
| Attributes: []hcl.AttributeSchema{ |
| { |
| Name: "source", |
| Required: true, |
| }, |
| { |
| Name: "version", |
| }, |
| { |
| Name: "count", |
| }, |
| { |
| Name: "for_each", |
| }, |
| { |
| Name: "depends_on", |
| }, |
| { |
| Name: "providers", |
| }, |
| }, |
| Blocks: []hcl.BlockHeaderSchema{ |
| {Type: "_"}, // meta-argument escaping block |
| |
| // These are all reserved for future use. |
| {Type: "lifecycle"}, |
| {Type: "locals"}, |
| {Type: "provider", LabelNames: []string{"type"}}, |
| }, |
| } |
| |
| func moduleSourceAddrEntersNewPackage(addr addrs.ModuleSource) bool { |
| switch addr.(type) { |
| case nil: |
| // There are only two situations where we should get here: |
| // - We've been asked about the source address of the root module, |
| // which is always nil. |
| // - We've been asked about a ModuleCall that is part of the partial |
| // result of a failed decode. |
| // The root module exists outside of all module packages, so we'll |
| // just return false for that case. For the error case it doesn't |
| // really matter what we return as long as we don't panic, because |
| // we only make a best-effort to allow careful inspection of objects |
| // representing invalid configuration. |
| return false |
| case addrs.ModuleSourceLocal: |
| // Local source addresses are the only address type that remains within |
| // the same package. |
| return false |
| default: |
| // All other address types enter a new package. |
| return true |
| } |
| } |