diff --git a/sdk/dotnet/Pulumi/CustomResource.cs b/sdk/dotnet/Pulumi/CustomResource.cs index 3ced0c7d9..1759530fd 100644 --- a/sdk/dotnet/Pulumi/CustomResource.cs +++ b/sdk/dotnet/Pulumi/CustomResource.cs @@ -1,19 +1,30 @@ +using Google.Protobuf.WellKnownTypes; +using Pulumirpc; using System; using System.Collections.Generic; using System.Threading.Tasks; namespace Pulumi { - public class CustomResource : Resource { + public abstract class CustomResource : Resource { protected Task m_registrationResponse; public Task Id { get; private set;} - public CustomResource(string type, string name, Dictionary properties = null, ResourceOptions options = default(ResourceOptions)) { - m_registrationResponse = RegisterAsync(type, name, true, properties, options); + TaskCompletionSource m_IdCompletionSoruce; + public CustomResource() { + m_IdCompletionSoruce = new TaskCompletionSource(); + Id = m_IdCompletionSoruce.Task; + } - Id = m_registrationResponse.ContinueWith((x) => { - return x.Result.Id; - }); + protected override void OnResourceRegistrationCompete(Task resp) { + base.OnResourceRegistrationCompete(resp); + if (resp.IsCanceled) { + m_IdCompletionSoruce.SetCanceled(); + } else if (resp.IsFaulted) { + m_IdCompletionSoruce.SetException(resp.Exception); + } else { + m_IdCompletionSoruce.SetResult(resp.Result.Id); + } } } } \ No newline at end of file diff --git a/sdk/dotnet/Pulumi/Resource.cs b/sdk/dotnet/Pulumi/Resource.cs index 72bd41370..e79636e50 100644 --- a/sdk/dotnet/Pulumi/Resource.cs +++ b/sdk/dotnet/Pulumi/Resource.cs @@ -9,14 +9,27 @@ namespace Pulumi public abstract class Resource { public Task Urn { get; private set; } + private TaskCompletionSource m_UrnCompletionSource; public const string UnkownResourceId = "04da6b54-80e4-46f7-96ec-b56ff0331ba9"; protected Resource() { + m_UrnCompletionSource = new TaskCompletionSource(); + Urn = m_UrnCompletionSource.Task; } - public Task RegisterAsync(string type, string name, bool custom, Dictionary properties, ResourceOptions options) { + protected virtual void OnResourceRegistrationCompete(Task resp) { + if (resp.IsCanceled) { + m_UrnCompletionSource.SetCanceled(); + } else if (resp.IsFaulted) { + m_UrnCompletionSource.SetException(resp.Exception); + } else { + m_UrnCompletionSource.SetResult(resp.Result.Urn); + } + } + + public async void RegisterAsync(string type, string name, bool custom, Dictionary properties, ResourceOptions options) { Serilog.Log.Debug("RegisterAsync({type}, {name})", type, name); if (string.IsNullOrEmpty(type)) @@ -29,7 +42,6 @@ namespace Pulumi throw new ArgumentException(nameof(name)); } - // Figure out the parent URN. If an explicit parent was passed in, use that. Otherwise use the global root URN. In the case where that hasn't been set yet, we must be creating // the ComponentResource that represents the global stack object, so pass along no parent. Task parentUrn; @@ -41,23 +53,23 @@ namespace Pulumi parentUrn = Task.FromResult(""); } - var res = Runtime.Monitor.RegisterResourceAsync( + // Kick off the registration, and when it completes, call the OnResourceRegistrationCompete method which will resolve all the tasks to their values. The fact that we don't + // await here is by design. This method is called by child classes in their constructors, where were do not want to block. + #pragma warning disable 4014 + Runtime.Monitor.RegisterResourceAsync( new RegisterResourceRequest() { Type = type, Name = name, Custom = custom, Protect = false, - Object = SerializeProperties(properties), + Object = await SerializeProperties(properties), Parent = parentUrn.Result - } - ); - - Urn = res.ResponseAsync.ContinueWith((x) => x.Result.Urn); - return res.ResponseAsync; + }).ResponseAsync.ContinueWith((x) => OnResourceRegistrationCompete(x));; + #pragma warning restore 4014 } - private Struct SerializeProperties(Dictionary properties) { + private async Task SerializeProperties(Dictionary properties) { if (properties == null) { return new Struct(); } @@ -65,19 +77,19 @@ namespace Pulumi var s = new Struct(); foreach (var kvp in properties) { - s.Fields.Add(kvp.Key, SerializeProperty(kvp.Value)); + s.Fields.Add(kvp.Key, await SerializeProperty(kvp.Value)); } return s; } - private Value SerializeProperty(object o) { + private async Task SerializeProperty(object o) { Serilog.Log.Debug("SerializeProperty({o})", o); var input = o as IInput; if (input != null) { // Get the ground value. - var v = input.GetTask().Result; + var v = await input.GetTask(); if (v == null) { return Value.ForNull(); @@ -90,7 +102,7 @@ namespace Pulumi // We marshal custom resources as strings of their provider generated IDs. var cr = v as CustomResource; if (cr != null) { - return Value.ForString(cr.Id.Result); + return Value.ForString(await cr.Id); } throw new NotImplementedException($"cannot marshal Input with underlying type ${input.GetType()}"); diff --git a/sdk/dotnet/Pulumi/TfGenStyleResources.cs b/sdk/dotnet/Pulumi/TfGenStyleResources.cs index d74980b90..00b3f7620 100644 --- a/sdk/dotnet/Pulumi/TfGenStyleResources.cs +++ b/sdk/dotnet/Pulumi/TfGenStyleResources.cs @@ -2,6 +2,7 @@ // the shape right "by hand" and then work on the code-gen to stub everything else out: using Pulumi; +using Pulumirpc; using System; using System.Threading.Tasks; using System.Collections.Generic; @@ -11,19 +12,34 @@ namespace AWS.S3 { // TODO(ellismg): These should be Output. public Task BucketDomainName { get; private set; } - public Bucket(string name, BucketArgs args = default(BucketArgs), ResourceOptions opts = default(ResourceOptions)) : - base("aws:s3/bucket:Bucket", name, new Dictionary { - {"acl", args.Acl}, - }, opts) + private TaskCompletionSource m_BucketDomainNameCompletionSource; + public Bucket(string name, BucketArgs args = default(BucketArgs), ResourceOptions opts = default(ResourceOptions)) { - BucketDomainName = m_registrationResponse.ContinueWith((x) => { - var fields = x.Result.Object.Fields; + m_BucketDomainNameCompletionSource = new TaskCompletionSource(); + BucketDomainName = m_BucketDomainNameCompletionSource.Task; - // TODO(ellismg): This is not correct, the result from the provider may not contain this key in the case - // where it's value is unknown, e.g. during an initial preview. When we do the Output work here - // we'll need to have a better story. - return fields.ContainsKey("bucketDomainName") ? fields["bucketDomainName"].StringValue : null; - }); + RegisterAsync("aws:s3/bucket:Bucket", name, true, new Dictionary { + {"acl", args.Acl}, + }, opts); + } + + protected override void OnResourceRegistrationCompete(Task resp) + { + base.OnResourceRegistrationCompete(resp); + + if (resp.IsCanceled) { + m_BucketDomainNameCompletionSource.SetCanceled(); + } else if (resp.IsFaulted) { + m_BucketDomainNameCompletionSource.SetException(resp.Exception); + } + + var fields = resp.Result.Object.Fields; + + foreach (var kvp in fields) { + Serilog.Log.Debug("got property {key}", kvp.Key); + } + + m_BucketDomainNameCompletionSource.SetResult(fields.ContainsKey("bucketDomainName") ? fields["bucketDomainName"].StringValue : null); } } @@ -33,16 +49,14 @@ namespace AWS.S3 { } public class BucketObject : CustomResource{ - public BucketObject(string name, BucketObjectArgs args = default(BucketObjectArgs), ResourceOptions opts = default(ResourceOptions)) : - base("aws:s3/bucketObject:BucketObject", name, new Dictionary { + public BucketObject(string name, BucketObjectArgs args = default(BucketObjectArgs), ResourceOptions opts = default(ResourceOptions)) { + RegisterAsync("aws:s3/bucketObject:BucketObject", name, true, new Dictionary { {"acl", args.Acl}, {"bucket", args.Bucket}, {"contentBase64", args.ContentBase64}, {"contentType", args.ContentType}, {"key", args.Key}, - }, opts) - { - + }, opts); } } diff --git a/sdk/dotnet/build-and-run.sh b/sdk/dotnet/build-and-run.sh index 16eac4005..8bba86d43 100755 --- a/sdk/dotnet/build-and-run.sh +++ b/sdk/dotnet/build-and-run.sh @@ -5,4 +5,4 @@ IFS="\n\t" GOBIN=/opt/pulumi/bin go install ./cmd/pulumi-language-dotnet export PATH=/opt/pulumi/bin:$PATH cd examples/bucket -pulumi update --diff --yes +pulumi preview --diff diff --git a/sdk/dotnet/examples/bucket/Program.cs b/sdk/dotnet/examples/bucket/Program.cs index cece3b14e..9551d53c9 100644 --- a/sdk/dotnet/examples/bucket/Program.cs +++ b/sdk/dotnet/examples/bucket/Program.cs @@ -7,7 +7,7 @@ class Program { static void Main(string[] args) { - Deployment.Run(async () => { + Deployment.Run(() => { Config config = new Config("hello-dotnet"); // Create the bucket, and make it readable. @@ -32,9 +32,9 @@ class Program // // TODO(ellismg): We need to come up with a solution here. We probably want to track all the pending tasks generated // by Pulumi during execution and await them to complete in the host itself... - Console.WriteLine($"Bucket ID id : {await bucket.Id}"); - Console.WriteLine($"Content ID id : {await content.Id}"); - Console.WriteLine($"https://{await bucket.BucketDomainName}/hello.txt"); + Console.WriteLine($"Bucket ID id : {bucket.Id.Result}"); + Console.WriteLine($"Content ID id : {content.Id.Result}"); + Console.WriteLine($"https://{bucket.BucketDomainName.Result}/hello.txt"); }); } }