Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/v1.15/NEW FEATURES-20251205-171418.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: NEW FEATURES
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.
time: 2025-12-05T17:14:18.623477+01:00
custom:
Issue: "37795"
5 changes: 5 additions & 0 deletions internal/addrs/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ func (c AbsCheck) CheckRule(typ CheckRuleType, i int) CheckRule {
}
}

// ModuleInstance returns the module instance portion of the address.
func (c AbsCheck) ModuleInstance() ModuleInstance {
return c.Module
}

// ConfigCheckable returns the ConfigCheck address for this absolute reference.
func (c AbsCheck) ConfigCheckable() ConfigCheckable {
return ConfigCheck{
Expand Down
5 changes: 5 additions & 0 deletions internal/addrs/check_rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,8 @@ func (c CheckRuleType) Description() string {
return "Condition"
}
}

// ModuleInstance returns the module instance address containing this check rule.
func (c CheckRule) ModuleInstance() ModuleInstance {
return c.Container.ModuleInstance()
}
1 change: 1 addition & 0 deletions internal/addrs/checkable.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type Checkable interface {

CheckableKind() CheckableKind
String() string
ModuleInstance() ModuleInstance
}

var (
Expand Down
5 changes: 5 additions & 0 deletions internal/addrs/input_variable.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ func (v AbsInputVariableInstance) CheckRule(typ CheckRuleType, i int) CheckRule
}
}

// ModuleInstance returns the module instance portion of the address.
func (v AbsInputVariableInstance) ModuleInstance() ModuleInstance {
return v.Module
}

