* Fix 8322

* Untabify

* Untabify again

* Yet More untabify

* More untabify

* Final untabify

* Add CHANGELOG_PENDING

* Apply suggestions from code review

Co-authored-by: Justin Van Patten <jvp@justinvp.com>

* PR feedback

Co-authored-by: Justin Van Patten <jvp@justinvp.com>
This commit is contained in:
Anton Tayanovskyy 2021-11-08 10:45:26 -05:00 committed by GitHub
parent 99fdad0ed9
commit e37892ac4a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 209 additions and 23 deletions

View file

@ -3,5 +3,9 @@
### Bug Fixes
- [sdk/dotnet] - Fixes failing preview for programs that call data
sources (`F.Invoke`) with unknown outputs
[#8339](https://github.com/pulumi/pulumi/pull/8339)
- [programgen/go] - Don't change imported resource names.
[#8353](https://github.com/pulumi/pulumi/pull/8353)

View file

@ -17,6 +17,18 @@ namespace Pulumi.Tests.Mocks
}
}
public sealed class GetRoleInvokeArgs : Pulumi.InvokeArgs
{
/// <summary>
/// The friendly IAM role name to match.
/// </summary>
[Input("name", required: true)]
public Input<string> Name { get; set; } = null!;
public GetRoleInvokeArgs()
{
}
}
[OutputType]
public sealed class GetRoleResult
@ -42,5 +54,8 @@ namespace Pulumi.Tests.Mocks
{
public static Task<GetRoleResult> InvokeAsync(GetRoleArgs args, InvokeOptions? options = null)
=> Pulumi.Deployment.Instance.InvokeAsync<GetRoleResult>("aws:iam/getRole:getRole", args ?? new GetRoleArgs(), options);
public static Output<GetRoleResult> Invoke(GetRoleInvokeArgs args, InvokeOptions? options = null)
=> Pulumi.Deployment.Instance.Invoke<GetRoleResult>("aws:iam/getRole:getRole", args ?? new GetRoleInvokeArgs(), options);
}
}

View file

@ -0,0 +1,56 @@
// Copyright 2016-2021, Pulumi Corporation
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Pulumi.Serialization;
using Pulumi.Testing;
namespace Pulumi.Tests.Mocks
{
/// <summary>
/// Supports testing that Invoke gets skippped when passed unknown inputs,
/// instead of failing and breaking preview.
///
/// See pulumi/pulumi#8322.
/// </summary>
public sealed class Issue8322
{
public class ReproStack : Stack
{
[Output("result")]
public Output<string> Result { get; private set; }
public ReproStack()
{
// First we need an unknown output (in preview). Use
// Instance here as Output.CreateUnknown is not public.
var instance = new Instance("instance1", new InstanceArgs());
var unknown = instance.PublicIp;
// To reproduce the bug, we call an Invoke with unknowns.
var result = GetRole.Invoke(new GetRoleInvokeArgs() { Name = unknown });
this.Result = result.Apply(r => r.Id);
}
}
public class ReproMocks : IMocks
{
public Task<object> CallAsync(MockCallArgs args)
{
throw new Exception("CallAsync should not be called");
}
public Task<(string? id, object state)> NewResourceAsync(MockResourceArgs args)
=> args.Type switch
{
"aws:ec2/instance:Instance" => Task.FromResult<(string?, object)>(
("i-1234567890abcdef0",
new Dictionary<string, object>
{
{ "publicIp", Constants.UnknownValue },
})),
_ => throw new Exception($"Unknown resource {args.Type}")
};
}
}
}

View file

