pulumi/sdk/dotnet/Pulumi/Serialization/Deserializer.cs
Pat Gavlin 3d2e31289a
Add support for serialized resource references. (#5041)
Resources are serialized as their URN, ID, and package version. Each
Pulumi package is expected to register itself with the SDK. The package
will be invoked to construct appropriate instances of rehydrated
resources. Packages are distinguished by their name and their version.

This is the foundation of cross-process resources.

Related to #2430.

Co-authored-by: Mikhail Shilkov <github@mikhail.io>
Co-authored-by: Luke Hoban <luke@pulumi.com>
Co-authored-by: Levi Blackstone <levi@pulumi.com>
2020-10-27 10:12:12 -07:00

274 lines
12 KiB
C#

// Copyright 2016-2019, Pulumi Corporation
using System;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Google.Protobuf.Collections;
using Google.Protobuf.WellKnownTypes;
namespace Pulumi.Serialization
{
internal static class Deserializer
{
private static OutputData<T> DeserializeCore<T>(Value value, Func<Value, OutputData<T>> func)
{
var (innerVal, isSecret) = UnwrapSecret(value);
value = innerVal;
if (value.KindCase == Value.KindOneofCase.StringValue &&
value.StringValue == Constants.UnknownValue)
{
// always deserialize unknown as the null value.
return new OutputData<T>(ImmutableHashSet<Resource>.Empty, default!, isKnown: false, isSecret);
}
if (TryDeserializeAssetOrArchive(value, out var assetOrArchive))
{
return new OutputData<T>(ImmutableHashSet<Resource>.Empty, (T)(object)assetOrArchive, isKnown: true, isSecret);
}
else if (TryDeserializeResource(value, out var resource))
{
return new OutputData<T>(ImmutableHashSet<Resource>.Empty, (T)(object)resource, isKnown: true, isSecret);
}
var innerData = func(value);
return OutputData.Create(innerData.Resources, innerData.Value, innerData.IsKnown, isSecret || innerData.IsSecret);
}
private static OutputData<T> DeserializeOneOf<T>(Value value, Value.KindOneofCase kind, Func<Value, OutputData<T>> func)
=> DeserializeCore(value, v =>
v.KindCase == kind ? func(v) : throw new InvalidOperationException($"Trying to deserialize {v.KindCase} as a {kind}"));
private static OutputData<T> DeserializePrimitive<T>(Value value, Value.KindOneofCase kind, Func<Value, T> func)
=> DeserializeOneOf(value, kind, v => OutputData.Create(
ImmutableHashSet<Resource>.Empty, func(v), isKnown: true, isSecret: false));
private static OutputData<bool> DeserializeBoolean(Value value)
=> DeserializePrimitive(value, Value.KindOneofCase.BoolValue, v => v.BoolValue);
private static OutputData<string> DeserializerString(Value value)
=> DeserializePrimitive(value, Value.KindOneofCase.StringValue, v => v.StringValue);
private static OutputData<double> DeserializerDouble(Value value)
=> DeserializePrimitive(value, Value.KindOneofCase.NumberValue, v => v.NumberValue);
private static OutputData<ImmutableArray<object?>> DeserializeList(Value value)
=> DeserializeOneOf(value, Value.KindOneofCase.ListValue,
v =>
{
var resources = ImmutableHashSet.CreateBuilder<Resource>();
var result = ImmutableArray.CreateBuilder<object?>();
var isKnown = true;
var isSecret = false;
foreach (var element in v.ListValue.Values)
{
var elementData = Deserialize(element);
(isKnown, isSecret) = OutputData.Combine(elementData, isKnown, isSecret);
resources.UnionWith(elementData.Resources);
result.Add(elementData.Value);
}
return OutputData.Create(resources.ToImmutable(), result.ToImmutable(), isKnown, isSecret);
});
private static OutputData<ImmutableDictionary<string, object?>> DeserializeStruct(Value value)
=> DeserializeOneOf(value, Value.KindOneofCase.StructValue,
v =>
{
var resources = ImmutableHashSet.CreateBuilder<Resource>();
var result = ImmutableDictionary.CreateBuilder<string, object?>();
var isKnown = true;
var isSecret = false;
foreach (var (key, element) in v.StructValue.Fields)
{
// Unilaterally skip properties considered internal by the Pulumi engine.
// These don't actually contribute to the exposed shape of the object, do
// not need to be passed back to the engine, and often will not match the
// expected type we are deserializing into.
if (key.StartsWith("__", StringComparison.Ordinal))
continue;
var elementData = Deserialize(element);
(isKnown, isSecret) = OutputData.Combine(elementData, isKnown, isSecret);
result.Add(key, elementData.Value);
resources.UnionWith(elementData.Resources);
}
return OutputData.Create(resources.ToImmutable(), result.ToImmutable(), isKnown, isSecret);
});
public static OutputData<object?> Deserialize(Value value)
=> DeserializeCore(value,
v => v.KindCase switch
{
Value.KindOneofCase.NumberValue => DeserializerDouble(v),
Value.KindOneofCase.StringValue => DeserializerString(v),
Value.KindOneofCase.BoolValue => DeserializeBoolean(v),
Value.KindOneofCase.StructValue => DeserializeStruct(v),
Value.KindOneofCase.ListValue => DeserializeList(v),
Value.KindOneofCase.NullValue => new OutputData<object?>(ImmutableHashSet<Resource>.Empty, null, isKnown: true, isSecret: false),
Value.KindOneofCase.None => throw new InvalidOperationException("Should never get 'None' type when deserializing protobuf"),
_ => throw new InvalidOperationException("Unknown type when deserializing protobuf: " + v.KindCase),
});
private static (Value unwrapped, bool isSecret) UnwrapSecret(Value value)
{
var isSecret = false;
while (IsSpecialStruct(value, out var sig) &&
sig == Constants.SpecialSecretSig)
{
if (!value.StructValue.Fields.TryGetValue(Constants.SecretValueName, out var secretValue))
throw new InvalidOperationException("Secrets must have a field called 'value'");
isSecret = true;
value = secretValue;
}
return (value, isSecret);
}
private static bool IsSpecialStruct(
Value value, [NotNullWhen(true)] out string? sig)
{
if (value.KindCase == Value.KindOneofCase.StructValue &&
value.StructValue.Fields.TryGetValue(Constants.SpecialSigKey, out var sigVal) &&
sigVal.KindCase == Value.KindOneofCase.StringValue)
{
sig = sigVal.StringValue;
return true;
}
sig = null;
return false;
}
private static bool TryDeserializeAssetOrArchive(
Value value, [NotNullWhen(true)] out AssetOrArchive? assetOrArchive)
{
if (IsSpecialStruct(value, out var sig))
{
if (sig == Constants.SpecialAssetSig)
{
assetOrArchive = DeserializeAsset(value);
return true;
}
else if (sig == Constants.SpecialArchiveSig)
{
assetOrArchive = DeserializeArchive(value);
return true;
}
}
assetOrArchive = null;
return false;
}
private static Archive DeserializeArchive(Value value)
{
if (TryGetStringValue(value.StructValue.Fields, Constants.AssetOrArchivePathName, out var path))
return new FileArchive(path);
if (TryGetStringValue(value.StructValue.Fields, Constants.AssetOrArchiveUriName, out var uri))
return new RemoteArchive(uri);
if (value.StructValue.Fields.TryGetValue(Constants.ArchiveAssetsName, out var assetsValue))
{
if (assetsValue.KindCase == Value.KindOneofCase.StructValue)
{
var assets = ImmutableDictionary.CreateBuilder<string, AssetOrArchive>();
foreach (var (name, val) in assetsValue.StructValue.Fields)
{
if (!TryDeserializeAssetOrArchive(val, out var innerAssetOrArchive))
throw new InvalidOperationException("AssetArchive contained an element that wasn't itself an Asset or Archive.");
assets[name] = innerAssetOrArchive;
}
return new AssetArchive(assets.ToImmutable());
}
}
throw new InvalidOperationException("Value was marked as Archive, but did not conform to required shape.");
}
private static Asset DeserializeAsset(Value value)
{
if (TryGetStringValue(value.StructValue.Fields, Constants.AssetOrArchivePathName, out var path))
return new FileAsset(path);
if (TryGetStringValue(value.StructValue.Fields, Constants.AssetOrArchiveUriName, out var uri))
return new RemoteAsset(uri);
if (TryGetStringValue(value.StructValue.Fields, Constants.AssetTextName, out var text))
return new StringAsset(text);
throw new InvalidOperationException("Value was marked as Asset, but did not conform to required shape.");
}
private static bool TryDeserializeResource(
Value value, [NotNullWhen(true)] out Resource? resource)
{
if (!IsSpecialStruct(value, out var sig) || sig != Constants.SpecialResourceSig)
{
resource = null;
return false;
}
string? urn;
if (!TryGetStringValue(value.StructValue.Fields, Constants.ResourceUrnName, out urn))
{
throw new InvalidOperationException("Value was marked as a Resource, but did not conform to required shape.");
}
string? version;
if (!TryGetStringValue(value.StructValue.Fields, Constants.ResourceVersionName, out version))
{
throw new InvalidOperationException("Value was marked as a Resource, but did not conform to required shape.");
}
var urnParts = urn.Split("::");
var qualifiedType = urnParts[2];
var qualifiedTypeParts = qualifiedType.Split('$');
var type = qualifiedTypeParts[qualifiedTypeParts.Length-1];
var typeParts = type.Split(':');
var pkgName = typeParts[0];
var modName = typeParts.Length > 1 ? typeParts[1] : "";
var typeName = typeParts.Length > 2 ? typeParts[2] : "";
var isProvider = pkgName == "pulumi" && modName == "providers";
if (isProvider) {
pkgName = typeName;
}
IResourcePackage? package;
if (!ResourcePackages.TryGetResourcePackage(pkgName, version, out package))
{
throw new InvalidOperationException($"Unable to deserialize resource URN {urn}, no resource package is registered for type {type}.");
}
var urnName = urnParts[3];
resource = !isProvider ?
package.Construct(urnName, type, null, urn) :
package.ConstructProvider(urnName, type, null, urn);
return true;
}
private static bool TryGetStringValue(
MapField<string, Value> fields, string keyName, [NotNullWhen(true)] out string? result)
{
if (fields.TryGetValue(keyName, out var value) &&
value.KindCase == Value.KindOneofCase.StringValue)
{
result = value.StringValue;
return true;
}
result = null;
return false;
}
}
}