diff --git a/internal/configs/named_values.go b/internal/configs/named_values.go index c9c92e45e074..ab7b3a7a654b 100644 --- a/internal/configs/named_values.go +++ b/internal/configs/named_values.go @@ -48,6 +48,10 @@ type Variable struct { Nullable bool NullableSet bool + Deprecated string + DeprecatedSet bool + DeprecatedRange hcl.Range + DeclRange hcl.Range } @@ -186,6 +190,13 @@ func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagno v.Default = val } + if attr, exists := content.Attributes["deprecated"]; exists { + valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Deprecated) + diags = append(diags, valDiags...) + v.DeprecatedSet = true + v.DeprecatedRange = attr.Range + } + for _, block := range content.Blocks { switch block.Type { @@ -499,6 +510,9 @@ var variableBlockSchema = &hcl.BodySchema{ { Name: "nullable", }, + { + Name: "deprecated", + }, }, Blocks: []hcl.BlockHeaderSchema{ { diff --git a/internal/configs/named_values_test.go b/internal/configs/named_values_test.go index 0626157c031e..bf9606a1074d 100644 --- a/internal/configs/named_values_test.go +++ b/internal/configs/named_values_test.go @@ -48,3 +48,30 @@ func TestVariableInvalidDefault(t *testing.T) { } } } + +func TestVariableDeprecation(t *testing.T) { + src := ` + variable "foo" { + type = string + deprecated = "This variable is deprecated, use bar instead" + } + ` + + hclF, diags := hclsyntax.ParseConfig([]byte(src), "test.tf", hcl.InitialPos) + if diags.HasErrors() { + t.Fatal(diags.Error()) + } + + b, diags := parseConfigFile(hclF.Body, nil, false, false) + if diags.HasErrors() { + t.Fatalf("unexpected error: %q", diags) + } + + if !b.Variables[0].DeprecatedSet { + t.Fatalf("expected variable to be deprecated") + } + + if b.Variables[0].Deprecated != "This variable is deprecated, use bar instead" { + t.Fatalf("expected variable to have deprecation message") + } +} diff --git a/internal/terraform/context_plan2_test.go b/internal/terraform/context_plan2_test.go index e3bf573c72c8..5becfef25cf2 100644 --- a/internal/terraform/context_plan2_test.go +++ b/internal/terraform/context_plan2_test.go @@ -6068,3 +6068,136 @@ data "test_data_source" "foo" { _, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) assertNoErrors(t, diags) } + +func TestContext2Plan_deprecated_variable(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "mod/main.tf": ` +variable "old-and-used" { + type = string + deprecated = "module variable deprecation" + default = "optional" +} + +variable "old-and-unused" { + type = string + deprecated = "module variable deprecation" + default = "optional" +} + +variable "new" { + type = string + default = "optional" +} + +output "use-everything" { + value = { + used = var.old-and-used + unused = var.old-and-unused + new = var.new + } +} +`, + "main.tf": ` +variable "root-old-and-used" { + type = string + deprecated = "root variable deprecation" + default = "optional" +} + +variable "root-old-and-unused" { + type = string + deprecated = "root variable deprecation" + default = "optional" +} + +variable "new" { + type = string + default = "new" +} + +module "old-mod" { + source = "./mod" + old-and-used = "old" +} + +module "new-mod" { + source = "./mod" + new = "new" +} + +output "use-everything" { + value = { + used = var.root-old-and-used + unused = var.root-old-and-unused + new = var.new + } +} +`, + }) + + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "attr": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + vars := InputValues{ + "root-old-and-used": { + Value: cty.StringVal("root-old-and-used"), + }, + "root-old-and-unused": { + Value: cty.NullVal(cty.String), + }, + "new": { + Value: cty.StringVal("new"), + }, + } + + // We can find module variable deprecation during validation + diags := ctx.Validate(m, &ValidateOpts{}) + var expectedValidateDiags tfdiags.Diagnostics + expectedValidateDiags = expectedValidateDiags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Usage of deprecated variable", + Detail: "module variable deprecation", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 21, Column: 20, Byte: 346}, + End: hcl.Pos{Line: 21, Column: 25, Byte: 351}, + }, + }, + ) + assertDiagnosticsMatch(t, diags, expectedValidateDiags) + + _, diags = ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, vars)) + var expectedPlanDiags tfdiags.Diagnostics + expectedPlanDiags = expectedPlanDiags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Usage of deprecated variable", + Detail: "root variable deprecation", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 4, Column: 2, Byte: 48}, + End: hcl.Pos{Line: 4, Column: 42, Byte: 88}, + }, + }, + ) + + assertDiagnosticsMatch(t, diags, expectedPlanDiags) +} diff --git a/internal/terraform/node_module_variable.go b/internal/terraform/node_module_variable.go index 1fc34ad5f9d2..cf7c8adf56d9 100644 --- a/internal/terraform/node_module_variable.go +++ b/internal/terraform/node_module_variable.go @@ -309,6 +309,15 @@ func (n *nodeModuleVariable) evalModuleVariable(ctx EvalContext, validateOnly bo finalVal, moreDiags := prepareFinalInputVariableValue(n.Addr, rawVal, n.Config) diags = diags.Append(moreDiags) + if n.Config.DeprecatedSet && givenVal.IsWhollyKnown() && !givenVal.IsNull() && validateOnly { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Usage of deprecated variable", + Detail: n.Config.Deprecated, + Subject: n.Expr.Range().Ptr(), + }) + } + return finalVal, diags.ErrWithWarnings() } diff --git a/internal/terraform/node_root_variable.go b/internal/terraform/node_root_variable.go index 3255cd211c28..924110c605f9 100644 --- a/internal/terraform/node_root_variable.go +++ b/internal/terraform/node_root_variable.go @@ -92,6 +92,15 @@ func (n *NodeRootVariable) Execute(ctx EvalContext, op walkOperation) tfdiags.Di } } + if n.Config.DeprecatedSet && givenVal.Value.IsWhollyKnown() && !givenVal.Value.IsNull() && op == walkPlan { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Usage of deprecated variable", + Detail: n.Config.Deprecated, + Subject: &n.Config.DeprecatedRange, + }) + } + if n.Planning { if checkState := ctx.Checks(); checkState.ConfigHasChecks(n.Addr.InModule(addrs.RootModule)) { ctx.Checks().ReportCheckableObjects(