Skip to content

Commit 90a9119

Browse files
ephemeral: handle write-only attributes in stackdata
1 parent 8a22769 commit 90a9119

File tree

8 files changed

+1057
-825
lines changed

8 files changed

+1057
-825
lines changed

internal/rpcapi/terraform1/stacks/conversion.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,13 @@ func ToDynamicValue(from cty.Value, ty cty.Type) (*DynamicValue, error) {
5353
// Separate out sensitive marks from the decoded value so we can re-serialize it
5454
// with MessagePack. Sensitive paths get encoded separately in the final message.
5555
unmarkedValue, markses := from.UnmarkDeepWithPaths()
56-
sensitivePaths, otherMarkses := marks.PathsWithMark(markses, marks.Sensitive)
56+
57+
// TODO: I used this pattern a bunch of times but only now noticed that values which are marked
58+
// two ways (which is explicitly allowed by the spec) are in both lists. I will need to handle that somehow.
59+
60+
sensitivePaths, nonSensitiveMarks := marks.PathsWithMark(markses, marks.Sensitive)
61+
ephemeralPaths, otherMarkses := marks.PathsWithMark(nonSensitiveMarks, marks.Ephemeral)
62+
5763
if len(otherMarkses) != 0 {
5864
// Any other marks should've been dealt with by our caller before
5965
// getting here, since we only know how to preserve the sensitive
@@ -67,7 +73,7 @@ func ToDynamicValue(from cty.Value, ty cty.Type) (*DynamicValue, error) {
6773
if err != nil {
6874
return nil, err
6975
}
70-
return NewDynamicValue(encValue, sensitivePaths), nil
76+
return NewDynamicValue(encValue, sensitivePaths, ephemeralPaths), nil
7177
}
7278

7379
// NewDynamicValue constructs a [DynamicValue] message object from a
@@ -77,7 +83,7 @@ func ToDynamicValue(from cty.Value, ty cty.Type) (*DynamicValue, error) {
7783
// The plans package represents the sensitive value mark as a separate field
7884
// in [plans.ChangeSrc] rather than as part of the value itself, so callers must
7985
// also provide a separate set of paths that are marked as sensitive.
80-
func NewDynamicValue(from plans.DynamicValue, sensitivePaths []cty.Path) *DynamicValue {
86+
func NewDynamicValue(from plans.DynamicValue, sensitivePaths []cty.Path, writeOnlyPaths []cty.Path) *DynamicValue {
8187
// plans.DynamicValue is always MessagePack-serialized today, so we'll
8288
// just write its bytes into the field for msgpack serialization
8389
// unconditionally. If plans.DynamicValue grows to support different
@@ -93,6 +99,13 @@ func NewDynamicValue(from plans.DynamicValue, sensitivePaths []cty.Path) *Dynami
9399
}
94100
}
95101

102+
if len(writeOnlyPaths) != 0 {
103+
ret.WriteOnly = make([]*AttributePath, 0, len(writeOnlyPaths))
104+
for _, path := range writeOnlyPaths {
105+
ret.WriteOnly = append(ret.WriteOnly, NewAttributePath(path))
106+
}
107+
}
108+
96109
return ret
97110
}
98111

internal/rpcapi/terraform1/stacks/stacks.pb.go

Lines changed: 786 additions & 772 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/rpcapi/terraform1/stacks/stacks.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ message InspectExpressionResult {
274274
message DynamicValue {
275275
bytes msgpack = 1; // The default serialization format
276276
repeated AttributePath sensitive = 2; // Paths to any sensitive-marked values.
277+
repeated AttributePath write_only = 3; // Paths to any write-only-marked values.
277278
}
278279

279280
// Represents a change of some object from one dynamic value to another.

internal/stacks/stackplan/planned_change.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,10 +481,12 @@ func (pc *PlannedChangeResourceInstancePlanned) ChangeDescription() (*stacks.Pla
481481
Old: stacks.NewDynamicValue(
482482
pc.ChangeSrc.Before,
483483
pc.ChangeSrc.BeforeSensitivePaths,
484+
pc.ChangeSrc.BeforeWriteOnlyPaths,
484485
),
485486
New: stacks.NewDynamicValue(
486487
pc.ChangeSrc.After,
487488
pc.ChangeSrc.AfterSensitivePaths,
489+
pc.ChangeSrc.AfterWriteOnlyPaths,
488490
),
489491
},
490492
ReplacePaths: replacePaths,

internal/stacks/tfstackdata1/convert.go

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ func ResourceInstanceObjectStateToTFStackData1(objSrc *states.ResourceInstanceOb
2929
// attribute paths here just so we don't need to reimplement the
3030
// slice-of-paths conversion in yet another place. We don't
3131
// actually do anything with the value part of this.
32-
protoValue := stacks.NewDynamicValue(plans.DynamicValue(nil), objSrc.AttrSensitivePaths)
32+
protoValue := stacks.NewDynamicValue(plans.DynamicValue(nil), objSrc.AttrSensitivePaths, objSrc.AttrWriteOnlyPaths)
3333
rawMsg := &StateResourceInstanceObjectV1{
3434
SchemaVersion: objSrc.SchemaVersion,
3535
ValueJson: objSrc.AttrsJSON,
3636
SensitivePaths: Terraform1ToPlanProtoAttributePaths(protoValue.Sensitive),
37+
WriteOnlyPaths: Terraform1ToPlanProtoAttributePaths(protoValue.WriteOnly),
3738
CreateBeforeDestroy: objSrc.CreateBeforeDestroy,
3839
ProviderConfigAddr: providerConfigAddr.String(),
3940
ProviderSpecificData: objSrc.Private,
@@ -61,6 +62,7 @@ func Terraform1ToStackDataDynamicValue(value *stacks.DynamicValue) *DynamicValue
6162
Msgpack: value.Msgpack,
6263
},
6364
SensitivePaths: Terraform1ToPlanProtoAttributePaths(value.Sensitive),
65+
WriteOnlyPaths: Terraform1ToPlanProtoAttributePaths(value.WriteOnly),
6466
}
6567
}
6668

@@ -87,6 +89,20 @@ func DynamicValueFromTFStackData1(protoVal *DynamicValue, ty cty.Type) (cty.Valu
8789
})
8890
}
8991
}
92+
93+
if len(protoVal.WriteOnlyPaths) != 0 {
94+
marks := cty.NewValueMarks(marks.Ephemeral)
95+
for _, protoPath := range protoVal.WriteOnlyPaths {
96+
path, err := planfile.PathFromProto(protoPath)
97+
if err != nil {
98+
return cty.NilVal, fmt.Errorf("invalid ephemeral value path: %w", err)
99+
}
100+
markses = append(markses, cty.PathValueMarks{
101+
Path: path,
102+
Marks: marks,
103+
})
104+
}
105+
}
90106
return unmarkedV.MarkWithPaths(markses), nil
91107
}
92108

@@ -173,16 +189,25 @@ func DecodeProtoResourceInstanceObject(protoObj *StateResourceInstanceObjectV1)
173189
return nil, fmt.Errorf("unsupported status %s", protoObj.Status.String())
174190
}
175191

176-
paths := make([]cty.Path, 0, len(protoObj.SensitivePaths))
192+
sensitivePaths := make([]cty.Path, 0, len(protoObj.SensitivePaths))
177193
for _, p := range protoObj.SensitivePaths {
178194
path, err := planfile.PathFromProto(p)
179195
if err != nil {
180196
return nil, err
181197
}
182-
paths = append(paths, path)
198+
sensitivePaths = append(sensitivePaths, path)
199+
}
200+
objSrc.AttrSensitivePaths = sensitivePaths
201+
202+
writeOnlyPaths := make([]cty.Path, 0, len(protoObj.WriteOnlyPaths))
203+
for _, p := range protoObj.WriteOnlyPaths {
204+
path, err := planfile.PathFromProto(p)
205+
if err != nil {
206+
return nil, err
207+
}
208+
writeOnlyPaths = append(writeOnlyPaths, path)
183209
}
184-
objSrc.AttrSensitivePaths = paths
185-
// TODO: Handle write only paths
210+
objSrc.AttrWriteOnlyPaths = writeOnlyPaths
186211

