diff --git a/internal/stacks/stackruntime/internal/stackeval/planning_test.go b/internal/stacks/stackruntime/internal/stackeval/planning_test.go index 7325b6a808..8fe80f948b 100644 --- a/internal/stacks/stackruntime/internal/stackeval/planning_test.go +++ b/internal/stacks/stackruntime/internal/stackeval/planning_test.go @@ -6,8 +6,10 @@ package stackeval import ( "context" "fmt" + "strings" "testing" + "github.com/davecgh/go-spew/spew" "github.com/google/go-cmp/cmp" "github.com/zclconf/go-cty/cty" "google.golang.org/protobuf/reflect/protoreflect" @@ -23,6 +25,7 @@ import ( "github.com/hashicorp/terraform/internal/stacks/stackstate/statekeys" "github.com/hashicorp/terraform/internal/stacks/tfstackdata1" "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" ) func TestPlanning_DestroyMode(t *testing.T) { @@ -577,3 +580,44 @@ func TestPlanning_RequiredComponents(t *testing.T) { } }) } + +func TestPlanning_NoWorkspaceNameRef(t *testing.T) { + // This test verifies that a reference to terraform.workspace is treated + // as invalid for modules used in a stacks context, because there's + // no comparable single string to use in stacks context and we expect + // modules used in stack components to vary declarations based only + // on their input variables. + // + // (If something needs to vary between stack deployments then that's + // a good candidate for an input variable on the root stack configuration, + // set differently for each deployment, and then passed in to the + // components that need it.) + + cfg := testStackConfig(t, "planning", "no_workspace_name_ref") + main := NewForPlanning(cfg, stackstate.NewState(), PlanOpts{ + PlanningMode: plans.NormalMode, + }) + + inPromisingTask(t, func(ctx context.Context, t *testing.T) { + _, diags := testPlan(t, main) + if !diags.HasErrors() { + t.Fatal("success; want error about invalid terraform.workspace reference") + } + + // At least one of the diagnostics must mention the terraform.workspace + // attribute in its detail. + seenRelevantDiag := false + for _, diag := range diags { + if diag.Severity() != tfdiags.Error { + continue + } + if strings.Contains(diag.Description().Detail, "terraform.workspace") { + seenRelevantDiag = true + break + } + } + if !seenRelevantDiag { + t.Fatalf("none of the error diagnostics mentions terraform.workspace\n%s", spew.Sdump(diags.ForRPC())) + } + }) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/no_workspace_name_ref/no-workspace-name-ref.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/no_workspace_name_ref/no-workspace-name-ref.tf new file mode 100644 index 0000000000..e911ee4453 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/no_workspace_name_ref/no-workspace-name-ref.tf @@ -0,0 +1,6 @@ +output "invalid" { + # terraform.workspace is not available when this module is used as part + # of a stack component, so this should produce an error during + # planning. + value = terraform.workspace +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/no_workspace_name_ref/no-workspace-name-ref.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/no_workspace_name_ref/no-workspace-name-ref.tfstack.hcl new file mode 100644 index 0000000000..ac4b9e559f --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/no_workspace_name_ref/no-workspace-name-ref.tfstack.hcl @@ -0,0 +1,3 @@ +component "mod" { + source = "./" +} diff --git a/internal/terraform/evaluate.go b/internal/terraform/evaluate.go index 5a10dc7ee9..45f75dd4c6 100644 --- a/internal/terraform/evaluate.go +++ b/internal/terraform/evaluate.go @@ -811,6 +811,37 @@ func (d *evaluationStateData) getResourceSchema(addr addrs.Resource, providerAdd 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":