Skip to content

Commit 82b8b89

Browse files
variable and output deprecation
Variables can be deprecated through the `deprecated` attribute. If set the variable will emit a diagnostic if a values is passed to it. This entails both root level and module variables. Outputs can be deprecated through the `deprecated` attribute as well. If set wherever the value is used a diagnostic will be emitted. Root level outputs can not be deprecated. The only acceptable usage of a deprecated output is another deprecated output (forwarding the deprecation to the module user). If modules not under your control have deprecation warnings you can add a `suppress_deprecations_warnigns` attribute to the module call in question to silence any deeply nested warnings.
1 parent f591872 commit 82b8b89

33 files changed

+2392
-39
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: NEW FEATURES
2+
body: You can set a `deprecated` attribute on variable and output blocks to indicate that they are deprecated. This will produce warnings when passing in a value for a deprecated variable or when referencing a deprecated output.
3+
time: 2025-12-05T17:14:18.623477+01:00
4+
custom:
5+
Issue: "37795"

internal/addrs/check.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ func (c AbsCheck) CheckRule(typ CheckRuleType, i int) CheckRule {
106106
}
107107
}
108108

109+
// ModuleInstance returns the module instance portion of the address.
110+
func (c AbsCheck) ModuleInstance() ModuleInstance {
111+
return c.Module
112+
}
113+
109114
// ConfigCheckable returns the ConfigCheck address for this absolute reference.
110115
func (c AbsCheck) ConfigCheckable() ConfigCheckable {
111116
return ConfigCheck{

internal/addrs/check_rule.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,8 @@ func (c CheckRuleType) Description() string {
115115
return "Condition"
116116
}
117117
}
118+
119+
// ModuleInstance returns the module instance address containing this check rule.
120+
func (c CheckRule) ModuleInstance() ModuleInstance {
121+
return c.Container.ModuleInstance()
122+
}

internal/addrs/checkable.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ type Checkable interface {
3939

4040
CheckableKind() CheckableKind
4141
String() string
42+
ModuleInstance() ModuleInstance
4243
}
4344

4445
var (

internal/addrs/input_variable.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ func (v AbsInputVariableInstance) CheckRule(typ CheckRuleType, i int) CheckRule
8181
}
8282
}
8383

