mirror of
https://github.com/hashicorp/terraform.git
synced 2026-06-09 00:42:48 -04:00
Rework most of the configuration loading
We previously used a loader -> BuildConfig flow to load configuration. This commit changes most (but not all yet) flows to use the new graph-based approach. Instead of simply recursively loading the modules, we now need to take a stepped approach: 1. Load the root module 2. Collect the variables and their values 3. Build the configuration with the graph-based approach Because this approach relies on different parts from different packages, it can't easliy be done within the `configload` package. So, now we do most of in the backend or command.
This commit is contained in:
parent
fb60c15670
commit
4ef9684188
35 changed files with 641 additions and 340 deletions
|
|
@ -14,7 +14,6 @@ import (
|
|||
"github.com/hashicorp/terraform/internal/command/arguments"
|
||||
"github.com/hashicorp/terraform/internal/command/clistate"
|
||||
"github.com/hashicorp/terraform/internal/command/views"
|
||||
"github.com/hashicorp/terraform/internal/configs"
|
||||
"github.com/hashicorp/terraform/internal/configs/configload"
|
||||
"github.com/hashicorp/terraform/internal/depsfile"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
|
|
@ -175,15 +174,6 @@ func (o *Operation) HasConfig() bool {
|
|||
return o.ConfigLoader.IsConfigDir(o.ConfigDir)
|
||||
}
|
||||
|
||||
// Config loads the configuration that the operation applies to, using the
|
||||
// ConfigDir and ConfigLoader fields within the receiving operation.
|
||||
func (o *Operation) Config() (*configs.Config, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
config, hclDiags := o.ConfigLoader.LoadConfig(o.ConfigDir)
|
||||
diags = diags.Append(hclDiags)
|
||||
return config, diags
|
||||
}
|
||||
|
||||
// ReportResult is a helper for the common chore of setting the status of
|
||||
// a running operation and showing any diagnostics produced during that
|
||||
// operation.
|
||||
|
|
|
|||
|
|
@ -200,3 +200,60 @@ func ParseVariableValues(vv map[string]arguments.UnparsedVariableValue, decls ma
|
|||
|
||||
return ret, diags
|
||||
}
|
||||
|
||||
func ParseConstVariableValues(vv map[string]arguments.UnparsedVariableValue, decls map[string]*configs.Variable) (terraform.InputValues, tfdiags.Diagnostics) {
|
||||
ret, diags := ParseDeclaredVariableValues(vv, decls)
|
||||
undeclared, diagsUndeclared := ParseUndeclaredVariableValues(vv, decls)
|
||||
|
||||
diags = diags.Append(diagsUndeclared)
|
||||
|
||||
// By this point we should've gathered all of the required root module
|
||||
// variables from one of the many possible sources. We'll now populate
|
||||
// any we haven't gathered as unset placeholders which Terraform Core
|
||||
// can then react to.
|
||||
for name, vc := range decls {
|
||||
if isDefinedAny(name, ret, undeclared) {
|
||||
continue
|
||||
}
|
||||
|
||||
// This check is redundant with a check made in Terraform Core when
|
||||
// processing undeclared variables, but allows us to generate a more
|
||||
// specific error message which mentions -var and -var-file command
|
||||
// line options, whereas the one in Terraform Core is more general
|
||||
// due to supporting both root and child module variables.
|
||||
if vc.Const && vc.Required() {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "No value for required variable",
|
||||
Detail: fmt.Sprintf("The root module input variable %q is not set, and has no default value. Use a -var or -var-file command line argument to provide a value for this variable.", name),
|
||||
Subject: vc.DeclRange.Ptr(),
|
||||
})
|
||||
}
|
||||
|
||||
if vc.Required() {
|
||||
// We'll include a placeholder value anyway, just so that our
|
||||
// result is complete for any calling code that wants to cautiously
|
||||
// analyze it for diagnostic purposes. Since our diagnostics now
|
||||
// includes an error, normal processing will ignore this result.
|
||||
ret[name] = &terraform.InputValue{
|
||||
Value: cty.DynamicVal,
|
||||
SourceType: terraform.ValueFromConfig,
|
||||
SourceRange: tfdiags.SourceRangeFromHCL(vc.DeclRange),
|
||||
}
|
||||
} else {
|
||||
// We're still required to put an entry for this variable
|
||||
// in the mapping to be explicit to Terraform Core that we
|
||||
// visited it, but its value will be cty.NilVal to represent
|
||||
// that it wasn't set at all at this layer, and so Terraform Core
|
||||
// should substitute a default if available, or generate an error
|
||||
// if not.
|
||||
ret[name] = &terraform.InputValue{
|
||||
Value: cty.NilVal,
|
||||
SourceType: terraform.ValueFromConfig,
|
||||
SourceRange: tfdiags.SourceRangeFromHCL(vc.DeclRange),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret, diags
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,39 +146,11 @@ func (b *Local) localRun(op *backendrun.Operation) (*backendrun.LocalRun, *confi
|
|||
func (b *Local) localRunDirect(op *backendrun.Operation, run *backendrun.LocalRun, coreOpts *terraform.ContextOpts, s statemgr.Full) (*backendrun.LocalRun, *configload.Snapshot, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
// Load the configuration using the caller-provided configuration loader.
|
||||
config, configSnap, configDiags := op.ConfigLoader.LoadConfigWithSnapshot(op.ConfigDir)
|
||||
rootMod, configDiags := op.ConfigLoader.LoadRootModule(op.ConfigDir)
|
||||
diags = diags.Append(configDiags)
|
||||
if configDiags.HasErrors() {
|
||||
return nil, nil, diags
|
||||
}
|
||||
run.Config = config
|
||||
|
||||
if errs := config.VerifyDependencySelections(op.DependencyLocks); len(errs) > 0 {
|
||||
var buf strings.Builder
|
||||
for _, err := range errs {
|
||||
fmt.Fprintf(&buf, "\n - %s", err.Error())
|
||||
}
|
||||
var suggestion string
|
||||
switch {
|
||||
case op.DependencyLocks == nil:
|
||||
// If we get here then it suggests that there's a caller that we
|
||||
// didn't yet update to populate DependencyLocks, which is a bug.
|
||||
suggestion = "This run has no dependency lock information provided at all, which is a bug in Terraform; please report it!"
|
||||
case op.DependencyLocks.Empty():
|
||||
suggestion = "To make the initial dependency selections that will initialize the dependency lock file, run:\n terraform init"
|
||||
default:
|
||||
suggestion = "To update the locked dependency selections to match a changed configuration, run:\n terraform init -upgrade"
|
||||
}
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Inconsistent dependency lock file",
|
||||
fmt.Sprintf(
|
||||
"The following dependency selections recorded in the lock file are inconsistent with the current configuration:%s\n\n%s",
|
||||
buf.String(), suggestion,
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
var rawVariables map[string]arguments.UnparsedVariableValue
|
||||
if op.AllowUnsetVariables {
|
||||
|
|
@ -186,16 +158,16 @@ func (b *Local) localRunDirect(op *backendrun.Operation, run *backendrun.LocalRu
|
|||
// but unset variables with unknown values to represent that they are
|
||||
// placeholders for values the user would need to provide for other
|
||||
// operations.
|
||||
rawVariables = b.stubUnsetRequiredVariables(op.Variables, config.Module.Variables)
|
||||
rawVariables = b.stubUnsetRequiredVariables(op.Variables, rootMod.Variables)
|
||||
} else {
|
||||
// If interactive input is enabled, we might gather some more variable
|
||||
// values through interactive prompts.
|
||||
// TODO: Need to route the operation context through into here, so that
|
||||
// the interactive prompts can be sensitive to its timeouts/etc.
|
||||
rawVariables = b.interactiveCollectVariables(context.TODO(), op.Variables, config.Module.Variables, op.UIIn)
|
||||
rawVariables = b.interactiveCollectVariables(context.TODO(), op.Variables, rootMod.Variables, op.UIIn)
|
||||
}
|
||||
|
||||
variables, varDiags := backendrun.ParseVariableValues(rawVariables, config.Module.Variables)
|
||||
variables, varDiags := backendrun.ParseVariableValues(rawVariables, rootMod.Variables)
|
||||
diags = diags.Append(varDiags)
|
||||
if diags.HasErrors() {
|
||||
return nil, nil, diags
|
||||
|
|
@ -224,6 +196,52 @@ func (b *Local) localRunDirect(op *backendrun.Operation, run *backendrun.LocalRu
|
|||
return nil, nil, diags
|
||||
}
|
||||
run.Core = tfCtx
|
||||
|
||||
walkerSnapshot, configSnap := op.ConfigLoader.ModuleWalkerSnapshot()
|
||||
config, buildDiags := terraform.BuildConfigWithGraph(
|
||||
rootMod,
|
||||
walkerSnapshot,
|
||||
variables,
|
||||
configs.MockDataLoaderFunc(op.ConfigLoader.LoadExternalMockData),
|
||||
)
|
||||
diags = diags.Append(buildDiags)
|
||||
if buildDiags.HasErrors() {
|
||||
return nil, nil, diags
|
||||
}
|
||||
run.Config = config
|
||||
|
||||
snapDiags := op.ConfigLoader.AddRootModuleToSnapshot(configSnap, op.ConfigDir)
|
||||
diags = diags.Append(snapDiags)
|
||||
if snapDiags.HasErrors() {
|
||||
return nil, nil, diags
|
||||
}
|
||||
|
||||
if errs := config.VerifyDependencySelections(op.DependencyLocks); len(errs) > 0 {
|
||||
var buf strings.Builder
|
||||
for _, err := range errs {
|
||||
fmt.Fprintf(&buf, "\n - %s", err.Error())
|
||||
}
|
||||
var suggestion string
|
||||
switch {
|
||||
case op.DependencyLocks == nil:
|
||||
// If we get here then it suggests that there's a caller that we
|
||||
// didn't yet update to populate DependencyLocks, which is a bug.
|
||||
suggestion = "This run has no dependency lock information provided at all, which is a bug in Terraform; please report it!"
|
||||
case op.DependencyLocks.Empty():
|
||||
suggestion = "To make the initial dependency selections that will initialize the dependency lock file, run:\n terraform init"
|
||||
default:
|
||||
suggestion = "To update the locked dependency selections to match a changed configuration, run:\n terraform init -upgrade"
|
||||
}
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Inconsistent dependency lock file",
|
||||
fmt.Sprintf(
|
||||
"The following dependency selections recorded in the lock file are inconsistent with the current configuration:%s\n\n%s",
|
||||
buf.String(), suggestion,
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
return run, configSnap, diags
|
||||
}
|
||||
|
||||
|
|
@ -235,6 +253,7 @@ func (b *Local) localRunForPlanFile(op *backendrun.Operation, pf *planfile.Reade
|
|||
// A plan file has a snapshot of configuration embedded inside it, which
|
||||
// is used instead of whatever configuration might be already present
|
||||
// in the filesystem.
|
||||
//TODO why not use pf.ReadConfig?
|
||||
snap, err := pf.ReadConfigSnapshot()
|
||||
if err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
|
|
@ -246,32 +265,16 @@ func (b *Local) localRunForPlanFile(op *backendrun.Operation, pf *planfile.Reade
|
|||
}
|
||||
loader := configload.NewLoaderFromSnapshot(snap)
|
||||
loader.AllowLanguageExperiments(op.ConfigLoader.AllowsLanguageExperiments())
|
||||
config, configDiags := loader.LoadConfig(snap.Modules[""].Dir)
|
||||
diags = diags.Append(configDiags)
|
||||
if configDiags.HasErrors() {
|
||||
return nil, snap, diags
|
||||
rootMod, rootDiags := loader.LoadRootModule(snap.Modules[""].Dir)
|
||||
diags = diags.Append(rootDiags)
|
||||
if rootDiags.HasErrors() {
|
||||
return nil, nil, diags
|
||||
}
|
||||
run.Config = config
|
||||
|
||||
// NOTE: We're intentionally comparing the current locks with the
|
||||
// configuration snapshot, rather than the lock snapshot in the plan file,
|
||||
// because it's the current locks which dictate our plugin selections
|
||||
// in coreOpts below. However, we'll also separately check that the
|
||||
// plan file has identical locked plugins below, and thus we're effectively
|
||||
// checking consistency with both here.
|
||||
if errs := config.VerifyDependencySelections(op.DependencyLocks); len(errs) > 0 {
|
||||
var buf strings.Builder
|
||||
for _, err := range errs {
|
||||
fmt.Fprintf(&buf, "\n - %s", err.Error())
|
||||
}
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Inconsistent dependency lock file",
|
||||
fmt.Sprintf(
|
||||
"The following dependency selections recorded in the lock file are inconsistent with the configuration in the saved plan:%s\n\nA saved plan can be applied only to the same configuration it was created from. Create a new plan from the updated configuration.",
|
||||
buf.String(),
|
||||
),
|
||||
))
|
||||
variables, varDiags := backendrun.ParseVariableValues(op.Variables, rootMod.Variables)
|
||||
diags = diags.Append(varDiags)
|
||||
if diags.HasErrors() {
|
||||
return nil, nil, diags
|
||||
}
|
||||
|
||||
// This check is an important complement to the check above: the locked
|
||||
|
|
@ -359,6 +362,40 @@ func (b *Local) localRunForPlanFile(op *backendrun.Operation, pf *planfile.Reade
|
|||
return nil, nil, diags
|
||||
}
|
||||
run.Core = tfCtx
|
||||
|
||||
config, buildDiags := terraform.BuildConfigWithGraph(
|
||||
rootMod,
|
||||
loader.ModuleWalker(),
|
||||
variables,
|
||||
configs.MockDataLoaderFunc(loader.LoadExternalMockData),
|
||||
)
|
||||
diags = diags.Append(buildDiags)
|
||||
if buildDiags.HasErrors() {
|
||||
return nil, nil, diags
|
||||
}
|
||||
run.Config = config
|
||||
|
||||
// NOTE: We're intentionally comparing the current locks with the
|
||||
// configuration snapshot, rather than the lock snapshot in the plan file,
|
||||
// because it's the current locks which dictate our plugin selections
|
||||
// in coreOpts below. However, we'll also separately check that the
|
||||
// plan file has identical locked plugins below, and thus we're effectively
|
||||
// checking consistency with both here.
|
||||
if errs := config.VerifyDependencySelections(op.DependencyLocks); len(errs) > 0 {
|
||||
var buf strings.Builder
|
||||
for _, err := range errs {
|
||||
fmt.Fprintf(&buf, "\n - %s", err.Error())
|
||||
}
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Inconsistent dependency lock file",
|
||||
fmt.Sprintf(
|
||||
"The following dependency selections recorded in the lock file are inconsistent with the configuration in the saved plan:%s\n\nA saved plan can be applied only to the same configuration it was created from. Create a new plan from the updated configuration.",
|
||||
buf.String(),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
return run, snap, diags
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -249,11 +249,8 @@ func (b *Remote) waitForRun(stopCtx, cancelCtx context.Context, op *backendrun.O
|
|||
// remote system's responsibility to do final validation of the input.
|
||||
func (b *Remote) hasExplicitVariableValues(op *backendrun.Operation) bool {
|
||||
// Load the configuration using the caller-provided configuration loader.
|
||||
config, _, configDiags := op.ConfigLoader.LoadConfigWithSnapshot(op.ConfigDir)
|
||||
config, configDiags := op.ConfigLoader.LoadRootModule(op.ConfigDir)
|
||||
if configDiags.HasErrors() {
|
||||
// If we can't load the configuration then we'll assume no explicit
|
||||
// variable values just to let the remote operation start and let
|
||||
// the remote system return the same set of configuration errors.
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -262,7 +259,7 @@ func (b *Remote) hasExplicitVariableValues(op *backendrun.Operation) bool {
|
|||
// goal here is just to make a best effort count of how many variable
|
||||
// values are coming from -var or -var-file CLI arguments so that we can
|
||||
// hint the user that those are not supported for remote operations.
|
||||
variables, _ := backendrun.ParseVariableValues(op.Variables, config.Module.Variables)
|
||||
variables, _ := backendrun.ParseVariableValues(op.Variables, config.Variables)
|
||||
|
||||
// Check for explicitly-defined (-var and -var-file) variables, which the
|
||||
// remote backend does not support. All other source types are okay,
|
||||
|
|
|
|||
|
|
@ -81,19 +81,18 @@ func (b *Remote) LocalRun(op *backendrun.Operation) (*backendrun.LocalRun, state
|
|||
ret.InputState = stateMgr.State()
|
||||
|
||||
log.Printf("[TRACE] backend/remote: loading configuration for the current working directory")
|
||||
config, configDiags := op.ConfigLoader.LoadConfig(op.ConfigDir)
|
||||
rootMod, configDiags := op.ConfigLoader.LoadRootModule(op.ConfigDir)
|
||||
diags = diags.Append(configDiags)
|
||||
if configDiags.HasErrors() {
|
||||
return nil, nil, diags
|
||||
}
|
||||
ret.Config = config
|
||||
|
||||
if op.AllowUnsetVariables {
|
||||
// If we're not going to use the variables in an operation we'll be
|
||||
// more lax about them, stubbing out any unset ones as unknown.
|
||||
// This gives us enough information to produce a consistent context,
|
||||
// but not enough information to run a real operation (plan, apply, etc)
|
||||
ret.PlanOpts.SetVariables = stubAllVariables(op.Variables, config.Module.Variables)
|
||||
ret.PlanOpts.SetVariables = stubAllVariables(op.Variables, rootMod.Variables)
|
||||
} else {
|
||||
// The underlying API expects us to use the opaque workspace id to request
|
||||
// variables, so we'll need to look that up using our organization name
|
||||
|
|
@ -136,7 +135,7 @@ func (b *Remote) LocalRun(op *backendrun.Operation) (*backendrun.LocalRun, state
|
|||
}
|
||||
|
||||
if op.Variables != nil {
|
||||
variables, varDiags := backendrun.ParseVariableValues(op.Variables, config.Module.Variables)
|
||||
variables, varDiags := backendrun.ParseVariableValues(op.Variables, rootMod.Variables)
|
||||
diags = diags.Append(varDiags)
|
||||
if diags.HasErrors() {
|
||||
return nil, nil, diags
|
||||
|
|
@ -148,6 +147,24 @@ func (b *Remote) LocalRun(op *backendrun.Operation) (*backendrun.LocalRun, state
|
|||
tfCtx, ctxDiags := terraform.NewContext(&opts)
|
||||
diags = diags.Append(ctxDiags)
|
||||
ret.Core = tfCtx
|
||||
if diags.HasErrors() {
|
||||
return nil, nil, diags
|
||||
}
|
||||
|
||||
log.Printf("[TRACE] backend/remote: building configuration for the current working directory")
|
||||
|
||||
config, buildDiags := terraform.BuildConfigWithGraph(
|
||||
rootMod,
|
||||
op.ConfigLoader.ModuleWalker(),
|
||||
ret.PlanOpts.SetVariables,
|
||||
configs.MockDataLoaderFunc(op.ConfigLoader.LoadExternalMockData),
|
||||
)
|
||||
diags = diags.Append(buildDiags)
|
||||
if diags.HasErrors() {
|
||||
return nil, nil, diags
|
||||
}
|
||||
|
||||
ret.Config = config
|
||||
|
||||
log.Printf("[TRACE] backend/remote: finished building terraform.Context")
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ func TestChecksHappyPath(t *testing.T) {
|
|||
const fixtureDir = "testdata/happypath"
|
||||
loader, close := configload.NewLoaderForTests(t)
|
||||
defer close()
|
||||
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, nil)
|
||||
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, nil, nil)
|
||||
_, instDiags := inst.InstallModules(context.Background(), fixtureDir, "tests", true, false, initwd.ModuleInstallHooksImpl{})
|
||||
if instDiags.HasErrors() {
|
||||
t.Fatal(instDiags.Err())
|
||||
|
|
@ -29,7 +29,7 @@ func TestChecksHappyPath(t *testing.T) {
|
|||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
|
||||
cfg, hclDiags := loader.LoadConfig(fixtureDir)
|
||||
cfg, hclDiags := loader.LoadStaticConfig(fixtureDir)
|
||||
if hclDiags.HasErrors() {
|
||||
t.Fatalf("invalid configuration: %s", hclDiags.Error())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -654,12 +654,12 @@ in order to capture the filesystem context the remote workspace expects:
|
|||
}
|
||||
|
||||
func (b *Cloud) parseRunVariables(op *backendrun.Operation) ([]*tfe.RunVariable, error) {
|
||||
config, _, configDiags := op.ConfigLoader.LoadConfigWithSnapshot(op.ConfigDir)
|
||||
config, configDiags := op.ConfigLoader.LoadRootModule(op.ConfigDir)
|
||||
if configDiags.HasErrors() {
|
||||
return nil, fmt.Errorf("error loading config with snapshot: %w", configDiags.Errs()[0])
|
||||
}
|
||||
|
||||
variables, varDiags := ParseCloudRunVariables(op.Variables, config.Module.Variables)
|
||||
variables, varDiags := ParseCloudRunVariables(op.Variables, config.Variables)
|
||||
|
||||
if varDiags.HasErrors() {
|
||||
return nil, varDiags.Err()
|
||||
|
|
|
|||
|
|
@ -79,20 +79,19 @@ func (b *Cloud) LocalRun(op *backendrun.Operation) (*backendrun.LocalRun, statem
|
|||
log.Printf("[TRACE] cloud: retrieving remote state snapshot for workspace %q", remoteWorkspaceName)
|
||||
ret.InputState = stateMgr.State()
|
||||
|
||||
log.Printf("[TRACE] cloud: loading configuration for the current working directory")
|
||||
config, configDiags := op.ConfigLoader.LoadConfig(op.ConfigDir)
|
||||
log.Printf("[TRACE] cloud: loading root module for the current working directory")
|
||||
rootMod, configDiags := op.ConfigLoader.LoadRootModule(op.ConfigDir)
|
||||
diags = diags.Append(configDiags)
|
||||
if configDiags.HasErrors() {
|
||||
return nil, nil, diags
|
||||
}
|
||||
ret.Config = config
|
||||
|
||||
if op.AllowUnsetVariables {
|
||||
// If we're not going to use the variables in an operation we'll be
|
||||
// more lax about them, stubbing out any unset ones as unknown.
|
||||
// This gives us enough information to produce a consistent context,
|
||||
// but not enough information to run a real operation (plan, apply, etc)
|
||||
ret.PlanOpts.SetVariables = stubAllVariables(op.Variables, config.Module.Variables)
|
||||
ret.PlanOpts.SetVariables = stubAllVariables(op.Variables, rootMod.Variables)
|
||||
} else {
|
||||
// The underlying API expects us to use the opaque workspace id to request
|
||||
// variables, so we'll need to look that up using our organization name
|
||||
|
|
@ -136,7 +135,7 @@ func (b *Cloud) LocalRun(op *backendrun.Operation) (*backendrun.LocalRun, statem
|
|||
}
|
||||
|
||||
if op.Variables != nil {
|
||||
variables, varDiags := backendrun.ParseVariableValues(op.Variables, config.Module.Variables)
|
||||
variables, varDiags := backendrun.ParseVariableValues(op.Variables, rootMod.Variables)
|
||||
diags = diags.Append(varDiags)
|
||||
if diags.HasErrors() {
|
||||
return nil, nil, diags
|
||||
|
|
@ -148,6 +147,24 @@ func (b *Cloud) LocalRun(op *backendrun.Operation) (*backendrun.LocalRun, statem
|
|||
tfCtx, ctxDiags := terraform.NewContext(&opts)
|
||||
diags = diags.Append(ctxDiags)
|
||||
ret.Core = tfCtx
|
||||
if diags.HasErrors() {
|
||||
return nil, nil, diags
|
||||
}
|
||||
|
||||
log.Printf("[TRACE] cloud: building configuration for the current working directory")
|
||||
|
||||
config, buildDiags := terraform.BuildConfigWithGraph(
|
||||
rootMod,
|
||||
op.ConfigLoader.ModuleWalker(),
|
||||
ret.PlanOpts.SetVariables,
|
||||
configs.MockDataLoaderFunc(op.ConfigLoader.LoadExternalMockData),
|
||||
)
|
||||
diags = diags.Append(buildDiags)
|
||||
if diags.HasErrors() {
|
||||
return nil, nil, diags
|
||||
}
|
||||
|
||||
ret.Config = config
|
||||
|
||||
log.Printf("[TRACE] cloud: finished building terraform.Context")
|
||||
|
||||
|
|
|
|||
|
|
@ -267,7 +267,7 @@ func TestTest_Verbose(t *testing.T) {
|
|||
loader, close := configload.NewLoaderForTests(t)
|
||||
defer close()
|
||||
|
||||
config, configDiags := loader.LoadConfigWithTests(directory, "tests")
|
||||
config, configDiags := loader.LoadStaticConfigWithTests(directory, "tests")
|
||||
if configDiags.HasErrors() {
|
||||
t.Fatalf("failed to load config: %v", configDiags.Error())
|
||||
}
|
||||
|
|
@ -664,7 +664,7 @@ func TestTest_ForceCancel(t *testing.T) {
|
|||
loader, close := configload.NewLoaderForTests(t)
|
||||
defer close()
|
||||
|
||||
config, configDiags := loader.LoadConfigWithTests("testdata/test-force-cancel", "tests")
|
||||
config, configDiags := loader.LoadStaticConfigWithTests("testdata/test-force-cancel", "tests")
|
||||
if configDiags.HasErrors() {
|
||||
t.Fatalf("failed to load config: %v", configDiags.Error())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -359,7 +359,7 @@ Options:
|
|||
Defaults to 10.
|
||||
|
||||
-replace=resource Terraform will plan to replace this resource instance
|
||||
instead of doing an update or no-op action.
|
||||
instead of doing an update or no-op action.
|
||||
|
||||
-state=path Path to read and save state (unless state-out
|
||||
is specified). Defaults to "terraform.tfstate".
|
||||
|
|
@ -372,7 +372,7 @@ Options:
|
|||
Legacy option for the local backend only. See the local
|
||||
backend's documentation for more information.
|
||||
|
||||
|
||||
|
||||
-var 'foo=bar' Set a value for one of the input variables in the root
|
||||
module of the configuration. Use this option more than
|
||||
once to set more than one variable.
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import (
|
|||
"github.com/hashicorp/terraform/internal/states/statefile"
|
||||
"github.com/hashicorp/terraform/internal/states/statemgr"
|
||||
"github.com/hashicorp/terraform/internal/terminal"
|
||||
"github.com/hashicorp/terraform/internal/terraform"
|
||||
"github.com/hashicorp/terraform/version"
|
||||
)
|
||||
|
||||
|
|
@ -158,15 +159,31 @@ func testModuleWithSnapshot(t *testing.T, name string) (*configs.Config, *config
|
|||
// Test modules usually do not refer to remote sources, and for local
|
||||
// sources only this ultimately just records all of the module paths
|
||||
// in a JSON file so that we can load them below.
|
||||
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil))
|
||||
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil), nil)
|
||||
_, instDiags := inst.InstallModules(context.Background(), dir, "tests", true, false, initwd.ModuleInstallHooksImpl{})
|
||||
if instDiags.HasErrors() {
|
||||
t.Fatal(instDiags.Err())
|
||||
}
|
||||
|
||||
config, snap, diags := loader.LoadConfigWithSnapshot(dir)
|
||||
if diags.HasErrors() {
|
||||
t.Fatal(diags.Error())
|
||||
rootMod, configDiags := loader.LoadRootModule(dir)
|
||||
if configDiags.HasErrors() {
|
||||
t.Fatal(configDiags.Error())
|
||||
}
|
||||
|
||||
walkerSnapshot, snap := loader.ModuleWalkerSnapshot()
|
||||
config, buildDiags := terraform.BuildConfigWithGraph(
|
||||
rootMod,
|
||||
walkerSnapshot,
|
||||
nil,
|
||||
configs.MockDataLoaderFunc(loader.LoadExternalMockData),
|
||||
)
|
||||
if buildDiags.HasErrors() {
|
||||
t.Fatal(buildDiags.Err())
|
||||
}
|
||||
|
||||
snapDiags := loader.AddRootModuleToSnapshot(snap, dir)
|
||||
if snapDiags.HasErrors() {
|
||||
t.Fatal(snapDiags.Error())
|
||||
}
|
||||
|
||||
return config, snap
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ func TestGraph_resourcesOnly(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
inst := initwd.NewModuleInstaller(".terraform/modules", loader, registry.NewClient(nil, nil))
|
||||
inst := initwd.NewModuleInstaller(".terraform/modules", loader, registry.NewClient(nil, nil), nil)
|
||||
_, instDiags := inst.InstallModules(context.Background(), ".", "tests", true, false, initwd.ModuleInstallHooksImpl{})
|
||||
if instDiags.HasErrors() {
|
||||
t.Fatal(instDiags.Err())
|
||||
|
|
|
|||
101
internal/command/init2_test.go
Normal file
101
internal/command/init2_test.go
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
// Copyright IBM Corp. 2014, 2026
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/cli"
|
||||
)
|
||||
|
||||
func TestInit2_versionConstraintAdded(t *testing.T) {
|
||||
// This test is for what happens when there is a version constraint added
|
||||
// to a module that previously didn't have one.
|
||||
td := t.TempDir()
|
||||
testCopyDir(t, testFixturePath(filepath.Join("dynamic-module-sources", "add-version-constraint")), td)
|
||||
t.Chdir(td)
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
view, done := testView(t)
|
||||
c := &InitCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{"-get=false"}
|
||||
code := c.Run(args)
|
||||
testOutput := done(t)
|
||||
if code != 1 {
|
||||
t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, testOutput.Stderr(), testOutput.Stdout())
|
||||
}
|
||||
got := testOutput.All()
|
||||
|
||||
want := "Module version requirements have changed"
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("wrong error\ngot:\n%s\n\nwant: containing %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInit2_invalidRegistrySourceWithModule(t *testing.T) {
|
||||
td := t.TempDir()
|
||||
testCopyDir(t, testFixturePath(filepath.Join("dynamic-module-sources", "invalid-registry-source-with-module")), td)
|
||||
t.Chdir(td)
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
view, done := testView(t)
|
||||
c := &InitCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{}
|
||||
code := c.Run(args)
|
||||
testOutput := done(t)
|
||||
if code != 1 {
|
||||
t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, testOutput.Stderr(), testOutput.Stdout())
|
||||
}
|
||||
got := testOutput.All()
|
||||
|
||||
want := "Invalid registry module source address"
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("wrong error\ngot:\n%s\n\nwant: containing %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInit2_localSourceWithVersion(t *testing.T) {
|
||||
td := t.TempDir()
|
||||
testCopyDir(t, testFixturePath(filepath.Join("dynamic-module-sources", "local-source-with-version")), td)
|
||||
t.Chdir(td)
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
view, done := testView(t)
|
||||
c := &InitCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{}
|
||||
code := c.Run(args)
|
||||
testOutput := done(t)
|
||||
if code != 1 {
|
||||
t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, testOutput.Stderr(), testOutput.Stdout())
|
||||
}
|
||||
got := testOutput.All()
|
||||
|
||||
want := "Invalid registry module source address"
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("wrong error\ngot:\n%s\n\nwant: containing %q", got, want)
|
||||
}
|
||||
}
|
||||
|
|
@ -825,12 +825,6 @@ func (m *Meta) applyStateArguments(args *arguments.State) {
|
|||
func (m *Meta) checkRequiredVersion() tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
loader, err := m.initConfigLoader()
|
||||
if err != nil {
|
||||
diags = diags.Append(err)
|
||||
return diags
|
||||
}
|
||||
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
diags = diags.Append(fmt.Errorf("Error getting pwd: %s", err))
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import (
|
|||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/backend/backendrun"
|
||||
"github.com/hashicorp/terraform/internal/configs"
|
||||
"github.com/hashicorp/terraform/internal/configs/configload"
|
||||
"github.com/hashicorp/terraform/internal/configs/configschema"
|
||||
|
|
@ -48,8 +49,28 @@ func (m *Meta) loadConfig(rootDir string) (*configs.Config, tfdiags.Diagnostics)
|
|||
return nil, diags
|
||||
}
|
||||
|
||||
config, hclDiags := loader.LoadConfig(rootDir)
|
||||
rootMod, hclDiags := loader.LoadRootModule(rootDir)
|
||||
diags = diags.Append(hclDiags)
|
||||
if rootMod == nil || diags.HasErrors() {
|
||||
cfg := &configs.Config{
|
||||
Module: rootMod,
|
||||
}
|
||||
cfg.Root = cfg // Root module is self-referential.
|
||||
return cfg, diags
|
||||
}
|
||||
betterVars, parseDiags := backendrun.ParseVariableValues(m.VariableValues, rootMod.Variables)
|
||||
diags = diags.Append(parseDiags)
|
||||
if parseDiags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
config, buildDiags := terraform.BuildConfigWithGraph(
|
||||
rootMod,
|
||||
loader.ModuleWalker(),
|
||||
betterVars,
|
||||
configs.MockDataLoaderFunc(loader.LoadExternalMockData),
|
||||
)
|
||||
diags = diags.Append(buildDiags)
|
||||
|
||||
return config, diags
|
||||
}
|
||||
|
||||
|
|
@ -65,8 +86,28 @@ func (m *Meta) loadConfigWithTests(rootDir, testDir string) (*configs.Config, tf
|
|||
return nil, diags
|
||||
}
|
||||
|
||||
config, hclDiags := loader.LoadConfigWithTests(rootDir, testDir)
|
||||
rootMod, hclDiags := loader.LoadRootModuleWithTests(rootDir, testDir)
|
||||
diags = diags.Append(hclDiags)
|
||||
if rootMod == nil || diags.HasErrors() {
|
||||
cfg := &configs.Config{
|
||||
Module: rootMod,
|
||||
}
|
||||
cfg.Root = cfg // Root module is self-referential.
|
||||
return cfg, diags
|
||||
}
|
||||
betterVars, parseDiags := backendrun.ParseConstVariableValues(m.VariableValues, rootMod.Variables)
|
||||
diags = diags.Append(parseDiags)
|
||||
if parseDiags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
config, buildDiags := terraform.BuildConfigWithGraph(
|
||||
rootMod,
|
||||
loader.ModuleWalker(),
|
||||
betterVars,
|
||||
configs.MockDataLoaderFunc(loader.LoadExternalMockData),
|
||||
)
|
||||
diags = diags.Append(buildDiags)
|
||||
|
||||
return config, diags
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5707,7 +5707,7 @@ func testModuleInline(t *testing.T, sources map[string]string) (*configs.Config,
|
|||
// Test modules usually do not refer to remote sources, and for local
|
||||
// sources only this ultimately just records all of the module paths
|
||||
// in a JSON file so that we can load them below.
|
||||
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil))
|
||||
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil), nil)
|
||||
_, instDiags := inst.InstallModules(context.Background(), cfgPath, "tests", true, false, initwd.ModuleInstallHooksImpl{})
|
||||
if instDiags.HasErrors() {
|
||||
t.Fatal(instDiags.Err())
|
||||
|
|
@ -5719,7 +5719,7 @@ func testModuleInline(t *testing.T, sources map[string]string) (*configs.Config,
|
|||
t.Fatalf("failed to refresh modules after installation: %s", err)
|
||||
}
|
||||
|
||||
config, diags := loader.LoadConfigWithTests(cfgPath, "tests")
|
||||
config, diags := loader.LoadStaticConfigWithTests(cfgPath, "tests")
|
||||
if diags.HasErrors() {
|
||||
t.Fatal(diags.Error())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@
|
|||
{
|
||||
"Key": "",
|
||||
"Source": "",
|
||||
"Dir": "testdata/add-version-constraint"
|
||||
"Dir": ""
|
||||
},
|
||||
{
|
||||
"Key": "child",
|
||||
"Source": "hashicorp/module-installer-acctest/aws",
|
||||
"Dir": "testdata/add-version-constraint/.terraform/modules/child"
|
||||
"Dir": ".terraform/modules/child"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
module "test" {
|
||||
source = "../boop" # ERROR: Invalid registry module source address
|
||||
source = "---.com/HashiCorp/Consul/aws"
|
||||
version = "1.0.0" # Makes Terraform assume "source" is a module address
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
module "test" {
|
||||
source = "---.com/HashiCorp/Consul/aws" # ERROR: Invalid registry module source address
|
||||
source = "../boop"
|
||||
version = "1.0.0" # Makes Terraform assume "source" is a module address
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ package command
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
|
@ -190,10 +190,6 @@ func TestModuleWithIncorrectNameShouldFail(t *testing.T) {
|
|||
if !strings.Contains(output.Stderr(), wantError) {
|
||||
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
|
||||
}
|
||||
wantError = `Error: Variables not allowed`
|
||||
if !strings.Contains(output.Stderr(), wantError) {
|
||||
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
func TestWronglyUsedInterpolationShouldFail(t *testing.T) {
|
||||
|
|
@ -406,14 +402,14 @@ func TestValidate_json(t *testing.T) {
|
|||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.path, func(t *testing.T) {
|
||||
var want, got map[string]interface{}
|
||||
var want, got map[string]any
|
||||
|
||||
wantFile, err := os.Open(path.Join(testFixturePath(tc.path), "output.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open output file: %s", err)
|
||||
}
|
||||
defer wantFile.Close()
|
||||
wantBytes, err := ioutil.ReadAll(wantFile)
|
||||
wantBytes, err := io.ReadAll(wantFile)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read output file: %s", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,10 @@ import (
|
|||
|
||||
version "github.com/hashicorp/go-version"
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/gohcl"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/getmodules/moduleaddrs"
|
||||
)
|
||||
|
||||
// BuildConfig constructs a Config from a root module by loading all of its
|
||||
|
|
@ -32,6 +34,20 @@ func BuildConfig(root *Module, walker ModuleWalker, loader MockDataLoader) (*Con
|
|||
}
|
||||
cfg.Root = cfg // Root module is self-referential.
|
||||
cfg.Children, diags = buildChildModules(cfg, walker)
|
||||
diags = append(diags, FinalizeConfig(cfg, walker, loader)...)
|
||||
|
||||
return cfg, diags
|
||||
}
|
||||
|
||||
// FinalizeConfig performs the post-load validation and setup steps that are
|
||||
// shared by different configuration loaders.
|
||||
//
|
||||
// Callers must ensure cfg.Root is set correctly before calling this function.
|
||||
func FinalizeConfig(cfg *Config, walker ModuleWalker, loader MockDataLoader) hcl.Diagnostics {
|
||||
var diags hcl.Diagnostics
|
||||
if cfg == nil {
|
||||
return diags
|
||||
}
|
||||
diags = append(diags, buildTestModules(cfg, walker)...)
|
||||
|
||||
// Skip provider resolution if there are any errors, since the provider
|
||||
|
|
@ -42,7 +58,7 @@ func BuildConfig(root *Module, walker ModuleWalker, loader MockDataLoader) (*Con
|
|||
providers := cfg.resolveProviderTypes()
|
||||
cfg.resolveProviderTypesForTests(providers)
|
||||
|
||||
if cfg.Module.StateStore != nil {
|
||||
if cfg.Module != nil && cfg.Module.StateStore != nil {
|
||||
stateProviderDiags := cfg.resolveStateStoreProviderType()
|
||||
diags = append(diags, stateProviderDiags...)
|
||||
}
|
||||
|
|
@ -54,7 +70,7 @@ func BuildConfig(root *Module, walker ModuleWalker, loader MockDataLoader) (*Con
|
|||
// Final step, let's side load any external mock data into our test files.
|
||||
diags = append(diags, installMockDataFiles(cfg, loader)...)
|
||||
|
||||
return cfg, diags
|
||||
return diags
|
||||
}
|
||||
|
||||
func installMockDataFiles(root *Config, loader MockDataLoader) hcl.Diagnostics {
|
||||
|
|
@ -148,6 +164,108 @@ func buildTestModules(root *Config, walker ModuleWalker) hcl.Diagnostics {
|
|||
return diags
|
||||
}
|
||||
|
||||
// legacySourceHelper is used to decode module sources from the old-style
|
||||
// string-only "source". It assumes that the expression does not contain any
|
||||
// references and can be decoded without an evaluation context.
|
||||
// In the long term, we want to get rid of this helper method.
|
||||
func legacySourceHelper(expr hcl.Expression, haveVersionArg bool) (addrs.ModuleSource, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
var sourceAddrRaw string
|
||||
var addr addrs.ModuleSource
|
||||
|
||||
valDiags := gohcl.DecodeExpression(expr, nil, &sourceAddrRaw)
|
||||
diags = append(diags, valDiags...)
|
||||
if !valDiags.HasErrors() {
|
||||
var err error
|
||||
if haveVersionArg {
|
||||
addr, err = moduleaddrs.ParseModuleSourceRegistry(sourceAddrRaw)
|
||||
} else {
|
||||
addr, err = moduleaddrs.ParseModuleSource(sourceAddrRaw)
|
||||
}
|
||||
if err != nil {
|
||||
// NOTE: We leave addr 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.
|
||||
addr = 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 *moduleaddrs.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: expr.Range().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: expr.Range().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: expr.Range().Ptr(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return addr, diags
|
||||
}
|
||||
|
||||
// legacyVersionHelper is used to decode version constraints from the old-style
|
||||
// string-only "version". It assumes that the expression does not contain any
|
||||
// references and can be decoded without an evaluation context.
|
||||
// In the long term, we want to get rid of this helper method.
|
||||
func legacyVersionHelper(expr hcl.Expression) (VersionConstraint, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
var versionRaw string
|
||||
|
||||
ret := VersionConstraint{
|
||||
DeclRange: expr.Range(),
|
||||
}
|
||||
|
||||
valDiags := gohcl.DecodeExpression(expr, nil, &versionRaw)
|
||||
diags = append(diags, valDiags...)
|
||||
if !valDiags.HasErrors() {
|
||||
constraints, err := version.NewConstraint(versionRaw)
|
||||
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.", // Not very actionable :(
|
||||
Subject: expr.Range().Ptr(),
|
||||
})
|
||||
return ret, diags
|
||||
}
|
||||
ret.Required = constraints
|
||||
}
|
||||
|
||||
return ret, diags
|
||||
}
|
||||
|
||||
func buildChildModules(parent *Config, walker ModuleWalker) (map[string]*Config, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
ret := map[string]*Config{}
|
||||
|
|
@ -161,12 +279,28 @@ func buildChildModules(parent *Config, walker ModuleWalker) (map[string]*Config,
|
|||
path := slices.Clone(parent.Path)
|
||||
path = append(path, call.Name)
|
||||
|
||||
sourceAddr, sourceDiags := legacySourceHelper(call.SourceExpr, call.VersionExpr != nil)
|
||||
diags = append(diags, sourceDiags...)
|
||||
if sourceDiags.HasErrors() {
|
||||
continue
|
||||
}
|
||||
|
||||
var versionConstraint VersionConstraint
|
||||
if call.VersionExpr != nil {
|
||||
var versionDiags hcl.Diagnostics
|
||||
versionConstraint, versionDiags = legacyVersionHelper(call.VersionExpr)
|
||||
diags = append(diags, versionDiags...)
|
||||
if versionDiags.HasErrors() {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
req := ModuleRequest{
|
||||
Name: call.Name,
|
||||
Path: path,
|
||||
SourceAddr: call.SourceAddr,
|
||||
SourceAddrRange: call.SourceAddrRange,
|
||||
VersionConstraint: call.Version,
|
||||
SourceAddr: sourceAddr,
|
||||
SourceAddrRange: call.SourceExpr.Range(),
|
||||
VersionConstraint: versionConstraint,
|
||||
Parent: parent,
|
||||
CallRange: call.DeclRange,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,3 +187,8 @@ func (l *Loader) AllowLanguageExperiments(allowed bool) {
|
|||
func (l *Loader) AllowsLanguageExperiments() bool {
|
||||
return l.parser.AllowsLanguageExperiments()
|
||||
}
|
||||
|
||||
// ModuleWalker returns a walker suitable for loading already-installed modules.
|
||||
func (l *Loader) ModuleWalker() configs.ModuleWalker {
|
||||
return configs.ModuleWalkerFunc(l.moduleWalkerLoad)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,16 +22,26 @@ import (
|
|||
//
|
||||
// LoadConfig performs the basic syntax and uniqueness validations that are
|
||||
// required to process the individual modules
|
||||
func (l *Loader) LoadConfig(rootDir string) (*configs.Config, hcl.Diagnostics) {
|
||||
func (l *Loader) LoadStaticConfig(rootDir string) (*configs.Config, hcl.Diagnostics) {
|
||||
return l.loadConfig(l.parser.LoadConfigDir(rootDir, l.parserOpts...))
|
||||
}
|
||||
|
||||
// LoadConfigWithTests matches LoadConfig, except the configs.Config contains
|
||||
// any relevant .tftest.hcl files.
|
||||
func (l *Loader) LoadConfigWithTests(rootDir string, testDir string) (*configs.Config, hcl.Diagnostics) {
|
||||
func (l *Loader) LoadStaticConfigWithTests(rootDir string, testDir string) (*configs.Config, hcl.Diagnostics) {
|
||||
return l.loadConfig(l.parser.LoadConfigDir(rootDir, append(l.parserOpts, configs.MatchTestFiles(testDir))...))
|
||||
}
|
||||
|
||||
// LoadRootModule reads the root module using the loader's parser options.
|
||||
func (l *Loader) LoadRootModule(rootDir string) (*configs.Module, hcl.Diagnostics) {
|
||||
return l.parser.LoadConfigDir(rootDir, l.parserOpts...)
|
||||
}
|
||||
|
||||
// LoadRootModuleWithTests reads the root module and includes test files from the given directory.
|
||||
func (l *Loader) LoadRootModuleWithTests(rootDir string, testDir string) (*configs.Module, hcl.Diagnostics) {
|
||||
return l.parser.LoadConfigDir(rootDir, append(l.parserOpts, configs.MatchTestFiles(testDir))...)
|
||||
}
|
||||
|
||||
func (l *Loader) loadConfig(rootMod *configs.Module, diags hcl.Diagnostics) (*configs.Config, hcl.Diagnostics) {
|
||||
if rootMod == nil || diags.HasErrors() {
|
||||
// Ensure we return any parsed modules here so that required_version
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ func TestLoaderLoadConfig_okay(t *testing.T) {
|
|||
t.Fatalf("unexpected error from NewLoader: %s", err)
|
||||
}
|
||||
|
||||
cfg, diags := loader.LoadConfig(fixtureDir)
|
||||
cfg, diags := loader.LoadStaticConfig(fixtureDir)
|
||||
assertNoDiagnostics(t, diags)
|
||||
if cfg == nil {
|
||||
t.Fatalf("config is nil; want non-nil")
|
||||
|
|
@ -62,28 +62,6 @@ func TestLoaderLoadConfig_okay(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestLoaderLoadConfig_addVersion(t *testing.T) {
|
||||
// This test is for what happens when there is a version constraint added
|
||||
// to a module that previously didn't have one.
|
||||
fixtureDir := filepath.Clean("testdata/add-version-constraint")
|
||||
loader, err := NewLoader(&Config{
|
||||
ModulesDir: filepath.Join(fixtureDir, ".terraform/modules"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error from NewLoader: %s", err)
|
||||
}
|
||||
|
||||
_, diags := loader.LoadConfig(fixtureDir)
|
||||
if !diags.HasErrors() {
|
||||
t.Fatalf("success; want error")
|
||||
}
|
||||
got := diags.Error()
|
||||
want := "Module version requirements have changed"
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("wrong error\ngot:\n%s\n\nwant: containing %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoaderLoadConfig_loadDiags(t *testing.T) {
|
||||
// building a config which didn't load correctly may cause configs to panic
|
||||
fixtureDir := filepath.Clean("testdata/invalid-names")
|
||||
|
|
@ -94,7 +72,7 @@ func TestLoaderLoadConfig_loadDiags(t *testing.T) {
|
|||
t.Fatalf("unexpected error from NewLoader: %s", err)
|
||||
}
|
||||
|
||||
cfg, diags := loader.LoadConfig(fixtureDir)
|
||||
cfg, diags := loader.LoadStaticConfig(fixtureDir)
|
||||
if !diags.HasErrors() {
|
||||
t.Fatal("success; want error")
|
||||
}
|
||||
|
|
@ -118,7 +96,7 @@ func TestLoaderLoadConfig_loadDiagsFromSubmodules(t *testing.T) {
|
|||
t.Fatalf("unexpected error from NewLoader: %s", err)
|
||||
}
|
||||
|
||||
cfg, diags := loader.LoadConfig(fixtureDir)
|
||||
cfg, diags := loader.LoadStaticConfig(fixtureDir)
|
||||
if !diags.HasErrors() {
|
||||
t.Fatalf("loading succeeded; want an error")
|
||||
}
|
||||
|
|
@ -168,7 +146,7 @@ func TestLoaderLoadConfig_childProviderGrandchildCount(t *testing.T) {
|
|||
t.Fatalf("unexpected error from NewLoader: %s", err)
|
||||
}
|
||||
|
||||
cfg, diags := loader.LoadConfig(fixtureDir)
|
||||
cfg, diags := loader.LoadStaticConfig(fixtureDir)
|
||||
assertNoDiagnostics(t, diags)
|
||||
if cfg == nil {
|
||||
t.Fatalf("config is nil; want non-nil")
|
||||
|
|
@ -198,7 +176,7 @@ func TestLoaderLoadConfig_childProviderGrandchildCount(t *testing.T) {
|
|||
t.Fatalf("unexpected error from NewLoader: %s", err)
|
||||
}
|
||||
|
||||
_, diags := loader.LoadConfig(fixtureDir)
|
||||
_, diags := loader.LoadStaticConfig(fixtureDir)
|
||||
if !diags.HasErrors() {
|
||||
t.Fatalf("loading succeeded; want an error")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,26 +20,16 @@ import (
|
|||
"github.com/hashicorp/terraform/internal/modsdir"
|
||||
)
|
||||
|
||||
// LoadConfigWithSnapshot is a variant of LoadConfig that also simultaneously
|
||||
// creates an in-memory snapshot of the configuration files used, which can
|
||||
// be later used to create a loader that may read only from this snapshot.
|
||||
func (l *Loader) LoadConfigWithSnapshot(rootDir string) (*configs.Config, *Snapshot, hcl.Diagnostics) {
|
||||
rootMod, diags := l.parser.LoadConfigDir(rootDir, l.parserOpts...)
|
||||
if rootMod == nil {
|
||||
return nil, nil, diags
|
||||
}
|
||||
|
||||
func (l *Loader) ModuleWalkerSnapshot() (configs.ModuleWalker, *Snapshot) {
|
||||
snap := &Snapshot{
|
||||
Modules: map[string]*SnapshotModule{},
|
||||
}
|
||||
walker := l.makeModuleWalkerSnapshot(snap)
|
||||
cfg, cDiags := configs.BuildConfig(rootMod, walker, configs.MockDataLoaderFunc(l.LoadExternalMockData))
|
||||
diags = append(diags, cDiags...)
|
||||
|
||||
addDiags := l.addModuleToSnapshot(snap, "", rootDir, "", nil)
|
||||
diags = append(diags, addDiags...)
|
||||
return l.makeModuleWalkerSnapshot(snap), snap
|
||||
}
|
||||
|
||||
return cfg, snap, diags
|
||||
func (l *Loader) AddRootModuleToSnapshot(snap *Snapshot, rootDir string) hcl.Diagnostics {
|
||||
return l.addModuleToSnapshot(snap, "", rootDir, "", nil)
|
||||
}
|
||||
|
||||
// NewLoaderFromSnapshot creates a Loader that reads files only from the
|
||||
|
|
|
|||
|
|
@ -1,145 +0,0 @@
|
|||
// Copyright IBM Corp. 2014, 2026
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package configload
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/go-test/deep"
|
||||
)
|
||||
|
||||
func TestLoadConfigWithSnapshot(t *testing.T) {
|
||||
fixtureDir := filepath.Clean("testdata/already-installed")
|
||||
loader, err := NewLoader(&Config{
|
||||
ModulesDir: filepath.Join(fixtureDir, ".terraform/modules"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error from NewLoader: %s", err)
|
||||
}
|
||||
|
||||
_, got, diags := loader.LoadConfigWithSnapshot(fixtureDir)
|
||||
assertNoDiagnostics(t, diags)
|
||||
if got == nil {
|
||||
t.Fatalf("snapshot is nil; want non-nil")
|
||||
}
|
||||
|
||||
t.Log(spew.Sdump(got))
|
||||
|
||||
{
|
||||
gotModuleDirs := map[string]string{}
|
||||
for k, m := range got.Modules {
|
||||
gotModuleDirs[k] = m.Dir
|
||||
}
|
||||
wantModuleDirs := map[string]string{
|
||||
"": "testdata/already-installed",
|
||||
"child_a": "testdata/already-installed/.terraform/modules/child_a",
|
||||
"child_a.child_c": "testdata/already-installed/.terraform/modules/child_a/child_c",
|
||||
"child_b": "testdata/already-installed/.terraform/modules/child_b",
|
||||
"child_b.child_d": "testdata/already-installed/.terraform/modules/child_b.child_d",
|
||||
}
|
||||
|
||||
problems := deep.Equal(wantModuleDirs, gotModuleDirs)
|
||||
for _, problem := range problems {
|
||||
t.Error(problem)
|
||||
}
|
||||
if len(problems) > 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
gotRoot := got.Modules[""]
|
||||
wantRoot := &SnapshotModule{
|
||||
Dir: "testdata/already-installed",
|
||||
Files: map[string][]byte{
|
||||
"root.tf": []byte(`
|
||||
module "child_a" {
|
||||
source = "example.com/foo/bar_a/baz"
|
||||
version = ">= 1.0.0"
|
||||
}
|
||||
|
||||
module "child_b" {
|
||||
source = "example.com/foo/bar_b/baz"
|
||||
version = ">= 1.0.0"
|
||||
}
|
||||
`),
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(gotRoot, wantRoot) {
|
||||
t.Errorf("wrong root module snapshot\ngot: %swant: %s", spew.Sdump(gotRoot), spew.Sdump(wantRoot))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestLoadConfigWithSnapshot_invalidSource(t *testing.T) {
|
||||
fixtureDir := filepath.Clean("testdata/already-installed-now-invalid")
|
||||
|
||||
old, _ := os.Getwd()
|
||||
os.Chdir(fixtureDir)
|
||||
defer os.Chdir(old)
|
||||
|
||||
loader, err := NewLoader(&Config{
|
||||
ModulesDir: ".terraform/modules",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error from NewLoader: %s", err)
|
||||
}
|
||||
|
||||
_, _, diags := loader.LoadConfigWithSnapshot(".")
|
||||
if !diags.HasErrors() {
|
||||
t.Error("LoadConfigWithSnapshot succeeded; want errors")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSnapshotRoundtrip(t *testing.T) {
|
||||
fixtureDir := filepath.Clean("testdata/already-installed")
|
||||
loader, err := NewLoader(&Config{
|
||||
ModulesDir: filepath.Join(fixtureDir, ".terraform/modules"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error from NewLoader: %s", err)
|
||||
}
|
||||
|
||||
_, snap, diags := loader.LoadConfigWithSnapshot(fixtureDir)
|
||||
assertNoDiagnostics(t, diags)
|
||||
if snap == nil {
|
||||
t.Fatalf("snapshot is nil; want non-nil")
|
||||
}
|
||||
|
||||
snapLoader := NewLoaderFromSnapshot(snap)
|
||||
if loader == nil {
|
||||
t.Fatalf("loader is nil; want non-nil")
|
||||
}
|
||||
|
||||
config, diags := snapLoader.LoadConfig(fixtureDir)
|
||||
assertNoDiagnostics(t, diags)
|
||||
if config == nil {
|
||||
t.Fatalf("config is nil; want non-nil")
|
||||
}
|
||||
if config.Module == nil {
|
||||
t.Fatalf("config has no root module")
|
||||
}
|
||||
if got, want := config.Module.SourceDir, "testdata/already-installed"; got != want {
|
||||
t.Errorf("wrong root module sourcedir %q; want %q", got, want)
|
||||
}
|
||||
if got, want := len(config.Module.ModuleCalls), 2; got != want {
|
||||
t.Errorf("wrong number of module calls in root module %d; want %d", got, want)
|
||||
}
|
||||
childA := config.Children["child_a"]
|
||||
if childA == nil {
|
||||
t.Fatalf("child_a config is nil; want non-nil")
|
||||
}
|
||||
if childA.Module == nil {
|
||||
t.Fatalf("child_a config has no module")
|
||||
}
|
||||
if got, want := childA.Module.SourceDir, "testdata/already-installed/.terraform/modules/child_a"; got != want {
|
||||
t.Errorf("wrong child_a sourcedir %q; want %q", got, want)
|
||||
}
|
||||
if got, want := len(childA.Module.ModuleCalls), 1; got != want {
|
||||
t.Errorf("wrong number of module calls in child_a %d; want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,13 +16,6 @@ import (
|
|||
)
|
||||
|
||||
func TestParseConfigResourceFromExpression(t *testing.T) {
|
||||
mustExpr := func(expr hcl.Expression, diags hcl.Diagnostics) hcl.Expression {
|
||||
if diags != nil {
|
||||
panic(diags.Error())
|
||||
}
|
||||
return expr
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
expr hcl.Expression
|
||||
expect addrs.ConfigResource
|
||||
|
|
@ -280,3 +273,10 @@ func mustAbsResourceInstanceAddr(str string) addrs.AbsResourceInstance {
|
|||
}
|
||||
return addr
|
||||
}
|
||||
|
||||
func mustExpr(expr hcl.Expression, diags hcl.Diagnostics) hcl.Expression {
|
||||
if diags != nil {
|
||||
panic(diags.Error())
|
||||
}
|
||||
return expr
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
variable "module_version" { default = "v1.0" }
|
||||
|
||||
module "foo" {
|
||||
source = "./ff"
|
||||
version = var.module_version
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ func testAnalyzer(t *testing.T, fixtureName string) *Analyzer {
|
|||
loader, cleanup := configload.NewLoaderForTests(t)
|
||||
defer cleanup()
|
||||
|
||||
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil))
|
||||
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil), nil)
|
||||
_, instDiags := inst.InstallModules(context.Background(), configDir, "tests", true, false, initwd.ModuleInstallHooksImpl{})
|
||||
if instDiags.HasErrors() {
|
||||
t.Fatalf("unexpected module installation errors: %s", instDiags.Err().Error())
|
||||
|
|
@ -33,7 +33,7 @@ func testAnalyzer(t *testing.T, fixtureName string) *Analyzer {
|
|||
t.Fatalf("failed to refresh modules after install: %s", err)
|
||||
}
|
||||
|
||||
cfg, loadDiags := loader.LoadConfig(configDir)
|
||||
cfg, loadDiags := loader.LoadStaticConfig(configDir)
|
||||
if loadDiags.HasErrors() {
|
||||
t.Fatalf("unexpected configuration errors: %s", loadDiags.Error())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -835,7 +835,7 @@ func testModuleInline(t *testing.T, sources map[string]string) *configs.Config {
|
|||
// Test modules usually do not refer to remote sources, and for local
|
||||
// sources only this ultimately just records all of the module paths
|
||||
// in a JSON file so that we can load them below.
|
||||
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil))
|
||||
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil), nil)
|
||||
_, instDiags := inst.InstallModules(context.Background(), cfgPath, "tests", true, false, initwd.ModuleInstallHooksImpl{})
|
||||
if instDiags.HasErrors() {
|
||||
t.Fatal(instDiags.Err())
|
||||
|
|
@ -847,7 +847,7 @@ func testModuleInline(t *testing.T, sources map[string]string) *configs.Config {
|
|||
t.Fatalf("failed to refresh modules after installation: %s", err)
|
||||
}
|
||||
|
||||
config, diags := loader.LoadConfigWithTests(cfgPath, "tests")
|
||||
config, diags := loader.LoadStaticConfigWithTests(cfgPath, "tests")
|
||||
if diags.HasErrors() {
|
||||
t.Fatal(diags.Error())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
|
|
@ -205,13 +206,7 @@ func (i *Installer) EnsureProviderVersions(ctx context.Context, locks *depsfile.
|
|||
if provider.IsBuiltIn() {
|
||||
// Built in providers do not require installation but we'll still
|
||||
// verify that the requested provider name is valid.
|
||||
valid := false
|
||||
for _, name := range i.builtInProviderTypes {
|
||||
if name == provider.Type {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
valid := slices.Contains(i.builtInProviderTypes, provider.Type)
|
||||
var err error
|
||||
if valid {
|
||||
if len(versionConstraints) == 0 {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import (
|
|||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/hashicorp/hcl/v2/hcltest"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/gocty"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
|
|
@ -519,7 +521,7 @@ func loadRefactoringFixture(t *testing.T, dir string) (*configs.Config, instance
|
|||
loader, cleanup := configload.NewLoaderForTests(t)
|
||||
defer cleanup()
|
||||
|
||||
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil))
|
||||
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil), nil)
|
||||
_, instDiags := inst.InstallModules(context.Background(), dir, "tests", true, false, initwd.ModuleInstallHooksImpl{})
|
||||
if instDiags.HasErrors() {
|
||||
t.Fatal(instDiags.Err())
|
||||
|
|
@ -531,7 +533,7 @@ func loadRefactoringFixture(t *testing.T, dir string) (*configs.Config, instance
|
|||
t.Fatalf("failed to refresh modules after installation: %s", err)
|
||||
}
|
||||
|
||||
rootCfg, diags := loader.LoadConfig(dir)
|
||||
rootCfg, diags := loader.LoadStaticConfig(dir)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("failed to load root module: %s", diags.Error())
|
||||
}
|
||||
|
|
@ -565,7 +567,7 @@ func staticPopulateExpanderModule(t *testing.T, rootCfg *configs.Config, moduleA
|
|||
// module to be something that counts as a separate package,
|
||||
// so we can test rules relating to crossing package boundaries
|
||||
// even though we really just loaded the module from a local path.
|
||||
call.SourceAddr = fakeExternalModuleSource
|
||||
call.SourceExpr = hcltest.MockExprLiteral(cty.StringVal(fakeExternalModuleSource.String()))
|
||||
}
|
||||
|
||||
// In order to get a valid, useful set of instances here we're going
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import (
|
|||
"github.com/hashicorp/terraform/internal/provisioners"
|
||||
"github.com/hashicorp/terraform/internal/registry"
|
||||
"github.com/hashicorp/terraform/internal/states"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
|
||||
_ "github.com/hashicorp/terraform/internal/logging"
|
||||
)
|
||||
|
|
@ -67,7 +68,7 @@ func testModuleWithSnapshot(t *testing.T, name string) (*configs.Config, *config
|
|||
// Test modules usually do not refer to remote sources, and for local
|
||||
// sources only this ultimately just records all of the module paths
|
||||
// in a JSON file so that we can load them below.
|
||||
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil))
|
||||
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil), nil)
|
||||
_, instDiags := inst.InstallModules(context.Background(), dir, "tests", true, false, initwd.ModuleInstallHooksImpl{})
|
||||
if instDiags.HasErrors() {
|
||||
t.Fatal(instDiags.Err())
|
||||
|
|
@ -79,14 +80,44 @@ func testModuleWithSnapshot(t *testing.T, name string) (*configs.Config, *config
|
|||
t.Fatalf("failed to refresh modules after installation: %s", err)
|
||||
}
|
||||
|
||||
config, snap, diags := loader.LoadConfigWithSnapshot(dir)
|
||||
config, snap, diags := testLoadWithSnapshot(dir, loader, nil)
|
||||
if diags.HasErrors() {
|
||||
t.Fatal(diags.Error())
|
||||
t.Fatal(diags.Err())
|
||||
}
|
||||
|
||||
return config, snap
|
||||
}
|
||||
|
||||
func testLoadWithSnapshot(dir string, loader *configload.Loader, vars InputValues) (*configs.Config, *configload.Snapshot, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
rootMod, configDiags := loader.LoadRootModule(dir)
|
||||
if configDiags.HasErrors() {
|
||||
diags = diags.Append(configDiags)
|
||||
return nil, nil, diags
|
||||
}
|
||||
|
||||
walkerSnapshot, snap := loader.ModuleWalkerSnapshot()
|
||||
config, buildDiags := BuildConfigWithGraph(
|
||||
rootMod,
|
||||
walkerSnapshot,
|
||||
vars,
|
||||
configs.MockDataLoaderFunc(loader.LoadExternalMockData),
|
||||
)
|
||||
if buildDiags.HasErrors() {
|
||||
diags = diags.Append(buildDiags)
|
||||
return nil, nil, diags
|
||||
}
|
||||
|
||||
snapDiags := loader.AddRootModuleToSnapshot(snap, dir)
|
||||
if snapDiags.HasErrors() {
|
||||
diags = diags.Append(snapDiags)
|
||||
return nil, nil, diags
|
||||
}
|
||||
|
||||
return config, snap, nil
|
||||
}
|
||||
|
||||
// testModuleInline takes a map of path -> config strings and yields a config
|
||||
// structure with those files loaded from disk
|
||||
func testModuleInline(t testing.TB, sources map[string]string, parserOpts ...configs.Option) *configs.Config {
|
||||
|
|
@ -127,7 +158,7 @@ func testModuleInline(t testing.TB, sources map[string]string, parserOpts ...con
|
|||
// Test modules usually do not refer to remote sources, and for local
|
||||
// sources only this ultimately just records all of the module paths
|
||||
// in a JSON file so that we can load them below.
|
||||
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil))
|
||||
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil), nil)
|
||||
_, instDiags := inst.InstallModules(context.Background(), cfgPath, "tests", true, false, initwd.ModuleInstallHooksImpl{})
|
||||
if instDiags.HasErrors() {
|
||||
t.Fatal(instDiags.Err())
|
||||
|
|
@ -139,7 +170,7 @@ func testModuleInline(t testing.TB, sources map[string]string, parserOpts ...con
|
|||
t.Fatalf("failed to refresh modules after installation: %s", err)
|
||||
}
|
||||
|
||||
config, diags := loader.LoadConfigWithTests(cfgPath, "tests")
|
||||
config, diags := loader.LoadStaticConfigWithTests(cfgPath, "tests")
|
||||
if diags.HasErrors() {
|
||||
t.Fatal(diags.Error())
|
||||
}
|
||||
|
|
@ -147,6 +178,49 @@ func testModuleInline(t testing.TB, sources map[string]string, parserOpts ...con
|
|||
return config
|
||||
}
|
||||
|
||||
func testRootModuleInline(t testing.TB, sources map[string]string) *configs.Module {
|
||||
t.Helper()
|
||||
|
||||
cfgPath, err := filepath.EvalSymlinks(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for path, configStr := range sources {
|
||||
dir := filepath.Dir(path)
|
||||
if dir != "." {
|
||||
err := os.MkdirAll(filepath.Join(cfgPath, dir), os.FileMode(0777))
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating subdir: %s", err)
|
||||
}
|
||||
}
|
||||
// Write the configuration
|
||||
cfgF, err := os.Create(filepath.Join(cfgPath, path))
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating temporary file for config: %s", err)
|
||||
}
|
||||
|
||||
_, err = io.Copy(cfgF, strings.NewReader(configStr))
|
||||
cfgF.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating temporary file for config: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
loader, cleanup := configload.NewLoaderForTests(t)
|
||||
defer cleanup()
|
||||
|
||||
// We need to be able to exercise experimental features in our integration tests.
|
||||
loader.AllowLanguageExperiments(true)
|
||||
|
||||
mod, diags := loader.Parser().LoadConfigDir(cfgPath)
|
||||
if diags.HasErrors() {
|
||||
t.Fatal(diags.Error())
|
||||
}
|
||||
|
||||
return mod
|
||||
}
|
||||
|
||||
// testSetResourceInstanceCurrent is a helper function for tests that sets a Current,
|
||||
// Ready resource instance for the given module.
|
||||
func testSetResourceInstanceCurrent(module *states.Module, resource, attrsJson, provider string) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue