Skip to content

Commit 2a07ad6

Browse files
authored
Add support for deserializing output values (#298)
This change adds support for deserializing output values and ensures they work for transforms. This is necessary as the engine will send output values to transforms, so we need to handle deserializing them before calling the transform and then properly serializing them when returning the results. Fixes #297
1 parent 0856a1d commit 2a07ad6

File tree

6 files changed

+427
-56
lines changed

6 files changed

+427
-56
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
component: sdk
2+
kind: Bug Fixes
3+
body: Add support for deserializing output values and use them from transforms
4+
time: 2024-07-16T19:46:29.831748-07:00
5+
custom:
6+
PR: "298"

integration_tests/transformations_remote/Program.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,46 @@ public TransformsStack() : base(new StackOptions { ResourceTransforms = {Scenari
105105
}
106106
}
107107
});
108+
109+
// Scenario #6 - make the length property a secret.
110+
var res6 = new Random("res6", new RandomArgs { Length = 10 }, new CustomResourceOptions
111+
{
112+
ResourceTransforms =
113+
{
114+
async (args, _) =>
115+
{
116+
if (args.Type == "testprovider:index:Random")
117+
{
118+
var resultArgs = args.Args;
119+
var length = (double)resultArgs["length"] * 2;
120+
resultArgs = resultArgs.SetItem("length", Output.CreateSecret(length));
121+
return new ResourceTransformResult(resultArgs, args.Options);
122+
}
123+
124+
return null;
125+
}
126+
}
127+
});
128+
129+
// Scenario #7 - Unsecret
130+
var res7 = new Random("res7", new RandomArgs { Length = Output.CreateSecret(21) }, new CustomResourceOptions
131+
{
132+
ResourceTransforms =
133+
{
134+
async (args, _) =>
135+
{
136+
if (args.Type == "testprovider:index:Random")
137+
{
138+
var resultArgs = args.Args;
139+
var length = ((Output<double>)resultArgs["length"]).Apply(v => v * 2);
140+
resultArgs = resultArgs.SetItem("length", Output.Unsecret(length));
141+
return new ResourceTransformResult(resultArgs, args.Options);
142+
}
143+
144+
return null;
145+
}
146+
}
147+
});
108148
}
109149

110150
// Scenario #3 - apply a transformation to the Stack to transform all (future) resources in the stack