@ -125,6 +125,18 @@ namespace Pulumi.Tests.Mocks
Assert.Contains("Grpc.Core.RpcException: Status(StatusCode=\"Unknown\", Detail=\"error code 404\")", exception!.Message);
}
[Fact]
public async Task TestInvokeToleratesUnknownsInPreview()
{
var resources = await Deployment.TestAsync<Issue8322.ReproStack>(
new Issue8322.ReproMocks(),
new TestOptions() { IsPreview = true }
);
var stack = resources.OfType<Issue8322.ReproStack>().Single();
var result = await stack.Result.GetValueAsync(whenUnknown: "unknown!");
Assert.Equal("unknown!", result);
}
[Fact]
public async Task TestStackWithInvalidSchema()
{

View file

@ -55,7 +55,22 @@ namespace Pulumi
// the provider fully responsible for depdendency
// tracking, which is a good future direction also for
// `Invoke`.
var result = await InvokeRawAsync(token, args, options).ConfigureAwait(false);
var keepResources = await this.MonitorSupportsResourceReferences().ConfigureAwait(false);
var serializedArgs = await SerializeInvokeArgs(token, args, keepResources);
// Short-circuit actually invoking if `Unknowns` are
// present in `args`, otherwise preview can break.
if (Serializer.ContainsUnknowns(serializedArgs.PropertyValues))
{
return new OutputData<T>(resources: ImmutableHashSet<Resource>.Empty,
value: default!,
isKnown: false,
isSecret: false);
}
var protoArgs = serializedArgs.ToSerializationResult();
var result = await InvokeRawAsync(token, protoArgs, options).ConfigureAwait(false);
var data = Converter.ConvertValue<T>(err => Log.Warn(err), $"{token} result",
new Value { StructValue = result.Serialized });
var resources = ImmutableHashSet.CreateRange(
@ -81,27 +96,8 @@ namespace Pulumi
return data.Value;
}
private async Task<SerializationResult> InvokeRawAsync(string token, InvokeArgs args, InvokeOptions? options)
private async Task<SerializationResult> InvokeRawAsync(string token, SerializationResult argsSerializationResult, InvokeOptions? options)
{
var label = $"Invoking function: token={token} asynchronously";
Log.Debug(label);
// Be resilient to misbehaving callers.
// ReSharper disable once ConstantNullCoalescingCondition
args ??= InvokeArgs.Empty;
// Wait for all values to be available, and then perform the RPC.
var argsDict = await args.ToDictionaryAsync().ConfigureAwait(false);
var keepResources = await this.MonitorSupportsResourceReferences().ConfigureAwait(false);
var argsSerializationResult = await SerializeFilteredPropertiesAsync(
label: $"invoke:{token}",
args: argsDict,
acceptKey: key => true,
keepResources: keepResources
).ConfigureAwait(false);
var serialized = argsSerializationResult.Serialized;
Log.Debug($"Invoke RPC prepared: token={token}" +
@ -137,6 +133,33 @@ namespace Pulumi
return new SerializationResult(result.Return, argsSerializationResult.PropertyToDependentResources);
}
private async Task<RawSerializationResult> SerializeInvokeArgs(string token, InvokeArgs args, bool keepResources)
{
Log.Debug($"Invoking function: token={token} asynchronously");
// Be resilient to misbehaving callers.
// ReSharper disable once ConstantNullCoalescingCondition
args ??= InvokeArgs.Empty;
// Wait for all values to be available.
var argsDict = await args.ToDictionaryAsync().ConfigureAwait(false);
return await SerializeFilteredPropertiesRawAsync(
label: $"invoke:{token}",
args: argsDict,
acceptKey: key => true,
keepResources: keepResources
).ConfigureAwait(false);
}
private async Task<SerializationResult> InvokeRawAsync(string token, InvokeArgs args, InvokeOptions? options)
{
var keepResources = await this.MonitorSupportsResourceReferences().ConfigureAwait(false);
var argsSerializationRawResult = await SerializeInvokeArgs(token, args, keepResources);
var argsSerializationResult = argsSerializationRawResult.ToSerializationResult();
return await InvokeRawAsync(token, argsSerializationResult, options);
}
private static ProviderResource? GetProvider(string token, InvokeOptions? options)
=> options?.Provider ?? options?.Parent?.GetProvider(token);

View file

@ -40,6 +40,17 @@ namespace Pulumi
/// </summary>
private static async Task<SerializationResult> SerializeFilteredPropertiesAsync(
string label, IDictionary<string, object?> args, Predicate<string> acceptKey, bool keepResources)
{
var result = await SerializeFilteredPropertiesRawAsync(label, args, acceptKey, keepResources);
return result.ToSerializationResult();
}
/// <summary>
/// Acts as `SerializeFilteredPropertiesAsync` without the
/// last step of encoding the value into a Protobuf form.
/// </summary>
private static async Task<RawSerializationResult> SerializeFilteredPropertiesRawAsync(
string label, IDictionary<string, object?> args, Predicate<string> acceptKey, bool keepResources)
{
var propertyToDependentResources = ImmutableDictionary.CreateBuilder<string, HashSet<Resource>>();
var result = ImmutableDictionary.CreateBuilder<string, object>();
@ -59,8 +70,8 @@ namespace Pulumi
}
}
return new SerializationResult(
Serializer.CreateStruct(result.ToImmutable()),
return new RawSerializationResult(
result.ToImmutable(),
propertyToDependentResources.ToImmutable());
}
@ -85,5 +96,24 @@ namespace Pulumi
propertyToDependentResources = PropertyToDependentResources;
}
}
private readonly struct RawSerializationResult
{
public readonly ImmutableDictionary<string, object> PropertyValues;
public readonly ImmutableDictionary<string, HashSet<Resource>> PropertyToDependentResources;
public RawSerializationResult(
ImmutableDictionary<string, object> propertyValues,
ImmutableDictionary<string, HashSet<Resource>> propertyToDependentResources)
{
PropertyValues = propertyValues;
PropertyToDependentResources = propertyToDependentResources;
}
public SerializationResult ToSerializationResult()
=> new SerializationResult(
Serializer.CreateStruct(PropertyValues),
PropertyToDependentResources);
}
}
}

View file

@ -0,0 +1,25 @@
// Copyright 2016-2021, Pulumi Corporation
using System;
using System.Collections.Immutable;
namespace Pulumi.Serialization
{
internal static class ImmutableDictionaryExtensions
{
public static bool AnyValues<TKey, TValue>(
this ImmutableDictionary<TKey, TValue> immutableDictionary,
Func<TValue, bool> predicate)
where TKey : notnull
{
foreach (var (_, val) in immutableDictionary)
{
if (predicate(val))
{
return true;
}
}
return false;
}
}
}

View file

@ -385,6 +385,27 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output:
_ => throw new InvalidOperationException("Unsupported value when converting to protobuf: " + value.GetType().FullName),
};
/// <summary>
/// Detects encoded `Unknown` values in objects that conform
/// to the grammar returned by `SerializeAsync`.
///
/// This possibly needs to be revisited to detect `Unknown`
/// values before `SerializeAsync` converts them, in the more
/// generic Output representation.
/// </summary>
internal static bool ContainsUnknowns(object? value)
=> value switch
{
null => false,
int _ => false,
double d => false,
bool b => false,
string s => s == Constants.UnknownValue,
ImmutableArray<object> list => list.Any(v => ContainsUnknowns(v)),
ImmutableDictionary<string, object> dict => dict.AnyValues(v => ContainsUnknowns(v)),
_ => throw new InvalidOperationException("Unsupported value when converting to protobuf: " + value.GetType().FullName),
};
/// <summary>
/// Given a <see cref="ImmutableDictionary{TKey, TValue}"/> produced by <see cref="SerializeAsync"/>,
/// produces the equivalent <see cref="Struct"/> that can be passed to the Pulumi engine.