Skip to content

Commit ee8fa12

Browse files
julienpjustinvp
andauthored
Allow specifying dependencies for output invokes (#412)
Provider functions that take inputs as arguments, and return an output (aka output form invokes), now allow specifying a depends_on option. This allows programs to ensure that invokes are executed after things they depend on, similar to the depdends_on resource option. Fixes pulumi/pulumi#17753 --------- Co-authored-by: Justin Van Patten <jvp@justinvp.com>
1 parent 01756d1 commit ee8fa12

File tree

7 files changed

+337
-1
lines changed

7 files changed

+337
-1
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: Improvements
3+
body: Allow specifying dependencies for output invokes
4+
time: 2024-11-27T14:37:33.985033+01:00
5+
custom:
6+
PR: "412"
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright 2024, Pulumi Corporation
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Threading.Tasks;
7+
8+
using Microsoft.Extensions.Logging;
9+
using Xunit;
10+
11+
using Pulumi.Testing;
12+
using Pulumi.Tests.Mocks;
13+
14+
namespace Pulumi.Tests
15+
{
16+
public class DeploymentInvokeDependsOnTests
17+
{
18+
[Fact]
19+
public async Task DeploymentInvokeDependsOn()
20+
{
21+
22+
var mocks = new InvokeMocks();
23+
var testOptions = new TestOptions();
24+
var (resources, outputs) = await Deployment.TestAsync(mocks, testOptions, () =>
25+
{
26+
var resource = new MyCustomResource("some-resource", null, new CustomResourceOptions());
27+
var deps = new InputList<Resource>();
28+
deps.Add(resource);
29+
30+
var resultOutput = TestFunction.Invoke(new FunctionArgs(), new InvokeOutputOptions { DependsOn = deps }).Apply(result =>
31+
{
32+
Assert.Equal("my value", result.TestValue);
33+
Assert.True(mocks.Resolved); // The resource should have been awaited
34+
return result;
35+
});
36+
37+
return new Dictionary<string, object?>
38+
{
39+
["functionResult"] = resultOutput,
40+
};
41+
});
42+
43+
if (outputs["functionResult"] is Output<FunctionResult> functionResult)
44+
{
45+
46+
// functionResult
47+
48+
var value = await functionResult.GetValueAsync(new FunctionResult(""));
49+
Assert.Equal("my value", value.TestValue);
50+
}
51+
else
52+
{
53+
throw new Exception($"Expected result to be of type Output<FunctionResult>");
54+
}
55+
}
56+
57+
public sealed class MyArgs : ResourceArgs { }
58+
59+
[ResourceType("test:DeploymentInvokeDependsOnTests:resource", null)]
60+
private class MyCustomResource : CustomResource
61+
{
62+
public MyCustomResource(string name, MyArgs? args, CustomResourceOptions? options = null)
63+
: base("test:DeploymentInvokeDependsOnTests:resource", name, args ?? new MyArgs(), options)
64+
{
65+
}
66+
}
67+
68+
public sealed class FunctionArgs : global::Pulumi.InvokeArgs { }
69+
70+
[OutputType]
71+
public sealed class FunctionResult
72+
{
73+
[Output("testValue")]
74+
public string TestValue { get; set; }
75+
76+
[OutputConstructor]
77+
public FunctionResult(string testValue)
78+
{
79+
TestValue = testValue;
80+
}
81+
}
82+
83+
public static class TestFunction
84+
{
85+
public static Output<FunctionResult> Invoke(FunctionArgs args, InvokeOptions? options = null)
86+
=> global::Pulumi.Deployment.Instance.Invoke<FunctionResult>("mypkg::func", args ?? new FunctionArgs(), options);
87+
}
88+
89+
class InvokeMocks : IMocks
90+
{
91+
public bool Resolved = false;
92+
93+
94+
public Task<object> CallAsync(MockCallArgs args)
95+
{
96+
return Task.FromResult<object>(new Dictionary<string, object>()
97+
{
98+
["testValue"] = "my value"
99+
});
100+
}
101+
102+
public async Task<(string? id, object state)> NewResourceAsync(MockResourceArgs args)
103+
{
104+
await Task.Delay(3000);
105+
Resolved = true;
106+
return ("id", new Dictionary<string, object>());
107+
}
108+
}
109+
}
110+
}

sdk/Pulumi/Deployment/DeploymentInstance.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,28 @@ public Output<T> Invoke<T>(
4242
RegisterPackageRequest? registerPackageRequest)
4343
=> _deployment.Invoke<T>(token, args, options, registerPackageRequest);
4444

45+
/// <inheritdoc />
46+
public Output<T> Invoke<T>(
47+
string token,
48+
InvokeArgs args,
49+
InvokeOutputOptions options,
50+
RegisterPackageRequest? registerPackageRequest)
51+
=> _deployment.Invoke<T>(token, args, options, registerPackageRequest);
52+
4553
/// <inheritdoc />
4654
public Output<T> Invoke<T>(
4755
string token,
4856
InvokeArgs args,
4957
InvokeOptions? options = null)
5058
=> _deployment.Invoke<T>(token, args, options, null);
5159

60+
/// <inheritdoc />
61+
public Output<T> Invoke<T>(
62+
string token,
63+
InvokeArgs args,
64+
InvokeOutputOptions options)
65+
=> _deployment.Invoke<T>(token, args, options, null);
66+
5267
/// <inheritdoc />
5368
public Output<T> InvokeSingle<T>(
5469
string token,
@@ -57,13 +72,28 @@ public Output<T> InvokeSingle<T>(
5772
RegisterPackageRequest? registerPackageRequest)
5873
=> _deployment.InvokeSingle<T>(token, args, options, registerPackageRequest);
5974

75+
/// <inheritdoc />
76+
public Output<T> InvokeSingle<T>(
77+
string token,
78+
InvokeArgs args,
79+
InvokeOutputOptions options,
80+
RegisterPackageRequest? registerPackageRequest)
81+
=> _deployment.InvokeSingle<T>(token, args, options, registerPackageRequest);
82+
6083
/// <inheritdoc />
6184
public Output<T> InvokeSingle<T>(
6285
string token,
6386
InvokeArgs args,
6487
InvokeOptions? options = null)
6588
=> _deployment.InvokeSingle<T>(token, args, options, null);
6689

90+
/// <inheritdoc />
91+
public Output<T> InvokeSingle<T>(
92+
string token,
93+
InvokeArgs args,
94+
InvokeOutputOptions options)
95+
=> _deployment.InvokeSingle<T>(token, args, options, null);
96+
6797
/// <inheritdoc />
6898
public Task<T> InvokeAsync<T>(
6999
string token,

sdk/Pulumi/Deployment/Deployment_Invoke.cs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,18 @@ Output<T> IDeployment.Invoke<T>(
5454
RegisterPackageRequest? registerPackageRequest)
5555
=> new Output<T>(RawInvoke<T>(token, args, options, registerPackageRequest));
5656

57+
Output<T> IDeployment.Invoke<T>(
58+
string token,
59+
InvokeArgs args,
60+
InvokeOutputOptions options,
61+
RegisterPackageRequest? registerPackageRequest)
62+
=> new Output<T>(RawInvoke<T>(token, args, options, registerPackageRequest));
63+
5764
Output<T> IDeployment.Invoke<T>(string token, InvokeArgs args, InvokeOptions? options)
5865
=> new Output<T>(RawInvoke<T>(token, args, options, registerPackageRequest: null));
5966

67+
Output<T> IDeployment.Invoke<T>(string token, InvokeArgs args, InvokeOutputOptions options)
68+
=> new Output<T>(RawInvoke<T>(token, args, options, registerPackageRequest: null));
6069

6170

6271
Output<T> IDeployment.InvokeSingle<T>(
@@ -69,13 +78,30 @@ Output<T> IDeployment.InvokeSingle<T>(
6978
return outputResult.Apply(outputs => outputs.Values.First());
7079
}
7180

81+
Output<T> IDeployment.InvokeSingle<T>(
82+
string token,
83+
InvokeArgs args,
84+
InvokeOutputOptions options,
85+
RegisterPackageRequest? registerPackageRequest)
86+
{
87+
var outputResult = new Output<Dictionary<string, T>>(RawInvoke<Dictionary<string, T>>(token, args, options, registerPackageRequest));
88+
return outputResult.Apply(outputs => outputs.Values.First());
89+
}
90+
7291
Output<T> IDeployment.InvokeSingle<T>(string token, InvokeArgs args, InvokeOptions? options)
7392
{
7493
var outputResult = new Output<Dictionary<string, T>>(
7594
RawInvoke<Dictionary<string, T>>(token, args, options, registerPackageRequest: null));
7695
return outputResult.Apply(outputs => outputs.Values.First());
7796
}
7897

98+
Output<T> IDeployment.InvokeSingle<T>(string token, InvokeArgs args, InvokeOutputOptions options)
99+
{
100+
var outputResult = new Output<Dictionary<string, T>>(
101+
RawInvoke<Dictionary<string, T>>(token, args, options, registerPackageRequest: null));
102+
return outputResult.Apply(outputs => outputs.Values.First());
103+
}
104+
79105
private async Task<OutputData<T>> RawInvoke<T>(
80106
string token,
81107
InvokeArgs args,
@@ -128,13 +154,27 @@ private async Task<OutputData<T>> RawInvoke<T>(
128154
isSecret: false);
129155
}
130156

157+
// Resource dependencies that will be added to the Output
158+
var resourceDependencies = new HashSet<Resource>();
159+
if (options is InvokeOutputOptions outputOptions)
160+
{
161+
var deps = outputOptions.DependsOn;
162+
if (deps != null)
163+
{
164+
// Wait for all the resource dependencies from dependsOn to be available before we call the invoke
165+
var resourceList = await GatherExplicitDependenciesAsync(deps).ConfigureAwait(false);
166+
await GetAllTransitivelyReferencedResourceUrnsAsync(resourceList.ToHashSet()).ConfigureAwait(false);
167+
resourceDependencies.UnionWith(resourceList);
168+
}
169+
}
170+
131171
var protoArgs = serializedArgs.ToSerializationResult();
132172
var result = await InvokeRawAsync(token, protoArgs, options, registerPackageRequest).ConfigureAwait(false);
133173
var data = Pulumi.Serialization.Converter.ConvertValue<T>(err => Log.Warn(err), $"{token} result",
134174
new Value { StructValue = result.Serialized });
135175
var resources = ImmutableHashSet.CreateRange(
136176
result.PropertyToDependentResources.Values.SelectMany(r => r)
137-
.Union(data.Resources));
177+
.Union(data.Resources).Union(resourceDependencies));
138178
return new OutputData<T>(resources: resources,
139179
value: data.Value,
140180
isKnown: data.IsKnown,

sdk/Pulumi/Deployment/IDeployment.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,22 @@ Output<T> Invoke<T>(
103103
InvokeArgs args,
104104
InvokeOptions? options = null);
105105

106+
/// <summary>
107+
/// Dynamically invokes the function '<paramref name="token"/>', which is offered by a
108+
/// provider plugin.
109+
/// <para/>
110+
/// The result of <see cref="Invoke{T}(string, InvokeArgs, InvokeOptions)"/> will be a <see cref="Output"/> resolved to the
111+
/// result value of the provider plugin.
112+
/// <para/>
113+
/// The <paramref name="args"/> inputs can be a bag of computed values(including, `T`s,
114+
/// <see cref="Task{TResult}"/>s, <see cref="Output{T}"/>s etc.).
115+
/// </summary>
116+
Output<T> Invoke<T>(
117+
string token,
118+
InvokeArgs args,
119+
InvokeOutputOptions options);
120+
121+
106122
/// <summary>
107123
/// Dynamically invokes the function '<paramref name="token"/>', which is offered by a
108124
/// provider plugin.
@@ -119,6 +135,22 @@ Output<T> Invoke<T>(
119135
InvokeOptions? options,
120136
RegisterPackageRequest? registerPackageRequest);
121137

138+
/// <summary>
139+
/// Dynamically invokes the function '<paramref name="token"/>', which is offered by a
140+
/// provider plugin.
141+
/// <para/>
142+
/// The result of <see cref="Invoke{T}(string, InvokeArgs, InvokeOutputOptions, RegisterPackageRequest)"/> will be a <see cref="Output"/> resolved to the
143+
/// result value of the provider plugin.
144+
/// <para/>
145+
/// The <paramref name="args"/> inputs can be a bag of computed values(including, `T`s,
146+
/// <see cref="Task{TResult}"/>s, <see cref="Output{T}"/>s etc.).
147+
/// </summary>
148+
Output<T> Invoke<T>(
149+
string token,
150+
InvokeArgs args,
151+
InvokeOutputOptions options,
152+
RegisterPackageRequest? registerPackageRequest);
153+
122154
/// <summary>
123155
/// Dynamically invokes the function '<paramref name="token"/>', which is offered by a
124156
/// provider plugin.
@@ -134,6 +166,21 @@ Output<T> InvokeSingle<T>(
134166
InvokeArgs args,
135167
InvokeOptions? options = null);
136168

169+
/// <summary>
170+
/// Dynamically invokes the function '<paramref name="token"/>', which is offered by a
171+
/// provider plugin.
172+
/// <para/>
173+
/// The result of <see cref="InvokeSingle{T}(string, InvokeArgs, InvokeOutputOptions)"/> will be a <see cref="Output"/> resolved to the
174+
/// result value of the provider plugin that returns a bag of properties with a single value that is returned.
175+
/// <para/>
176+
/// The <paramref name="args"/> inputs can be a bag of computed values(including, `T`s,
177+
/// <see cref="Task{TResult}"/>s, <see cref="Output{T}"/>s etc.).
178+
/// </summary>
179+
Output<T> InvokeSingle<T>(
180+
string token,
181+
InvokeArgs args,
182+
InvokeOutputOptions options);
183+
137184
/// <summary>
138185
/// Dynamically invokes the function '<paramref name="token"/>', which is offered by a
139186
/// provider plugin.
@@ -150,6 +197,22 @@ Output<T> InvokeSingle<T>(
150197
InvokeOptions? options,
151198
RegisterPackageRequest? registerPackageRequest);
152199

200+
/// <summary>
201+
/// Dynamically invokes the function '<paramref name="token"/>', which is offered by a
202+
/// provider plugin.
203+
/// <para/>
204+
/// The result of <see cref="InvokeSingle{T}(string, InvokeArgs, InvokeOutputOptions, RegisterPackageRequest)"/> will be a <see cref="Output"/> resolved to the
205+
/// result value of the provider plugin that returns a bag of properties with a single value that is returned.
206+
/// <para/>
207+
/// The <paramref name="args"/> inputs can be a bag of computed values(including, `T`s,
208+
/// <see cref="Task{TResult}"/>s, <see cref="Output{T}"/>s etc.).
209+
/// </summary>
210+
Output<T> InvokeSingle<T>(
211+
string token,
212+
InvokeArgs args,
213+
InvokeOutputOptions options,
214+
RegisterPackageRequest? registerPackageRequest);
215+
153216
/// <summary>
154217
/// Same as <see cref="InvokeAsync{T}(string, InvokeArgs, InvokeOptions)"/>, however the
155218
/// return value is ignored.

sdk/Pulumi/Deployment/InvokeOptions.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,21 @@ public class InvokeOptions
3131
/// </summary>
3232
public string? PluginDownloadURL { get; set; }
3333
}
34+
35+
/// <summary>
36+
/// Options to help control the behavior of <see cref="IDeployment.Invoke{T}(string, InvokeArgs, InvokeOutputOptions, RegisterPackageRequest)"/>.
37+
/// </summary>
38+
public class InvokeOutputOptions : InvokeOptions
39+
{
40+
private InputList<Resource>? _dependsOn;
41+
42+
/// <summary>
43+
/// Optional additional explicit dependencies on resources.
44+
/// </summary>
45+
public InputList<Resource> DependsOn
46+
{
47+
get => _dependsOn ??= new InputList<Resource>();
48+
set => _dependsOn = value;
49+
}
50+
}
3451
}

0 commit comments

Comments
 (0)