Skip to content

Commit 5b56d0d

Browse files
committed
dotnet remote transforms
1 parent 125d467 commit 5b56d0d

27 files changed

+1783
-145
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
component: sdk
2+
kind: Improvements
3+
body: Add experimental support for the new transforms systemAdd experimental support
4+
for the new transforms system
5+
time: 2024-03-04T13:12:07.2773112Z
6+
custom:
7+
PR: "234"

.github/workflows/pr.yml

+2
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ jobs:
142142
run: dotnet run integration test TestProvider
143143
- name: TestDeletedWith
144144
run: dotnet run integration test TestDeletedWith
145+
- name: TestDotNetTransforms
146+
run: dotnet run integration test TestDotNetTransforms
145147

146148
info:
147149
name: gather

integration_tests/integration_util_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func prepareDotnetProject(projInfo *engine.Projinfo) error {
8686
// if a package reference already exists
8787
// then pulumi is a transitive reference
8888
// no need to add it
89-
if strings.Contains(string(projectContent), "Include=\"Pulumi") {
89+
if strings.Contains(string(projectContent), "Include=\"Pulumi\"") {
9090
return nil
9191
}
9292

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/.pulumi/
2+
[Bb]in/
3+
[Oo]bj/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright 2016-2024, Pulumi Corporation. All rights reserved.
2+
3+
using System;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Pulumi;
7+
8+
class MyComponent : ComponentResource
9+
{
10+
public Random Child { get; }
11+
12+
public MyComponent(string name, ComponentResourceOptions? options = null)
13+
: base("my:component:MyComponent", name, options)
14+
{
15+
this.Child = new Random($"{name}-child",
16+
new RandomArgs { Length = 5 },
17+
new CustomResourceOptions {Parent = this, AdditionalSecretOutputs = {"length"} });
18+
}
19+
}
20+
21+
class TransformsStack : Stack
22+
{
23+
public TransformsStack() : base(new StackOptions { XResourceTransforms = {Scenario3} })
24+
{
25+
// Scenario #1 - apply a transformation to a CustomResource
26+
var res1 = new Random("res1", new RandomArgs { Length = 5 }, new CustomResourceOptions
27+
{
28+
XResourceTransforms =
29+
{
30+
async (args, _) =>
31+
{
32+
var options = CustomResourceOptions.Merge(
33+
(CustomResourceOptions)args.Options,
34+
new CustomResourceOptions {AdditionalSecretOutputs = {"result"}});
35+
return new ResourceTransformResult(args.Args, options);
36+
}
37+
}
38+
});
39+
40+
// Scenario #2 - apply a transformation to a Component to transform its children
41+
var res2 = new MyComponent("res2", new ComponentResourceOptions
42+
{
43+
XResourceTransforms =
44+
{
45+
async (args, _) =>
46+
{
47+
if (args.Type == "testprovider:index:Random")
48+
{
49+
var resultArgs = args.Args;
50+
resultArgs = resultArgs.SetItem("prefix", "newDefault");
51+
52+
var resultOpts = CustomResourceOptions.Merge(
53+
(CustomResourceOptions)args.Options,
54+
new CustomResourceOptions {AdditionalSecretOutputs = {"result"}});
55+
56+
return new ResourceTransformResult(resultArgs, resultOpts);
57+
}
58+
59+
return null;
60+
}
61+
}
62+
});
63+
64+
// Scenario #3 - apply a transformation to the Stack to transform all resources in the stack.
65+
var res3 = new Random("res3", new RandomArgs { Length = Output.CreateSecret(5) });
66+
67+
// Scenario #4 - Transforms are applied in order of decreasing specificity
68+
// 1. (not in this example) Child transformation
69+
// 2. First parent transformation
70+
// 3. Second parent transformation
71+
// 4. Stack transformation
72+
var res4 = new MyComponent("res4", new ComponentResourceOptions
73+
{
74+
XResourceTransforms = { (args, _) => scenario4(args, "default1"), (args, _) => scenario4(args, "default2") }
75+
});
76+
77+
async Task<ResourceTransformResult?> scenario4(ResourceTransformArgs args, string v)
78+
{
79+
if (args.Type == "testprovider:index:Random")
80+
{
81+
var resultArgs = args.Args;
82+
resultArgs = resultArgs.SetItem("prefix", v);
83+
return new ResourceTransformResult(resultArgs, args.Options);
84+
}
85+
86+
return null;
87+
}
88+
89+
// Scenario #5 - mutate the properties of a resource
90+
var res5 = new Random("res5", new RandomArgs { Length = 10 }, new CustomResourceOptions
91+
{
92+
XResourceTransforms =
93+
{
94+
async (args, _) =>
95+
{
96+
if (args.Type == "testprovider:index:Random")
97+
{
98+
var resultArgs = args.Args;
99+
var length = (double)resultArgs["length"] * 2;
100+
resultArgs = resultArgs.SetItem("length", length);
101+
return new ResourceTransformResult(resultArgs, args.Options);
102+
}
103+
104+
return null;
105+
}
106+
}
107+
});
108+
}
109+
110+
// Scenario #3 - apply a transformation to the Stack to transform all (future) resources in the stack
111+
private static async Task<ResourceTransformResult?> Scenario3(ResourceTransformArgs args, CancellationToken ct)
112+
{
113+
if (args.Type == "testprovider:index:Random")
114+
{
115+
var resultArgs = args.Args;
116+
resultArgs = resultArgs.SetItem("prefix", "stackDefault");
117+
118+
var resultOpts = CustomResourceOptions.Merge(
119+
(CustomResourceOptions)args.Options,
120+
new CustomResourceOptions {AdditionalSecretOutputs = {"result"}});
121+
122+
return new ResourceTransformResult(resultArgs, resultOpts);
123+
}
124+
125+
return null;
126+
}
127+
}
128+
129+
class Program
130+
{
131+
static Task<int> Main(string[] args) => Deployment.RunAsync<TransformsStack>();
132+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
name: transformations_dotnet
2+
description: A simple .NET program that uses transformations.
3+
runtime: dotnet
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright 2016-2021, Pulumi Corporation. All rights reserved.
2+
3+
// Exposes the Random resource from the testprovider.
4+
5+
using Pulumi;
6+
7+
public partial class Random : Pulumi.CustomResource
8+
{
9+
[Output("length")]
10+
public Output<int> Length { get; private set; } = null!;
11+
12+
[Output("result")]
13+
public Output<string> Result { get; private set; } = null!;
14+
15+
public Random(string name, RandomArgs args, CustomResourceOptions? options = null)
16+
: base("testprovider:index:Random", name, args ?? new RandomArgs(), options)
17+
{
18+
}
19+
}
20+
21+
public sealed class RandomArgs : Pulumi.ResourceArgs
22+
{
23+
[Input("length", required: true)]
24+
public Input<int> Length { get; set; } = null!;
25+
26+
public RandomArgs()
27+
{
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net6.0</TargetFramework>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
</Project>

integration_tests/transformations_simple/Program.cs

+14-14
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
class MyComponent : ComponentResource
99
{
1010
public RandomString Child { get; }
11-
11+
1212
public MyComponent(string name, ComponentResourceOptions? options = null)
1313
: base("my:component:MyComponent", name, options)
1414
{
@@ -24,29 +24,29 @@ class MyOtherComponent : ComponentResource
2424
{
2525
public RandomString Child1 { get; }
2626
public RandomString Child2 { get; }
27-
27+
2828
public MyOtherComponent(string name, ComponentResourceOptions? options = null)
2929
: base("my:component:MyComponent", name, options)
3030
{
3131
this.Child1 = new RandomString($"{name}-child1",
3232
new RandomStringArgs { Length = 5 },
3333
new CustomResourceOptions { Parent = this });
34-
34+
3535
this.Child2 = new RandomString($"{name}-child2",
3636
new RandomStringArgs { Length = 6 },
3737
new CustomResourceOptions { Parent = this });
3838
}
3939
}
4040

4141
class TransformationsStack : Stack
42-
{
42+
{
4343
public TransformationsStack() : base(new StackOptions { ResourceTransformations = {Scenario3} })
4444
{
4545
// Scenario #1 - apply a transformation to a CustomResource
4646
var res1 = new RandomString("res1", new RandomStringArgs { Length = 5 }, new CustomResourceOptions
4747
{
4848
ResourceTransformations =
49-
{
49+
{
5050
args =>
5151
{
5252
var options = CustomResourceOptions.Merge(
@@ -56,7 +56,7 @@ public TransformationsStack() : base(new StackOptions { ResourceTransformations
5656
}
5757
}
5858
});
59-
59+
6060
// Scenario #2 - apply a transformation to a Component to transform its children
6161
var res2 = new MyComponent("res2", new ComponentResourceOptions
6262
{
@@ -76,10 +76,10 @@ public TransformationsStack() : base(new StackOptions { ResourceTransformations
7676
}
7777
}
7878
});
79-
79+
8080
// Scenario #3 - apply a transformation to the Stack to transform all resources in the stack.
8181
var res3 = new RandomString("res3", new RandomStringArgs { Length = 5 });
82-
82+
8383
// Scenario #4 - transformations are applied in order of decreasing specificity
8484
// 1. (not in this example) Child transformation
8585
// 2. First parent transformation
@@ -89,7 +89,7 @@ public TransformationsStack() : base(new StackOptions { ResourceTransformations
8989
{
9090
ResourceTransformations = { args => scenario4(args, "value1"), args => scenario4(args, "value2") }
9191
});
92-
92+
9393
ResourceTransformationResult? scenario4(ResourceTransformationArgs args, string v)
9494
{
9595
if (args.Resource.GetResourceType() == RandomStringType && args.Args is RandomStringArgs oldArgs)
@@ -145,18 +145,18 @@ ResourceTransformation transformChild1DependsOnChild2()
145145
return child2Args.Length;
146146
})
147147
.Apply(output => output);
148-
148+
149149
var newArgs = new RandomStringArgs {Length = child2Length};
150-
150+
151151
return new ResourceTransformationResult(newArgs, args.Options);
152152
}
153153
}
154-
154+
155155
return null;
156156
}
157157
}
158158
}
159-
159+
160160
// Scenario #3 - apply a transformation to the Stack to transform all (future) resources in the stack
161161
private static ResourceTransformationResult? Scenario3(ResourceTransformationArgs args)
162162
{
@@ -172,7 +172,7 @@ ResourceTransformation transformChild1DependsOnChild2()
172172
}
173173

174174
return null;
175-
}
175+
}
176176

177177
private const string RandomStringType = "random:index/randomString:RandomString";
178178
}

integration_tests/transformations_simple_test.go

+81
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,84 @@ func dotNetValidator() func(t *testing.T, stack integration.RuntimeValidationSta
8585
assert.True(t, foundRes5Child)
8686
}
8787
}
88+
89+
func TestDotNetTransforms(t *testing.T) {
90+
testDotnetProgram(t, &integration.ProgramTestOptions{
91+
Dir: "transformations_remote",
92+
Quick: true,
93+
ExtraRuntimeValidation: Validator,
94+
LocalProviders: []integration.LocalDependency{
95+
{
96+
Package: "testprovider",
97+
Path: "testprovider",
98+
},
99+
},
100+
})
101+
}
102+
103+
func Validator(t *testing.T, stack integration.RuntimeValidationStackInfo) {
104+
randomResName := "testprovider:index:Random"
105+
foundRes1 := false
106+
foundRes2Child := false
107+
foundRes3 := false
108+
foundRes4Child := false
109+
foundRes5 := false
110+
for _, res := range stack.Deployment.Resources {
111+
// "res1" has a transformation which adds additionalSecretOutputs
112+
if res.URN.Name() == "res1" {
113+
foundRes1 = true
114+
assert.Equal(t, res.Type, tokens.Type(randomResName))
115+
assert.Contains(t, res.AdditionalSecretOutputs, resource.PropertyKey("result"))
116+
}
117+
// "res2" has a transformation which adds additionalSecretOutputs to it's
118+
// "child"
119+
if res.URN.Name() == "res2-child" {
120+
foundRes2Child = true
121+
assert.Equal(t, res.Type, tokens.Type(randomResName))
122+
assert.Equal(t, res.Parent.Type(), tokens.Type("my:component:MyComponent"))
123+
assert.Contains(t, res.AdditionalSecretOutputs, resource.PropertyKey("result"))
124+
assert.Contains(t, res.AdditionalSecretOutputs, resource.PropertyKey("length"))
125+
}
126+
// "res3" is impacted by a global stack transformation which sets
127+
// optionalDefault to "stackDefault"
128+
if res.URN.Name() == "res3" {
129+
foundRes3 = true
130+
assert.Equal(t, res.Type, tokens.Type(randomResName))
131+
optionalPrefix := res.Inputs["prefix"]
132+
assert.NotNil(t, optionalPrefix)
133+
assert.Equal(t, "stackDefault", optionalPrefix.(string))
134+
length := res.Inputs["length"]
135+
assert.NotNil(t, length)
136+
// length should be secret
137+
secret, ok := length.(map[string]interface{})
138+
assert.True(t, ok, "length should be a secret")
139+
assert.Equal(t, resource.SecretSig, secret[resource.SigKey])
140+
assert.Contains(t, res.AdditionalSecretOutputs, resource.PropertyKey("result"))
141+
}
142+
// "res4" is impacted by two component parent transformations which set
143+
// optionalDefault to "default1" and then "default2" and also a global stack
144+
// transformation which sets optionalDefault to "stackDefault". The end
145+
// result should be "stackDefault".
146+
if res.URN.Name() == "res4-child" {
147+
foundRes4Child = true
148+
assert.Equal(t, res.Type, tokens.Type(randomResName))
149+
assert.Equal(t, res.Parent.Type(), tokens.Type("my:component:MyComponent"))
150+
optionalPrefix := res.Inputs["prefix"]
151+
assert.NotNil(t, optionalPrefix)
152+
assert.Equal(t, "stackDefault", optionalPrefix.(string))
153+
}
154+
// "res5" should have mutated the length
155+
if res.URN.Name() == "res5" {
156+
foundRes5 = true
157+
assert.Equal(t, res.Type, tokens.Type(randomResName))
158+
length := res.Inputs["length"]
159+
assert.NotNil(t, length)
160+
assert.Equal(t, 20.0, length.(float64))
161+
}
162+
}
163+
assert.True(t, foundRes1)
164+
assert.True(t, foundRes2Child)
165+
assert.True(t, foundRes3)
166+
assert.True(t, foundRes4Child)
167+
assert.True(t, foundRes5)
168+
}

0 commit comments

Comments
 (0)