Skip to content

Commit 60aaaf3

Browse files
authored
Merge pull request #36619 from hashicorp/jbardin/filter-ephemeral-marks
Filter ephemeral marks from planned value
2 parents 3f4ed7e + fdf25b6 commit 60aaaf3

File tree

5 files changed

+99
-8
lines changed

5 files changed

+99
-8
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: BUG FIXES
2+
body: Combining ephemeral and sensitive marks could fail when serializing planned changes
3+
time: 2025-03-03T14:40:20.606817-05:00
4+
custom:
5+
Issue: "36619"

internal/lang/marks/paths.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,41 @@ func PathsWithMark(pvms []cty.PathValueMarks, wantMark any) (withWanted []cty.Pa
3131
if _, ok := pvm.Marks[wantMark]; ok {
3232
withWanted = append(withWanted, pvm.Path)
3333
}
34+
3435
for mark := range pvm.Marks {
3536
if mark != wantMark {
3637
withOthers = append(withOthers, pvm)
38+
// only add a path with unwanted marks a single time
39+
break
3740
}
3841
}
3942
}
4043

4144
return withWanted, withOthers
4245
}
4346

47+
// RemoveAll take a series of PathValueMarks and removes the unwanted mark from
48+
// all paths. Paths with no remaining marks will be removed entirely. The
49+
// PathValuesMarks passed in are not cloned, and RemoveAll will modify the
50+
// original values, so the prior set of marks should not be retained for use.
51+
func RemoveAll(pvms []cty.PathValueMarks, remove any) []cty.PathValueMarks {
52+
if len(pvms) == 0 {
53+
// No-allocations path for the common case where there are no marks at all.
54+
return nil
55+
}
56+
57+
var res []cty.PathValueMarks
58+
59+
for _, pvm := range pvms {
60+
delete(pvm.Marks, remove)
61+
if len(pvm.Marks) > 0 {
62+
res = append(res, pvm)
63+
}
64+
}
65+
66+
return res
67+
}
68+
4469
// MarkPaths transforms the given value by marking each of the given paths
4570
// with the given mark value.
4671
func MarkPaths(val cty.Value, mark any, paths []cty.Path) cty.Value {

internal/lang/marks/paths_test.go

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,23 @@ func TestPathsWithMark(t *testing.T) {
1616
input := []cty.PathValueMarks{
1717
{
1818
Path: cty.GetAttrPath("sensitive"),
19-
Marks: cty.NewValueMarks(Sensitive),
19+
Marks: cty.NewValueMarks("sensitive"),
2020
},
2121
{
2222
Path: cty.GetAttrPath("other"),
2323
Marks: cty.NewValueMarks("other"),
2424
},
2525
{
2626
Path: cty.GetAttrPath("both"),
27-
Marks: cty.NewValueMarks(Sensitive, "other"),
27+
Marks: cty.NewValueMarks("sensitive", "other"),
28+
},
29+
{
30+
Path: cty.GetAttrPath("neither"),
31+
Marks: cty.NewValueMarks("x", "y"),
2832
},
2933
}
3034

31-
gotPaths, gotOthers := PathsWithMark(input, Sensitive)
35+
gotPaths, gotOthers := PathsWithMark(input, "sensitive")
3236
wantPaths := []cty.Path{
3337
cty.GetAttrPath("sensitive"),
3438
cty.GetAttrPath("both"),
@@ -40,14 +44,18 @@ func TestPathsWithMark(t *testing.T) {
4044
},
4145
{
4246
Path: cty.GetAttrPath("both"),
43-
Marks: cty.NewValueMarks(Sensitive, "other"),
47+
Marks: cty.NewValueMarks("sensitive", "other"),
4448
// Note that this intentionally preserves the fact that the
4549
// attribute was both sensitive _and_ had another mark, since
4650
// that gives the caller the most possible information to
4751
// potentially handle this combination in a special way in
4852
// an error message, or whatever. It also conveniently avoids
4953
// allocating a new mark set, which is nice.
5054
},
55+
{
56+
Path: cty.GetAttrPath("neither"),
57+
Marks: cty.NewValueMarks("x", "y"),
58+
},
5159
}
5260

5361
if diff := cmp.Diff(wantPaths, gotPaths, ctydebug.CmpOptions); diff != "" {
@@ -58,6 +66,40 @@ func TestPathsWithMark(t *testing.T) {
5866
}
5967
}
6068

69+
func TestRemoveAll(t *testing.T) {
70+
input := []cty.PathValueMarks{
71+
{
72+
Path: cty.GetAttrPath("sensitive"),
73+
Marks: cty.NewValueMarks("sensitive"),
74+
},
75+
{
76+
Path: cty.GetAttrPath("other"),
77+
Marks: cty.NewValueMarks("other"),
78+
},
79+
{
80+
Path: cty.GetAttrPath("both"),
81+
Marks: cty.NewValueMarks("sensitive", "other"),
82+
},
83+
}
84+
85+
want := []cty.PathValueMarks{
86+
{
87+
Path: cty.GetAttrPath("sensitive"),
88+
Marks: cty.NewValueMarks("sensitive"),
89+
},
90+
{
91+
Path: cty.GetAttrPath("both"),
92+
Marks: cty.NewValueMarks("sensitive"),
93+
},
94+
}
95+
96+
got := RemoveAll(input, "other")
97+
98+
if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" {
99+
t.Errorf("wrong matched paths\n%s", diff)
100+
}
101+
}
102+
61103
func TestMarkPaths(t *testing.T) {
62104
value := cty.ObjectVal(map[string]cty.Value{
63105
"s": cty.StringVal(".s"),

internal/terraform/context_plan_ephemeral_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,24 @@ resource "ephem_write_only" "test" {
565565
expectValidateEphemeralResourceConfigCalled: true,
566566
expectCloseEphemeralResourceCalled: true,
567567
},
568+
"write_only_sensitive_and_ephem": {
569+
module: map[string]string{
570+
"main.tf": `
571+
variable "in" {
572+
sensitive = true
573+
ephemeral = true
574+
}
575+
resource "ephem_write_only" "test" {
576+
write_only = var.in
577+
}
578+
`,
579+
},
580+
inputs: InputValues{
581+
"in": &InputValue{
582+
Value: cty.StringVal("test"),
583+
},
584+
},
585+
},
568586
} {
569587
t.Run(name, func(t *testing.T) {
570588
m := testModuleInline(t, tc.module)

internal/terraform/node_resource_abstract_instance.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,8 +1066,9 @@ func (n *NodeAbstractResourceInstance) plan(
10661066
// for write-only attributes (the only place where ephemeral values are allowed).
10671067
// This is verified in objchange.AssertPlanValid already.
10681068
unmarkedPlannedNewVal := plannedNewVal
1069-
_, nonEphemeralMarks := marks.PathsWithMark(unmarkedPaths, marks.Ephemeral)
1070-
plannedNewVal = plannedNewVal.MarkWithPaths(nonEphemeralMarks)
1069+
unmarkedPaths = marks.RemoveAll(unmarkedPaths, marks.Ephemeral)
1070+
1071+
plannedNewVal = plannedNewVal.MarkWithPaths(unmarkedPaths)
10711072
if sensitivePaths := schema.SensitivePaths(plannedNewVal, nil); len(sensitivePaths) != 0 {
10721073
plannedNewVal = marks.MarkPaths(plannedNewVal, marks.Sensitive, sensitivePaths)
10731074
}
@@ -1153,8 +1154,8 @@ func (n *NodeAbstractResourceInstance) plan(
11531154
plannedNewVal = resp.PlannedState
11541155
plannedPrivate = resp.PlannedPrivate
11551156

1156-
if len(nonEphemeralMarks) > 0 {
1157-
plannedNewVal = plannedNewVal.MarkWithPaths(nonEphemeralMarks)
1157+
if len(unmarkedPaths) > 0 {
1158+
plannedNewVal = plannedNewVal.MarkWithPaths(unmarkedPaths)
11581159
}
11591160

11601161
for _, err := range plannedNewVal.Type().TestConformance(schema.ImpliedType()) {

0 commit comments

Comments
 (0)