187212
if len(protoObj.Dependencies) != 0 {
188213
objSrc.Dependencies = make([]addrs.ConfigResource, len(protoObj.Dependencies))

internal/stacks/tfstackdata1/convert_test.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,153 @@ func TestDynamicValueFromTFStackData1(t *testing.T) {
148148
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want)
149149
}
150150
}
151+
152+
func TestDynamicValueWithEphemeralMarks(t *testing.T) {
153+
startVal := cty.ObjectVal(map[string]cty.Value{
154+
"x": cty.StringVal("x").Mark(marks.Ephemeral),
155+
"y": cty.StringVal("y"),
156+
"z": cty.ListVal([]cty.Value{
157+
cty.StringVal("z[0]"),
158+
cty.StringVal("z[1]").Mark(marks.Ephemeral),
159+
}),
160+
})
161+
ty := startVal.Type()
162+
163+
partial, err := stacks.ToDynamicValue(startVal, ty)
164+
if err != nil {
165+
t.Fatalf("unexpected error: %s", err)
166+
}
167+
got := Terraform1ToStackDataDynamicValue(partial)
168+
want := &DynamicValue{
169+
Value: &planproto.DynamicValue{
170+
Msgpack: []byte("\x83\xa1x\xa1x\xa1y\xa1y\xa1z\x92\xa4z[0]\xa4z[1]"),
171+
},
172+
173+
WriteOnlyPaths: []*planproto.Path{
174+
{
175+
Steps: []*planproto.Path_Step{
176+
{
177+
Selector: &planproto.Path_Step_AttributeName{
178+
AttributeName: "x",
179+
},
180+
},
181+
},
182+
},
183+
{
184+
Steps: []*planproto.Path_Step{
185+
{
186+
Selector: &planproto.Path_Step_AttributeName{
187+
AttributeName: "z",
188+
},
189+
},
190+
{
191+
Selector: &planproto.Path_Step_ElementKey{
192+
ElementKey: &planproto.DynamicValue{
193+
Msgpack: []byte{0b00000001}, // MessagePack-encoded fixint 1
194+
},
195+
},
196+
},
197+
},
198+
},
199+
},
200+
}
201+
202+
if len(got.WriteOnlyPaths) == 2 && len(got.WriteOnlyPaths[0].Steps) == 2 {
203+
got.WriteOnlyPaths[0], got.WriteOnlyPaths[1] = got.WriteOnlyPaths[1], got.WriteOnlyPaths[0]
204+
}
205+
206+
if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" {
207+
t.Errorf("wrong result\n%s", diff)
208+
}
209+
}
210+
211+
func TestDynamicValueWithEphemeralAndSensitiveMarks(t *testing.T) {
212+
startVal := cty.ObjectVal(map[string]cty.Value{
213+
"m": cty.StringVal("m").Mark(marks.Ephemeral).Mark(marks.Sensitive),
214+
"n": cty.StringVal("n"),
215+
"o": cty.ListVal([]cty.Value{
216+
cty.StringVal("o[0]"),
217+
cty.StringVal("o[1]").Mark(marks.Ephemeral).Mark(marks.Sensitive),
218+
}),
219+
})
220+
ty := startVal.Type()
221+
222+
partial, err := stacks.ToDynamicValue(startVal, ty)
223+
if err != nil {
224+
t.Fatalf("unexpected error: %s", err)
225+
}
226+
got := Terraform1ToStackDataDynamicValue(partial)
227+
want := &DynamicValue{
228+
Value: &planproto.DynamicValue{
229+
Msgpack: []byte("\x83\xa1m\xa1m\xa1n\xa1n\xa1o\x92\xa4o[0]\xa4o[1]"),
230+
},
231+
232+
SensitivePaths: []*planproto.Path{
233+
{
234+
Steps: []*planproto.Path_Step{
235+
{
236+
Selector: &planproto.Path_Step_AttributeName{
237+
AttributeName: "m",
238+
},
239+
},
240+
},
241+
},
242+
{
243+
Steps: []*planproto.Path_Step{
244+
{
245+
Selector: &planproto.Path_Step_AttributeName{
246+
AttributeName: "o",
247+
},
248+
},
249+
{
250+
Selector: &planproto.Path_Step_ElementKey{
251+
ElementKey: &planproto.DynamicValue{
252+
Msgpack: []byte{0b00000001}, // MessagePack-encoded fixint 1
253+
},
254+
},
255+
},
256+
},
257+
},
258+
},
259+
260+
WriteOnlyPaths: []*planproto.Path{
261+
{
262+
Steps: []*planproto.Path_Step{
263+
{
264+
Selector: &planproto.Path_Step_AttributeName{
265+
AttributeName: "m",
266+
},
267+
},
268+
},
269+
},
270+
{
271+
Steps: []*planproto.Path_Step{
272+
{
273+
Selector: &planproto.Path_Step_AttributeName{
274+
AttributeName: "o",
275+
},
276+
},
277+
{
278+
Selector: &planproto.Path_Step_ElementKey{
279+
ElementKey: &planproto.DynamicValue{
280+
Msgpack: []byte{0b00000001}, // MessagePack-encoded fixint 1
281+
},
282+
},
283+
},
284+
},
285+
},
286+
},
287+
}
288+
289+
if len(got.SensitivePaths) == 2 && len(got.SensitivePaths[0].Steps) == 2 {
290+
got.SensitivePaths[0], got.SensitivePaths[1] = got.SensitivePaths[1], got.SensitivePaths[0]
291+
}
292+
293+
if len(got.WriteOnlyPaths) == 2 && len(got.WriteOnlyPaths[0].Steps) == 2 {
294+
got.WriteOnlyPaths[0], got.WriteOnlyPaths[1] = got.WriteOnlyPaths[1], got.WriteOnlyPaths[0]
295+
}
296+
297+
if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" {
298+
t.Errorf("wrong result\n%s", diff)
299+
}
300+
}

0 commit comments

Comments
 (0)