84+
// ModuleInstance returns the module instance portion of the address.
85+
func (v AbsInputVariableInstance) ModuleInstance() ModuleInstance {
86+
return v.Module
87+
}
88+
8489
func (v AbsInputVariableInstance) ConfigCheckable() ConfigCheckable {
8590
return ConfigInputVariable{
8691
Module: v.Module.Module(),

internal/addrs/output_value.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ func (m ModuleInstance) OutputValue(name string) AbsOutputValue {
7777
}
7878
}
7979

80+
// ModuleInstance returns the module instance portion of the address.
81+
func (v AbsOutputValue) ModuleInstance() ModuleInstance {
82+
return v.Module
83+
}
84+
8085
func (v AbsOutputValue) CheckRule(t CheckRuleType, i int) CheckRule {
8186
return CheckRule{
8287
Container: v,

internal/addrs/resource.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,11 @@ func (m ModuleInstance) ResourceInstance(mode ResourceMode, typeName string, nam
280280
}
281281
}
282282

283+
// ModuleInstance returns the module instance portion of the address.
284+
func (r AbsResourceInstance) ModuleInstance() ModuleInstance {
285+
return r.Module
286+
}
287+
283288
// ContainingResource returns the address of the resource that contains the
284289
// receving resource instance. In other words, it discards the key portion
285290
// of the address to produce an AbsResource value.

internal/configs/module_call.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/hashicorp/hcl/v2"
1010
"github.com/hashicorp/hcl/v2/gohcl"
1111
"github.com/hashicorp/hcl/v2/hclsyntax"
12+
"github.com/zclconf/go-cty/cty"
1213

1314
"github.com/hashicorp/terraform/internal/addrs"
1415
"github.com/hashicorp/terraform/internal/getmodules/moduleaddrs"
@@ -35,6 +36,8 @@ type ModuleCall struct {
3536
DependsOn []hcl.Traversal
3637

3738
DeclRange hcl.Range
39+
40+
IgnoreNestedDeprecations bool
3841
}
3942

4043
func decodeModuleBlock(block *hcl.Block, override bool) (*ModuleCall, hcl.Diagnostics) {
@@ -163,6 +166,30 @@ func decodeModuleBlock(block *hcl.Block, override bool) (*ModuleCall, hcl.Diagno
163166
mc.Providers = append(mc.Providers, providers...)
164167
}
165168

169+
if attr, exists := content.Attributes["ignore_nested_deprecations"]; exists {
170+
// We only allow static boolean values for this argument.
171+
val, evalDiags := attr.Expr.Value(&hcl.EvalContext{})
172+
if len(evalDiags.Errs()) > 0 {
173+
diags = append(diags, &hcl.Diagnostic{
174+
Severity: hcl.DiagError,
175+
Summary: "Invalid value for ignore_nested_deprecations",
176+
Detail: "The value for ignore_nested_deprecations must be a static boolean (true or false).",
177+
Subject: attr.Expr.Range().Ptr(),
178+
})
179+
}
180+
181+
if val.Type() != cty.Bool {
182+
diags = append(diags, &hcl.Diagnostic{
183+
Severity: hcl.DiagError,
184+
Summary: "Invalid type for ignore_nested_deprecations",
185+
Detail: fmt.Sprintf("The value for ignore_nested_deprecations must be a boolean (true or false), but the given value has type %s.", val.Type().FriendlyName()),
186+
Subject: attr.Expr.Range().Ptr(),
187+
})
188+
}
189+
190+
mc.IgnoreNestedDeprecations = val.True()
191+
}
192+
166193
var seenEscapeBlock *hcl.Block
167194
for _, block := range content.Blocks {
168195
switch block.Type {
@@ -278,6 +305,9 @@ var moduleBlockSchema = &hcl.BodySchema{
278305
{
279306
Name: "providers",
280307
},
308+
{
309+
Name: "ignore_nested_deprecations",
310+
},
281311
},
282312
Blocks: []hcl.BlockHeaderSchema{
283313
{Type: "_"}, // meta-argument escaping block

internal/configs/named_values.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ type Variable struct {
4848
Nullable bool
4949
NullableSet bool
5050

51+
Deprecated string
52+
DeprecatedSet bool
53+
DeprecatedRange hcl.Range
54+
5155
DeclRange hcl.Range
5256
}
5357

@@ -186,6 +190,13 @@ func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagno
186190
v.Default = val
187191
}
188192

193+
if attr, exists := content.Attributes["deprecated"]; exists {
194+
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Deprecated)
195+
diags = append(diags, valDiags...)
196+
v.DeprecatedSet = true
197+
v.DeprecatedRange = attr.Range
198+
}
199+
189200
for _, block := range content.Blocks {
190201
switch block.Type {
191202

@@ -345,14 +356,17 @@ type Output struct {
345356
DependsOn []hcl.Traversal
346357
Sensitive bool
347358
Ephemeral bool
359+
Deprecated string
348360

349361
Preconditions []*CheckRule
350362

351363
DescriptionSet bool
352364
SensitiveSet bool
353365
EphemeralSet bool
366+
DeprecatedSet bool
354367

355-
DeclRange hcl.Range
368+
DeclRange hcl.Range
369+
DeprecatedRange hcl.Range
356370
}
357371

358372
func decodeOutputBlock(block *hcl.Block, override bool) (*Output, hcl.Diagnostics) {
@@ -402,6 +416,13 @@ func decodeOutputBlock(block *hcl.Block, override bool) (*Output, hcl.Diagnostic
402416
o.EphemeralSet = true
403417
}
404418

419+
if attr, exists := content.Attributes["deprecated"]; exists {
420+
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Deprecated)
421+
diags = append(diags, valDiags...)
422+
o.DeprecatedSet = true
423+
o.DeprecatedRange = attr.Range
424+
}
425+
405426
if attr, exists := content.Attributes["depends_on"]; exists {
406427
deps, depsDiags := DecodeDependsOn(attr)
407428
diags = append(diags, depsDiags...)
@@ -441,6 +462,7 @@ func (o *Output) Addr() addrs.OutputValue {
441462
type Local struct {
442463
Name string
443464
Expr hcl.Expression
465+
Body hcl.Body // for better diagnostics
444466

445467
DeclRange hcl.Range
446468
}
@@ -466,6 +488,7 @@ func decodeLocalsBlock(block *hcl.Block) ([]*Local, hcl.Diagnostics) {
466488
Name: name,
467489
Expr: attr.Expr,
468490
DeclRange: attr.Range,
491+
Body: block.Body,
469492
})
470493
}
471494
return locals, diags
@@ -499,6 +522,9 @@ var variableBlockSchema = &hcl.BodySchema{
499522
{
500523
Name: "nullable",
501524
},
525+
{
526+
Name: "deprecated",
527+
},
502528
},
503529
Blocks: []hcl.BlockHeaderSchema{
504530
{
@@ -525,6 +551,9 @@ var outputBlockSchema = &hcl.BodySchema{
525551
{
526552
Name: "ephemeral",
527553
},
554+
{
555+
Name: "deprecated",
556+
},
528557
},
529558
Blocks: []hcl.BlockHeaderSchema{
530559
{Type: "precondition"},

internal/configs/named_values_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,56 @@ func TestVariableInvalidDefault(t *testing.T) {
4848
}
4949
}
5050
}
51+
52+
func TestOutputDeprecation(t *testing.T) {
53+
src := `
54+
output "foo" {
55+
value = "bar"
56+
deprecated = "This output is deprecated"
57+
}
58+
`
59+
60+
hclF, diags := hclsyntax.ParseConfig([]byte(src), "test.tf", hcl.InitialPos)
61+
if diags.HasErrors() {
62+
t.Fatal(diags.Error())
63+
}
64+
65+
b, diags := parseConfigFile(hclF.Body, nil, false, false)
66+
if diags.HasErrors() {
67+
t.Fatalf("unexpected error: %q", diags)
68+
}
69+
if !b.Outputs[0].DeprecatedSet {
70+
t.Fatalf("expected output to be deprecated")
71+
}
72+
73+
if b.Outputs[0].Deprecated != "This output is deprecated" {
74+
t.Fatalf("expected output to have deprecation message")
75+
}
76+
}
77+
78+
func TestVariableDeprecation(t *testing.T) {
79+
src := `
80+
variable "foo" {
81+
type = string
82+
deprecated = "This variable is deprecated, use bar instead"
83+
}
84+
`
85+
86+
hclF, diags := hclsyntax.ParseConfig([]byte(src), "test.tf", hcl.InitialPos)
87+
if diags.HasErrors() {
88+
t.Fatal(diags.Error())
89+
}
90+
91+
b, diags := parseConfigFile(hclF.Body, nil, false, false)
92+
if diags.HasErrors() {
93+
t.Fatalf("unexpected error: %q", diags)
94+
}
95+
96+
if !b.Variables[0].DeprecatedSet {
97+
t.Fatalf("expected variable to be deprecated")
98+
}
99+
100+
if b.Variables[0].Deprecated != "This variable is deprecated, use bar instead" {
101+
t.Fatalf("expected variable to have deprecation message")
102+
}
103+
}

0 commit comments

Comments
 (0)