-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Component provider host for auto-inferred components (#507)
Similar to other language SDKs, this PR implements a high-level component provider host that discovers schemas and constructors of all components in a given assembly or list of types at runtime. Instantiating a component provider is now as simple as authoring a component and then calling ```csharp class Program { public static Task Main(string []args) => ComponentProviderHost.Serve(args); } ``` (see the provider_component_host example) Resolve #469 --------- Co-authored-by: Thomas Gummerer <[email protected]>
- Loading branch information
1 parent
e6178b3
commit e3323bd
Showing
13 changed files
with
469 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
component: sdk/provider | ||
kind: Improvements | ||
body: Implement component provider host for auto-inferred components | ||
time: 2025-02-24T16:07:06.547901+01:00 | ||
custom: | ||
PR: "507" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
using System; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using Pulumi; | ||
|
||
public sealed class ComponentArgs : ResourceArgs | ||
{ | ||
[Input("passwordLength", required: true)] | ||
public Input<int> PasswordLength { get; set; } = null!; | ||
|
||
[Input("complex")] | ||
public Input<ComplexTypeArgs> Complex { get; set; } = null!; | ||
} | ||
|
||
public sealed class ComplexTypeArgs : ResourceArgs | ||
{ | ||
[Input("name", required: true)] | ||
public string Name { get; set; } = null!; | ||
|
||
[Input("intValue", required: true)] | ||
public int IntValue { get; set; } | ||
} | ||
|
||
[OutputType] | ||
public sealed class ComplexType | ||
{ | ||
[Output("name")] | ||
public string Name { get; set; } | ||
|
||
[Output("intValue")] | ||
public int IntValue { get; set; } | ||
|
||
[OutputConstructor] | ||
public ComplexType(string name, int intValue) | ||
{ | ||
Name = name; | ||
IntValue = intValue; | ||
} | ||
} | ||
|
||
class Component : ComponentResource | ||
{ | ||
private static readonly char[] Chars = | ||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".ToCharArray(); | ||
|
||
[Output("passwordResult")] | ||
public Output<string> PasswordResult { get; set; } | ||
|
||
[Output("complexResult")] | ||
public Output<ComplexType> ComplexResult { get; set; } | ||
|
||
public Component(string name, ComponentArgs args, ComponentResourceOptions? opts = null) | ||
: base("test:index:Test", name, args, opts) | ||
{ | ||
PasswordResult = args.PasswordLength.Apply(GenerateRandomString); | ||
if (args.Complex != null) | ||
{ | ||
ComplexResult = args.Complex.Apply(complex => Output.Create(AsTask(new ComplexType(complex.Name, complex.IntValue)))); | ||
} | ||
} | ||
|
||
private static Output<string> GenerateRandomString(int length) | ||
{ | ||
var result = new StringBuilder(length); | ||
var random = new Random(); | ||
|
||
for (var i = 0; i < length; i++) | ||
{ | ||
result.Append(Chars[random.Next(Chars.Length)]); | ||
} | ||
|
||
return Output.CreateSecret(result.ToString()); | ||
} | ||
|
||
private async Task<T> AsTask<T>(T value) | ||
{ | ||
await Task.Delay(10); | ||
return value; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Pulumi.Experimental.Provider; | ||
|
||
class Program | ||
{ | ||
public static Task Main(string []args) => ComponentProviderHost.Serve(args); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
runtime: dotnet |
14 changes: 14 additions & 0 deletions
14
integration_tests/provider_component_host/TestProvider.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework Condition=" '$(TARGET_FRAMEWORK)' != '' ">$(TARGET_FRAMEWORK)</TargetFramework> | ||
<TargetFramework Condition=" '$(TARGET_FRAMEWORK)' == '' ">net8.0</TargetFramework> | ||
<Nullable>enable</Nullable> | ||
<AssemblyName>dotnet-components</AssemblyName> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\..\sdk\Pulumi\Pulumi.csproj"/> | ||
</ItemGroup> | ||
</Project> |
13 changes: 13 additions & 0 deletions
13
integration_tests/provider_component_host/example/Pulumi.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
name: dotnet-component-yaml | ||
runtime: yaml | ||
plugins: | ||
providers: | ||
- name: dotnet-components | ||
path: .. | ||
resources: | ||
hello: | ||
type: dotnet-components:index:Component | ||
properties: | ||
passwordLength: 12 | ||
outputs: | ||
value: ${hello.passwordResult} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Reflection; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Xunit; | ||
using Pulumi.Experimental.Provider; | ||
|
||
namespace Pulumi.Tests.Provider | ||
{ | ||
public class ComponentProviderTests | ||
{ | ||
private ComponentProvider _provider; | ||
|
||
public ComponentProviderTests() | ||
{ | ||
var assembly = typeof(TestComponent).Assembly; | ||
_provider = new ComponentProvider(assembly, "test-package", new[] { typeof(TestComponent) }); | ||
} | ||
|
||
[Fact] | ||
public async Task GetSchema_ShouldReturnValidSchema() | ||
{ | ||
var request = new GetSchemaRequest(1, null, null); | ||
var response = await _provider.GetSchema(request, CancellationToken.None); | ||
|
||
Assert.NotNull(response); | ||
Assert.NotNull(response.Schema); | ||
Assert.Contains("TestComponent", response.Schema); | ||
Assert.Contains("testProperty", response.Schema); | ||
} | ||
|
||
[Fact] | ||
public async Task Construct_ValidComponent_ShouldThrowExpectedDeploymentException() | ||
{ | ||
var name = "test-component"; | ||
var inputs = new Dictionary<string, PropertyValue> | ||
{ | ||
["testProperty"] = new PropertyValue("test-value") | ||
}.ToImmutableDictionary(); | ||
var options = new ComponentResourceOptions(); | ||
var request = new ConstructRequest( | ||
"test-package:index:TestComponent", | ||
name, | ||
inputs, | ||
options | ||
); | ||
|
||
// We haven't initiated the deployment, so component construction will fail. Expect that specific exception, | ||
// since it will indicate that the rest of the provider is working. The checks are somewhat brittle and may fail | ||
// if we change the exception message or stack, but hopefully that will not happen too often. | ||
var exception = await Assert.ThrowsAsync<TargetInvocationException>( | ||
async () => await _provider.Construct(request, CancellationToken.None) | ||
); | ||
|
||
Assert.IsType<InvalidOperationException>(exception.InnerException); | ||
Assert.Contains("Deployment", exception.InnerException!.Message); | ||
} | ||
|
||
[Fact] | ||
public async Task Construct_InvalidPackageName_ShouldThrowException() | ||
{ | ||
var request = new ConstructRequest( | ||
"wrong:index:TestComponent", | ||
"test", | ||
ImmutableDictionary<string, PropertyValue>.Empty, | ||
new ComponentResourceOptions() | ||
); | ||
|
||
var exception = await Assert.ThrowsAsync<ArgumentException>( | ||
async () => await _provider.Construct(request, CancellationToken.None) | ||
); | ||
|
||
Assert.Contains("Invalid resource type", exception.Message); | ||
} | ||
|
||
[Fact] | ||
public async Task Construct_NonExistentComponent_ShouldThrowException() | ||
{ | ||
var request = new ConstructRequest( | ||
"test-package:index:NonExistentComponent", | ||
"test", | ||
ImmutableDictionary<string, PropertyValue>.Empty, | ||
new ComponentResourceOptions() | ||
); | ||
|
||
var exception = await Assert.ThrowsAsync<ArgumentException>( | ||
async () => await _provider.Construct(request, CancellationToken.None) | ||
); | ||
|
||
Assert.Contains("Component type not found", exception.Message); | ||
} | ||
} | ||
|
||
class TestComponentArgs : ResourceArgs | ||
{ | ||
[Input("testProperty", required: true)] | ||
public Input<string> TestProperty { get; set; } = null!; | ||
} | ||
|
||
class TestComponent : ComponentResource | ||
{ | ||
public TestComponent(string name, TestComponentArgs args, ComponentResourceOptions? options = null) | ||
: base("test-package:index:TestComponent", name, args, options) | ||
{ | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.