From df75f0ed95cbd2274b723f2bee947e7262032137 Mon Sep 17 00:00:00 2001 From: Justin Van Patten Date: Tue, 6 Oct 2020 10:19:22 -0700 Subject: [PATCH] Support remote components in .NET (#5485) --- CHANGELOG.md | 2 + sdk/dotnet/Pulumi.Tests/StackTests.cs | 3 +- .../Deployment_ReadOrRegisterResource.cs | 29 +- .../Deployment/Deployment_ReadResource.cs | 5 +- .../Deployment/Deployment_RegisterResource.cs | 36 +- .../Pulumi/Deployment/IDeploymentInternal.cs | 7 +- sdk/dotnet/Pulumi/PublicAPI.Shipped.txt | 1 + .../Pulumi/Resources/ComponentResource.cs | 29 +- sdk/dotnet/Pulumi/Resources/CustomResource.cs | 47 ++- .../Resources/DependencyProviderResource.cs | 36 ++ .../Pulumi/Resources/DependencyResource.cs | 24 ++ .../Pulumi/Resources/ProviderResource.cs | 23 +- sdk/dotnet/Pulumi/Resources/Resource.cs | 18 +- sdk/dotnet/Pulumi/Serialization/Converter.cs | 9 +- .../Serialization/OutputCompletionSource.cs | 2 +- .../construct_component/dotnet/.gitignore | 353 ++++++++++++++++++ .../construct_component/dotnet/Component.cs | 23 ++ .../dotnet/ConstructComponent.csproj | 9 + .../construct_component/dotnet/MyStack.cs | 13 + .../construct_component/dotnet/Program.cs | 7 + .../construct_component/dotnet/Pulumi.yaml | 3 + tests/integration/integration_dotnet_test.go | 51 +++ 22 files changed, 680 insertions(+), 50 deletions(-) create mode 100644 sdk/dotnet/Pulumi/Resources/DependencyProviderResource.cs create mode 100644 sdk/dotnet/Pulumi/Resources/DependencyResource.cs create mode 100644 tests/integration/construct_component/dotnet/.gitignore create mode 100644 tests/integration/construct_component/dotnet/Component.cs create mode 100644 tests/integration/construct_component/dotnet/ConstructComponent.csproj create mode 100644 tests/integration/construct_component/dotnet/MyStack.cs create mode 100644 tests/integration/construct_component/dotnet/Program.cs create mode 100644 tests/integration/construct_component/dotnet/Pulumi.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index c708aeca8..450fde971 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ CHANGELOG - Fixing gzip compression for alternative backends. [#5484](https://github.com/pulumi/pulumi/pull/5484) +- Add internal scaffolding for using cross-language components from .NET. + [#5485](https://github.com/pulumi/pulumi/pull/5485) ## 2.11.2 (2020-10-01) diff --git a/sdk/dotnet/Pulumi.Tests/StackTests.cs b/sdk/dotnet/Pulumi.Tests/StackTests.cs index 0676066ce..f49df3254 100644 --- a/sdk/dotnet/Pulumi.Tests/StackTests.cs +++ b/sdk/dotnet/Pulumi.Tests/StackTests.cs @@ -94,7 +94,8 @@ namespace Pulumi.Tests mock.Setup(d => d.ProjectName).Returns("TestProject"); mock.Setup(d => d.StackName).Returns("TestStack"); mock.SetupSet(content => content.Stack = It.IsAny()); - mock.Setup(d => d.ReadOrRegisterResource(It.IsAny(), It.IsAny(), It.IsAny())); + mock.Setup(d => d.ReadOrRegisterResource(It.IsAny(), It.IsAny(), + It.IsAny>(), It.IsAny(), It.IsAny())); mock.Setup(d => d.RegisterResourceOutputs(It.IsAny(), It.IsAny>>())) .Callback((Resource _, Output> o) => outputs = o); diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment_ReadOrRegisterResource.cs b/sdk/dotnet/Pulumi/Deployment/Deployment_ReadOrRegisterResource.cs index a6b6d24eb..153100e82 100644 --- a/sdk/dotnet/Pulumi/Deployment/Deployment_ReadOrRegisterResource.cs +++ b/sdk/dotnet/Pulumi/Deployment/Deployment_ReadOrRegisterResource.cs @@ -11,7 +11,8 @@ namespace Pulumi public partial class Deployment { void IDeploymentInternal.ReadOrRegisterResource( - Resource resource, ResourceArgs args, ResourceOptions options) + Resource resource, bool remote, Func newDependency, ResourceArgs args, + ResourceOptions options) { // ReadOrRegisterResource is called in a fire-and-forget manner. Make sure we keep // track of this task so that the application will not quit until this async work @@ -31,11 +32,12 @@ namespace Pulumi _runner.RegisterTask( $"{nameof(IDeploymentInternal.ReadOrRegisterResource)}: {resource.GetResourceType()}-{resource.GetResourceName()}", - CompleteResourceAsync(resource, args, options, completionSources)); + CompleteResourceAsync(resource, remote, newDependency, args, options, completionSources)); } - private async Task<(string urn, string id, Struct data)> ReadOrRegisterResourceAsync( - Resource resource, ResourceArgs args, ResourceOptions options) + private async Task<(string urn, string id, Struct data, ImmutableDictionary> dependencies)> ReadOrRegisterResourceAsync( + Resource resource, bool remote, Func newDependency, ResourceArgs args, + ResourceOptions options) { if (options.Id != null) { @@ -54,7 +56,7 @@ namespace Pulumi // resource's properties will be resolved asynchronously after the operation completes, // so that dependent computations resolve normally. If we are just planning, on the // other hand, values will never resolve. - return await RegisterResourceAsync(resource, args, options).ConfigureAwait(false); + return await RegisterResourceAsync(resource, remote, newDependency, args, options).ConfigureAwait(false); } /// @@ -63,14 +65,16 @@ namespace Pulumi /// the results of it. /// private async Task CompleteResourceAsync( - Resource resource, ResourceArgs args, ResourceOptions options, - ImmutableDictionary completionSources) + Resource resource, bool remote, Func newDependency, ResourceArgs args, + ResourceOptions options, ImmutableDictionary completionSources) { // Run in a try/catch/finally so that we always resolve all the outputs of the resource // regardless of whether we encounter an errors computing the action. try { - var response = await ReadOrRegisterResourceAsync(resource, args, options).ConfigureAwait(false); + var response = await ReadOrRegisterResourceAsync( + resource, remote, newDependency, args, options).ConfigureAwait(false); + completionSources[Constants.UrnPropertyName].SetStringValue(response.urn, isKnown: true); if (resource is CustomResource customResource) { @@ -94,8 +98,13 @@ namespace Pulumi // rest. if (response.data.Fields.TryGetValue(fieldName, out var value)) { - var converted = Converter.ConvertValue( - $"{resource.GetType().FullName}.{fieldName}", value, completionSource.TargetType); + if (!response.dependencies.TryGetValue(fieldName, out var dependencies)) + { + dependencies = ImmutableHashSet.Empty; + } + + var converted = Converter.ConvertValue($"{resource.GetType().FullName}.{fieldName}", value, + completionSource.TargetType, dependencies); completionSource.SetValue(converted); } } diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment_ReadResource.cs b/sdk/dotnet/Pulumi/Deployment/Deployment_ReadResource.cs index 716064810..ba235253d 100644 --- a/sdk/dotnet/Pulumi/Deployment/Deployment_ReadResource.cs +++ b/sdk/dotnet/Pulumi/Deployment/Deployment_ReadResource.cs @@ -1,6 +1,7 @@ // Copyright 2016-2019, Pulumi Corporation using System; +using System.Collections.Immutable; using System.Threading.Tasks; using Google.Protobuf.WellKnownTypes; using Pulumi.Serialization; @@ -10,7 +11,7 @@ namespace Pulumi { public partial class Deployment { - private async Task<(string urn, string id, Struct data)> ReadResourceAsync( + private async Task<(string urn, string id, Struct data, ImmutableDictionary> dependencies)> ReadResourceAsync( Resource resource, string id, ResourceArgs args, ResourceOptions options) { var name = resource.GetResourceName(); @@ -43,7 +44,7 @@ namespace Pulumi // Now run the operation, serializing the invocation if necessary. var response = await this.Monitor.ReadResourceAsync(resource, request); - return (response.Urn, id, response.Properties); + return (response.Urn, id, response.Properties, ImmutableDictionary>.Empty); } } } diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment_RegisterResource.cs b/sdk/dotnet/Pulumi/Deployment/Deployment_RegisterResource.cs index a404b670b..7efef9068 100644 --- a/sdk/dotnet/Pulumi/Deployment/Deployment_RegisterResource.cs +++ b/sdk/dotnet/Pulumi/Deployment/Deployment_RegisterResource.cs @@ -1,6 +1,7 @@ // Copyright 2016-2019, Pulumi Corporation using System; +using System.Collections.Immutable; using System.Threading.Tasks; using Google.Protobuf.WellKnownTypes; using Pulumirpc; @@ -9,28 +10,41 @@ namespace Pulumi { public partial class Deployment { - private async Task<(string urn, string id, Struct data)> RegisterResourceAsync( - Resource resource, ResourceArgs args, ResourceOptions options) + private async Task<(string urn, string id, Struct data, ImmutableDictionary> dependencies)> RegisterResourceAsync( + Resource resource, bool remote, Func newDependency, ResourceArgs args, + ResourceOptions options) { var name = resource.GetResourceName(); var type = resource.GetResourceType(); var custom = resource is CustomResource; var label = $"resource:{name}[{type}]"; - Log.Debug($"Registering resource start: t={type}, name={name}, custom={custom}"); + Log.Debug($"Registering resource start: t={type}, name={name}, custom={custom}, remote={remote}"); - var request = CreateRegisterResourceRequest(type, name, custom, options); + var request = CreateRegisterResourceRequest(type, name, custom, remote, options); - Log.Debug($"Preparing resource: t={type}, name={name}, custom={custom}"); + Log.Debug($"Preparing resource: t={type}, name={name}, custom={custom}, remote={remote}"); var prepareResult = await PrepareResourceAsync(label, resource, custom, args, options).ConfigureAwait(false); - Log.Debug($"Prepared resource: t={type}, name={name}, custom={custom}"); + Log.Debug($"Prepared resource: t={type}, name={name}, custom={custom}, remote={remote}"); PopulateRequest(request, prepareResult); - Log.Debug($"Registering resource monitor start: t={type}, name={name}, custom={custom}"); + Log.Debug($"Registering resource monitor start: t={type}, name={name}, custom={custom}, remote={remote}"); var result = await this.Monitor.RegisterResourceAsync(resource, request); - Log.Debug($"Registering resource monitor end: t={type}, name={name}, custom={custom}"); - return (result.Urn, result.Id, result.Object); + Log.Debug($"Registering resource monitor end: t={type}, name={name}, custom={custom}, remote={remote}"); + + var dependencies = ImmutableDictionary.CreateBuilder>(); + foreach (var (key, propertyDependencies) in result.PropertyDependencies) + { + var urns = ImmutableHashSet.CreateBuilder(); + foreach (var urn in propertyDependencies.Urns) + { + urns.Add(newDependency(urn)); + } + dependencies[key] = urns.ToImmutable(); + } + + return (result.Urn, result.Id, result.Object, dependencies.ToImmutable()); } private static void PopulateRequest(RegisterResourceRequest request, PrepareResult prepareResult) @@ -49,7 +63,8 @@ namespace Pulumi } } - private static RegisterResourceRequest CreateRegisterResourceRequest(string type, string name, bool custom, ResourceOptions options) + private static RegisterResourceRequest CreateRegisterResourceRequest( + string type, string name, bool custom, bool remote, ResourceOptions options) { var customOpts = options as CustomResourceOptions; var deleteBeforeReplace = customOpts?.DeleteBeforeReplace; @@ -71,6 +86,7 @@ namespace Pulumi Delete = TimeoutString(options.CustomTimeouts?.Delete), Update = TimeoutString(options.CustomTimeouts?.Update), }, + Remote = remote, }; if (customOpts != null) diff --git a/sdk/dotnet/Pulumi/Deployment/IDeploymentInternal.cs b/sdk/dotnet/Pulumi/Deployment/IDeploymentInternal.cs index 762cdfc6f..769385f17 100644 --- a/sdk/dotnet/Pulumi/Deployment/IDeploymentInternal.cs +++ b/sdk/dotnet/Pulumi/Deployment/IDeploymentInternal.cs @@ -1,8 +1,7 @@ // Copyright 2016-2019, Pulumi Corporation +using System; using System.Collections.Generic; -using System.Threading.Tasks; -using Pulumirpc; namespace Pulumi { @@ -15,7 +14,9 @@ namespace Pulumi ILogger Logger { get; } IRunner Runner { get; } - void ReadOrRegisterResource(Resource resource, ResourceArgs args, ResourceOptions opts); + void ReadOrRegisterResource( + Resource resource, bool remote, Func newDependency, ResourceArgs args, + ResourceOptions opts); void RegisterResourceOutputs(Resource resource, Output> outputs); } } diff --git a/sdk/dotnet/Pulumi/PublicAPI.Shipped.txt b/sdk/dotnet/Pulumi/PublicAPI.Shipped.txt index 0cc2aab3b..a421e757d 100644 --- a/sdk/dotnet/Pulumi/PublicAPI.Shipped.txt +++ b/sdk/dotnet/Pulumi/PublicAPI.Shipped.txt @@ -23,6 +23,7 @@ Pulumi.AssetArchive.AssetArchive(System.Collections.Generic.IDictionary void +Pulumi.ComponentResource.ComponentResource(string type, string name, Pulumi.ResourceArgs args, Pulumi.ComponentResourceOptions options = null, bool remote = false) -> void Pulumi.ComponentResource.RegisterOutputs() -> void Pulumi.ComponentResource.RegisterOutputs(Pulumi.Output> outputs) -> void Pulumi.ComponentResource.RegisterOutputs(System.Collections.Generic.IDictionary outputs) -> void diff --git a/sdk/dotnet/Pulumi/Resources/ComponentResource.cs b/sdk/dotnet/Pulumi/Resources/ComponentResource.cs index a3fa19dfe..34434fc28 100644 --- a/sdk/dotnet/Pulumi/Resources/ComponentResource.cs +++ b/sdk/dotnet/Pulumi/Resources/ComponentResource.cs @@ -9,7 +9,7 @@ namespace Pulumi { /// /// A that aggregates one or more other child resources into a higher - /// level abstraction.The component resource itself is a resource, but does not require custom + /// level abstraction. The component resource itself is a resource, but does not require custom /// CRUD operations for provisioning. /// public class ComponentResource : Resource @@ -21,11 +21,30 @@ namespace Pulumi /// for this component, and [options.dependsOn] is an optional list of other resources that /// this resource depends on, controlling the order in which we perform resource operations. /// -#pragma warning disable RS0022 // Constructor make noninheritable base class inheritable + /// The type of the resource. + /// The unique name of the resource. + /// A bag of options that control this resource's behavior. public ComponentResource(string type, string name, ComponentResourceOptions? options = null) - : base(type, name, custom: false, - args: ResourceArgs.Empty, - options ?? new ComponentResourceOptions()) + : this(type, name, ResourceArgs.Empty, options) + { + } + + /// + /// Creates and registers a new component resource. is the fully + /// qualified type token and is the "name" part to use in creating a + /// stable and globally unique URN for the object. options.parent is the optional parent + /// for this component, and [options.dependsOn] is an optional list of other resources that + /// this resource depends on, controlling the order in which we perform resource operations. + /// + /// The type of the resource. + /// The unique name of the resource. + /// The arguments to use to populate the new resource. + /// A bag of options that control this resource's behavior. + /// True if this is a remote component resource. +#pragma warning disable RS0022 // Constructor make noninheritable base class inheritable + public ComponentResource( + string type, string name, ResourceArgs? args, ComponentResourceOptions? options = null, bool remote = false) + : base(type, name, custom: false, args ?? ResourceArgs.Empty, options ?? new ComponentResourceOptions(), remote) #pragma warning restore RS0022 // Constructor make noninheritable base class inheritable { } diff --git a/sdk/dotnet/Pulumi/Resources/CustomResource.cs b/sdk/dotnet/Pulumi/Resources/CustomResource.cs index 637d994ce..c942c4926 100644 --- a/sdk/dotnet/Pulumi/Resources/CustomResource.cs +++ b/sdk/dotnet/Pulumi/Resources/CustomResource.cs @@ -5,7 +5,7 @@ using Pulumi.Serialization; namespace Pulumi { /// - /// CustomResource is a resource whose create, read, update, and delete(CRUD) operations are + /// CustomResource is a resource whose create, read, update, and delete (CRUD) operations are /// managed by performing external operations on some physical entity. The engine understands /// how to diff and perform partial updates of them, and these CRUD operations are implemented /// in a dynamically loaded plugin for the defining package. @@ -18,22 +18,49 @@ namespace Pulumi /// // Set using reflection, so we silence the NRT warnings with `null!`. [Output(Constants.IdPropertyName)] - public Output Id { get; private set; } = null!; + public Output Id { get; private protected set; } = null!; /// - /// Creates and registers a new managed resource. t is the fully qualified type token and - /// name is the "name" part to use in creating a stable and globally unique URN for the - /// object. dependsOn is an optional list of other resources that this resource depends on, - /// controlling the order in which we perform resource operations.Creating an instance does - /// not necessarily perform a create on the physical entity which it represents, and - /// instead, this is dependent upon the diffing of the new goal state compared to the - /// current known resource state. + /// Creates and registers a new managed resource. is the fully + /// qualified type token and is the "name" part to use in creating a + /// stable and globally unique URN for the object. + /// is an optional list of other resources that this resource depends on, controlling the + /// order in which we perform resource operations. Creating an instance does not necessarily + /// perform a create on the physical entity which it represents, and instead, this is + /// dependent upon the diffing of the new goal state compared to the current known resource + /// state. /// + /// The type of the resource. + /// The unique name of the resource. + /// The arguments to use to populate the new resource. + /// A bag of options that control this resource's behavior. #pragma warning disable RS0022 // Constructor make noninheritable base class inheritable public CustomResource(string type, string name, ResourceArgs? args, CustomResourceOptions? options = null) - : base(type, name, custom: true, args ?? ResourceArgs.Empty, options ?? new CustomResourceOptions()) + : this(type, name, args, options, dependency: false) #pragma warning restore RS0022 // Constructor make noninheritable base class inheritable { } + + /// + /// Creates and registers a new managed resource. is the fully + /// qualified type token and is the "name" part to use in creating a + /// stable and globally unique URN for the object. + /// is an optional list of other resources that this resource depends on, controlling the + /// order in which we perform resource operations. Creating an instance does not necessarily + /// perform a create on the physical entity which it represents, and instead, this is + /// dependent upon the diffing of the new goal state compared to the current known resource + /// state. + /// + /// The type of the resource. + /// The unique name of the resource. + /// The arguments to use to populate the new resource. + /// A bag of options that control this resource's behavior. + /// True if this is a synthetic resource used internally for dependency tracking. + private protected CustomResource( + string type, string name, ResourceArgs? args, CustomResourceOptions? options = null, bool dependency = false) + : base(type, name, custom: true, args ?? ResourceArgs.Empty, options ?? new CustomResourceOptions(), + remote: false, dependency: dependency) + { + } } } diff --git a/sdk/dotnet/Pulumi/Resources/DependencyProviderResource.cs b/sdk/dotnet/Pulumi/Resources/DependencyProviderResource.cs new file mode 100644 index 000000000..245514b86 --- /dev/null +++ b/sdk/dotnet/Pulumi/Resources/DependencyProviderResource.cs @@ -0,0 +1,36 @@ +// Copyright 2016-2020, Pulumi Corporation + +using System; +using System.Collections.Immutable; +using System.Threading.Tasks; +using Pulumi.Serialization; + +namespace Pulumi +{ + /// + /// is a that is used by the provider SDK as a + /// stand-in for a provider that is only used for its reference. Its only valid properties are its URN and ID. + /// + internal sealed class DependencyProviderResource : ProviderResource + { + public DependencyProviderResource(string reference) + : base(package: "", name: "", args: ResourceArgs.Empty, dependency: true) + { + int lastSep = reference.LastIndexOf("::", StringComparison.Ordinal); + if (lastSep == -1) + { + throw new ArgumentException($"Expected \"::\" in provider reference ${reference}."); + } + string urn = reference.Substring(0, lastSep); + string id = reference.Substring(lastSep + 2); + + var resources = ImmutableHashSet.Create(this); + + var urnData = OutputData.Create(resources, urn, isKnown: true, isSecret: false); + this.Urn = new Output(Task.FromResult(urnData)); + + var idData = OutputData.Create(resources, id, isKnown: true, isSecret: false); + this.Id = new Output(Task.FromResult(idData)); + } + } +} diff --git a/sdk/dotnet/Pulumi/Resources/DependencyResource.cs b/sdk/dotnet/Pulumi/Resources/DependencyResource.cs new file mode 100644 index 000000000..c5e686777 --- /dev/null +++ b/sdk/dotnet/Pulumi/Resources/DependencyResource.cs @@ -0,0 +1,24 @@ +// Copyright 2016-2020, Pulumi Corporation + +using System.Collections.Immutable; +using System.Threading.Tasks; +using Pulumi.Serialization; + +namespace Pulumi +{ + /// + /// is a that is used to indicate that an + /// has a dependency on a particular resource. These resources are only created when dealing + /// with remote component resources. + /// + internal sealed class DependencyResource : CustomResource + { + public DependencyResource(string urn) + : base(type: "", name: "", args: ResourceArgs.Empty, dependency: true) + { + var resources = ImmutableHashSet.Create(this); + var data = OutputData.Create(resources, urn, isKnown: true, isSecret: false); + this.Urn = new Output(Task.FromResult(data)); + } + } +} diff --git a/sdk/dotnet/Pulumi/Resources/ProviderResource.cs b/sdk/dotnet/Pulumi/Resources/ProviderResource.cs index 2f85e39b3..66bf76c14 100644 --- a/sdk/dotnet/Pulumi/Resources/ProviderResource.cs +++ b/sdk/dotnet/Pulumi/Resources/ProviderResource.cs @@ -19,10 +19,27 @@ namespace Pulumi /// /// Creates and registers a new provider resource for a particular package. /// - public ProviderResource( + /// The package associated with this provider. + /// The unique name of the provider. + /// The configuration to use for this provider. + /// A bag of options that control this provider's behavior. + public ProviderResource(string package, string name, ResourceArgs args, CustomResourceOptions? options = null) + : this(package, name, args, options, dependency: false) + { + } + + /// + /// Creates and registers a new provider resource for a particular package. + /// + /// The package associated with this provider. + /// The unique name of the provider. + /// The configuration to use for this provider. + /// A bag of options that control this provider's behavior. + /// True if this is a synthetic resource used internally for dependency tracking. + private protected ProviderResource( string package, string name, - ResourceArgs args, CustomResourceOptions? options = null) - : base($"pulumi:providers:{package}", name, args, options) + ResourceArgs args, CustomResourceOptions? options = null, bool dependency = false) + : base($"pulumi:providers:{package}", name, args, options, dependency) { this.Package = package; } diff --git a/sdk/dotnet/Pulumi/Resources/Resource.cs b/sdk/dotnet/Pulumi/Resources/Resource.cs index b6e1b9f81..b8c2cff86 100644 --- a/sdk/dotnet/Pulumi/Resources/Resource.cs +++ b/sdk/dotnet/Pulumi/Resources/Resource.cs @@ -64,7 +64,7 @@ namespace Pulumi /// // Set using reflection, so we silence the NRT warnings with `null!`. [Output(Constants.UrnPropertyName)] - public Output Urn { get; private set; } = null!; + public Output Urn { get; private protected set; } = null!; /// /// When set to true, protect ensures this resource cannot be deleted. @@ -110,10 +110,22 @@ namespace Pulumi /// True to indicate that this is a custom resource, managed by a plugin. /// The arguments to use to populate the new resource. /// A bag of options that control this resource's behavior. + /// True if this is a remote component resource. + /// True if this is a synthetic resource used internally for dependency tracking. private protected Resource( string type, string name, bool custom, - ResourceArgs args, ResourceOptions options) + ResourceArgs args, ResourceOptions options, + bool remote = false, bool dependency = false) { + if (dependency) + { + _type = ""; + _name = ""; + _protect = false; + _providers = ImmutableDictionary.Empty; + return; + } + if (string.IsNullOrEmpty(type)) throw new ArgumentException("'type' cannot be null or empty.", nameof(type)); @@ -243,7 +255,7 @@ namespace Pulumi } this._aliases = aliases.ToImmutable(); - Deployment.InternalInstance.ReadOrRegisterResource(this, args, options); + Deployment.InternalInstance.ReadOrRegisterResource(this, remote, urn => new DependencyResource(urn), args, options); } /// diff --git a/sdk/dotnet/Pulumi/Serialization/Converter.cs b/sdk/dotnet/Pulumi/Serialization/Converter.cs index a842c7ee6..d3c9913d0 100644 --- a/sdk/dotnet/Pulumi/Serialization/Converter.cs +++ b/sdk/dotnet/Pulumi/Serialization/Converter.cs @@ -20,14 +20,19 @@ namespace Pulumi.Serialization } public static OutputData ConvertValue(string context, Value value, System.Type targetType) + { + return ConvertValue(context, value, targetType, ImmutableHashSet.Empty); + } + + public static OutputData ConvertValue( + string context, Value value, System.Type targetType, ImmutableHashSet resources) { CheckTargetType(context, targetType, new HashSet()); var (deserialized, isKnown, isSecret) = Deserializer.Deserialize(value); var converted = ConvertObject(context, deserialized, targetType); - return new OutputData( - ImmutableHashSet.Empty, converted, isKnown, isSecret); + return new OutputData(resources, converted, isKnown, isSecret); } private static object? ConvertObject(string context, object? val, System.Type targetType) diff --git a/sdk/dotnet/Pulumi/Serialization/OutputCompletionSource.cs b/sdk/dotnet/Pulumi/Serialization/OutputCompletionSource.cs index d515f3c09..f00113b04 100644 --- a/sdk/dotnet/Pulumi/Serialization/OutputCompletionSource.cs +++ b/sdk/dotnet/Pulumi/Serialization/OutputCompletionSource.cs @@ -45,7 +45,7 @@ namespace Pulumi.Serialization public void SetValue(OutputData data) => _taskCompletionSource.SetResult(new OutputData( - _resources, (T)data.Value!, data.IsKnown, data.IsSecret)); + _resources.Union(data.Resources), (T)data.Value!, data.IsKnown, data.IsSecret)); public void TrySetDefaultResult(bool isKnown) => _taskCompletionSource.TrySetResult(new OutputData( diff --git a/tests/integration/construct_component/dotnet/.gitignore b/tests/integration/construct_component/dotnet/.gitignore new file mode 100644 index 000000000..e64527066 --- /dev/null +++ b/tests/integration/construct_component/dotnet/.gitignore @@ -0,0 +1,353 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ diff --git a/tests/integration/construct_component/dotnet/Component.cs b/tests/integration/construct_component/dotnet/Component.cs new file mode 100644 index 000000000..0d17afffc --- /dev/null +++ b/tests/integration/construct_component/dotnet/Component.cs @@ -0,0 +1,23 @@ +// Copyright 2016-2020, Pulumi Corporation. All rights reserved. + +using Pulumi; + +class ComponentArgs : Pulumi.ResourceArgs +{ + [Input("echo")] + public Input? Echo { get; set; } +} + +class Component : Pulumi.ComponentResource +{ + [Output("echo")] + public Output Echo { get; private set; } = null!; + + [Output("childId")] + public Output ChildId { get; private set; } = null!; + + public Component(string name, ComponentArgs args, ComponentResourceOptions opts = null) + : base("testcomponent:index:Component", name, args, opts, remote: true) + { + } +} diff --git a/tests/integration/construct_component/dotnet/ConstructComponent.csproj b/tests/integration/construct_component/dotnet/ConstructComponent.csproj new file mode 100644 index 000000000..de1adbd7d --- /dev/null +++ b/tests/integration/construct_component/dotnet/ConstructComponent.csproj @@ -0,0 +1,9 @@ + + + + Exe + netcoreapp3.1 + enable + + + diff --git a/tests/integration/construct_component/dotnet/MyStack.cs b/tests/integration/construct_component/dotnet/MyStack.cs new file mode 100644 index 000000000..2f59bd6e2 --- /dev/null +++ b/tests/integration/construct_component/dotnet/MyStack.cs @@ -0,0 +1,13 @@ +// Copyright 2016-2020, Pulumi Corporation. All rights reserved. + +using Pulumi; + +class MyStack : Stack +{ + public MyStack() + { + var componentA = new Component("a", new ComponentArgs { Echo = 42 }); + var componentB = new Component("b", new ComponentArgs { Echo = componentA.Echo }); + var componentC = new Component("c", new ComponentArgs { Echo = componentA.ChildId }); + } +} diff --git a/tests/integration/construct_component/dotnet/Program.cs b/tests/integration/construct_component/dotnet/Program.cs new file mode 100644 index 000000000..2cb5c6c69 --- /dev/null +++ b/tests/integration/construct_component/dotnet/Program.cs @@ -0,0 +1,7 @@ +using System.Threading.Tasks; +using Pulumi; + +class Program +{ + static Task Main() => Deployment.RunAsync(); +} diff --git a/tests/integration/construct_component/dotnet/Pulumi.yaml b/tests/integration/construct_component/dotnet/Pulumi.yaml new file mode 100644 index 000000000..277cf95e2 --- /dev/null +++ b/tests/integration/construct_component/dotnet/Pulumi.yaml @@ -0,0 +1,3 @@ +name: construct_component_dotnet +description: A program that constructs remote component resources. +runtime: dotnet diff --git a/tests/integration/integration_dotnet_test.go b/tests/integration/integration_dotnet_test.go index 7504914df..8d8bcd7fa 100644 --- a/tests/integration/integration_dotnet_test.go +++ b/tests/integration/integration_dotnet_test.go @@ -170,3 +170,54 @@ func TestLargeResourceDotNet(t *testing.T) { Dir: filepath.Join("large_resource", "dotnet"), }) } + +// Test remote component construction in .NET. +func TestConstructDotnet(t *testing.T) { + pathEnv, err := testComponentPathEnv() + if err != nil { + t.Fatalf("failed to build test component PATH: %v", err) + } + + // TODO[pulumi/pulumi#5455]: Dynamic providers fail to load when used from multi-lang components. + // Until we've addressed this, set PULUMI_TEST_YARN_LINK_PULUMI, which tells the integration test + // module to run `yarn install && yarn link @pulumi/pulumi` in the .NET program's directory, allowing + // the Node.js dynamic provider plugin to load. + // When the underlying issue has been fixed, the use of this environment variable inside the integration + // test module should be removed. + const testYarnLinkPulumiEnv = "PULUMI_TEST_YARN_LINK_PULUMI=true" + + var opts *integration.ProgramTestOptions + opts = &integration.ProgramTestOptions{ + Env: []string{pathEnv, testYarnLinkPulumiEnv}, + Dir: filepath.Join("construct_component", "dotnet"), + Dependencies: []string{"Pulumi"}, + Quick: true, + ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) { + assert.NotNil(t, stackInfo.Deployment) + if assert.Equal(t, 9, len(stackInfo.Deployment.Resources)) { + stackRes := stackInfo.Deployment.Resources[0] + assert.NotNil(t, stackRes) + assert.Equal(t, resource.RootStackType, stackRes.Type) + assert.Equal(t, "", string(stackRes.Parent)) + + // Check that dependencies flow correctly between the originating program and the remote component + // plugin. + urns := make(map[string]resource.URN) + for _, res := range stackInfo.Deployment.Resources[1:] { + assert.NotNil(t, res) + + urns[string(res.URN.Name())] = res.URN + switch res.URN.Name() { + case "child-a", "child-b": + for _, deps := range res.PropertyDependencies { + assert.Empty(t, deps) + } + case "child-c": + assert.Equal(t, []resource.URN{urns["child-a"]}, res.PropertyDependencies["echo"]) + } + } + } + }, + } + integration.ProgramTest(t, opts) +}