diff --git a/internal/terraform/eval_context_builtin.go b/internal/terraform/eval_context_builtin.go index d13b38551294..d5c3df9ee5eb 100644 --- a/internal/terraform/eval_context_builtin.go +++ b/internal/terraform/eval_context_builtin.go @@ -443,7 +443,10 @@ func (ctx *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, source switch scope := ctx.scope.(type) { case evalContextModuleInstance: data := &evaluationStateData{ - Evaluator: ctx.Evaluator, + evaluationData: &evaluationData{ + Evaluator: ctx.Evaluator, + Module: scope.Addr.Module(), + }, ModulePath: scope.Addr, InstanceKeyData: keyData, Operation: ctx.Evaluator.Operation, @@ -463,7 +466,10 @@ func (ctx *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, source return evalScope case evalContextPartialExpandedModule: data := &evaluationPlaceholderData{ - Evaluator: ctx.Evaluator, + evaluationData: &evaluationData{ + Evaluator: ctx.Evaluator, + Module: scope.Addr.Module(), + }, ModulePath: scope.Addr, CountAvailable: keyData.CountIndex != cty.NilVal, EachAvailable: keyData.EachKey != cty.NilVal, diff --git a/internal/terraform/evaluate.go b/internal/terraform/evaluate.go index dc76b9988d0a..6e59436af7b5 100644 --- a/internal/terraform/evaluate.go +++ b/internal/terraform/evaluate.go @@ -6,8 +6,6 @@ package terraform import ( "fmt" "log" - "os" - "path/filepath" "time" "github.com/hashicorp/hcl/v2" @@ -98,7 +96,7 @@ func (e *Evaluator) Scope(data lang.Data, self addrs.Referenceable, source addrs // evaluationStateData is an implementation of lang.Data that resolves // references primarily (but not exclusively) using information from a State. type evaluationStateData struct { - Evaluator *Evaluator + *evaluationData // ModulePath is the path through the dynamic module tree to the module // that references will be resolved relative to. @@ -482,75 +480,6 @@ func (d *evaluationStateData) GetModule(addr addrs.ModuleCall, rng tfdiags.Sourc } } -func (d *evaluationStateData) GetPathAttr(addr addrs.PathAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - switch addr.Name { - - case "cwd": - var err error - var wd string - if d.Evaluator.Meta != nil { - // Meta is always non-nil in the normal case, but some test cases - // are not so realistic. - wd = d.Evaluator.Meta.OriginalWorkingDir - } - if wd == "" { - wd, err = os.Getwd() - if err != nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: `Failed to get working directory`, - Detail: fmt.Sprintf(`The value for path.cwd cannot be determined due to a system error: %s`, err), - Subject: rng.ToHCL().Ptr(), - }) - return cty.DynamicVal, diags - } - } - // The current working directory should always be absolute, whether we - // just looked it up or whether we were relying on ContextMeta's - // (possibly non-normalized) path. - wd, err = filepath.Abs(wd) - if err != nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: `Failed to get working directory`, - Detail: fmt.Sprintf(`The value for path.cwd cannot be determined due to a system error: %s`, err), - Subject: rng.ToHCL().Ptr(), - }) - return cty.DynamicVal, diags - } - - return cty.StringVal(filepath.ToSlash(wd)), diags - - case "module": - moduleConfig := d.Evaluator.Config.DescendentForInstance(d.ModulePath) - if moduleConfig == nil { - // should never happen, since we can't be evaluating in a module - // that wasn't mentioned in configuration. - panic(fmt.Sprintf("module.path read from module %s, which has no configuration", d.ModulePath)) - } - sourceDir := moduleConfig.Module.SourceDir - return cty.StringVal(filepath.ToSlash(sourceDir)), diags - - case "root": - sourceDir := d.Evaluator.Config.Module.SourceDir - return cty.StringVal(filepath.ToSlash(sourceDir)), diags - - default: - suggestion := didyoumean.NameSuggestion(addr.Name, []string{"cwd", "module", "root"}) - if suggestion != "" { - suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) - } - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: `Invalid "path" attribute`, - Detail: fmt.Sprintf(`The "path" object does not have an attribute named %q.%s`, addr.Name, suggestion), - Subject: rng.ToHCL().Ptr(), - }) - return cty.DynamicVal, diags - } -} - func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics // First we'll consult the configuration to see if an resource of this @@ -840,68 +769,6 @@ func (d *evaluationStateData) getResourceSchema(addr addrs.Resource, providerAdd return schema } -func (d *evaluationStateData) GetTerraformAttr(addr addrs.TerraformAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - - if d.Evaluator.Meta == nil || d.Evaluator.Meta.Env == "" { - // The absense of an "env" (really: workspace) name suggests that - // we're running in a non-workspace context, such as in a component - // of a stack. terraform.workspace -- and the terraform symbol in - // general -- is a legacy thing from workspaces mode that isn't - // carried forward to stacks, because stack configurations can instead - // vary their behavior based on input variables provided in the - // deployment configuration. - switch addr.Name { - case "workspace": - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: `Invalid reference`, - Detail: `The terraform.workspace attribute is only available for modules used in Terraform workspaces. Use input variables instead to create variations between different instances of this module.`, - Subject: rng.ToHCL().Ptr(), - }) - default: - // A more generic error for any other attribute name, since no - // others are valid anyway but it would be confusing to mention - // terraform.workspace here. - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: `Invalid reference`, - Detail: `The "terraform" object is only available for modules used in Terraform workspaces.`, - Subject: rng.ToHCL().Ptr(), - }) - } - return cty.DynamicVal, diags - } - - switch addr.Name { - - case "workspace": - workspaceName := d.Evaluator.Meta.Env - return cty.StringVal(workspaceName), diags - - case "env": - // Prior to Terraform 0.12 there was an attribute "env", which was - // an alias name for "workspace". This was deprecated and is now - // removed. - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: `Invalid "terraform" attribute`, - Detail: `The terraform.env attribute was deprecated in v0.10 and removed in v0.12. The "state environment" concept was renamed to "workspace" in v0.12, and so the workspace name can now be accessed using the terraform.workspace attribute.`, - Subject: rng.ToHCL().Ptr(), - }) - return cty.DynamicVal, diags - - default: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: `Invalid "terraform" attribute`, - Detail: fmt.Sprintf(`The "terraform" object does not have an attribute named %q. The only supported attribute is terraform.workspace, the name of the currently-selected workspace.`, addr.Name), - Subject: rng.ToHCL().Ptr(), - }) - return cty.DynamicVal, diags - } -} - func (d *evaluationStateData) GetOutput(addr addrs.OutputValue, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics @@ -957,30 +824,6 @@ func (d *evaluationStateData) GetOutput(addr addrs.OutputValue, rng tfdiags.Sour return val, diags } -func (d *evaluationStateData) GetCheckBlock(addr addrs.Check, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { - // For now, check blocks don't contain any meaningful data and can only - // be referenced from the testing scope within an expect_failures attribute. - // - // We've added them into the scope explicitly since they are referencable, - // but we'll actually just return an error message saying they can't be - // referenced in this context. - var diags tfdiags.Diagnostics - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Reference to \"check\" in invalid context", - Detail: "The \"check\" object can only be referenced from an \"expect_failures\" attribute within a Terraform testing \"run\" block.", - Subject: rng.ToHCL().Ptr(), - }) - return cty.NilVal, diags -} - -func (d *evaluationStateData) GetRunBlock(run addrs.Run, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { - // We should not get here because any scope that has an [evaluationStateData] - // as its Data should have a reference parser that doesn't accept addrs.Run - // addresses. - panic("GetRunBlock called on non-test evaluation dataset") -} - // moduleDisplayAddr returns a string describing the given module instance // address that is appropriate for returning to users in situations where the // root module is possible. Specifically, it returns "the root module" if the diff --git a/internal/terraform/evaluate_data.go b/internal/terraform/evaluate_data.go new file mode 100644 index 000000000000..8fd58d8dd97e --- /dev/null +++ b/internal/terraform/evaluate_data.go @@ -0,0 +1,190 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/didyoumean" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// evaluationData is the base struct for evaluating data from within Terraform +// Core. It contains some common data and functions shared by the various +// implemented evaluators. +type evaluationData struct { + Evaluator *Evaluator + + // Module is the unexpanded module that this data is being evaluated within. + Module addrs.Module +} + +// GetPathAttr implements lang.Data. +func (d *evaluationData) GetPathAttr(addr addrs.PathAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + switch addr.Name { + + case "cwd": + var err error + var wd string + if d.Evaluator.Meta != nil { + // Meta is always non-nil in the normal case, but some test cases + // are not so realistic. + wd = d.Evaluator.Meta.OriginalWorkingDir + } + if wd == "" { + wd, err = os.Getwd() + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Failed to get working directory`, + Detail: fmt.Sprintf(`The value for path.cwd cannot be determined due to a system error: %s`, err), + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } + } + // The current working directory should always be absolute, whether we + // just looked it up or whether we were relying on ContextMeta's + // (possibly non-normalized) path. + wd, err = filepath.Abs(wd) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Failed to get working directory`, + Detail: fmt.Sprintf(`The value for path.cwd cannot be determined due to a system error: %s`, err), + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } + + return cty.StringVal(filepath.ToSlash(wd)), diags + + case "module": + moduleConfig := d.Evaluator.Config.Descendent(d.Module) + if moduleConfig == nil { + // should never happen, since we can't be evaluating in a module + // that wasn't mentioned in configuration. + panic(fmt.Sprintf("module.path read from module %s, which has no configuration", d.Module)) + } + sourceDir := moduleConfig.Module.SourceDir + return cty.StringVal(filepath.ToSlash(sourceDir)), diags + + case "root": + sourceDir := d.Evaluator.Config.Module.SourceDir + return cty.StringVal(filepath.ToSlash(sourceDir)), diags + + default: + suggestion := didyoumean.NameSuggestion(addr.Name, []string{"cwd", "module", "root"}) + if suggestion != "" { + suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) + } + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Invalid "path" attribute`, + Detail: fmt.Sprintf(`The "path" object does not have an attribute named %q.%s`, addr.Name, suggestion), + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } +} + +// GetTerraformAttr implements lang.Data. +func (d *evaluationData) GetTerraformAttr(addr addrs.TerraformAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + if d.Evaluator.Meta == nil || d.Evaluator.Meta.Env == "" { + // The absense of an "env" (really: workspace) name suggests that + // we're running in a non-workspace context, such as in a component + // of a stack. terraform.workspace -- and the terraform symbol in + // general -- is a legacy thing from workspaces mode that isn't + // carried forward to stacks, because stack configurations can instead + // vary their behavior based on input variables provided in the + // deployment configuration. + switch addr.Name { + case "workspace": + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Invalid reference`, + Detail: `The terraform.workspace attribute is only available for modules used in Terraform workspaces. Use input variables instead to create variations between different instances of this module.`, + Subject: rng.ToHCL().Ptr(), + }) + default: + // A more generic error for any other attribute name, since no + // others are valid anyway but it would be confusing to mention + // terraform.workspace here. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Invalid reference`, + Detail: `The "terraform" object is only available for modules used in Terraform workspaces.`, + Subject: rng.ToHCL().Ptr(), + }) + } + return cty.DynamicVal, diags + } + + switch addr.Name { + + case "workspace": + workspaceName := d.Evaluator.Meta.Env + return cty.StringVal(workspaceName), diags + + case "env": + // Prior to Terraform 0.12 there was an attribute "env", which was + // an alias name for "workspace". This was deprecated and is now + // removed. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Invalid "terraform" attribute`, + Detail: `The terraform.env attribute was deprecated in v0.10 and removed in v0.12. The "state environment" concept was renamed to "workspace" in v0.12, and so the workspace name can now be accessed using the terraform.workspace attribute.`, + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Invalid "terraform" attribute`, + Detail: fmt.Sprintf(`The "terraform" object does not have an attribute named %q. The only supported attribute is terraform.workspace, the name of the currently-selected workspace.`, addr.Name), + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } +} + +// StaticValidateReferences implements lang.Data. +func (d *evaluationData) StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable, source addrs.Referenceable) tfdiags.Diagnostics { + return d.Evaluator.StaticValidateReferences(refs, d.Module, self, source) +} + +// GetRunBlock implements lang.Data. +func (d *evaluationData) GetRunBlock(addrs.Run, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + // We should not get here because any scope that has an [evaluationPlaceholderData] + // as its Data should have a reference parser that doesn't accept addrs.Run + // addresses. + panic("GetRunBlock called on non-test evaluation dataset") +} + +func (d *evaluationData) GetCheckBlock(addr addrs.Check, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + // For now, check blocks don't contain any meaningful data and can only + // be referenced from the testing scope within an expect_failures attribute. + // + // We've added them into the scope explicitly since they are referencable, + // but we'll actually just return an error message saying they can't be + // referenced in this context. + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to \"check\" in invalid context", + Detail: "The \"check\" object can only be referenced from an \"expect_failures\" attribute within a Terraform testing \"run\" block.", + Subject: rng.ToHCL().Ptr(), + }) + return cty.NilVal, diags +} diff --git a/internal/terraform/evaluate_placeholder.go b/internal/terraform/evaluate_placeholder.go index 70359bf6dccb..d087665b269b 100644 --- a/internal/terraform/evaluate_placeholder.go +++ b/internal/terraform/evaluate_placeholder.go @@ -9,6 +9,7 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/tfdiags" @@ -20,7 +21,7 @@ import ( // only what we know to be true for all possible final module instances // that could exist for the prefix. type evaluationPlaceholderData struct { - Evaluator *Evaluator + *evaluationData // ModulePath is the partially-expanded path through the dynamic module // tree to a set of possible module instances that share a common known @@ -55,21 +56,6 @@ type evaluationPlaceholderData struct { var _ lang.Data = (*evaluationPlaceholderData)(nil) -// GetCheckBlock implements lang.Data. -func (d *evaluationPlaceholderData) GetCheckBlock(addr addrs.Check, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { - // check blocks don't produce any useful data and can only be referred - // to within an expect_failures attribute in the test language. - var diags tfdiags.Diagnostics - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Reference to \"check\" in invalid context", - Detail: "The \"check\" object can only be used from an \"expect_failures\" attribute within a Terraform testing \"run\" block.", - Subject: rng.ToHCL().Ptr(), - }) - return cty.NilVal, diags - -} - // GetCountAttr implements lang.Data. func (d *evaluationPlaceholderData) GetCountAttr(addr addrs.CountAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics @@ -224,21 +210,6 @@ func (d *evaluationPlaceholderData) GetOutput(addr addrs.OutputValue, rng tfdiag } -// GetPathAttr implements lang.Data. -func (d *evaluationPlaceholderData) GetPathAttr(addrs.PathAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { - // TODO: It would be helpful to perform the same logic here as we do - // in the full-evaluation case, since the paths we'd return here cannot - // vary based on dynamic data, but we'll need to factor out the logic - // into a common location we can call from both places first. For now, - // we'll just leave these all as unknown value placeholders. - // - // What we _do_ know is that all valid attributes of "path" are strings - // that are definitely not null, so we can at least catch situations - // where someone tries to use them in a place where a string is - // unacceptable. - return cty.UnknownVal(cty.String).RefineNotNull(), nil -} - // GetResource implements lang.Data. func (d *evaluationPlaceholderData) GetResource(addrs.Resource, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { // TODO: Once we've implemented the evaluation of placeholders for @@ -251,25 +222,3 @@ func (d *evaluationPlaceholderData) GetResource(addrs.Resource, tfdiags.SourceRa // really help references to single-instance resources. return cty.DynamicVal, nil } - -// GetRunBlock implements lang.Data. -func (d *evaluationPlaceholderData) GetRunBlock(addrs.Run, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { - // We should not get here because any scope that has an [evaluationPlaceholderData] - // as its Data should have a reference parser that doesn't accept addrs.Run - // addresses. - panic("GetRunBlock called on non-test evaluation dataset") -} - -// GetTerraformAttr implements lang.Data. -func (d *evaluationPlaceholderData) GetTerraformAttr(addrs.TerraformAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { - // TODO: It would be helpful to perform the same validation checks that - // occur in evaluationStateData.GetTerraformAttr, so authors can catch - // invalid usage of the "terraform" object even when under an unexpanded - // module prefix. - return cty.DynamicVal, nil -} - -// StaticValidateReferences implements lang.Data. -func (d *evaluationPlaceholderData) StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable, source addrs.Referenceable) tfdiags.Diagnostics { - return d.Evaluator.StaticValidateReferences(refs, d.ModulePath.Module(), self, source) -} diff --git a/internal/terraform/evaluate_test.go b/internal/terraform/evaluate_test.go index 7b3dd176b1e3..7dbabb467207 100644 --- a/internal/terraform/evaluate_test.go +++ b/internal/terraform/evaluate_test.go @@ -30,7 +30,9 @@ func TestEvaluatorGetTerraformAttr(t *testing.T) { NamedValues: namedvals.NewState(), } data := &evaluationStateData{ - Evaluator: evaluator, + evaluationData: &evaluationData{ + Evaluator: evaluator, + }, } scope := evaluator.Scope(data, nil, nil, lang.ExternalFuncs{}) @@ -61,7 +63,9 @@ func TestEvaluatorGetPathAttr(t *testing.T) { NamedValues: namedvals.NewState(), } data := &evaluationStateData{ - Evaluator: evaluator, + evaluationData: &evaluationData{ + Evaluator: evaluator, + }, } scope := evaluator.Scope(data, nil, nil, lang.ExternalFuncs{}) @@ -127,7 +131,9 @@ func TestEvaluatorGetOutputValue(t *testing.T) { } data := &evaluationStateData{ - Evaluator: evaluator, + evaluationData: &evaluationData{ + Evaluator: evaluator, + }, } scope := evaluator.Scope(data, nil, nil, lang.ExternalFuncs{}) @@ -196,7 +202,9 @@ func TestEvaluatorGetInputVariable(t *testing.T) { } data := &evaluationStateData{ - Evaluator: evaluator, + evaluationData: &evaluationData{ + Evaluator: evaluator, + }, } scope := evaluator.Scope(data, nil, nil, lang.ExternalFuncs{}) @@ -355,7 +363,9 @@ func TestEvaluatorGetResource(t *testing.T) { } data := &evaluationStateData{ - Evaluator: evaluator, + evaluationData: &evaluationData{ + Evaluator: evaluator, + }, } scope := evaluator.Scope(data, nil, nil, lang.ExternalFuncs{}) @@ -522,7 +532,9 @@ func TestEvaluatorGetResource_changes(t *testing.T) { } data := &evaluationStateData{ - Evaluator: evaluator, + evaluationData: &evaluationData{ + Evaluator: evaluator, + }, } scope := evaluator.Scope(data, nil, nil, lang.ExternalFuncs{}) @@ -554,7 +566,9 @@ func TestEvaluatorGetModule(t *testing.T) { cty.StringVal("bar").Mark(marks.Sensitive), ) data := &evaluationStateData{ - Evaluator: evaluator, + evaluationData: &evaluationData{ + Evaluator: evaluator, + }, } scope := evaluator.Scope(data, nil, nil, lang.ExternalFuncs{}) want := cty.ObjectVal(map[string]cty.Value{"out": cty.StringVal("bar").Mark(marks.Sensitive)})