Fix null exceptions when reading unknown outputs (#7762)

* Fix null exceptions when reading unknown outputs

* Fix test compilation

* Add a test reproducing the actual problem

* Revert the change in behavior that was not clearny an improvement

* Unique resource UUID

* Add a CHANGELOG entry
This commit is contained in:
Anton Tayanovskyy 2021-08-17 09:30:54 -04:00 committed by GitHub
parent 065ae27b91
commit 2223c6b8b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 122 additions and 37 deletions

View file

@ -2,3 +2,6 @@
### Bug Fixes
- [sdk/dotnet] - Fix an exception when passing an unknown `Output` to
the `DependsOn` resource option.
[#7762](https://github.com/pulumi/pulumi/pull/7762)

View file

@ -0,0 +1,79 @@
// Copyright 2016-2021, Pulumi Corporation
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Xunit;
using Pulumi.Testing;
using Pulumi.Tests.Mocks;
namespace Pulumi.Tests
{
public class DeploymentResourceDependencyGatheringTests
{
[Fact]
public async Task DeploysResourcesWithUnknownDependsOn()
{
var deployResult = await Deployment.TryTestAsync<DeploysResourcesWithUnknownDependsOnStack>(
new EmptyMocks(isPreview: true),
new TestOptions()
{
IsPreview = true,
});
Assert.Null(deployResult.Exception);
}
class DeploysResourcesWithUnknownDependsOnStack : Stack
{
public DeploysResourcesWithUnknownDependsOnStack()
{
new MyCustomResource("r1", null, new CustomResourceOptions()
{
DependsOn = Output<Resource[]>.CreateUnknown(new Resource[]{}),
});
}
}
public sealed class MyArgs : ResourceArgs
{
}
[ResourceType("test:DeploymentResourceDependencyGatheringTests:resource", null)]
private class MyCustomResource : CustomResource
{
public MyCustomResource(string name, MyArgs? args, CustomResourceOptions? options = null)
: base("test:DeploymentResourceDependencyGatheringTests:resource", name, args ?? new MyArgs(), options)
{
}
}
class EmptyMocks : IMocks
{
public bool IsPreview { get; private set; }
public EmptyMocks(bool isPreview)
{
this.IsPreview = isPreview;
}
public Task<object> CallAsync(MockCallArgs args)
{
return Task.FromResult<object>(args);
}
public Task<(string? id, object state)> NewResourceAsync(MockResourceArgs args)
{
if (args.Type == "test:DeploymentResourceDependencyGatheringTests:resource")
{
return Task.FromResult<(string?, object)>((this.IsPreview ? null : "id",
new Dictionary<string, object>()));
}
throw new Exception($"Unknown resource {args.Type}");
}
}
}
}

View file

@ -23,7 +23,7 @@ namespace Pulumi.Tests
Assert.IsType<RunException>(deployResult.Exception!);
Assert.Contains("Deliberate test error", deployResult.Exception!.Message);
var stack = (TerminatesEarlyOnExceptionStack)deployResult.Resources[0];
Assert.False(stack.SlowOutput.GetValueAsync().IsCompleted);
Assert.False(stack.SlowOutput.GetValueAsync(whenUnknown: default!).IsCompleted);
}
class TerminatesEarlyOnExceptionStack : Stack

View file

@ -36,7 +36,7 @@ namespace Pulumi.Tests.Mocks
var instance = resources.OfType<Instance>().FirstOrDefault();
Assert.NotNull(instance);
var ip = await instance!.PublicIp.GetValueAsync();
var ip = await instance!.PublicIp.GetValueAsync(whenUnknown: default!);
Assert.Equal("203.0.113.12", ip);
}
@ -48,10 +48,10 @@ namespace Pulumi.Tests.Mocks
var myCustom = resources.OfType<MyCustom>().FirstOrDefault();
Assert.NotNull(myCustom);
var instance = await myCustom!.Instance.GetValueAsync();
var instance = await myCustom!.Instance.GetValueAsync(whenUnknown: default!);
Assert.IsType<Instance>(instance);
var ip = await instance.PublicIp.GetValueAsync();
var ip = await instance.PublicIp.GetValueAsync(whenUnknown: default!);
Assert.Equal("203.0.113.12", ip);
}
@ -63,7 +63,7 @@ namespace Pulumi.Tests.Mocks
var stack = resources.OfType<MyStack>().FirstOrDefault();
Assert.NotNull(stack);
var ip = await stack!.PublicIp.GetValueAsync();
var ip = await stack!.PublicIp.GetValueAsync(whenUnknown: default!);
Assert.Equal("203.0.113.12", ip);
}
}

View file