func (v AbsInputVariableInstance) ConfigCheckable() ConfigCheckable {
return ConfigInputVariable{
Module: v.Module.Module(),
Expand Down
5 changes: 5 additions & 0 deletions internal/addrs/output_value.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ func (m ModuleInstance) OutputValue(name string) AbsOutputValue {
}
}

// ModuleInstance returns the module instance portion of the address.
func (v AbsOutputValue) ModuleInstance() ModuleInstance {
return v.Module
}

func (v AbsOutputValue) CheckRule(t CheckRuleType, i int) CheckRule {
return CheckRule{
Container: v,
Expand Down
5 changes: 5 additions & 0 deletions internal/addrs/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,11 @@ func (m ModuleInstance) ResourceInstance(mode ResourceMode, typeName string, nam
}
}

// ModuleInstance returns the module instance portion of the address.
func (r AbsResourceInstance) ModuleInstance() ModuleInstance {
return r.Module
}

// ContainingResource returns the address of the resource that contains the
// receving resource instance. In other words, it discards the key portion
// of the address to produce an AbsResource value.
Expand Down
1 change: 1 addition & 0 deletions internal/command/jsonstate/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,7 @@ func SensitiveAsBool(val cty.Value) cty.Value {
func unmarkValueForMarshaling(v cty.Value) (unmarkedV cty.Value, sensitivePaths []cty.Path, err error) {
val, pvms := v.UnmarkDeepWithPaths()
sensitivePaths, otherMarks := marks.PathsWithMark(pvms, marks.Sensitive)
_, otherMarks = marks.PathsWithMark(otherMarks, marks.Deprecation)
if len(otherMarks) != 0 {
return cty.NilVal, nil, fmt.Errorf(
"%s: cannot serialize value marked as %#v for inclusion in a state snapshot (this is a bug in Terraform)",
Expand Down
3 changes: 3 additions & 0 deletions internal/configs/configschema/validate_traversal.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ func (b *Block) StaticValidateTraversal(traversal hcl.Traversal) tfdiags.Diagnos
// traversal alone. More precise detection of deprecated attributes
// would require adding metadata like marks to the cty value itself, to
// be caught during evaluation.
//
// For all other kinds of deprecations we have marks.Deprecation, but since
// we return an unknown value here, we can not attach marks to it.
if attrS.Deprecated {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Expand Down
30 changes: 30 additions & 0 deletions internal/configs/module_call.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"

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

DeclRange hcl.Range

IgnoreNestedDeprecations bool
}

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

if attr, exists := content.Attributes["ignore_nested_deprecations"]; exists {
// We only allow static boolean values for this argument.
val, evalDiags := attr.Expr.Value(&hcl.EvalContext{})
if len(evalDiags.Errs()) > 0 {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid value for ignore_nested_deprecations",
Detail: "The value for ignore_nested_deprecations must be a static boolean (true or false).",
Subject: attr.Expr.Range().Ptr(),
})
}

if val.Type() != cty.Bool {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid type for ignore_nested_deprecations",
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()),
Subject: attr.Expr.Range().Ptr(),
})
}

mc.IgnoreNestedDeprecations = val.True()
}

var seenEscapeBlock *hcl.Block
for _, block := range content.Blocks {
switch block.Type {
Expand Down Expand Up @@ -278,6 +305,9 @@ var moduleBlockSchema = &hcl.BodySchema{
{
Name: "providers",
},
{
Name: "ignore_nested_deprecations",
},
},
Blocks: []hcl.BlockHeaderSchema{
{Type: "_"}, // meta-argument escaping block
Expand Down
31 changes: 30 additions & 1 deletion internal/configs/named_values.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ type Variable struct {
Nullable bool
NullableSet bool

Deprecated string
DeprecatedSet bool
DeprecatedRange hcl.Range

DeclRange hcl.Range
}

Expand Down Expand Up @@ -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 {

Expand Down Expand Up @@ -345,14 +356,17 @@ type Output struct {
DependsOn []hcl.Traversal
Sensitive bool
Ephemeral bool
Deprecated string

Preconditions []*CheckRule

DescriptionSet bool
SensitiveSet bool
EphemeralSet bool
DeprecatedSet bool

DeclRange hcl.Range
DeclRange hcl.Range
DeprecatedRange hcl.Range
}

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

if attr, exists := content.Attributes["deprecated"]; exists {
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Deprecated)
diags = append(diags, valDiags...)
o.DeprecatedSet = true
o.DeprecatedRange = attr.Range
}

if attr, exists := content.Attributes["depends_on"]; exists {
deps, depsDiags := DecodeDependsOn(attr)
diags = append(diags, depsDiags...)
Expand Down Expand Up @@ -441,6 +462,7 @@ func (o *Output) Addr() addrs.OutputValue {
type Local struct {
Name string
Expr hcl.Expression
Body hcl.Body // for better diagnostics

DeclRange hcl.Range
}
Expand All @@ -466,6 +488,7 @@ func decodeLocalsBlock(block *hcl.Block) ([]*Local, hcl.Diagnostics) {
Name: name,
Expr: attr.Expr,
DeclRange: attr.Range,
Body: block.Body,
})
}
return locals, diags
Expand Down Expand Up @@ -499,6 +522,9 @@ var variableBlockSchema = &hcl.BodySchema{
{
Name: "nullable",
},
{
Name: "deprecated",
},
},
Blocks: []hcl.BlockHeaderSchema{
{
Expand All @@ -525,6 +551,9 @@ var outputBlockSchema = &hcl.BodySchema{
{
Name: "ephemeral",
},
{
Name: "deprecated",
},
},
Blocks: []hcl.BlockHeaderSchema{
{Type: "precondition"},
Expand Down
53 changes: 53 additions & 0 deletions internal/configs/named_values_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,56 @@ func TestVariableInvalidDefault(t *testing.T) {
}
}
}

func TestOutputDeprecation(t *testing.T) {
src := `
output "foo" {
value = "bar"
deprecated = "This output is deprecated"
}
`

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.Outputs[0].DeprecatedSet {
t.Fatalf("expected output to be deprecated")
}

if b.Outputs[0].Deprecated != "This output is deprecated" {
t.Fatalf("expected output to have deprecation message")
}
}

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")
}
}
96 changes: 96 additions & 0 deletions internal/deprecation/deprecation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package deprecation

import (
"sync"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)

// Deprecations keeps track of meta-information related to deprecation, e.g. which module calls
// suppress deprecation warnings.
type Deprecations struct {
// Must hold this lock when accessing all fields after this one.
mu sync.Mutex

suppressedModules addrs.Set[addrs.Module]
}

func NewDeprecations() *Deprecations {
return &Deprecations{
suppressedModules: addrs.MakeSet[addrs.Module](),
}
}

func (d *Deprecations) SuppressModuleCallDeprecation(addr addrs.Module) {
d.mu.Lock()
defer d.mu.Unlock()

d.suppressedModules.Add(addr)
}

func (d *Deprecations) Validate(value cty.Value, module addrs.Module, rng *hcl.Range) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
deprecationMarks := marks.GetDeprecationMarks(value)
if len(deprecationMarks) == 0 {
return value, diags
}

notDeprecatedValue := marks.RemoveDeprecationMarks(value)

// Check if we need to suppress deprecation warnings for this module call.
if d.IsModuleCallDeprecationSuppressed(module) {
return notDeprecatedValue, diags
}

for _, depMark := range deprecationMarks {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Deprecated value used",
Detail: depMark.Message,
Subject: rng,
})
}

return notDeprecatedValue, diags
}

func (d *Deprecations) ValidateAsConfig(value cty.Value, module addrs.Module) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
_, pvms := value.UnmarkDeepWithPaths()

if len(pvms) == 0 || d.IsModuleCallDeprecationSuppressed(module) {
return diags
}

for _, pvm := range pvms {
for m := range pvm.Marks {
if depMark, ok := m.(marks.DeprecationMark); ok {
diags = diags.Append(
tfdiags.AttributeValue(
tfdiags.Warning,
"Deprecated value used",
depMark.Message,
pvm.Path,
),
)
}
}
}
return diags
}

func (d *Deprecations) IsModuleCallDeprecationSuppressed(addr addrs.Module) bool {
for _, mod := range d.suppressedModules {
if addr.Equal(mod) || mod.TargetContains(addr) {
return true
}
}
return false
}
Loading
Loading