integration_tests/transformations_simple_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ func Validator(t *testing.T, stack integration.RuntimeValidationStackInfo) {
107107
foundRes3 := false
108108
foundRes4Child := false
109109
foundRes5 := false
110+
foundRes6 := false
111+
foundRes7 := false
110112
for _, res := range stack.Deployment.Resources {
111113
// "res1" has a transformation which adds additionalSecretOutputs
112114
if res.URN.Name() == "res1" {
@@ -159,10 +161,31 @@ func Validator(t *testing.T, stack integration.RuntimeValidationStackInfo) {
159161
assert.NotNil(t, length)
160162
assert.Equal(t, 20.0, length.(float64))
161163
}
164+
// "res6" should have made the length a secret
165+
if res.URN.Name() == "res6" {
166+
foundRes6 = true
167+
assert.Equal(t, res.Type, tokens.Type(randomResName))
168+
length := res.Inputs["length"]
169+
assert.NotNil(t, length)
170+
// length should be secret
171+
secret, ok := length.(map[string]interface{})
172+
assert.True(t, ok, "length should be a secret")
173+
assert.Equal(t, resource.SecretSig, secret[resource.SigKey])
174+
}
175+
// "res6" should have mutated the length and unsecreted it
176+
if res.URN.Name() == "res7" {
177+
foundRes7 = true
178+
assert.Equal(t, res.Type, tokens.Type(randomResName))
179+
length := res.Inputs["length"]
180+
assert.NotNil(t, length)
181+
assert.Equal(t, 42.0, length.(float64))
182+
}
162183
}
163184
assert.True(t, foundRes1)
164185
assert.True(t, foundRes2Child)
165186
assert.True(t, foundRes3)
166187
assert.True(t, foundRes4Child)
167188
assert.True(t, foundRes5)
189+
assert.True(t, foundRes6)
190+
assert.True(t, foundRes7)
168191
}
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
// Copyright 2016-2024, Pulumi Corporation
2+
3+
using System.Collections.Immutable;
4+
using System.Threading.Tasks;
5+
using Google.Protobuf.WellKnownTypes;
6+
using Pulumi.Serialization;
7+
using Xunit;
8+
9+
namespace Pulumi.Tests.Serialization
10+
{
11+
public class OutputValueTests : ConverterTests
12+
{
13+
static Value CreateOutputValue(ImmutableHashSet<string> resources, Value? value = null, bool isSecret = false)
14+
{
15+
var result = new Value
16+
{
17+
StructValue = new Struct
18+
{
19+
Fields =
20+
{
21+
{ Constants.SpecialSigKey, new Value { StringValue = Constants.SpecialOutputValueSig } },
22+
}
23+
}
24+
};
25+
if (value is not null)
26+
{
27+
result.StructValue.Fields.Add(Constants.ValueName, value);
28+
}
29+
if (isSecret)
30+
{
31+
result.StructValue.Fields.Add(Constants.SecretName, new Value { BoolValue = true });
32+
}
33+
if (resources.Count > 0)
34+
{
35+
var dependencies = new Value { ListValue = new ListValue() };
36+
foreach (var resource in resources)
37+
{
38+
dependencies.ListValue.Values.Add(new Value { StringValue = resource });
39+
}
40+
result.StructValue.Fields.Add(Constants.DependenciesName, dependencies);
41+
}
42+
return result;
43+
}
44+
45+
[Fact]
46+
public async Task TestUnknown()
47+
{
48+
var input = CreateOutputValue(ImmutableHashSet<string>.Empty);
49+
var result = Deserializer.Deserialize(input);
50+
var o = Assert.IsType<Output<object>>(result.Value);
51+
var data = await o.DataTask.ConfigureAwait(false);
52+
Assert.Null(data.Value);
53+
Assert.False(data.IsKnown);
54+
Assert.False(data.IsSecret);
55+
Assert.Empty(data.Resources);
56+
}
57+
58+
[Fact]
59+
public async Task TestString()
60+
{
61+
var input = CreateOutputValue(ImmutableHashSet<string>.Empty, new Value { StringValue = "hello" });
62+
var result = Deserializer.Deserialize(input);
63+
var o = Assert.IsType<Output<string>>(result.Value);
64+
var data = await o.DataTask.ConfigureAwait(false);
65+
Assert.Equal("hello", data.Value);
66+
Assert.True(data.IsKnown);
67+
Assert.False(data.IsSecret);
68+
Assert.Empty(data.Resources);
69+
}
70+
71+
[Fact]
72+
public async Task TestStringSecret()
73+
{
74+
var input = CreateOutputValue(
75+
ImmutableHashSet<string>.Empty, new Value { StringValue = "hello" }, isSecret: true);
76+
var result = Deserializer.Deserialize(input);
77+
var o = Assert.IsType<Output<string>>(result.Value);
78+
var data = await o.DataTask.ConfigureAwait(false);
79+
Assert.Equal("hello", data.Value);
80+
Assert.True(data.IsKnown);
81+
Assert.True(data.IsSecret);
82+
Assert.Empty(data.Resources);
83+
}
84+
85+
[Fact]
86+
public async Task TestStringDependencies()
87+
{
88+
var input = CreateOutputValue(
89+
ImmutableHashSet<string>.Empty.Add("foo"), new Value { StringValue = "hello" });
90+
var result = Deserializer.Deserialize(input);
91+
var o = Assert.IsType<Output<string>>(result.Value);
92+
var data = await o.DataTask.ConfigureAwait(false);
93+
Assert.Equal("hello", data.Value);
94+
Assert.True(data.IsKnown);
95+
Assert.False(data.IsSecret);
96+
var resources = ImmutableHashSet<string>.Empty;
97+
foreach (var resource in data.Resources)
98+
{
99+
var urn = await resource.Urn.DataTask.ConfigureAwait(false);
100+
resources = resources.Add(urn.Value);
101+
}
102+
Assert.Equal(ImmutableHashSet<string>.Empty.Add("foo"), resources);
103+
}
104+
105+
[Fact]
106+
public async Task TestList()
107+
{
108+
var input = CreateOutputValue(ImmutableHashSet<string>.Empty, new Value
109+
{
110+
ListValue = new ListValue
111+
{
112+
Values =
113+
{
114+
new Value { StringValue = "hello" },
115+
new Value { StringValue = "world" },
116+
}
117+
}
118+
});
119+
var result = Deserializer.Deserialize(input);
120+
var o = Assert.IsType<Output<ImmutableArray<object>>>(result.Value);
121+
var data = await o.DataTask.ConfigureAwait(false);
122+
Assert.Equal(ImmutableArray<string>.Empty.Add("hello").Add("world"), data.Value);
123+
Assert.True(data.IsKnown);
124+
Assert.False(data.IsSecret);
125+
Assert.Empty(data.Resources);
126+
}
127+
128+
[Fact]
129+
public async Task TestListNestedOutput()
130+
{
131+
var input = new Value
132+
{
133+
ListValue = new ListValue
134+
{
135+
Values =
136+
{
137+
new Value { StringValue = "hello" },
138+
CreateOutputValue(ImmutableHashSet<string>.Empty, new Value { StringValue = "world" }),
139+
}
140+
}
141+
};
142+
143+
var result = Deserializer.Deserialize(input);
144+
var v = Assert.IsType<ImmutableArray<object>>(result.Value);
145+
Assert.Equal("hello", v[0]);
146+
var o = Assert.IsType<Output<string>>(v[1]);
147+
var data = await o.DataTask.ConfigureAwait(false);
148+
Assert.Equal("world", data.Value);
149+
Assert.True(data.IsKnown);
150+
Assert.False(data.IsSecret);
151+
Assert.Empty(data.Resources);
152+
}
153+
154+
[Fact]
155+
public async Task TestListNestedOutputUnknown()
156+
{
157+
var input = new Value
158+
{
159+
ListValue = new ListValue
160+
{
161+
Values =
162+
{
163+
new Value { StringValue = "hello" },
164+
CreateOutputValue(ImmutableHashSet<string>.Empty),
165+
}
166+
}
167+
};
168+
169+
var result = Deserializer.Deserialize(input);
170+
var v = Assert.IsType<ImmutableArray<object>>(result.Value);
171+
Assert.Equal("hello", v[0]);
172+
var o = Assert.IsType<Output<object?>>(v[1]);
173+
var data = await o.DataTask.ConfigureAwait(false);
174+
Assert.Null(data.Value);
175+
Assert.False(data.IsKnown);
176+
Assert.False(data.IsSecret);
177+
Assert.Empty(data.Resources);
178+
}
179+
180+
[Fact]
181+
public async Task TestStruct()
182+
{
183+
var input = CreateOutputValue(ImmutableHashSet<string>.Empty, new Value
184+
{
185+
StructValue = new Struct
186+
{
187+
Fields =
188+
{
189+
{ "hello", new Value { StringValue = "world" } },
190+
{ "foo", new Value { StringValue = "bar" } },
191+
}
192+
}
193+
});
194+
var result = Deserializer.Deserialize(input);
195+
var o = Assert.IsType<Output<ImmutableDictionary<string, object>>>(result.Value);
196+
var data = await o.DataTask.ConfigureAwait(false);
197+
var expected = ImmutableDictionary.Create<string, object>()
198+
.Add("hello", "world")
199+
.Add("foo", "bar");
200+
Assert.Equal(expected, data.Value);
201+
Assert.True(data.IsKnown);
202+
Assert.False(data.IsSecret);
203+
Assert.Empty(data.Resources);
204+
}
205+
206+
[Fact]
207+
public async Task TestStructNestedOutput()
208+
{
209+
var input = new Value
210+
{
211+
StructValue = new Struct
212+
{
213+
Fields =
214+
{
215+
{ "hello", new Value { StringValue = "world" } },
216+
{ "foo", CreateOutputValue(ImmutableHashSet<string>.Empty, new Value { StringValue = "bar" }) },
217+
}
218+
}
219+
};
220+
var result = Deserializer.Deserialize(input);
221+
var v = Assert.IsType<ImmutableDictionary<string, object>>(result.Value);
222+
Assert.Equal("world", v["hello"]);
223+
var o = Assert.IsType<Output<string>>(v["foo"]);
224+
var data = await o.DataTask.ConfigureAwait(false);
225+
Assert.Equal("bar", data.Value);
226+
Assert.True(data.IsKnown);
227+
Assert.False(data.IsSecret);
228+
Assert.Empty(data.Resources);
229+
}
230+
231+
[Fact]
232+
public async Task TestStructNestedOutputUnknown()
233+
{
234+
var input = new Value
235+
{
236+
StructValue = new Struct
237+
{
238+
Fields =
239+
{
240+
{ "hello", new Value { StringValue = "world" } },
241+
{ "foo", CreateOutputValue(ImmutableHashSet<string>.Empty) },
242+
}
243+
}
244+
};
245+
var result = Deserializer.Deserialize(input);
246+
var v = Assert.IsType<ImmutableDictionary<string, object?>>(result.Value);
247+
Assert.Equal("world", v["hello"]);
248+
var o = Assert.IsType<Output<object?>>(v["foo"]);
249+
var data = await o.DataTask.ConfigureAwait(false);
250+
Assert.Null(data.Value);
251+
Assert.False(data.IsKnown);
252+
Assert.False(data.IsSecret);
253+
Assert.Empty(data.Resources);
254+
}
255+
}
256+
}

0 commit comments

Comments
 (0)