@ -1,4 +1,4 @@
// Copyright 2016-2019, Pulumi Corporation
// Copyright 2016-2021, Pulumi Corporation
using System;
using System.Collections;
@ -147,7 +147,8 @@ namespace Pulumi
public async IAsyncEnumerator<Input<T>> GetAsyncEnumerator(CancellationToken cancellationToken)
{
var data = await _outputValue.GetValueAsync().ConfigureAwait(false);
var data = await _outputValue.GetValueAsync(whenUnknown: ImmutableArray<T>.Empty)
.ConfigureAwait(false);
foreach (var value in data)
{
yield return value;

View file

@ -1,9 +1,10 @@
// Copyright 2016-2019, Pulumi Corporation
// Copyright 2016-2021, Pulumi Corporation
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
namespace Pulumi
@ -64,7 +65,7 @@ namespace Pulumi
/// <summary>
/// Merge two instances of <see cref="InputMap{V}"/>. Returns a new <see cref="InputMap{V}"/>
/// without modifying any of the arguments.
/// without modifying any of the arguments.
/// <para/>If both maps contain the same key, the value from the second map takes over.
/// </summary>
/// <param name="first">The first <see cref="InputMap{V}"/>. Has lower priority in case of
@ -113,7 +114,8 @@ namespace Pulumi
public async IAsyncEnumerator<Input<KeyValuePair<string, V>>> GetAsyncEnumerator(CancellationToken cancellationToken)
{
var data = await _outputValue.GetValueAsync().ConfigureAwait(false);
var data = await _outputValue.GetValueAsync(whenUnknown: ImmutableDictionary<string, V>.Empty)
.ConfigureAwait(false);
foreach (var value in data)
{
yield return value;

View file

@ -1,4 +1,4 @@
// Copyright 2016-2019, Pulumi Corporation
// Copyright 2016-2021, Pulumi Corporation
using System;
using System.Collections.Generic;
@ -148,10 +148,10 @@ namespace Pulumi
}
}
internal async Task<T> GetValueAsync()
internal async Task<T> GetValueAsync(T whenUnknown)
{
var data = await DataTask.ConfigureAwait(false);
return data.Value;
return data.IsKnown ? data.Value : whenUnknown;
}
async Task<ImmutableHashSet<Resource>> IOutput.GetResourcesAsync()

View file

@ -1,4 +1,4 @@
// Copyright 2016-2020, Pulumi Corporation
// Copyright 2016-2021, Pulumi Corporation
using System;
using System.Collections.Immutable;
@ -9,7 +9,7 @@ namespace Pulumi.Utilities
/// <summary>
/// Allows extracting some internal insights about an instance of
/// <see cref="Output{T}"/>.
///
///
/// Danger: these utilities are intended for use in test and
/// debugging scenarios. In normal Pulumi programs, please
/// consider using `.Apply` instead to chain `Output{T}`
@ -61,7 +61,7 @@ namespace Pulumi.Utilities
/// </summary>
/// <param name="output">The <see cref="Output{T}"/> to evaluate.</param>
public static Task<T> GetValueAsync<T>(Output<T> output)
=> output.GetValueAsync();
=> output.GetValueAsync(whenUnknown: default!);
/// <summary>
/// Retrieve a set of resources that the given output depends on.

View file

@ -1,4 +1,4 @@
// Copyright 2016-2019, Pulumi Corporation
// Copyright 2016-2021, Pulumi Corporation
using System;
using System.Threading;
@ -141,7 +141,7 @@ namespace Pulumi
{
try
{
return await resource.Urn.GetValueAsync().ConfigureAwait(false);
return await resource.Urn.GetValueAsync(whenUnknown: "").ConfigureAwait(false);
}
catch
{

View file

@ -40,7 +40,7 @@ namespace Pulumi
// If no parent was provided, parent to the root resource.
LogExcessive($"Getting parent urn: t={type}, name={name}, custom={custom}, remote={remote}");
var parentUrn = options.Parent != null
? await options.Parent.Urn.GetValueAsync().ConfigureAwait(false)
? await options.Parent.Urn.GetValueAsync(whenUnknown: default!).ConfigureAwait(false)
: await GetRootResourceAsync(type).ConfigureAwait(false);
LogExcessive($"Got parent urn: t={type}, name={name}, custom={custom}, remote={remote}");
@ -97,8 +97,8 @@ namespace Pulumi
var uniqueAliases = new HashSet<string>();
foreach (var alias in res._aliases)
{
var aliasVal = await alias.ToOutput().GetValueAsync().ConfigureAwait(false);
if (uniqueAliases.Add(aliasVal))
var aliasVal = await alias.ToOutput().GetValueAsync(whenUnknown: "").ConfigureAwait(false);
if (aliasVal != "" && uniqueAliases.Add(aliasVal))
{
aliases.Add(aliasVal);
}
@ -121,7 +121,7 @@ namespace Pulumi
}
private static Task<ImmutableArray<Resource>> GatherExplicitDependenciesAsync(InputList<Resource> resources)
=> resources.ToOutput().GetValueAsync();
=> resources.ToOutput().GetValueAsync(whenUnknown: ImmutableArray<Resource>.Empty);
private static async Task<HashSet<string>> GetAllTransitivelyReferencedResourceUrnsAsync(
HashSet<Resource> resources)
@ -159,9 +159,9 @@ namespace Pulumi
default: return false; // Unreachable
}
});
var tasks = transitivelyReachableCustomResources.Select(r => r.Urn.GetValueAsync());
var tasks = transitivelyReachableCustomResources.Select(r => r.Urn.GetValueAsync(whenUnknown: ""));
var urns = await Task.WhenAll(tasks).ConfigureAwait(false);
return new HashSet<string>(urns);
return new HashSet<string>(urns.Where(urn => !string.IsNullOrEmpty(urn)));
}
/// <summary>

