diff --git a/internal/command/init2_test.go b/internal/command/init2_test.go index c0515984a617..0368d7169ad6 100644 --- a/internal/command/init2_test.go +++ b/internal/command/init2_test.go @@ -8,6 +8,8 @@ import ( "path/filepath" "strings" "testing" + + "github.com/hashicorp/cli" ) func TestInit2_dynamicSourceErrors(t *testing.T) { @@ -800,6 +802,13 @@ func TestInit2_dynamicProviderSourceSuccess(t *testing.T) { "hashicorp2/test": {"1.0.0"}, }, }, + "const with extra resource and provider local name": { + fixture: "provider-source-with-resources-and-provider-local-name", + args: []string{}, + providers: map[string][]string{ + "hashicorp2/test": {"1.0.0"}, + }, + }, } for name, tc := range tests { diff --git a/internal/command/init_run.go b/internal/command/init_run.go index cae8c7567297..e77b9e76ee91 100644 --- a/internal/command/init_run.go +++ b/internal/command/init_run.go @@ -114,8 +114,8 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int { return 0 } - // Load just the root module to begin backend and module initialization - rootModEarly, earlyConfDiags := c.loadSingleModuleWithTests(path, initArgs.TestsDirectory) + // Load just the raw root module to begin backend and module initialization. + rootModEarly, earlyConfDiags := c.loadRawModuleWithTests(path, initArgs.TestsDirectory) // There may be parsing errors in config loading but these will be shown later _after_ // checking for core version requirement errors. Not meeting the version requirement should diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index 00bc481382d5..d16b84320665 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -20,7 +20,7 @@ import ( "strings" "github.com/hashicorp/cli" - version "github.com/hashicorp/go-version" + "github.com/hashicorp/go-version" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hcldec" "github.com/zclconf/go-cty/cty" @@ -2070,7 +2070,7 @@ func (m *Meta) backend(configPath string, viewType arguments.ViewType) (backendr // Only return error diagnostics at this point. Any warnings will be caught // again later and duplicated in the output. - root, mDiags := m.loadSingleModule(configPath) + root, mDiags := m.loadRawModule(configPath) if mDiags.HasErrors() { diags = diags.Append(mDiags) return nil, diags diff --git a/internal/command/meta_config.go b/internal/command/meta_config.go index 6727a13daad5..2dc36277f66d 100644 --- a/internal/command/meta_config.go +++ b/internal/command/meta_config.go @@ -49,7 +49,7 @@ func (m *Meta) normalizePath(path string) string { // If no const variables are unsatisfied, or if the backend does not support // supplying variables, this method is a no-op. func (m *Meta) resolveConstVariables(rootDir string, viewType arguments.ViewType) tfdiags.Diagnostics { - rootMod, diags := m.loadSingleModule(rootDir) + rootMod, diags := m.loadRawModule(rootDir) if diags.HasErrors() { return diags } @@ -168,15 +168,8 @@ func (m *Meta) loadConfigWithTests(rootDir, testDir string) (*configs.Config, tf return config, diags } -// loadSingleModule reads configuration from the given directory and returns -// a description of that module only, without attempting to assemble a module -// tree for referenced child modules. -// -// Most callers should use loadConfig. This method exists to support early -// initialization use-cases where the root module must be inspected in order -// to determine what else needs to be installed before the full configuration -// can be used. -func (m *Meta) loadSingleModule(dir string) (*configs.Module, tfdiags.Diagnostics) { +// Load module without running init graph +func (m *Meta) loadRawModule(dir string) (*configs.Module, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics dir = m.normalizePath(dir) @@ -191,9 +184,7 @@ func (m *Meta) loadSingleModule(dir string) (*configs.Module, tfdiags.Diagnostic return module, diags } -// loadSingleModuleWithTests matches loadSingleModule except it also loads any -// tests for the target module. -func (m *Meta) loadSingleModuleWithTests(dir string, testDir string) (*configs.Module, tfdiags.Diagnostics) { +func (m *Meta) loadRawModuleWithTests(dir string, testDir string) (*configs.Module, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics dir = m.normalizePath(dir) @@ -208,6 +199,72 @@ func (m *Meta) loadSingleModuleWithTests(dir string, testDir string) (*configs.M return module, diags } +// loadSingleModule reads configuration from the given directory and returns +// a description of that module only, without attempting to assemble a module +// tree for referenced child modules. It runs the init graph to resolve any +// dynamic provider/module source expressions using the caller's const variable +// values. +// +// Most callers should use loadConfig. This method exists to support early +// initialization use-cases where the root module must be inspected in order +// to determine what else needs to be installed before the full configuration +// can be used. +func (m *Meta) loadSingleModule(dir string) (*configs.Module, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + dir = m.normalizePath(dir) + + loader, err := m.initConfigLoader() + if err != nil { + diags = diags.Append(err) + return nil, diags + } + + module, hclDiags := loader.Parser().LoadConfigDir(dir) + diags = diags.Append(hclDiags) + if diags.HasErrors() { + return nil, diags + } + + vars, varDiags := backendrun.ParseConstVariableValues(m.VariableValues, module.Variables) + diags = diags.Append(varDiags) + if varDiags.HasErrors() { + return nil, diags + } + + mod, buildDiags := terraform.BuildModuleWithGraph(module, vars) + diags = diags.Append(buildDiags) + return mod, diags +} + +// loadSingleModuleWithTests matches loadSingleModule except it also loads any +// tests for the target module. +//func (m *Meta) loadSingleModuleWithTests(dir string, testDir string) (*configs.Module, tfdiags.Diagnostics) { +// var diags tfdiags.Diagnostics +// dir = m.normalizePath(dir) +// +// loader, err := m.initConfigLoader() +// if err != nil { +// diags = diags.Append(err) +// return nil, diags +// } +// +// module, hclDiags := loader.Parser().LoadConfigDirWithTests(dir, testDir) +// diags = diags.Append(hclDiags) +// if diags.HasErrors() { +// return nil, diags +// } +// +// vars, varDiags := backendrun.ParseConstVariableValues(m.VariableValues, module.Variables) +// diags = diags.Append(varDiags) +// if varDiags.HasErrors() { +// return nil, diags +// } +// +// mod, buildDiags := terraform.BuildModuleWithGraph(module, vars) +// diags = diags.Append(buildDiags) +// return mod, diags +//} + // dirIsConfigPath checks if the given path is a directory that contains at // least one Terraform configuration file (.tf or .tf.json), returning true // if so. @@ -240,7 +297,9 @@ func (m *Meta) dirIsConfigPath(dir string) bool { // that a call to loadSingleModule or loadConfig could fail on the same // directory even if loadBackendConfig succeeded.) func (m *Meta) loadBackendConfig(rootDir string) (*configs.Backend, tfdiags.Diagnostics) { - mod, diags := m.loadSingleModule(rootDir) + // Use loadRawModule here (no init graph) because we only need the + // Backend and CloudConfig fields which are populated by HCL parsing. + mod, diags := m.loadRawModule(rootDir) // Only return error diagnostics at this point. Any warnings will be caught // again later and duplicated in the output. diff --git a/internal/command/modules.go b/internal/command/modules.go index 93584285eb2d..c8ec3e6ab08d 100644 --- a/internal/command/modules.go +++ b/internal/command/modules.go @@ -72,8 +72,8 @@ func (c *ModulesCommand) Run(rawArgs []string) int { return 1 } - // Read the root module path so we can then traverse the tree - rootModEarly, earlyConfDiags := c.loadSingleModule(rootModPath) + // Read the root module path so we can then traverse the tree. + rootModEarly, earlyConfDiags := c.loadRawModule(rootModPath) if rootModEarly == nil { diags = diags.Append(errors.New("root module not found. Please run terraform init"), earlyConfDiags) view.Diagnostics(diags) diff --git a/internal/command/test.go b/internal/command/test.go index a3376cacd743..415fa1ffd9ed 100644 --- a/internal/command/test.go +++ b/internal/command/test.go @@ -356,9 +356,12 @@ func (m *Meta) setupTestExecution(mode moduletest.CommandMode, command string, r // test runs rather than the root module. // // We do an early load of just the root module to discover which - // variables are const. We discard non-error diagnostics from this - // early load since loadConfigWithTests will re-parse and report them. - earlyMod, earlyDiags := m.loadSingleModuleWithTests(".", preparation.Args.TestDirectory) + // variables are const. We only need the Variables declarations here, + // so we use loadRawModuleWithTests (no init graph) to avoid a + // chicken-and-egg problem where const variable values aren't known yet. + // Non-error diagnostics are discarded since loadConfigWithTests will + // reparse and report them. + earlyMod, earlyDiags := m.loadRawModuleWithTests(".", preparation.Args.TestDirectory) if earlyDiags.HasErrors() { diags = diags.Append(earlyDiags) view.Diagnostics(nil, nil, diags) diff --git a/internal/command/testdata/dynamic-provider-sources/provider-source-with-resources-and-provider-local-name/main.tf b/internal/command/testdata/dynamic-provider-sources/provider-source-with-resources-and-provider-local-name/main.tf new file mode 100644 index 000000000000..cc04856ac93a --- /dev/null +++ b/internal/command/testdata/dynamic-provider-sources/provider-source-with-resources-and-provider-local-name/main.tf @@ -0,0 +1,17 @@ +terraform { + required_providers { + test-local-name = { + source = "${var.namespace}/test" + } + } +} + +variable "namespace" { + type = string + const = true + default = "hashicorp2" +} + +resource "test_instance" "example" { + provider = test-local-name +} diff --git a/internal/configs/module.go b/internal/configs/module.go index 1b25fa0212b6..3531485b91e1 100644 --- a/internal/configs/module.go +++ b/internal/configs/module.go @@ -6,7 +6,9 @@ package configs import ( "fmt" + "github.com/hashicorp/go-version" "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/experiments" @@ -190,6 +192,8 @@ func NewModule(primaryFiles, overrideFiles []*File) (*Module, hcl.Diagnostics) { } } + diags = append(diags, mod.resolveStaticProviderExprs()...) + for _, file := range primaryFiles { fileDiags := mod.appendFile(file) diags = append(diags, fileDiags...) @@ -963,6 +967,165 @@ func (m *Module) GatherProviderLocalNames() { m.ProviderLocalNames = providers } +func (m *Module) resolveStaticProviderExprs() hcl.Diagnostics { + var diags hcl.Diagnostics + + for name, expr := range m.ProviderRequirementExprs { + if expr.NeedsEvalContext() { + // Keep it deferred; will be resolved by nodeResolveProviderRequirements. + continue + } + + rp := &RequiredProvider{ + Name: name, + Aliases: expr.ConfigAliases, + DeclRange: expr.DeclRange, + } + + if expr.SourceExpr != nil { + sourceVal, valDiags := expr.SourceExpr.Value(nil) + if valDiags.HasErrors() || !sourceVal.Type().Equals(cty.String) { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid source", + Detail: "Source must be specified as a string.", + Subject: expr.SourceExpr.Range().Ptr(), + }) + continue + } + + fqn, sourceDiags := addrs.ParseProviderSourceString(sourceVal.AsString()) + if sourceDiags.HasErrors() { + hclDiags := sourceDiags.ToHCL() + for _, d := range hclDiags { + if d.Subject == nil { + d.Subject = expr.SourceExpr.Range().Ptr() + } + } + diags = append(diags, hclDiags...) + continue + } + + rp.Source = sourceVal.AsString() + rp.Type = fqn + } + + if expr.VersionExpr != nil { + constraintVal, valDiags := expr.VersionExpr.Value(nil) + if valDiags.HasErrors() || !constraintVal.Type().Equals(cty.String) { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid version constraint", + Detail: "Version must be specified as a string.", + Subject: expr.VersionExpr.Range().Ptr(), + }) + continue + } + + constraints, err := version.NewConstraint(constraintVal.AsString()) + if err != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid version constraint", + Detail: "This string does not use correct version constraint syntax.", + Subject: expr.VersionExpr.Range().Ptr(), + }) + continue + } + + rp.Requirement = VersionConstraint{ + Required: constraints, + DeclRange: expr.VersionExpr.Range(), + } + } + + if rp.Type.IsZero() { + pType, err := addrs.ParseProviderPart(name) + if err != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid provider name", + Detail: err.Error(), + Subject: expr.DeclRange.Ptr(), + }) + continue + } + rp.Type = addrs.ImpliedProviderForUnqualifiedType(pType) + } + + m.ProviderRequirements.RequiredProviders[name] = rp + delete(m.ProviderRequirementExprs, name) + } + + return diags +} + +// ResolveResourceProviders re-assigns the provider FQN for every resource in +// the module using the current state of ProviderRequirements. This must be +// called after ProviderRequirements has been fully populated (e.g. after +// dynamic required_provider expressions have been evaluated during init), so +// that resources whose provider FQN was set to a default during parsing are +// corrected to reflect the actual declared provider source. +func (m *Module) ResolveResourceProviders() { + for _, r := range m.ManagedResources { + if r.ProviderConfigRef != nil { + r.Provider = m.ProviderForLocalConfig(r.ProviderConfigAddr()) + } else { + implied, err := addrs.ParseProviderPart(r.Addr().ImpliedProvider()) + if err == nil { + r.Provider = m.ImpliedProviderForUnqualifiedType(implied) + } + } + } + + for _, r := range m.DataResources { + if r.ProviderConfigRef != nil { + r.Provider = m.ProviderForLocalConfig(r.ProviderConfigAddr()) + } else { + implied, err := addrs.ParseProviderPart(r.Addr().ImpliedProvider()) + if err == nil { + r.Provider = m.ImpliedProviderForUnqualifiedType(implied) + } + } + } + + for _, r := range m.EphemeralResources { + if r.ProviderConfigRef != nil { + r.Provider = m.ProviderForLocalConfig(r.ProviderConfigAddr()) + } else { + implied, err := addrs.ParseProviderPart(r.Addr().ImpliedProvider()) + if err == nil { + r.Provider = m.ImpliedProviderForUnqualifiedType(implied) + } + } + } + + for _, a := range m.Actions { + if a.ProviderConfigRef != nil { + a.Provider = m.ProviderForLocalConfig(a.ProviderConfigAddr()) + } else { + implied, err := addrs.ParseProviderPart(a.Addr().ImpliedProvider()) + if err == nil { + a.Provider = m.ImpliedProviderForUnqualifiedType(implied) + } + } + } + + for _, i := range m.Import { + if i.ProviderConfigRef != nil { + i.Provider = m.ProviderForLocalConfig(addrs.LocalProviderConfig{ + LocalName: i.ProviderConfigRef.Name, + Alias: i.ProviderConfigRef.Alias, + }) + } else { + implied, err := addrs.ParseProviderPart(i.ToResource.Resource.ImpliedProvider()) + if err == nil { + i.Provider = m.ImpliedProviderForUnqualifiedType(implied) + } + } + } +} + // resolveStateStoreProviderType uses the processed module to get tfaddr.Provider data for the provider // used for pluggable state storage, and assigns it to the ProviderAddr field in the module's state store data. // diff --git a/internal/configs/provider_requirement_expr.go b/internal/configs/provider_requirement_expr.go index a37636ef5eab..9a5eb9c7f6fb 100644 --- a/internal/configs/provider_requirement_expr.go +++ b/internal/configs/provider_requirement_expr.go @@ -23,3 +23,8 @@ type ProviderRequirementExpr struct { func (e *ProviderRequirementExpr) IsEmpty() bool { return e.SourceExpr == nil && e.VersionExpr == nil } + +func (e *ProviderRequirementExpr) NeedsEvalContext() bool { + return (e.SourceExpr != nil && len(e.SourceExpr.Variables()) > 0) || + (e.VersionExpr != nil && len(e.VersionExpr.Variables()) > 0) +} diff --git a/internal/configs/provider_requirements.go b/internal/configs/provider_requirements.go index a48215161c51..ccd840097555 100644 --- a/internal/configs/provider_requirements.go +++ b/internal/configs/provider_requirements.go @@ -6,7 +6,6 @@ package configs import ( "fmt" - "github.com/hashicorp/go-version" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/internal/addrs" "github.com/zclconf/go-cty/cty" @@ -123,92 +122,31 @@ func decodeRequiredProvidersBlock(block *hcl.Block) ( switch key.AsString() { case "version": versionExpr = kv.Value - - // Store the version expression if it contains variable that - // needs to be evaluated. - // - // Skip the "legacy" pure string resolution of the version - // attribute. - if vars := kv.Value.Variables(); len(vars) > 0 { - providerExpr.VersionExpr = kv.Value - continue - } - - vc := VersionConstraint{ - DeclRange: attr.Range, - } - - constraint, valDiags := kv.Value.Value(nil) - if valDiags.HasErrors() || !constraint.Type().Equals(cty.String) { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid version constraint", - Detail: "Version must be specified as a string.", - Subject: kv.Value.Range().Ptr(), - }) - continue - } - - constraintStr := constraint.AsString() - constraints, err := version.NewConstraint(constraintStr) - if err != nil { - // NewConstraint doesn't return user-friendly errors, so we'll just - // ignore the provided error and produce our own generic one. - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid version constraint", - Detail: "This string does not use correct version constraint syntax.", - Subject: kv.Value.Range().Ptr(), - }) - continue - } - - vc.Required = constraints - rp.Requirement = vc + providerExpr.VersionExpr = kv.Value case "source": sourceExpr = kv.Value - - // Store the source expression if it contains variable that - // needs to be evaluated. - // - // Skip the "legacy" pure string resolution of the source - // attribute. - if vars := kv.Value.Variables(); len(vars) > 0 { - providerExpr.SourceExpr = kv.Value - continue - } - - source, err := kv.Value.Value(nil) - if err != nil || !source.Type().Equals(cty.String) { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid source", - Detail: "Source must be specified as a string.", - Subject: kv.Value.Range().Ptr(), - }) - continue - } - - fqn, sourceDiags := addrs.ParseProviderSourceString(source.AsString()) - if sourceDiags.HasErrors() { - hclDiags := sourceDiags.ToHCL() - // The diagnostics from ParseProviderSourceString don't contain - // source location information because it has no context to compute - // them from, and so we'll add those in quickly here before we - // return. - for _, diag := range hclDiags { - if diag.Subject == nil { - diag.Subject = kv.Value.Range().Ptr() + providerExpr.SourceExpr = kv.Value + + // For static source strings (no variable references), validate the + // FQN at parse time so that errors are reported early. The actual + // resolution into RequiredProviders happens later via + // resolveStaticProviderExprs (called from NewModule). + if len(kv.Value.Variables()) == 0 { + if source, err := kv.Value.Value(nil); err == nil && source.Type().Equals(cty.String) { + if _, sourceDiags := addrs.ParseProviderSourceString(source.AsString()); sourceDiags.HasErrors() { + hclDiags := sourceDiags.ToHCL() + for _, d := range hclDiags { + if d.Subject == nil { + d.Subject = kv.Value.Range().Ptr() + } + } + diags = append(diags, hclDiags...) + continue } } - diags = append(diags, hclDiags...) - continue } - rp.Source = source.AsString() - rp.Type = fqn - case "configuration_aliases": exprs, listDiags := hcl.ExprList(kv.Value) if listDiags.HasErrors() { diff --git a/internal/configs/provider_requirements_test.go b/internal/configs/provider_requirements_test.go index a6039832cb94..fd9701e52417 100644 --- a/internal/configs/provider_requirements_test.go +++ b/internal/configs/provider_requirements_test.go @@ -324,7 +324,20 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - got, _, diags := decodeRequiredProvidersBlock(test.Block) + got, deferredExprs, diags := decodeRequiredProvidersBlock(test.Block) + + // Simulate the static resolution which is not being done in + // provider_requirements anymore + if len(deferredExprs) > 0 { + mod := &Module{ + ProviderRequirements: got, + ProviderRequirementExprs: deferredExprs, + } + resolveDiags := mod.resolveStaticProviderExprs() + diags = append(diags, resolveDiags...) + got = mod.ProviderRequirements + } + if diags.HasErrors() { if test.Error == "" { t.Fatalf("unexpected error: %v", diags) diff --git a/internal/terraform/config_graph_build.go b/internal/terraform/config_graph_build.go index e8451f238f8b..0bb0a748cdc5 100644 --- a/internal/terraform/config_graph_build.go +++ b/internal/terraform/config_graph_build.go @@ -40,3 +40,24 @@ func BuildConfigWithGraph(rootMod *configs.Module, walker configs.ModuleWalker, return cfg, diags } + +func BuildModuleWithGraph(mod *configs.Module, vars InputValues) (*configs.Module, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + ctx, ctxDiags := NewContext(&ContextOpts{ + Parallelism: 1, + }) + diags = diags.Append(ctxDiags) + if ctxDiags.HasErrors() { + return nil, diags + } + + cfg, initDiags := ctx.Init(mod, InitOpts{ + SetVariables: vars, + }) + diags = diags.Append(initDiags) + if diags.HasErrors() { + return mod, diags + } + + return cfg.Module, diags +} diff --git a/internal/terraform/node_resolve_provider_requirement.go b/internal/terraform/node_resolve_provider_requirement.go index 99eb1e4d807d..75d7f564d157 100644 --- a/internal/terraform/node_resolve_provider_requirement.go +++ b/internal/terraform/node_resolve_provider_requirement.go @@ -24,7 +24,7 @@ var ( _ GraphNodeExecutable = (*nodeResolveProviderRequirements)(nil) _ GraphNodeReferencer = (*nodeResolveProviderRequirements)(nil) _ GraphNodeModuleInstance = (*nodeResolveProviderRequirements)(nil) - _ dag.NamedVertex = (*nodeResolveProviderRequirements)(nil) + _ dag.Vertex = (*nodeResolveProviderRequirements)(nil) ) func (n *nodeResolveProviderRequirements) Name() string { @@ -49,6 +49,8 @@ func (n *nodeResolveProviderRequirements) Execute( n.Module.GatherProviderLocalNames() + n.Module.ResolveResourceProviders() + return diags } diff --git a/internal/terraform/transform_module_install.go b/internal/terraform/transform_module_install.go index 175ebc857444..9998340438ee 100644 --- a/internal/terraform/transform_module_install.go +++ b/internal/terraform/transform_module_install.go @@ -17,7 +17,7 @@ type ModuleTransformer struct { } func (t *ModuleTransformer) Transform(graph *Graph) error { - if t.Config == nil { + if t.Config == nil || t.Walker == nil { return nil }