Ejemplo n.º 1
0
        /// <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}");
        }