add deprecation marks

This commit is contained in:
Daniel Schmidt 2025-12-11 13:27:44 +01:00
parent 8a615f93e9
commit d813ad14f7
8 changed files with 321 additions and 13 deletions

View file

@ -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)",

View file

@ -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
}
// FilterDeprecationMarks returns all deprecation marks present in the given
// cty.ValueMarks.
func FilterDeprecationMarks(marks cty.ValueMarks) []DeprecationMark {
depMarks := []DeprecationMark{}
for mark := range marks {
if d, ok := mark.(DeprecationMark); ok {
depMarks = append(depMarks, d)
}
}
return depMarks
}
// GetDeprecationMarks returns all deprecation marks present on the given
// cty.Value.
func GetDeprecationMarks(val cty.Value) []DeprecationMark {
_, marks := val.UnmarkDeep()
return FilterDeprecationMarks(marks)
}
// RemoveDeprecationMarks returns a copy of the given cty.Value with all
// deprecation marks removed.
func RemoveDeprecationMarks(val cty.Value) cty.Value {
newVal, pvms := val.UnmarkDeepWithPaths()
otherPvms := RemoveAll(pvms, Deprecation)
return newVal.MarkWithPaths(otherPvms)
}
// 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,
}
}

View file

@ -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")
}
}

View file

@ -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)
}

View file

@ -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 {

View file

@ -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)",

View file

@ -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)",

View file

@ -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)",