View file

@ -1,4 +1,4 @@
// Copyright 2016-2019, Pulumi Corporation
// Copyright 2016-2021, Pulumi Corporation
using System;
using System.Collections.Immutable;
@ -46,16 +46,16 @@ namespace Pulumi
"pulumi:pulumi:getResource",
new GetResourceInvokeArgs {Urn = options.Urn},
new InvokeOptions()).ConfigureAwait(false);
var urn = result.Fields["urn"].StringValue;
var id = result.Fields["id"].StringValue;
var state = result.Fields["state"].StructValue;
return (urn, id, state, ImmutableDictionary<string, ImmutableHashSet<Resource>>.Empty);
}
if (options.Id != null)
{
var id = await options.Id.ToOutput().GetValueAsync().ConfigureAwait(false);
var id = await options.Id.ToOutput().GetValueAsync(whenUnknown: "").ConfigureAwait(false);
if (!string.IsNullOrEmpty(id))
{
if (!(resource is CustomResource))
@ -74,7 +74,7 @@ namespace Pulumi
}
/// <summary>
/// Calls <see cref="ReadOrRegisterResourceAsync"/> then completes all the
/// Calls <see cref="ReadOrRegisterResourceAsync"/> then completes all the
/// <see cref="IOutputCompletionSource"/> sources on the <paramref name="resource"/> with
/// the results of it.
/// </summary>

View file

@ -1,4 +1,4 @@
// Copyright 2016-2019, Pulumi Corporation
// Copyright 2016-2021, Pulumi Corporation
using System.Collections.Generic;
using System.Threading.Tasks;
@ -25,8 +25,8 @@ namespace Pulumi
// The registration could very well still be taking place, so we will need to wait for its URN.
// Additionally, the output properties might have come from other resources, so we must await those too.
var urn = await resource.Urn.GetValueAsync().ConfigureAwait(false);
var props = await outputs.GetValueAsync().ConfigureAwait(false);
var urn = await resource.Urn.GetValueAsync(whenUnknown: default!).ConfigureAwait(false);
var props = await outputs.GetValueAsync(whenUnknown: default!).ConfigureAwait(false);
var serialized = await SerializeAllPropertiesAsync(
opLabel, props, await MonitorSupportsResourceReferences().ConfigureAwait(false)).ConfigureAwait(false);

View file

@ -1,4 +1,4 @@
// Copyright 2016-2019, Pulumi Corporation
// Copyright 2016-2021, Pulumi Corporation
using System;
using System.Threading.Tasks;
@ -38,7 +38,7 @@ namespace Pulumi
private async Task<string> SetRootResourceWorkerAsync(Stack stack)
{
var resUrn = await stack.Urn.GetValueAsync().ConfigureAwait(false);
var resUrn = await stack.Urn.GetValueAsync(whenUnknown: default!).ConfigureAwait(false);
await this.Engine.SetRootResourceAsync(new SetRootResourceRequest
{
Urn = resUrn,

View file

@ -1,4 +1,4 @@
// Copyright 2016-2019, Pulumi Corporation
// Copyright 2016-2021, Pulumi Corporation
using System.Threading.Tasks;
using Pulumi.Serialization;
@ -53,8 +53,8 @@ namespace Pulumi
if (provider._registrationId == null)
{
var providerUrn = await provider.Urn.GetValueAsync().ConfigureAwait(false);
var providerId = await provider.Id.GetValueAsync().ConfigureAwait(false);
var providerUrn = await provider.Urn.GetValueAsync(whenUnknown: default!).ConfigureAwait(false);
var providerId = await provider.Id.GetValueAsync(whenUnknown: default!).ConfigureAwait(false);
if (string.IsNullOrEmpty(providerId))
{
providerId = Constants.UnknownValue;