diff --git a/internal/command/jsonstate/state.go b/internal/command/jsonstate/state.go index a29e0ebba57f..ca98e98bae48 100644 --- a/internal/command/jsonstate/state.go +++ b/internal/command/jsonstate/state.go @@ -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)", diff --git a/internal/configs/named_values.go b/internal/configs/named_values.go index c9c92e45e074..35e371facd29 100644 --- a/internal/configs/named_values.go +++ b/internal/configs/named_values.go @@ -345,12 +345,14 @@ 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 } @@ -402,6 +404,12 @@ 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 + } + if attr, exists := content.Attributes["depends_on"]; exists { deps, depsDiags := DecodeDependsOn(attr) diags = append(diags, depsDiags...) @@ -525,6 +533,9 @@ var outputBlockSchema = &hcl.BodySchema{ { Name: "ephemeral", }, + { + Name: "deprecated", + }, }, Blocks: []hcl.BlockHeaderSchema{ {Type: "precondition"}, diff --git a/internal/configs/named_values_test.go b/internal/configs/named_values_test.go index 0626157c031e..5190986d5e4e 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 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") + } +} diff --git a/internal/lang/checks.go b/internal/lang/checks.go index 9ad8d91e04d2..857784e82700 100644 --- a/internal/lang/checks.go +++ b/internal/lang/checks.go @@ -93,6 +93,17 @@ You can correct this by removing references to ephemeral values, or by using the return "", diags } + if depMarks := marks.FilterDeprecationMarks(valMarks); len(depMarks) > 0 { + for _, depMark := range depMarks { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: depMark.Message, + Subject: expr.Range().Ptr(), + }) + } + } + // NOTE: We've discarded any other marks the string might have been carrying, // aside from the sensitive mark. diff --git a/internal/lang/marks/marks.go b/internal/lang/marks/marks.go index 7ba2752d2286..172b9b85197a 100644 --- a/internal/lang/marks/marks.go +++ b/internal/lang/marks/marks.go @@ -4,6 +4,7 @@ package marks import ( + "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" ) @@ -17,16 +18,30 @@ func (m valueMark) GoString() string { } // Has returns true if and only if the cty.Value has the given mark. -func Has(val cty.Value, mark valueMark) bool { - return val.HasMark(mark) +func Has(val cty.Value, mark interface{}) bool { + switch m := mark.(type) { + case valueMark: + return val.HasMark(m) + + // For value marks Has returns true if a mark of the type is present + case DeprecationMark: + for depMark := range val.Marks() { + if _, ok := depMark.(DeprecationMark); ok { + return true + } + } + return false + default: + panic("Unknown mark type") + } } // Contains returns true if the cty.Value or any any value within it contains // the given mark. -func Contains(val cty.Value, mark valueMark) bool { +func Contains(val cty.Value, mark interface{}) bool { ret := false cty.Walk(val, func(_ cty.Path, v cty.Value) (bool, error) { - if v.HasMark(mark) { + if Has(v, mark) { ret = true return false, nil } @@ -35,6 +50,33 @@ func Contains(val cty.Value, mark valueMark) bool { return ret } +func FilterDeprecationMarks(marks cty.ValueMarks) []DeprecationMark { + depMarks := []DeprecationMark{} + for mark := range marks { + if d, ok := mark.(DeprecationMark); ok { + depMarks = append(depMarks, d) + } + } + return depMarks +} + +func GetDeprecationMarks(val cty.Value) []DeprecationMark { + _, marks := val.UnmarkDeep() + return FilterDeprecationMarks(marks) + +} + +func RemoveDeprecationMarks(val cty.Value) cty.Value { + newVal, marks := val.Unmark() + for mark := range marks { + if _, ok := mark.(DeprecationMark); ok { + continue + } + newVal = newVal.Mark(mark) + } + return newVal +} + // Sensitive indicates that this value is marked as sensitive in the context of // Terraform. const Sensitive = valueMark("Sensitive") @@ -51,3 +93,22 @@ const Ephemeral = valueMark("Ephemeral") // another value's type. This is part of the implementation of the console-only // `type` function. const TypeType = valueMark("TypeType") + +type DeprecationMark struct { + Message string + Origin *hcl.Range +} + +func (d DeprecationMark) GoString() string { + return "marks.deprecation<" + d.Message + ">" +} + +// Empty deprecation mark for usage in marks.Has / Contains / etc +var Deprecation = NewDeprecation("", nil) + +func NewDeprecation(message string, origin *hcl.Range) DeprecationMark { + return DeprecationMark{ + Message: message, + Origin: origin, + } +} diff --git a/internal/lang/marks/marks_test.go b/internal/lang/marks/marks_test.go new file mode 100644 index 000000000000..1e5f2a71f7a6 --- /dev/null +++ b/internal/lang/marks/marks_test.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package marks + +import ( + "testing" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" +) + +func TestDeprecationMark(t *testing.T) { + deprecationWithoutRange := cty.StringVal("OldValue").Mark(NewDeprecation("This is outdated", nil)) + deprecationWithRange := cty.StringVal("OldValue").Mark(NewDeprecation("This is outdated", &hcl.Range{Filename: "example.tf", Start: hcl.Pos{Line: 1, Column: 1}, End: hcl.Pos{Line: 1, Column: 10}})) + + composite := cty.ObjectVal(map[string]cty.Value{ + "foo": deprecationWithRange, + "bar": deprecationWithoutRange, + "baz": cty.StringVal("Not deprecated"), + }) + + if !deprecationWithRange.IsMarked() { + t.Errorf("Expected deprecationWithRange to be marked") + } + if !deprecationWithoutRange.IsMarked() { + t.Errorf("Expected deprecationWithoutRange to be marked") + } + if composite.IsMarked() { + t.Errorf("Expected composite to be marked") + } + + if !Has(deprecationWithRange, Deprecation) { + t.Errorf("Expected deprecationWithRange to be marked with Deprecation") + } + if !Has(deprecationWithoutRange, Deprecation) { + t.Errorf("Expected deprecationWithoutRange to be marked with Deprecation") + } + if Has(composite, Deprecation) { + t.Errorf("Expected composite to be marked with Deprecation") + } + + if !Contains(deprecationWithRange, Deprecation) { + t.Errorf("Expected deprecationWithRange to be contain Deprecation Mark") + } + if !Contains(deprecationWithoutRange, Deprecation) { + t.Errorf("Expected deprecationWithoutRange to be contain Deprecation Mark") + } + if !Contains(composite, Deprecation) { + t.Errorf("Expected composite to be contain Deprecation Mark") + } +} diff --git a/internal/lang/marks/paths.go b/internal/lang/marks/paths.go index 0bb81ae43b16..4cdcde85ed92 100644 --- a/internal/lang/marks/paths.go +++ b/internal/lang/marks/paths.go @@ -4,6 +4,7 @@ package marks import ( + "fmt" "sort" "strings" @@ -28,16 +29,36 @@ func PathsWithMark(pvms []cty.PathValueMarks, wantMark any) (withWanted []cty.Pa } for _, pvm := range pvms { - if _, ok := pvm.Marks[wantMark]; ok { + pathHasMark := false + pathHasOtherMarks := false + for mark := range pvm.Marks { + switch wantMark.(type) { + case valueMark, string: + if mark == wantMark { + pathHasMark = true + } else { + pathHasOtherMarks = true + } + + // For data marks we check if a mark of the type exists + case DeprecationMark: + if _, ok := mark.(DeprecationMark); ok { + pathHasMark = true + } else { + pathHasOtherMarks = true + } + + default: + panic(fmt.Sprintf("unexpected mark type %T", wantMark)) + } + } + + if pathHasMark { withWanted = append(withWanted, pvm.Path) } - for mark := range pvm.Marks { - if mark != wantMark { - withOthers = append(withOthers, pvm) - // only add a path with unwanted marks a single time - break - } + if pathHasOtherMarks { + withOthers = append(withOthers, pvm) } } @@ -57,7 +78,21 @@ func RemoveAll(pvms []cty.PathValueMarks, remove any) []cty.PathValueMarks { var res []cty.PathValueMarks for _, pvm := range pvms { - delete(pvm.Marks, remove) + switch remove.(type) { + case valueMark, string: + delete(pvm.Marks, remove) + + case DeprecationMark: + // We want to delete all marks of this type + for mark := range pvm.Marks { + if _, ok := mark.(DeprecationMark); ok { + delete(pvm.Marks, mark) + } + } + + default: + panic(fmt.Sprintf("unexpected mark type %T", remove)) + } if len(pvm.Marks) > 0 { res = append(res, pvm) } diff --git a/internal/lang/marks/paths_test.go b/internal/lang/marks/paths_test.go index f6adf437e388..e441c398e09d 100644 --- a/internal/lang/marks/paths_test.go +++ b/internal/lang/marks/paths_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty-debug/ctydebug" "github.com/zclconf/go-cty/cty" ) @@ -30,12 +31,25 @@ func TestPathsWithMark(t *testing.T) { Path: cty.GetAttrPath("neither"), Marks: cty.NewValueMarks("x", "y"), }, + { + Path: cty.GetAttrPath("deprecated"), + Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil)), + }, + { + Path: cty.GetAttrPath("multipleDeprecations"), + Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil), NewDeprecation("this is also deprecated", nil)), + }, + { + Path: cty.GetAttrPath("multipleDeprecationsAndSensitive"), + Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil), NewDeprecation("this is also deprecated", nil), "sensitive"), + }, } gotPaths, gotOthers := PathsWithMark(input, "sensitive") wantPaths := []cty.Path{ cty.GetAttrPath("sensitive"), cty.GetAttrPath("both"), + cty.GetAttrPath("multipleDeprecationsAndSensitive"), } wantOthers := []cty.PathValueMarks{ { @@ -56,6 +70,18 @@ func TestPathsWithMark(t *testing.T) { Path: cty.GetAttrPath("neither"), Marks: cty.NewValueMarks("x", "y"), }, + { + Path: cty.GetAttrPath("deprecated"), + Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil)), + }, + { + Path: cty.GetAttrPath("multipleDeprecations"), + Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil), NewDeprecation("this is also deprecated", nil)), + }, + { + Path: cty.GetAttrPath("multipleDeprecationsAndSensitive"), + Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil), NewDeprecation("this is also deprecated", nil), "sensitive"), + }, } if diff := cmp.Diff(wantPaths, gotPaths, ctydebug.CmpOptions); diff != "" { @@ -64,9 +90,46 @@ func TestPathsWithMark(t *testing.T) { if diff := cmp.Diff(wantOthers, gotOthers, ctydebug.CmpOptions); diff != "" { t.Errorf("wrong set of entries with other marks\n%s", diff) } + + gotPaths, gotOthers = PathsWithMark(input, Deprecation) + + wantPaths = []cty.Path{ + cty.GetAttrPath("deprecated"), + cty.GetAttrPath("multipleDeprecations"), + cty.GetAttrPath("multipleDeprecationsAndSensitive"), + } + wantOthers = []cty.PathValueMarks{ + { + Path: cty.GetAttrPath("sensitive"), + Marks: cty.NewValueMarks("sensitive"), + }, + { + Path: cty.GetAttrPath("other"), + Marks: cty.NewValueMarks("other"), + }, + { + Path: cty.GetAttrPath("both"), + Marks: cty.NewValueMarks("sensitive", "other"), + }, + { + Path: cty.GetAttrPath("neither"), + Marks: cty.NewValueMarks("x", "y"), + }, + { + Path: cty.GetAttrPath("multipleDeprecationsAndSensitive"), + Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil), NewDeprecation("this is also deprecated", nil), "sensitive"), + }, + } + + if diff := cmp.Diff(wantPaths, gotPaths, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong matched deprecation paths\n%s", diff) + } + if diff := cmp.Diff(wantOthers, gotOthers, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong set of entries with other than deprecation marks\n%s", diff) + } } -func TestRemoveAll(t *testing.T) { +func TestRemoveAll_valueMarks(t *testing.T) { input := []cty.PathValueMarks{ { Path: cty.GetAttrPath("sensitive"), @@ -100,6 +163,36 @@ func TestRemoveAll(t *testing.T) { } } +func TestRemoveAll_dataMarks(t *testing.T) { + input := []cty.PathValueMarks{ + { + Path: cty.GetAttrPath("deprecated"), + Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil)), + }, + { + Path: cty.GetAttrPath("multipleDeprecations"), + Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil), NewDeprecation("this is also deprecated", nil)), + }, + { + Path: cty.GetAttrPath("multipleDeprecationsAndSensitive"), + Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil), NewDeprecation("this is also deprecated", nil), "sensitive"), + }, + } + + want := []cty.PathValueMarks{ + { + Path: cty.GetAttrPath("multipleDeprecationsAndSensitive"), + Marks: cty.NewValueMarks("sensitive"), + }, + } + + got := RemoveAll(input, Deprecation) + + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong matched paths\n%s", diff) + } +} + func TestMarkPaths(t *testing.T) { value := cty.ObjectVal(map[string]cty.Value{ "s": cty.StringVal(".s"), @@ -150,6 +243,38 @@ func TestMarkPaths(t *testing.T) { if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { t.Errorf("wrong result\n%s", diff) } + + deprecatedPaths := []cty.Path{ + cty.GetAttrPath("s"), + cty.GetAttrPath("l").IndexInt(1), + cty.GetAttrPath("m").IndexString("a"), + cty.GetAttrPath("o").GetAttr("b"), + cty.GetAttrPath("t").IndexInt(0), + } + deprecationMark := NewDeprecation("this is deprecated", nil) + got = MarkPaths(value, deprecationMark, deprecatedPaths) + want = cty.ObjectVal(map[string]cty.Value{ + "s": cty.StringVal(".s").Mark(deprecationMark), + "l": cty.ListVal([]cty.Value{ + cty.StringVal(".l[0]"), + cty.StringVal(".l[1]").Mark(deprecationMark), + }), + "m": cty.MapVal(map[string]cty.Value{ + "a": cty.StringVal(`.m["a"]`).Mark(deprecationMark), + "b": cty.StringVal(`.m["b"]`), + }), + "o": cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal(".o.a"), + "b": cty.StringVal(".o.b").Mark(deprecationMark), + }), + "t": cty.TupleVal([]cty.Value{ + cty.StringVal(`.t[0]`).Mark(deprecationMark), + cty.StringVal(`.t[1]`), + }), + }) + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result\n%s", diff) + } } func TestMarksEqual(t *testing.T) { @@ -239,6 +364,33 @@ func TestMarksEqual(t *testing.T) { }, false, }, + { + []cty.PathValueMarks{ + {Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("same message", nil))}, + }, + []cty.PathValueMarks{ + {Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("same message", nil))}, + }, + true, + }, + { + []cty.PathValueMarks{ + {Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("different", nil))}, + }, + []cty.PathValueMarks{ + {Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("message", nil))}, + }, + false, + }, + { + []cty.PathValueMarks{ + {Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("same message", &hcl.Range{Filename: "test.tf", Start: hcl.Pos{Line: 1, Column: 1}, End: hcl.Pos{Line: 1, Column: 1}}))}, + }, + []cty.PathValueMarks{ + {Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("same message", &hcl.Range{Filename: "otherFile.tf", Start: hcl.Pos{Line: 1, Column: 1}, End: hcl.Pos{Line: 1, Column: 1}}))}, + }, + false, // TODO: Should this really be different? + }, } { t.Run(fmt.Sprint(i), func(t *testing.T) { if MarksEqual(tc.a, tc.b) != tc.equal { diff --git a/internal/plans/changes.go b/internal/plans/changes.go index ca362054b79d..29b5b80d3921 100644 --- a/internal/plans/changes.go +++ b/internal/plans/changes.go @@ -743,6 +743,7 @@ type Change struct { func (c *Change) Encode(schema *providers.Schema) (*ChangeSrc, error) { // We can't serialize value marks directly so we'll need to extract the // sensitive marks and store them in a separate field. + // We ignore Deprecation marks. // // We don't accept any other marks here. The caller should have dealt // with those somehow and replaced them with unmarked placeholders before @@ -751,6 +752,10 @@ func (c *Change) Encode(schema *providers.Schema) (*ChangeSrc, error) { unmarkedAfter, marksesAfter := c.After.UnmarkDeepWithPaths() sensitiveAttrsBefore, unsupportedMarksesBefore := marks.PathsWithMark(marksesBefore, marks.Sensitive) sensitiveAttrsAfter, unsupportedMarksesAfter := marks.PathsWithMark(marksesAfter, marks.Sensitive) + + _, unsupportedMarksesBefore = marks.PathsWithMark(unsupportedMarksesBefore, marks.Deprecation) + _, unsupportedMarksesAfter = marks.PathsWithMark(unsupportedMarksesAfter, marks.Deprecation) + if len(unsupportedMarksesBefore) != 0 { return nil, fmt.Errorf( "prior value %s: can't serialize value marked with %#v (this is a bug in Terraform)", diff --git a/internal/stacks/stackplan/planned_change.go b/internal/stacks/stackplan/planned_change.go index 7cb8888ca95e..fb458d685d9c 100644 --- a/internal/stacks/stackplan/planned_change.go +++ b/internal/stacks/stackplan/planned_change.go @@ -499,6 +499,7 @@ func (pc *PlannedChangeResourceInstancePlanned) ChangeDescription() (*stacks.Pla func DynamicValueToTerraform1(val cty.Value, ty cty.Type) (*stacks.DynamicValue, error) { unmarkedVal, markPaths := val.UnmarkDeepWithPaths() sensitivePaths, withOtherMarks := marks.PathsWithMark(markPaths, marks.Sensitive) + _, withOtherMarks = marks.PathsWithMark(withOtherMarks, marks.Sensitive) if len(withOtherMarks) != 0 { return nil, withOtherMarks[0].Path.NewErrorf( "can't serialize value marked with %#v (this is a bug in Terraform)", diff --git a/internal/states/instance_object.go b/internal/states/instance_object.go index 82a4ab1e2540..f9ce435b6872 100644 --- a/internal/states/instance_object.go +++ b/internal/states/instance_object.go @@ -205,6 +205,7 @@ func (o *ResourceInstanceObject) AsTainted() *ResourceInstanceObject { func unmarkValueForStorage(v cty.Value) (unmarkedV cty.Value, sensitivePaths []cty.Path, err error) { val, pvms := v.UnmarkDeepWithPaths() sensitivePaths, withOtherMarks := marks.PathsWithMark(pvms, marks.Sensitive) + _, withOtherMarks = marks.PathsWithMark(withOtherMarks, marks.Deprecation) if len(withOtherMarks) != 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)", diff --git a/internal/terraform/context_plan2_test.go b/internal/terraform/context_plan2_test.go index 5d20742144ca..7fda28e83ad6 100644 --- a/internal/terraform/context_plan2_test.go +++ b/internal/terraform/context_plan2_test.go @@ -7108,3 +7108,461 @@ func TestContext2Plan_resourceNamedList(t *testing.T) { }) } } + +func TestContext2Plan_deprecated_output(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "mod/main.tf": ` +output "old" { + deprecated = "Please stop using this" + value = "old" +} +output "old-and-unused" { + deprecated = "This should not show up in the errors, we are not using it" + value = "old" +} +output "new" { + value = "foo" +} +`, + "mod2/main.tf": ` +variable "input" { + type = string +} +`, + "main.tf": ` +module "mod" { + source = "./mod" +} +resource "test_resource" "test" { + attr = module.mod.old # WARNING +} +resource "test_resource" "test2" { + attr = module.mod.new # OK +} +resource "test_resource" "test3" { + attr = module.mod.old # WARNING +} +output "test_output" { + value = module.mod.old # WARNING +} +output "test_output_conditional" { + value = false ? module.mod.old : module.mod.new # WARNING (detectable during plan and not validate) +} +module "mod2" { + source = "./mod2" + input = module.mod.old # OK +} +`, + }) + + 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), + }, + }) + + _, diags := ctx.Plan(m, nil, SimplePlanOpts(plans.NormalMode, InputValues{})) + + tfdiags.AssertDiagnosticsMatch(t, diags, tfdiags.Diagnostics{}.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 6, Column: 12, Byte: 84}, + End: hcl.Pos{Line: 6, Column: 26, Byte: 98}, + }, + }, + ).Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 12, Column: 12, Byte: 225}, + End: hcl.Pos{Line: 12, Column: 26, Byte: 239}, + }, + }, + ).Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 15, Column: 10, Byte: 284}, + End: hcl.Pos{Line: 15, Column: 24, Byte: 298}, + }, + }, + ).Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 18, Column: 10, Byte: 355}, + End: hcl.Pos{Line: 18, Column: 49, Byte: 394}, + }, + }, + )) +} + +func TestContext2Plan_deprecated_output_expansion(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "mod/nested/main.tf": ` +output "old" { + deprecated = "mod/nested: Please stop using this" + value = "old" +} +`, + "mod/main.tf": ` +output "old" { + deprecated = "mod: Please stop using this" + value = "old" +} +module "modnested" { + source = "./nested" +} + +output "new" { + deprecated = "mod: The dependency is deprecated, please stop using this" + value = module.modnested.old +} +`, + "mod2/main.tf": ` +output "old" { + deprecated = "mod2: Please stop using this" + value = "old" +} +output "new" { + value = "new" +} +`, + "mod3/main.tf": ` +output "old" { + deprecated = "mod2: Please stop using this" + value = "old" +} +output "new" { + value = "new" +} +`, + "main.tf": ` +module "mod" { + source = "./mod" +} + +module "mod2" { + count = 2 + source = "./mod2" +} + +module "mod3" { + count = 2 + source = "./mod3" +} + +resource "test_resource" "foo" { + attr = module.mod.old # WARNING +} + +resource "test_resource" "bar" { + attr = module.mod2[0].old # WARNING +} + +resource "test_resource" "baz" { + attr = module.mod.new # WARNING +} + +output "test_output_no_warning" { + value = module.mod3[0].new # OK +} +`, + }) + + 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), + }, + }) + + _, diags := ctx.Plan(m, nil, SimplePlanOpts(plans.NormalMode, InputValues{})) + var expectedDiags tfdiags.Diagnostics + expectedDiags = expectedDiags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "mod: Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 17, Column: 10, Byte: 180}, + End: hcl.Pos{Line: 17, Column: 24, Byte: 194}, + }, + }, + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "mod2: Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 21, Column: 10, Byte: 250}, + End: hcl.Pos{Line: 21, Column: 28, Byte: 268}, + }, + }, + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "mod: The dependency is deprecated, please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 25, Column: 10, Byte: 324}, + End: hcl.Pos{Line: 25, Column: 24, Byte: 338}, + }, + }, + ) + + tfdiags.AssertDiagnosticsMatch(t, diags, expectedDiags) +} + +func TestContext2Plan_deprecated_output_expansion_with_splat(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "mod/main.tf": ` +output "old" { + deprecated = "Please stop using this" + value = "old" +} +`, + "main.tf": ` +module "mod" { + count = 2 + source = "./mod" +} +output "test_output2" { + value = module.mod[*].old # WARNING +} +`, + }) + + 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), + }, + }) + + _, diags := ctx.Plan(m, nil, SimplePlanOpts(plans.NormalMode, InputValues{})) + + tfdiags.AssertDiagnosticsMatch(t, diags, tfdiags.Diagnostics{}.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 7, Column: 10, Byte: 80}, + End: hcl.Pos{Line: 7, Column: 27, Byte: 97}, + }, + }, + )) +} + +func TestContext2Plan_deprecated_output_used_in_check_block(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "mod/main.tf": ` +output "old" { + deprecated = "Please stop using this" + value = "old" +} +output "old-and-unused" { + deprecated = "This should not show up in the errors, we are not using it" + value = "old" +} +output "new" { + value = "foo" +} +`, + "main.tf": ` +module "mod" { + source = "./mod" +} +check "deprecated_check" { + assert { + condition = !strcontains(module.mod.old, "hello-world") + error_message = "Neither condition nor error_message should contain ${module.mod.old} deprecated value" + } +} +`, + }) + + 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), + }, + }) + + _, diags := ctx.Plan(m, nil, SimplePlanOpts(plans.NormalMode, InputValues{})) + tfdiags.AssertDiagnosticsMatch(t, diags, tfdiags.Diagnostics{}.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 7, Column: 21, Byte: 97}, + End: hcl.Pos{Line: 7, Column: 64, Byte: 140}, + }, + }, + ).Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 8, Column: 21, Byte: 161}, + End: hcl.Pos{Line: 8, Column: 108, Byte: 248}, + }, + }, + )) +} + +func TestContext2Plan_deprecated_output_in_lifecycle_conditions(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "mod/main.tf": ` +output "old" { + deprecated = "Please stop using this" + value = "old" +} +`, + "main.tf": ` +module "mod" { + source = "./mod" +} + +locals { + a = "a" + b = "b" +} + +resource "test_resource" "test" { + attr = "not-the-problem" + + lifecycle { + precondition { + condition = module.mod.old == "old" + error_message = "This is okay." + } + + postcondition { + condition = local.a != local.b + error_message = "This should error with deprecated usage: ${module.mod.old}" + } + } +} +`, + }) + + 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), + }, + }) + + _, diags := ctx.Plan(m, nil, SimplePlanOpts(plans.NormalMode, InputValues{})) + + tfdiags.AssertDiagnosticsMatch(t, diags, tfdiags.Diagnostics{}.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 16, Column: 29, Byte: 207}, + End: hcl.Pos{Line: 16, Column: 52, Byte: 230}, + }, + }, + ).Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 22, Column: 29, Byte: 389}, + End: hcl.Pos{Line: 22, Column: 89, Byte: 449}, + }, + }, + )) +} diff --git a/internal/terraform/context_validate_test.go b/internal/terraform/context_validate_test.go index 44083b467e93..b6313d436b7b 100644 --- a/internal/terraform/context_validate_test.go +++ b/internal/terraform/context_validate_test.go @@ -8,6 +8,7 @@ import ( "fmt" "maps" "path/filepath" + "slices" "strings" "testing" @@ -3846,3 +3847,357 @@ func TestContext2Validate_noListValidated(t *testing.T) { }) } } + +func TestContext2Validate_deprecated_output_used_in_for_each(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "mod/main.tf": ` +output "old" { + deprecated = "Please stop using this" + value = toset(["old"]) +} +`, + "main.tf": ` +module "mod" { + source = "./mod" +} +resource "test_resource" "test" { + for_each = module.mod.old # WARNING + attr = "not-deprecated" +} +`, + }) + + 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), + }, + }) + + diags := ctx.Validate(m, &ValidateOpts{}) + subjects := []string{} + + // tfdiags.AssertDiagnosticsMatch didn't work + for _, d := range diags { + if d.Description().Summary != "Deprecated value used as for_each argument" { + t.Errorf("unexpected diagnostic: %s", d.Description().Summary) + } + + if d.Description().Detail != "Please stop using this" { + t.Errorf("unexpected diagnostic detail: %s", d.Description().Detail) + } + + subjects = append(subjects, fmt.Sprintf("%d:%d", d.Source().Subject.Start.Line, d.Source().Subject.Start.Column)) + } + + slices.Sort(subjects) + expectedSubjects := []string{ + "6:16", + } + slices.Sort(expectedSubjects) + if diff := cmp.Diff(subjects, expectedSubjects); diff != "" { + t.Errorf("unexpected diagnostic subjects: %s", diff) + } +} + +func TestContext2Validate_deprecated_output_used_in_count(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "mod/main.tf": ` +output "old" { + deprecated = "Please stop using this" + value = 42 +} +`, + "main.tf": ` +module "mod" { + source = "./mod" +} +resource "test_resource" "test" { + count = module.mod.old # WARNING + attr = "not-deprecated" +} +`, + }) + + 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), + }, + }) + + diags := ctx.Validate(m, &ValidateOpts{}) + subjects := []string{} + + // tfdiags.AssertDiagnosticsMatch didn't work + for _, d := range diags { + if d.Description().Summary != "Deprecated value used as count argument" { + t.Errorf("unexpected diagnostic: %s", d.Description().Summary) + } + + if d.Description().Detail != "Please stop using this" { + t.Errorf("unexpected diagnostic detail: %s", d.Description().Detail) + } + + subjects = append(subjects, fmt.Sprintf("%d:%d", d.Source().Subject.Start.Line, d.Source().Subject.Start.Column)) + } + + slices.Sort(subjects) + expectedSubjects := []string{ + "6:16", + } + slices.Sort(expectedSubjects) + if diff := cmp.Diff(subjects, expectedSubjects); diff != "" { + t.Errorf("unexpected diagnostic subjects: %s", diff) + } +} + +func TestContext2Validate_deprecated_output_used_in_resource_config(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "mod/main.tf": ` +output "old" { + deprecated = "Please stop using this" + value = "old" +} +output "old-and-unused" { + deprecated = "This should not show up in the errors, we are not using it" + value = "old" +} +output "new" { + value = "foo" +} +`, + "mod2/main.tf": ` +variable "input" { + type = string +} +`, + "main.tf": ` +module "mod" { + source = "./mod" +} +resource "test_resource" "test" { + attr = module.mod.old # WARNING +} +resource "test_resource" "test2" { + attr = module.mod.new # OK +} +resource "test_resource" "test3" { + attr = module.mod.old # WARNING +} +output "test_output" { + value = module.mod.old # WARNING +} +output "test_output_conditional" { + value = false ? module.mod.old : module.mod.new # Not detectable during validate +} +module "mod2" { + source = "./mod2" + input = module.mod.old # WARNING +} +`, + }) + + 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), + }, + }) + + diags := ctx.Validate(m, &ValidateOpts{}) + + tfdiags.AssertDiagnosticsMatch(t, diags, tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 6, Column: 12, Byte: 84}, + End: hcl.Pos{Line: 6, Column: 26, Byte: 98}, + }, + }).Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 12, Column: 12, Byte: 225}, + End: hcl.Pos{Line: 12, Column: 26, Byte: 239}, + }, + }).Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 15, Column: 10, Byte: 284}, + End: hcl.Pos{Line: 15, Column: 24, Byte: 298}, + }, + }).Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 18, Column: 10, Byte: 355}, + End: hcl.Pos{Line: 18, Column: 49, Byte: 394}, + }, + })) +} + +func TestContext2Validate_deprecated_output_only_triggers_once(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "nested/main.tf": ` +output "old" { + deprecated = "Please stop using this" + value = "old" +} +`, + "mod/main.tf": ` +module "nested" { + source = "../nested" +} + +# This is an acceptable use of a deprecated value, so no warning +output "redeprecated" { + deprecated = "This should not be in use, dependency is deprecated" + value = module.nested.old +} +`, + + "mod2/main.tf": ` +module "nested" { + source = "../nested" +} + +# This is an unacceptable use of a deprecated value, so warning +# but the value of this is not deprecated, we want the warning to exit where it is used +# not multiple times for the same value +output "undeprecated_use_of_deprecated_value" { + value = module.nested.old +} +`, + "main.tf": ` +module "mod" { + source = "./mod" +} +module "mod2" { + source = "./mod2" +} +resource "test_resource" "test" { + attr = module.mod.redeprecated # WARNING +} +resource "test_resource" "test2" { + attr = module.mod2.undeprecated_use_of_deprecated_value # OK - error was already thrown +} +output "test_output_deprecated_use" { + value = module.mod.redeprecated # WARNING +} +output "test_output_deprecated_use_with_deprecation" { + deprecated = "This is displayed in the UI, but does not produce an additional warning" + value = module.mod.redeprecated # OK +} +`, + }) + + 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), + }, + }) + + diags := ctx.Validate(m, &ValidateOpts{}) + + tfdiags.AssertDiagnosticsMatch( + t, + diags, + tfdiags.Diagnostics{}.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "This should not be in use, dependency is deprecated", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 9, Column: 12, Byte: 124}, + End: hcl.Pos{Line: 9, Column: 35, Byte: 147}, + }, + }, + ).Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "This should not be in use, dependency is deprecated", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 15, Column: 10, Byte: 336}, + End: hcl.Pos{Line: 15, Column: 33, Byte: 359}, + }, + }, + ).Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "mod2", "main.tf"), + Start: hcl.Pos{Line: 10, Column: 11, Byte: 295}, + End: hcl.Pos{Line: 10, Column: 28, Byte: 312}, + }, + }, + ), + ) +} diff --git a/internal/terraform/eval_conditions.go b/internal/terraform/eval_conditions.go index 6a221339e439..f9d4dc9e3c05 100644 --- a/internal/terraform/eval_conditions.go +++ b/internal/terraform/eval_conditions.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/lang/langrefs" + "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -159,6 +160,16 @@ func evalCheckRule(addr addrs.CheckRule, rule *configs.CheckRule, ctx EvalContex }) return checkResult{Status: checks.StatusError}, diags } + if depMarks := marks.GetDeprecationMarks(resultVal); len(depMarks) > 0 { + for _, depMark := range depMarks { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: depMark.Message, + Subject: rule.Condition.Range().Ptr(), + }) + } + } var err error resultVal, err = convert.Convert(resultVal, cty.Bool) if err != nil { diff --git a/internal/terraform/eval_count.go b/internal/terraform/eval_count.go index 0c2827a2f06f..a76ed1dbc572 100644 --- a/internal/terraform/eval_count.go +++ b/internal/terraform/eval_count.go @@ -102,6 +102,17 @@ func evaluateCountExpressionValue(expr hcl.Expression, ctx EvalContext) (cty.Val }) } + if marks.Has(countVal, marks.Deprecation) { + for _, depmark := range marks.GetDeprecationMarks(countVal) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used as count argument", + Detail: depmark.Message, + Subject: expr.Range().Ptr(), + }) + } + } + // Sensitive values are allowed in count but not for_each. This is a // somewhat-dubious decision because the number of instances planned // will disclose exactly what the value was, but in practice it's rare diff --git a/internal/terraform/eval_for_each.go b/internal/terraform/eval_for_each.go index 0abac36dda13..c171180230b8 100644 --- a/internal/terraform/eval_for_each.go +++ b/internal/terraform/eval_for_each.go @@ -326,6 +326,20 @@ func (ev *forEachEvaluator) validateResourceOrActionForEach(forEachVal cty.Value Extra: diagnosticCausedBySensitive(true), }) } + if marks.Has(forEachVal, marks.Deprecation) { + deprecationMarks := marks.GetDeprecationMarks(forEachVal) + for _, depMark := range deprecationMarks { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used as for_each argument", + Detail: depMark.Message, + Subject: ev.expr.Range().Ptr(), + Expression: ev.expr, + EvalContext: ev.hclCtx, + }) + } + + } diags = diags.Append(ev.ensureNotEphemeral(forEachVal)) diff --git a/internal/terraform/evaluate.go b/internal/terraform/evaluate.go index 343be4034a11..09b23213398c 100644 --- a/internal/terraform/evaluate.go +++ b/internal/terraform/evaluate.go @@ -411,9 +411,19 @@ func (d *evaluationStateData) GetModule(addr addrs.ModuleCall, rng tfdiags.Sourc // structural type of similar kind, so that it can be considered as // valid during both the validate and plan walks. if d.Operation == walkValidate { + // In case of non-expanded module calls we return a known object with unknonwn values + // In case of an expanded module call we return unknown list/map + // This means deprecation can only for non-expanded modules be detected during validate + // since we don't want false positives. The plan walk will give definitive warnings. atys := make(map[string]cty.Type, len(outputConfigs)) - for name := range outputConfigs { + as := make(map[string]cty.Value, len(outputConfigs)) + for name, c := range outputConfigs { atys[name] = cty.DynamicPseudoType // output values are dynamically-typed + val := cty.UnknownVal(cty.DynamicPseudoType) + if c.DeprecatedSet { + val = val.Mark(marks.NewDeprecation(c.Deprecated, nil)) + } + as[name] = val } instTy := cty.Object(atys) @@ -423,7 +433,8 @@ func (d *evaluationStateData) GetModule(addr addrs.ModuleCall, rng tfdiags.Sourc case callConfig.ForEach != nil: return cty.UnknownVal(cty.Map(instTy)), diags default: - return cty.UnknownVal(instTy), diags + val := cty.ObjectVal(as) + return val, diags } } @@ -469,6 +480,24 @@ func (d *evaluationStateData) GetModule(addr addrs.ModuleCall, rng tfdiags.Sourc if cfg.Sensitive { outputVal = outputVal.Mark(marks.Sensitive) } + + outputValIsDeprecated := marks.Has(outputVal, marks.Deprecation) + if cfg.DeprecatedSet && outputValIsDeprecated { + outputVal = marks.RemoveDeprecationMarks(outputVal) + outputVal = outputVal.Mark(marks.NewDeprecation(cfg.Deprecated, nil)) + } else if cfg.DeprecatedSet && !outputValIsDeprecated { + outputVal = outputVal.Mark(marks.NewDeprecation(cfg.Deprecated, nil)) + } else if !cfg.DeprecatedSet && outputValIsDeprecated { + for _, depMark := range marks.GetDeprecationMarks(outputVal) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated Value used in output", + Detail: depMark.Message, + Subject: rng.ToHCL().Ptr(), + }) + } + outputVal = marks.RemoveDeprecationMarks(outputVal) + } attrs[name] = outputVal } @@ -1128,6 +1157,9 @@ func (d *evaluationStateData) GetOutput(addr addrs.OutputValue, rng tfdiags.Sour if config.Ephemeral { value = value.Mark(marks.Ephemeral) } + if config.DeprecatedSet { + val = val.Mark(marks.NewDeprecation(config.Deprecated, config.DeclRange.Ptr())) + } return value, diags } diff --git a/internal/terraform/node_output.go b/internal/terraform/node_output.go index 3e505a28fb79..f543bef73386 100644 --- a/internal/terraform/node_output.go +++ b/internal/terraform/node_output.go @@ -518,6 +518,21 @@ If you do intend to export this data, annotate the output value as sensitive by return diags } + if !n.Config.DeprecatedSet && marks.Contains(val, marks.Deprecation) { + for _, depMarks := range marks.GetDeprecationMarks(val) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: depMarks.Message, + Subject: n.Config.Expr.Range().Ptr(), + }) + } + } + + if n.Config.DeprecatedSet { + val = val.Mark(marks.NewDeprecation(n.Config.Deprecated, &n.Config.DeclRange)) + } + n.setValue(ctx.NamedValues(), state, changes, ctx.Deferrals(), val) // If we were able to evaluate a new value, we can update that in the diff --git a/internal/terraform/node_resource_abstract_instance.go b/internal/terraform/node_resource_abstract_instance.go index c5569b5491df..f1bc3d3407e8 100644 --- a/internal/terraform/node_resource_abstract_instance.go +++ b/internal/terraform/node_resource_abstract_instance.go @@ -866,6 +866,7 @@ func (n *NodeAbstractResourceInstance) plan( diags = diags.Append( validateResourceForbiddenEphemeralValues(ctx, origConfigVal, schema.Body).InConfigBody(n.Config.Config, n.Addr.String()), ) + diags = diags.Append(validateConfigUsingDeprecatedValues(origConfigVal).InConfigBody(n.Config.Config, n.Addr.String())) if diags.HasErrors() { return nil, nil, deferred, keyData, diags } diff --git a/internal/terraform/node_resource_validate.go b/internal/terraform/node_resource_validate.go index dc12110da8ad..ac4f8e66013d 100644 --- a/internal/terraform/node_resource_validate.go +++ b/internal/terraform/node_resource_validate.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/terraform/internal/lang/ephemeral" "github.com/hashicorp/terraform/internal/lang/format" "github.com/hashicorp/terraform/internal/lang/langrefs" + "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/provisioners" "github.com/hashicorp/terraform/internal/tfdiags" @@ -104,7 +105,7 @@ func (n *NodeValidatableResource) validateProvisioner(ctx EvalContext, p *config } // Use unmarked value for validate request - unmarkedConfigVal, _ := configVal.UnmarkDeep() + unmarkedConfigVal, _ := configVal.UnmarkDeepWithPaths() req := provisioners.ValidateProvisionerConfigRequest{ Config: unmarkedConfigVal, } @@ -355,6 +356,7 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag diags = diags.Append( validateResourceForbiddenEphemeralValues(ctx, configVal, schema.Body).InConfigBody(n.Config.Config, n.Addr.String()), ) + diags = diags.Append(validateConfigUsingDeprecatedValues(configVal).InConfigBody(n.Config.Config, n.Addr.String())) if n.Config.Managed != nil { // can be nil only in tests with poorly-configured mocks for _, traversal := range n.Config.Managed.IgnoreChanges { @@ -434,6 +436,7 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag diags = diags.Append( validateResourceForbiddenEphemeralValues(ctx, configVal, schema.Body).InConfigBody(n.Config.Config, n.Addr.String()), ) + diags = diags.Append(validateConfigUsingDeprecatedValues(configVal)) // Use unmarked value for validate request unmarkedConfigVal, _ := configVal.UnmarkDeep() @@ -469,6 +472,7 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag } resp := provider.ValidateEphemeralResourceConfig(req) + diags = diags.Append(validateConfigUsingDeprecatedValues(configVal)) diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config, n.Addr.String())) case addrs.ListResourceMode: schema := providerSchema.SchemaForListResourceType(n.Config.Type) @@ -487,18 +491,21 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag if valDiags.HasErrors() { return diags } + diags = diags.Append(validateConfigUsingDeprecatedValues(blockVal)) limit, _, limitDiags := newLimitEvaluator(true).EvaluateExpr(ctx, n.Config.List.Limit) diags = diags.Append(limitDiags) if limitDiags.HasErrors() { return diags } + diags = diags.Append(validateConfigUsingDeprecatedValues(limit)) includeResource, _, includeDiags := newIncludeRscEvaluator(true).EvaluateExpr(ctx, n.Config.List.IncludeResource) diags = diags.Append(includeDiags) if includeDiags.HasErrors() { return diags } + diags = diags.Append(validateConfigUsingDeprecatedValues(includeResource)) // Use unmarked value for validate request unmarkedBlockVal, _ := blockVal.UnmarkDeep() @@ -885,3 +892,22 @@ func validateResourceForbiddenEphemeralValues(ctx EvalContext, value cty.Value, } return diags } + +func validateConfigUsingDeprecatedValues(config cty.Value) (diags tfdiags.Diagnostics) { + _, pvms := config.UnmarkDeepWithPaths() + 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 +}