Skip to content

Commit

Permalink
Avoid calling invokes with dependencies on unknown resources (#441)
Browse files Browse the repository at this point in the history
.NET implementation of pulumi/pulumi#18133

DependsOn for resources is an ordering constraint for register resource
calls. If a resource R1 depends on a resource R2, the register resource
call for R2 will happen after R1. This is ensured by awaiting the URN
for each resource dependency before calling register resource.

For invokes, this causes a problem when running under preview. During
preview, register resource immediately returns with the URN, however
this does not tell us if the resource "exists".

Instead of waiting for the dependency's URN, we wait for the ID. This
tells us that whether a physical resource exists (if the state is in
sync), and we can avoid calling the invoke when it is unknown.

The following example fails without this change:

```csharp
using Pulumi;
using Pulumi.Gcp.Organizations;
using Pulumi.Gcp.Compute;
using System.Collections.Generic;

return await Deployment.RunAsync(async () =>
{
    var config = new Config();
    var billingAccountId = config.Require("billing-account");

    var billingAccount = await GetBillingAccount.InvokeAsync(new GetBillingAccountArgs
    {
        BillingAccount = billingAccountId
    });

    var project = new Project("project", new ProjectArgs
    {
        BillingAccount = billingAccount.Id,
        Name = "project-nodejs",
        AutoCreateNetwork = false,
        DeletionPolicy = "DELETE",
    });

    var zones = GetZones.Invoke(new GetZonesInvokeArgs
    {
        Project = project.ProjectId,
        Region = "us-central1"
    }, new InvokeOutputOptions
    {
        DependsOn = { project }
    });

    return new Dictionary<string, object?>
    {
        ["zones"] = zones,
    };
});
```
  • Loading branch information
julienp authored Jan 7, 2025
1 parent a673844 commit 70f16dd
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 6 deletions.
6 changes: 6 additions & 0 deletions .changes/unreleased/bug-fixes-441.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
component: sdk
kind: bug-fixes
body: Avoid calling invokes with dependencies on unknown resources
time: 2025-01-06T14:03:11.053995+01:00
custom:
PR: "441"
51 changes: 49 additions & 2 deletions sdk/Pulumi.Tests/Deployment/DeploymentInvokeDependsOnTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class DeploymentInvokeDependsOnTests
public async Task DeploymentInvokeDependsOn()
{

var mocks = new InvokeMocks();
var mocks = new InvokeMocks(dryRun: false);
var testOptions = new TestOptions();
var (resources, outputs) = await Deployment.TestAsync(mocks, testOptions, () =>
{
Expand Down Expand Up @@ -54,6 +54,40 @@ public async Task DeploymentInvokeDependsOn()
}
}

[Fact]
public async Task DeploymentInvokeDependsOnUnknown()
{

var mocks = new InvokeMocks(dryRun: true);
var testOptions = new TestOptions();
var (resources, outputs) = await Deployment.TestAsync(mocks, testOptions, () =>
{
var resource = new MyCustomResource("some-resource", null, new CustomResourceOptions());
var deps = new InputList<Resource>();
deps.Add(resource);

var resultOutput = TestFunction.Invoke(new FunctionArgs(), new InvokeOutputOptions { DependsOn = deps });

return new Dictionary<string, object?>
{
["functionResult"] = resultOutput,
};
});


if (outputs["functionResult"] is Output<FunctionResult> functionResult)
{

var dataTask = await functionResult.DataTask.ConfigureAwait(false);
Assert.False(dataTask.IsKnown);
}
else
{
throw new Exception($"Expected result to be of type Output<FunctionResult>");
}
}


public sealed class MyArgs : ResourceArgs { }

[ResourceType("test:DeploymentInvokeDependsOnTests:resource", null)]
Expand Down Expand Up @@ -89,7 +123,12 @@ public static Output<FunctionResult> Invoke(FunctionArgs args, InvokeOptions? op
class InvokeMocks : IMocks
{
public bool Resolved = false;
public bool DryRun { get; }

public InvokeMocks(bool dryRun)
{
DryRun = dryRun;
}

public Task<object> CallAsync(MockCallArgs args)
{
Expand All @@ -103,7 +142,15 @@ public Task<object> CallAsync(MockCallArgs args)
{
await Task.Delay(3000);
Resolved = true;
return ("id", new Dictionary<string, object>());
if (DryRun)
{

return (null, new Dictionary<string, object>());
}
else
{
return ("id", new Dictionary<string, object>());
}
}
}
}
Expand Down
24 changes: 22 additions & 2 deletions sdk/Pulumi/Deployment/Deployment_Invoke.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,29 @@ private async Task<OutputData<T>> RawInvoke<T>(
var deps = outputOptions.DependsOn;
if (deps != null)
{
// Wait for all the resource dependencies from dependsOn to be available before we call the invoke
// The direct dependencies of the invoke.
var resourceList = await GatherExplicitDependenciesAsync(deps).ConfigureAwait(false);
await GetAllTransitivelyReferencedResourceUrnsAsync(resourceList.ToHashSet()).ConfigureAwait(false);
// The expanded set of dependencies, including children of components.
var expandedResourceList = GetAllTransitivelyReferencedResources(resourceList.ToHashSet());
// If we depend on any CustomResources, we need to ensure that their
// ID is known before proceeding. If it is not known, we will return
// an unknown result.
foreach (var resource in expandedResourceList)
{
// check if it's an instance of CustomResource
if (resource is CustomResource customResource)
{
var idData = await customResource.Id.DataTask.ConfigureAwait(false);
if (!idData.IsKnown)
{
return new OutputData<T>(resources: ImmutableHashSet<Resource>.Empty,
value: default!,
isKnown: false,
isSecret: false);
}

}
}
resourceDependencies.UnionWith(resourceList);
}
}
Expand Down
10 changes: 8 additions & 2 deletions sdk/Pulumi/Deployment/Deployment_Prepare.cs
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@ private static (Resource? parent, Input<string>? urn) GetParentInfo(Resource? de
private static Task<ImmutableArray<Resource>> GatherExplicitDependenciesAsync(InputList<Resource> resources)
=> resources.ToOutput().GetValueAsync(whenUnknown: ImmutableArray<Resource>.Empty);

internal static async Task<HashSet<string>> GetAllTransitivelyReferencedResourceUrnsAsync(
internal static IEnumerable<Resource> GetAllTransitivelyReferencedResources(
HashSet<Resource> resources)
{
// Go through 'resources', but transitively walk through **Component** resources, collecting any
Expand Down Expand Up @@ -559,7 +559,7 @@ internal static async Task<HashSet<string>> GetAllTransitivelyReferencedResource
// * Comp3 and Cust5 because Comp3 is a child of a remote component resource
var transitivelyReachableResources = GetTransitivelyReferencedChildResourcesOfComponentResources(resources);

var transitivelyReachableCustomResources = transitivelyReachableResources.Where(res =>
return transitivelyReachableResources.Where(res =>
{
switch (res)
{
Expand All @@ -568,6 +568,12 @@ internal static async Task<HashSet<string>> GetAllTransitivelyReferencedResource
default: return false; // Unreachable
}
});
}

internal static async Task<HashSet<string>> GetAllTransitivelyReferencedResourceUrnsAsync(
HashSet<Resource> resources)
{
var transitivelyReachableCustomResources = GetAllTransitivelyReferencedResources(resources);
var tasks = transitivelyReachableCustomResources.Select(r => r.Urn.GetValueAsync(whenUnknown: ""));
var urns = await Task.WhenAll(tasks).ConfigureAwait(false);
return new HashSet<string>(urns.Where(urn => !string.IsNullOrEmpty(urn)));
Expand Down

0 comments on commit 70f16dd

Please sign in to comment.