From 164a2ec818994db6ac6491fa315a5ed44071a5e8 Mon Sep 17 00:00:00 2001 From: Ian Wahbe Date: Fri, 12 Nov 2021 14:58:34 -0800 Subject: [PATCH] Enable output marshaling in .NET (#8316) --- CHANGELOG_PENDING.md | 3 + sdk/dotnet/Pulumi.Tests/AssertEx.cs | 1 + .../Serialization/ConverterTests.cs | 2 +- .../Serialization/MarshalOutputTests.cs | 141 ++++++++++++++++++ sdk/dotnet/Pulumi/Deployment/Deployment.cs | 13 +- .../Pulumi/Deployment/Deployment_Call.cs | 6 +- .../Pulumi/Deployment/Deployment_Invoke.cs | 3 +- .../Pulumi/Deployment/Deployment_Prepare.cs | 11 +- .../Deployment_RegisterResourceOutputs.cs | 4 +- .../Deployment/Deployment_Serialization.cs | 30 +++- sdk/dotnet/Pulumi/Serialization/Constants.cs | 9 +- .../Pulumi/Serialization/Deserializer.cs | 10 +- sdk/dotnet/Pulumi/Serialization/Serializer.cs | 130 +++++++++++----- 13 files changed, 299 insertions(+), 64 deletions(-) create mode 100644 sdk/dotnet/Pulumi.Tests/Serialization/MarshalOutputTests.cs diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 2bc87c24a..98f445d43 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -7,6 +7,9 @@ - [schema] Add IsOverlay option to disable codegen for particular types [#8338](https://github.com/pulumi/pulumi/pull/8338) +- [sdk/dotnet] - Marshal output values. + [#8316](https://github.com/pulumi/pulumi/pull/8316) + ### Bug Fixes - [engine] - Compute dependents correctly during targeted deletes. diff --git a/sdk/dotnet/Pulumi.Tests/AssertEx.cs b/sdk/dotnet/Pulumi.Tests/AssertEx.cs index 83fee9957..2be65dc25 100644 --- a/sdk/dotnet/Pulumi.Tests/AssertEx.cs +++ b/sdk/dotnet/Pulumi.Tests/AssertEx.cs @@ -1,6 +1,7 @@ // Copyright 2016-2019, Pulumi Corporation using System.Collections.Generic; +using System.Linq; using Xunit; namespace Pulumi.Tests diff --git a/sdk/dotnet/Pulumi.Tests/Serialization/ConverterTests.cs b/sdk/dotnet/Pulumi.Tests/Serialization/ConverterTests.cs index 745d88251..f80b6f910 100644 --- a/sdk/dotnet/Pulumi.Tests/Serialization/ConverterTests.cs +++ b/sdk/dotnet/Pulumi.Tests/Serialization/ConverterTests.cs @@ -19,7 +19,7 @@ namespace Pulumi.Tests.Serialization Fields = { { Constants.SpecialSigKey, new Value { StringValue = Constants.SpecialSecretSig } }, - { Constants.SecretValueName, value }, + { Constants.ValueName, value }, } } }; diff --git a/sdk/dotnet/Pulumi.Tests/Serialization/MarshalOutputTests.cs b/sdk/dotnet/Pulumi.Tests/Serialization/MarshalOutputTests.cs new file mode 100644 index 000000000..2331d0d93 --- /dev/null +++ b/sdk/dotnet/Pulumi.Tests/Serialization/MarshalOutputTests.cs @@ -0,0 +1,141 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Pulumi.Serialization; +using Xunit; + +namespace Pulumi.Tests.Serialization +{ + public class MarshalOutputTests : PulumiTest + { + public static IEnumerable BasicSerializeData => + from value in new object?[] + { + null, + 0.0, + 1.0, + "", + "hi", + ImmutableDictionary.Empty, + ImmutableArray.Empty, + } + from deps in new[] { Array.Empty(), new[] { "fakeURN1", "fakeURN2" } } + from isKnown in new[] { true, false } + from isSecret in new[] { true, false } + select new object[] { value, deps, isKnown, isSecret }; + + [Theory] + [MemberData(nameof(BasicSerializeData))] + public static Task TestBasicSerialize(object? value, string[] deps, bool isKnown, bool isSecret) => RunInNormal(async () => + { + var resources = ImmutableHashSet.CreateRange(deps.Select(d => new DependencyResource(d))); + var data = OutputData.Create(resources, value, isKnown, isSecret); + var input = new Output(Task.FromResult(data)); + + var expected = isKnown && !isSecret && deps.Length == 0 + ? value + : CreateOutputValue(value, isKnown, isSecret, deps); + + var s = new Serializer(excessiveDebugOutput: false); + var actual = await s.SerializeAsync("", input, keepResources: true, keepOutputValues: true); + Assert.Equal(expected, actual); + }); + + public sealed class FooArgs : ResourceArgs + { + [Input("foo")] + public Input? Foo { get; set; } + } + + public sealed class BarArgs : ResourceArgs + { + [Input("foo")] + public Input? Foo { get; set; } + } + + public static IEnumerable SerializeData() => new object[][] + { + new object[] + { + new FooArgs { Foo = "hello" }, + ImmutableDictionary.Empty.Add("foo", "hello") + }, + new object[] + { + new FooArgs { Foo = Output.Create("hello") }, + ImmutableDictionary.Empty.Add("foo", "hello") + }, + new object[] + { + new FooArgs { Foo = Output.CreateSecret("hello") }, + ImmutableDictionary.Empty.Add("foo", CreateOutputValue("hello", isSecret: true)) + }, + new object[] + { + new List> { "hello" }, + ImmutableArray.Empty.Add("hello") + }, + new object[] + { + new List> { Output.Create("hello") }, + ImmutableArray.Empty.Add("hello") + }, + new object[] + { + new List> { Output.CreateSecret("hello") }, + ImmutableArray.Empty.Add(CreateOutputValue("hello", isSecret: true)) + }, + new object[] + { + new Dictionary> { { "foo", "hello" } }, + ImmutableDictionary.Empty.Add("foo", "hello") + }, + new object[] + { + new Dictionary> { { "foo", Output.Create("hello") } }, + ImmutableDictionary.Empty.Add("foo", "hello") + }, + new object[] + { + new Dictionary> { { "foo", Output.CreateSecret("hello") } }, + ImmutableDictionary.Empty.Add("foo", CreateOutputValue("hello", isSecret: true)) + }, + new object[] + { + new BarArgs { Foo = new FooArgs { Foo = "hello" } }, + ImmutableDictionary.Empty.Add("foo", + ImmutableDictionary.Empty.Add("foo", "hello")) + }, + new object[] + { + new BarArgs { Foo = new FooArgs { Foo = Output.Create("hello") } }, + ImmutableDictionary.Empty.Add("foo", + ImmutableDictionary.Empty.Add("foo", "hello")) + }, + }; + + [Theory] + [MemberData(nameof(SerializeData))] + public static Task TestSerialize(object input, object expected) => RunInNormal(async () => + { + var s = new Serializer(excessiveDebugOutput: false); + var actual = await s.SerializeAsync("", input, keepResources: true, keepOutputValues: true); + Assert.Equal(expected, actual); + }); + + private static ImmutableDictionary CreateOutputValue( + object? value, bool isKnown = true, bool isSecret = false, params string[] deps) + { + var b = ImmutableDictionary.CreateBuilder(); + b.Add(Constants.SpecialSigKey, Constants.SpecialOutputValueSig); + if (isKnown) b.Add(Constants.ValueName, value); + if (isSecret) b.Add(Constants.SecretName, isSecret); + if (deps.Length > 0) b.Add(Constants.DependenciesName, deps.ToImmutableArray()); + return b.ToImmutableDictionary(); + } + } +} diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment.cs b/sdk/dotnet/Pulumi/Deployment/Deployment.cs index 994b81bac..9b91e89a8 100644 --- a/sdk/dotnet/Pulumi/Deployment/Deployment.cs +++ b/sdk/dotnet/Pulumi/Deployment/Deployment.cs @@ -192,7 +192,7 @@ namespace Pulumi { if (!this._featureSupport.ContainsKey(feature)) { - var request = new SupportsFeatureRequest {Id = feature }; + var request = new SupportsFeatureRequest { Id = feature }; var response = await this.Monitor.SupportsFeatureAsync(request).ConfigureAwait(false); this._featureSupport[feature] = response.HasSupport; } @@ -203,5 +203,16 @@ namespace Pulumi { return MonitorSupportsFeature("resourceReferences"); } + + /// + /// Check if the monitor supports the "outputValues" feature. + /// + internal Task MonitorSupportsOutputValues() + { + return MonitorSupportsFeature("outputValues"); + } + + // Because the secrets feature predates the Pulumi .NET SDK, we assume + // that the monitor supports secrets. } } diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment_Call.cs b/sdk/dotnet/Pulumi/Deployment/Deployment_Call.cs index fcfc36dc2..6e258b5d6 100644 --- a/sdk/dotnet/Pulumi/Deployment/Deployment_Call.cs +++ b/sdk/dotnet/Pulumi/Deployment/Deployment_Call.cs @@ -53,8 +53,10 @@ namespace Pulumi } var (serialized, argDependencies) = await SerializeFilteredPropertiesAsync( - $"call:{token}", - argsDict, _ => true, keepResources: true).ConfigureAwait(false); + $"call:{token}", + argsDict, _ => true, + keepResources: true, + keepOutputValues: await MonitorSupportsOutputValues().ConfigureAwait(false)).ConfigureAwait(false); Log.Debug($"Call RPC prepared: token={token}" + (_excessiveDebugOutput ? $", obj={serialized}" : "")); diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment_Invoke.cs b/sdk/dotnet/Pulumi/Deployment/Deployment_Invoke.cs index 7e1347656..3b860e1c0 100644 --- a/sdk/dotnet/Pulumi/Deployment/Deployment_Invoke.cs +++ b/sdk/dotnet/Pulumi/Deployment/Deployment_Invoke.cs @@ -148,7 +148,8 @@ namespace Pulumi label: $"invoke:{token}", args: argsDict, acceptKey: key => true, - keepResources: keepResources + keepResources: keepResources, + keepOutputValues: false ).ConfigureAwait(false); } diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment_Prepare.cs b/sdk/dotnet/Pulumi/Deployment/Deployment_Prepare.cs index 3edebd069..b2db07cb5 100644 --- a/sdk/dotnet/Pulumi/Deployment/Deployment_Prepare.cs +++ b/sdk/dotnet/Pulumi/Deployment/Deployment_Prepare.cs @@ -33,7 +33,8 @@ namespace Pulumi await SerializeResourcePropertiesAsync( label, dictionary, - await this.MonitorSupportsResourceReferences().ConfigureAwait(false)).ConfigureAwait(false); + await this.MonitorSupportsResourceReferences().ConfigureAwait(false), + keepOutputValues: remote && await MonitorSupportsOutputValues().ConfigureAwait(false)).ConfigureAwait(false); LogExcessive($"Serialized properties: t={type}, name={name}, custom={custom}, remote={remote}"); // Wait for the parent to complete. @@ -123,7 +124,7 @@ namespace Pulumi private static Task> GatherExplicitDependenciesAsync(InputList resources) => resources.ToOutput().GetValueAsync(whenUnknown: ImmutableArray.Empty); - private static async Task> GetAllTransitivelyReferencedResourceUrnsAsync( + internal static async Task> GetAllTransitivelyReferencedResourceUrnsAsync( HashSet resources) { // Go through 'resources', but transitively walk through **Component** resources, collecting any @@ -152,8 +153,10 @@ namespace Pulumi // * Comp3 and Cust5 because Comp3 is a child of a remote component resource var transitivelyReachableResources = GetTransitivelyReferencedChildResourcesOfComponentResources(resources); - var transitivelyReachableCustomResources = transitivelyReachableResources.Where(res => { - switch (res) { + var transitivelyReachableCustomResources = transitivelyReachableResources.Where(res => + { + switch (res) + { case CustomResource _: return true; case ComponentResource component: return component.remote; default: return false; // Unreachable diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment_RegisterResourceOutputs.cs b/sdk/dotnet/Pulumi/Deployment/Deployment_RegisterResourceOutputs.cs index ad0366828..ae5e1ed4c 100644 --- a/sdk/dotnet/Pulumi/Deployment/Deployment_RegisterResourceOutputs.cs +++ b/sdk/dotnet/Pulumi/Deployment/Deployment_RegisterResourceOutputs.cs @@ -28,8 +28,8 @@ namespace Pulumi 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); + var keepResources = await MonitorSupportsResourceReferences().ConfigureAwait(false); + var serialized = await SerializeAllPropertiesAsync(opLabel, props, keepResources).ConfigureAwait(false); Log.Debug($"RegisterResourceOutputs RPC prepared: urn={urn}" + (_excessiveDebugOutput ? $", outputs={JsonFormatter.Default.Format(serialized)}" : "")); diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment_Serialization.cs b/sdk/dotnet/Pulumi/Deployment/Deployment_Serialization.cs index 22e88177a..35b6efa90 100644 --- a/sdk/dotnet/Pulumi/Deployment/Deployment_Serialization.cs +++ b/sdk/dotnet/Pulumi/Deployment/Deployment_Serialization.cs @@ -20,16 +20,20 @@ namespace Pulumi /// to registerResource. /// private static Task SerializeResourcePropertiesAsync( - string label, IDictionary args, bool keepResources) + string label, IDictionary args, bool keepResources, bool keepOutputValues) { return SerializeFilteredPropertiesAsync( - label, args, key => key != Constants.IdPropertyName && key != Constants.UrnPropertyName, keepResources); + label, args, + key => key != Constants.IdPropertyName && key != Constants.UrnPropertyName, + keepResources, keepOutputValues: keepOutputValues); } private static async Task SerializeAllPropertiesAsync( - string label, IDictionary args, bool keepResources) + string label, IDictionary args, bool keepResources, bool keepOutputValues = false) { - var result = await SerializeFilteredPropertiesAsync(label, args, _ => true, keepResources).ConfigureAwait(false); + var result = await SerializeFilteredPropertiesAsync( + label, args, _ => true, + keepResources, keepOutputValues).ConfigureAwait(false); return result.Serialized; } @@ -38,10 +42,20 @@ namespace Pulumi /// awaiting all interior promises for properties with keys that match the provided filter, /// creating a reasonable POCO object that can be remoted over to registerResource. /// + /// + /// label + /// args + /// acceptKey + /// keepResources + /// + /// Specifies if we should marshal output values. It is the callers + /// responsibility to ensure that the monitor supports the OutputValues + /// feature. + /// private static async Task SerializeFilteredPropertiesAsync( - string label, IDictionary args, Predicate acceptKey, bool keepResources) + string label, IDictionary args, Predicate acceptKey, bool keepResources, bool keepOutputValues) { - var result = await SerializeFilteredPropertiesRawAsync(label, args, acceptKey, keepResources); + var result = await SerializeFilteredPropertiesRawAsync(label, args, acceptKey, keepResources, keepOutputValues); return result.ToSerializationResult(); } @@ -50,7 +64,7 @@ namespace Pulumi /// last step of encoding the value into a Protobuf form. /// private static async Task SerializeFilteredPropertiesRawAsync( - string label, IDictionary args, Predicate acceptKey, bool keepResources) + string label, IDictionary args, Predicate acceptKey, bool keepResources, bool keepOutputValues) { var propertyToDependentResources = ImmutableDictionary.CreateBuilder>(); var result = ImmutableDictionary.CreateBuilder(); @@ -61,7 +75,7 @@ namespace Pulumi { // We treat properties with null values as if they do not exist. var serializer = new Serializer(_excessiveDebugOutput); - var v = await serializer.SerializeAsync($"{label}.{key}", val, keepResources).ConfigureAwait(false); + var v = await serializer.SerializeAsync($"{label}.{key}", val, keepResources, keepOutputValues).ConfigureAwait(false); if (v != null) { result[key] = v; diff --git a/sdk/dotnet/Pulumi/Serialization/Constants.cs b/sdk/dotnet/Pulumi/Serialization/Constants.cs index a688c907d..5fa589889 100644 --- a/sdk/dotnet/Pulumi/Serialization/Constants.cs +++ b/sdk/dotnet/Pulumi/Serialization/Constants.cs @@ -34,7 +34,14 @@ namespace Pulumi.Serialization /// public const string SpecialResourceSig = "5cf8f73096256a8f31e491e813e4eb8e"; - public const string SecretValueName = "value"; + /// + /// SpecialOutputValueSig is a randomly assigned hash used to identify outputs in maps. See sdk/go/common/resource/properties.go. + /// + public const string SpecialOutputValueSig = "d0e6a833031e9bbcd3f4e8bde6ca49a4"; + + public const string SecretName = "secret"; + public const string ValueName = "value"; + public const string DependenciesName = "dependencies"; public const string AssetTextName = "text"; public const string ArchiveAssetsName = "assets"; diff --git a/sdk/dotnet/Pulumi/Serialization/Deserializer.cs b/sdk/dotnet/Pulumi/Serialization/Deserializer.cs index 607deb62c..befeb64b4 100644 --- a/sdk/dotnet/Pulumi/Serialization/Deserializer.cs +++ b/sdk/dotnet/Pulumi/Serialization/Deserializer.cs @@ -100,7 +100,7 @@ namespace Pulumi.Serialization }); public static OutputData Deserialize(Value value) - => DeserializeCore(value, + => DeserializeCore(value, v => v.KindCase switch { Value.KindOneofCase.NumberValue => DeserializerDouble(v), @@ -120,7 +120,7 @@ namespace Pulumi.Serialization while (IsSpecialStruct(value, out var sig) && sig == Constants.SpecialSecretSig) { - if (!value.StructValue.Fields.TryGetValue(Constants.SecretValueName, out var secretValue)) + if (!value.StructValue.Fields.TryGetValue(Constants.ValueName, out var secretValue)) throw new InvalidOperationException("Secrets must have a field called 'value'"); isSecret = true; @@ -222,7 +222,8 @@ namespace Pulumi.Serialization throw new InvalidOperationException("Value was marked as a Resource, but did not conform to required shape."); } - if (!TryGetStringValue(value.StructValue.Fields, Constants.ResourceVersionName, out var version)) { + if (!TryGetStringValue(value.StructValue.Fields, Constants.ResourceVersionName, out var version)) + { version = ""; } @@ -231,7 +232,8 @@ namespace Pulumi.Serialization var qualifiedTypeParts = qualifiedType.Split('$'); var type = qualifiedTypeParts[^1]; - if (ResourcePackages.TryConstruct(type, version, urn, out resource)) { + if (ResourcePackages.TryConstruct(type, version, urn, out resource)) + { return true; } diff --git a/sdk/dotnet/Pulumi/Serialization/Serializer.cs b/sdk/dotnet/Pulumi/Serialization/Serializer.cs index 5c31f0a2e..dc3bfa6bd 100644 --- a/sdk/dotnet/Pulumi/Serialization/Serializer.cs +++ b/sdk/dotnet/Pulumi/Serialization/Serializer.cs @@ -1,4 +1,4 @@ -// Copyright 2016-2019, Pulumi Corporation +// Copyright 2016-2021, Pulumi Corporation using System; using System.Collections; @@ -21,7 +21,7 @@ namespace Pulumi.Serialization public Serializer(bool excessiveDebugOutput) { - this.DependentResources = new HashSet(); + DependentResources = new HashSet(); _excessiveDebugOutput = excessiveDebugOutput; } @@ -39,7 +39,7 @@ namespace Pulumi.Serialization /// s /// s /// s - /// s + /// /// /// /// Additionally, other more complex objects can be serialized as long as they are built @@ -67,7 +67,7 @@ namespace Pulumi.Serialization /// /// No other result type are allowed to be returned. /// - public async Task SerializeAsync(string ctx, object? prop, bool keepResources) + public async Task SerializeAsync(string ctx, object? prop, bool keepResources, bool keepOutputValues = false) { // IMPORTANT: // IMPORTANT: Keep this in sync with serializesPropertiesSync in invoke.ts @@ -87,10 +87,15 @@ namespace Pulumi.Serialization } if (prop is InputArgs args) - return await SerializeInputArgsAsync(ctx, args, keepResources).ConfigureAwait(false); + { + return await SerializeInputArgsAsync(ctx, args, keepResources, keepOutputValues).ConfigureAwait(false); + } if (prop is AssetOrArchive assetOrArchive) + { + // There's no need to pass keepOutputValues when serializing assets or archives. return await SerializeAssetOrArchiveAsync(ctx, assetOrArchive, keepResources).ConfigureAwait(false); + } if (prop is Task) { @@ -105,7 +110,7 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output: Log.Debug($"Serialize property[{ctx}]: Recursing into IInput"); } - return await SerializeAsync(ctx, input.ToOutput(), keepResources).ConfigureAwait(false); + return await SerializeAsync(ctx, input.ToOutput(), keepResources, keepOutputValues).ConfigureAwait(false); } if (prop is IUnion union) @@ -115,7 +120,7 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output: Log.Debug($"Serialize property[{ctx}]: Recursing into IUnion"); } - return await SerializeAsync(ctx, union.Value, keepResources).ConfigureAwait(false); + return await SerializeAsync(ctx, union.Value, keepResources, keepOutputValues).ConfigureAwait(false); } if (prop is JsonElement element) @@ -134,9 +139,9 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output: { Log.Debug($"Serialize property[{ctx}]: Recursing into Output"); } - var data = await output.GetDataAsync().ConfigureAwait(false); - this.DependentResources.AddRange(data.Resources); + DependentResources.AddRange(data.Resources); + var propResources = new HashSet(data.Resources); // When serializing an Output, we will either serialize it as its resolved value or the "unknown value" // sentinel. We will do the former for all outputs created directly by user code (such outputs always @@ -144,15 +149,56 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output: var isKnown = data.IsKnown; var isSecret = data.IsSecret; + var valueSerializer = new Serializer(_excessiveDebugOutput); + var value = await valueSerializer.SerializeAsync($"{ctx}.id", data.Value, keepResources, keepOutputValues: false).ConfigureAwait(false); + var promiseDeps = valueSerializer.DependentResources; + DependentResources.UnionWith(promiseDeps); + propResources.UnionWith(promiseDeps); + + if (keepOutputValues) + { + if (isKnown && !isSecret && propResources.Count == 0) + { + return value; + } + + var urnDeps = new HashSet(); + foreach (var resource in propResources) + { + var urnSerializer = new Serializer(_excessiveDebugOutput); + await urnSerializer.SerializeAsync($"{ctx} dependency", resource.Urn, keepResources, keepOutputValues: false).ConfigureAwait(false); + urnDeps.UnionWith(urnSerializer.DependentResources); + } + DependentResources.UnionWith(urnDeps); + propResources.UnionWith(urnDeps); + + var dependencies = await Deployment.GetAllTransitivelyReferencedResourceUrnsAsync(propResources).ConfigureAwait(false); + var builder = ImmutableDictionary.CreateBuilder(); + builder.Add(Constants.SpecialSigKey, Constants.SpecialOutputValueSig); + if (isKnown) + { + builder.Add(Constants.ValueName, value); + } + if (isSecret) + { + builder.Add(Constants.SecretName, isSecret); + } + if (dependencies.Count > 0) + { + builder.Add(Constants.DependenciesName, + dependencies.OrderBy(x => x, StringComparer.Ordinal).ToImmutableArray()); + } + return builder.ToImmutable(); + } + if (!isKnown) return Constants.UnknownValue; - var value = await SerializeAsync($"{ctx}.id", data.Value, keepResources).ConfigureAwait(false); if (isSecret) { var builder = ImmutableDictionary.CreateBuilder(); builder.Add(Constants.SpecialSigKey, Constants.SpecialSecretSig); - builder.Add(Constants.SecretValueName, value); + builder.Add(Constants.ValueName, value); return builder.ToImmutable(); } @@ -167,12 +213,12 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output: Log.Debug($"Serialize property[{ctx}]: Encountered CustomResource"); } - this.DependentResources.Add(customResource); + DependentResources.Add(customResource); - var id = await SerializeAsync($"{ctx}.id", customResource.Id, keepResources).ConfigureAwait(false); + var id = await SerializeAsync($"{ctx}.id", customResource.Id, keepResources, keepOutputValues: false).ConfigureAwait(false); if (keepResources) { - var urn = await SerializeAsync($"{ctx}.urn", customResource.Urn, keepResources).ConfigureAwait(false); + var urn = await SerializeAsync($"{ctx}.urn", customResource.Urn, keepResources, keepOutputValues: false).ConfigureAwait(false); var builder = ImmutableDictionary.CreateBuilder(); builder.Add(Constants.SpecialSigKey, Constants.SpecialResourceSig); builder.Add(Constants.ResourceUrnName, urn); @@ -206,7 +252,7 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output: Log.Debug($"Serialize property[{ctx}]: Encountered ComponentResource"); } - var urn = await SerializeAsync($"{ctx}.urn", componentResource.Urn, keepResources).ConfigureAwait(false); + var urn = await SerializeAsync($"{ctx}.urn", componentResource.Urn, keepResources, keepOutputValues: false).ConfigureAwait(false); if (keepResources) { var builder = ImmutableDictionary.CreateBuilder(); @@ -218,10 +264,14 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output: } if (prop is IDictionary dictionary) - return await SerializeDictionaryAsync(ctx, dictionary, keepResources).ConfigureAwait(false); + { + return await SerializeDictionaryAsync(ctx, dictionary, keepResources, keepOutputValues).ConfigureAwait(false); + } if (prop is IList list) - return await SerializeListAsync(ctx, list, keepResources).ConfigureAwait(false); + { + return await SerializeListAsync(ctx, list, keepResources, keepOutputValues).ConfigureAwait(false); + } if (prop is Enum e && e.GetTypeCode() == TypeCode.Int32) { @@ -257,27 +307,27 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output: case JsonValueKind.False: return element.GetBoolean(); case JsonValueKind.Array: - { - var result = ImmutableArray.CreateBuilder(); - var index = 0; - foreach (var child in element.EnumerateArray()) { - result.Add(SerializeJson($"{ctx}[{index}]", child)); - index++; - } + var result = ImmutableArray.CreateBuilder(); + var index = 0; + foreach (var child in element.EnumerateArray()) + { + result.Add(SerializeJson($"{ctx}[{index}]", child)); + index++; + } - return result.ToImmutable(); - } + return result.ToImmutable(); + } case JsonValueKind.Object: - { - var result = ImmutableDictionary.CreateBuilder(); - foreach (var x in element.EnumerateObject()) { - result[x.Name] = SerializeJson($"{ctx}.{x.Name}", x.Value); - } + var result = ImmutableDictionary.CreateBuilder(); + foreach (var x in element.EnumerateObject()) + { + result[x.Name] = SerializeJson($"{ctx}.{x.Name}", x.Value); + } - return result.ToImmutable(); - } + return result.ToImmutable(); + } default: throw new InvalidOperationException($"Unknown {nameof(JsonElement)}.{nameof(JsonElement.ValueKind)}: {element.ValueKind}"); } @@ -296,7 +346,7 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output: throw new InvalidOperationException("Cannot serialize invalid archive"); var propName = assetOrArchive.PropName; - var value = await SerializeAsync(ctx + "." + propName, assetOrArchive.Value, keepResources).ConfigureAwait(false); + var value = await SerializeAsync(ctx + "." + propName, assetOrArchive.Value, keepResources, keepOutputValues: false).ConfigureAwait(false); var builder = ImmutableDictionary.CreateBuilder(); builder.Add(Constants.SpecialSigKey, assetOrArchive.SigKey); @@ -304,7 +354,7 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output: return builder.ToImmutable(); } - private async Task> SerializeInputArgsAsync(string ctx, InputArgs args, bool keepResources) + private async Task> SerializeInputArgsAsync(string ctx, InputArgs args, bool keepResources, bool keepOutputValues) { if (_excessiveDebugOutput) { @@ -312,10 +362,10 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output: } var dictionary = await args.ToDictionaryAsync().ConfigureAwait(false); - return await SerializeDictionaryAsync(ctx, dictionary, keepResources).ConfigureAwait(false); + return await SerializeDictionaryAsync(ctx, dictionary, keepResources, keepOutputValues).ConfigureAwait(false); } - private async Task> SerializeListAsync(string ctx, IList list, bool keepResources) + private async Task> SerializeListAsync(string ctx, IList list, bool keepResources, bool keepOutputValues) { if (_excessiveDebugOutput) { @@ -330,13 +380,13 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output: Log.Debug($"Serialize property[{ctx}]: array[{i}] element"); } - result.Add(await SerializeAsync($"{ctx}[{i}]", list[i], keepResources).ConfigureAwait(false)); + result.Add(await SerializeAsync($"{ctx}[{i}]", list[i], keepResources, keepOutputValues).ConfigureAwait(false)); } return result.MoveToImmutable(); } - private async Task> SerializeDictionaryAsync(string ctx, IDictionary dictionary, bool keepResources) + private async Task> SerializeDictionaryAsync(string ctx, IDictionary dictionary, bool keepResources, bool keepOutputValues) { if (_excessiveDebugOutput) { @@ -359,7 +409,7 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output: // When serializing an object, we omit any keys with null values. This matches // JSON semantics. - var v = await SerializeAsync($"{ctx}.{stringKey}", dictionary[stringKey], keepResources).ConfigureAwait(false); + var v = await SerializeAsync($"{ctx}.{stringKey}", dictionary[stringKey], keepResources, keepOutputValues).ConfigureAwait(false); if (v != null) { result[stringKey] = v;