From 0994e6fbf9fd5aeafe0654d7fcf9de807eecd342 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Fri, 19 Jan 2024 17:52:43 -0800 Subject: [PATCH] stacks: The terraform.workspace attr is not available in Stacks The terraform.workspace attribute is a rare example of a CLI- and Cloud- specific concern bleeding into the Terraform language, and it can only really have meaning when used in the traditional Terraform workflow because otherwise there's no workspace to return the name of. In Stacks any variations between instances of a module must be created through input variables. Within Terraform Cloud in particular it's also possible to use stack-level input variables that are assigned different values from different stack deployments, and thus an author can recreate the effect of terraform.workspace using a stack-level input variable that has a different value for each deployment. This is one of the few cases where the Terraform module language differs in stacks compared to traditional Terraform. Any module that makes use of terraform.workspace will need to be generalized to use input variables instead before it can be used within a stack component. Prior to this change, references to terraform.workspace from a module used in a stack component would just panic altogether, because the stacks runtime doesn't provide the object that the workspace name would be taken from. Now we'll return a user-oriented error instead. --- .../internal/stackeval/planning_test.go | 44 +++++++++++++++++++ .../no-workspace-name-ref.tf | 6 +++ .../no-workspace-name-ref.tfstack.hcl | 3 ++ internal/terraform/evaluate.go | 31 +++++++++++++ 4 files changed, 84 insertions(+) create mode 100644 internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/no_workspace_name_ref/no-workspace-name-ref.tf create mode 100644 internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/no_workspace_name_ref/no-workspace-name-ref.tfstack.hcl 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":