/// <summary> /// Takes in an arbitrary object and serializes it into a uniform form that can converted /// trivially to a protobuf to be passed to the Pulumi engine. /// <para/> /// The allowed 'basis' forms that can be serialized are: /// <list type="number"> /// <item><see langword="null"/>s</item> /// <item><see cref="bool"/>s</item> /// <item><see cref="int"/>s</item> /// <item><see cref="double"/>s</item> /// <item><see cref="string"/>s</item> /// <item><see cref="Asset"/>s</item> /// <item><see cref="Archive"/>s</item> /// <item><see cref="Resource"/>s</item> /// <item><see cref="ResourceArgs"/></item> /// <item><see cref="JsonElement"/></item> /// </list> /// Additionally, other more complex objects can be serialized as long as they are built /// out of serializable objects. These complex objects include: /// <list type="number"> /// <item><see cref="Input{T}"/>s. As long as they are an Input of a serializable type.</item> /// <item><see cref="Output{T}"/>s. As long as they are an Output of a serializable type.</item> /// <item><see cref="IList"/>s. As long as all elements in the list are serializable.</item> /// <item><see cref="IDictionary"/>. As long as the key of the dictionary are <see cref="string"/>s and as long as the value are all serializable.</item> /// </list> /// No other forms are allowed. /// <para/> /// This function will only return values of a very specific shape. Specifically, the /// result values returned will *only* be one of: /// <para/> /// <list type="number"> /// <item><see langword="null"/></item> /// <item><see cref="bool"/></item> /// <item><see cref="int"/></item> /// <item><see cref="double"/></item> /// <item><see cref="string"/></item> /// <item>An <see cref="ImmutableArray{T}"/> containing only these result value types.</item> /// <item>An <see cref="IImmutableDictionary{TKey, TValue}"/> where the keys are strings and /// the values are only these result value types.</item> /// </list> /// No other result type are allowed to be returned. /// </summary> public async Task <object?> SerializeAsync(string ctx, object?prop, bool keepResources, bool keepOutputValues = false) { // IMPORTANT: // IMPORTANT: Keep this in sync with serializesPropertiesSync in invoke.ts // IMPORTANT: if (prop == null || prop is bool || prop is int || prop is double || prop is string) { if (_excessiveDebugOutput) { Log.Debug($"Serialize property[{ctx}]: primitive={prop}"); } return(prop); } if (prop is InputArgs args) { 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) { throw new InvalidOperationException( $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output:\n\t{ctx}"); } if (prop is IInput input) { if (_excessiveDebugOutput) { Log.Debug($"Serialize property[{ctx}]: Recursing into IInput"); } return(await SerializeAsync(ctx, input.ToOutput(), keepResources, keepOutputValues).ConfigureAwait(false)); } if (prop is IUnion union) { if (_excessiveDebugOutput) { Log.Debug($"Serialize property[{ctx}]: Recursing into IUnion"); } return(await SerializeAsync(ctx, union.Value, keepResources, keepOutputValues).ConfigureAwait(false)); } if (prop is JsonElement element) { if (_excessiveDebugOutput) { Log.Debug($"Serialize property[{ctx}]: Recursing into Json"); } return(SerializeJson(ctx, element)); } if (prop is IOutput output) { if (_excessiveDebugOutput) { Log.Debug($"Serialize property[{ctx}]: Recursing into Output"); } var data = await output.GetDataAsync().ConfigureAwait(false); DependentResources.AddRange(data.Resources); var propResources = new HashSet <Resource>(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 // resolve isKnown to true) and for any resource outputs that were resolved with known values. var isKnown = data.IsKnown; var isSecret = data.IsSecret; var valueSerializer = new Serializer(_excessiveDebugOutput); // It is unsafe to serialize unknown values. object?value = isKnown ? await valueSerializer.SerializeAsync( $"{ctx}.id", data.Value, keepResources, keepOutputValues : false).ConfigureAwait(false) : null; var promiseDeps = valueSerializer.DependentResources; DependentResources.UnionWith(promiseDeps); propResources.UnionWith(promiseDeps); if (keepOutputValues) { if (isKnown && !isSecret && propResources.Count == 0) { return(value); } var urnDeps = new HashSet <Resource>(); 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 <string, object?>(); 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 <object>()); } return(builder.ToImmutable()); } if (!isKnown) { return(Constants.UnknownValue); } if (isSecret) { var builder = ImmutableDictionary.CreateBuilder <string, object?>(); builder.Add(Constants.SpecialSigKey, Constants.SpecialSecretSig); builder.Add(Constants.ValueName, value); return(builder.ToImmutable()); } return(value); } if (prop is CustomResource customResource) { // Resources aren't serializable; instead, we serialize them as references to the ID property. if (_excessiveDebugOutput) { Log.Debug($"Serialize property[{ctx}]: Encountered CustomResource"); } DependentResources.Add(customResource); var id = await SerializeAsync($"{ctx}.id", customResource.Id, keepResources, keepOutputValues : false).ConfigureAwait(false); if (keepResources) { var urn = await SerializeAsync($"{ctx}.urn", customResource.Urn, keepResources, keepOutputValues : false).ConfigureAwait(false); var builder = ImmutableDictionary.CreateBuilder <string, object?>(); builder.Add(Constants.SpecialSigKey, Constants.SpecialResourceSig); builder.Add(Constants.ResourceUrnName, urn); builder.Add(Constants.ResourceIdName, id as string == Constants.UnknownValue ? "" : id); return(builder.ToImmutable()); } return(id); } if (prop is ComponentResource componentResource) { // Component resources often can contain cycles in them. For example, an awsinfra // SecurityGroupRule can point a the awsinfra SecurityGroup, which in turn can point // back to its rules through its 'egressRules' and 'ingressRules' properties. If // serializing out the 'SecurityGroup' resource ends up trying to serialize out // those properties, a deadlock will happen, due to waiting on the child, which is // waiting on the parent. // // Practically, there is no need to actually serialize out a component. It doesn't // represent a real resource, nor does it have normal properties that need to be // tracked for differences (since changes to its properties don't represent changes // to resources in the real world). // // So, to avoid these problems, while allowing a flexible and simple programming // model, we just serialize out the component as its urn. This allows the component // to be identified and tracked in a reasonable manner, while not causing us to // compute or embed information about it that is not needed, and which can lead to // deadlocks. if (_excessiveDebugOutput) { Log.Debug($"Serialize property[{ctx}]: Encountered ComponentResource"); } var urn = await SerializeAsync($"{ctx}.urn", componentResource.Urn, keepResources, keepOutputValues : false).ConfigureAwait(false); if (keepResources) { var builder = ImmutableDictionary.CreateBuilder <string, object?>(); builder.Add(Constants.SpecialSigKey, Constants.SpecialResourceSig); builder.Add(Constants.ResourceUrnName, urn); return(builder.ToImmutable()); } return(urn); } if (prop is IDictionary dictionary) { return(await SerializeDictionaryAsync(ctx, dictionary, keepResources, keepOutputValues).ConfigureAwait(false)); } if (prop is IList list) { return(await SerializeListAsync(ctx, list, keepResources, keepOutputValues).ConfigureAwait(false)); } if (prop is Enum e && e.GetTypeCode() == TypeCode.Int32) { return((int)prop); } var propType = prop.GetType(); if (propType.IsValueType && propType.GetCustomAttribute <EnumTypeAttribute>() != null) { var mi = propType.GetMethod("op_Explicit", BindingFlags.Public | BindingFlags.Static, null, new[] { propType }, null); if (mi == null || (mi.ReturnType != typeof(string) && mi.ReturnType != typeof(double))) { throw new InvalidOperationException($"Expected {propType.FullName} to have an explicit conversion operator to String or Double.\n\t{ctx}"); } return(mi.Invoke(null, new[] { prop })); } throw new InvalidOperationException($"{propType.FullName} is not a supported argument type.\n\t{ctx}"); }