Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Component schema and runtime inference prototype #453

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,13 @@ class Component : ComponentResource
public Output<ComplexType> ComplexResult { get; set; }

public Component(string name, ComponentArgs args, ComponentResourceOptions? opts = null)
: base("test:index:Test", name, args, opts)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please duplicate this folder, it's useful having a low level test for components as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! This is just PoC, it needs more better separate tests.

: base("test:index:Component", name, args, opts)
{
PasswordResult = args.PasswordLength.Apply(GenerateRandomString);
ComplexResult = args.Complex.Apply(complex => Output.Create(AsTask(new ComplexType(complex.Name, complex.IntValue))));
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

class Program
{
public static Task Main(string []args) =>
Pulumi.Experimental.Provider.Provider.Serve(args, "0.0.1", host => new TestProviderImpl(), CancellationToken.None);
public static Task Main(string []args)
{
var provider = new Pulumi.Experimental.Provider.ComponentProviderImplementation(null, "test");
return Pulumi.Experimental.Provider.Provider.Serve(args, "0.0.1", host => provider, CancellationToken.None);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public override Task<ConstructResponse> Construct(ConstructRequest request, Canc
{
return request.Type switch
{
"test:index:Test" => Construct<ComponentArgs, Component>(request,
"test:index:Component" => Construct<ComponentArgs, Component>(request,
(name, args, options) => Task.FromResult(new Component(name, args, options))),
_ => throw new NotImplementedException()
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: test-yaml
runtime: yaml
plugins:
providers:
- name: test
path: ../
resources:
r1:
type: test:index:Component
properties:
passwordLength: 10
230 changes: 230 additions & 0 deletions sdk/Pulumi/Provider/Analyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Linq;

namespace Pulumi.Experimental.Provider
{
public class Analyzer
{
private readonly Metadata metadata;
private readonly Dictionary<string, TypeDefinition> typeDefinitions = new();

public Analyzer(Metadata metadata)
{
this.metadata = metadata;
}

public (Dictionary<string, ComponentDefinition>, Dictionary<string, TypeDefinition>) Analyze(Assembly assembly)
{
var components = new Dictionary<string, ComponentDefinition>();

// Find all component resources in the assembly
var types = assembly.GetTypes();
foreach (var type in types)
{
if (typeof(ComponentResource).IsAssignableFrom(type) && !type.IsAbstract)
{
components[type.Name] = AnalyzeComponent(type);
}
}

return (components, typeDefinitions);
}

private ComponentDefinition AnalyzeComponent(Type componentType)
{
// Get constructor args type
var argsType = GetArgsType(componentType);
var (inputs, inputsMapping) = AnalyzeType(argsType);
var (outputs, outputsMapping) = AnalyzeOutputs(componentType);

return new ComponentDefinition
{
Description = componentType.GetCustomAttribute<System.ComponentModel.DescriptionAttribute>()?.Description,
Inputs = inputs,
InputsMapping = inputsMapping,
Outputs = outputs,
OutputsMapping = outputsMapping
};
}

private static Type GetArgsType(Type componentType)
{
var constructor = componentType.GetConstructors().First();
var argsParameter = constructor.GetParameters()
.FirstOrDefault(p => p.Name == "args")
?? throw new ArgumentException($"Component {componentType.Name} must have an 'args' parameter in constructor");

return argsParameter.ParameterType;
}

private (Dictionary<string, PropertyDefinition>, Dictionary<string, string>) AnalyzeType(Type type)
{
var properties = new Dictionary<string, PropertyDefinition>();
var mapping = new Dictionary<string, string>();

foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
var schemaName = GetSchemaPropertyName(prop);
mapping[schemaName] = prop.Name;

var propertyDef = AnalyzeProperty(prop);
if (propertyDef != null)
{
properties[schemaName] = propertyDef;
}
}

return (properties, mapping);
}

private (Dictionary<string, PropertyDefinition>, Dictionary<string, string>) AnalyzeOutputs(Type type)
{
var properties = new Dictionary<string, PropertyDefinition>();
var mapping = new Dictionary<string, string>();

foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
// Only look at properties marked with [Output]
var outputAttr = prop.GetCustomAttribute<OutputAttribute>();
if (outputAttr == null) continue;

var schemaName = outputAttr.Name ?? GetSchemaPropertyName(prop);
mapping[schemaName] = prop.Name;

var propertyDef = AnalyzeProperty(prop);
if (propertyDef != null)
{
properties[schemaName] = propertyDef;
}
}

return (properties, mapping);
}

private PropertyDefinition? AnalyzeProperty(PropertyInfo prop)
{
var propType = prop.PropertyType;
var isOptional = IsOptionalProperty(prop);

// Handle Input<T> and Output<T>
if (IsInputOrOutput(propType, out var unwrappedType))
{
propType = unwrappedType;
}

// Get property description
var description = prop.GetCustomAttribute<System.ComponentModel.DescriptionAttribute>()?.Description;

if (IsBuiltinType(propType, out var builtinType))
{
return new PropertyDefinition
{
Description = description,
Type = builtinType,
Optional = isOptional
};
}
else if (propType.IsClass && propType != typeof(string))
{
// Complex type
var typeName = propType.Name;
var typeRef = $"#/types/{metadata.Name}:index:{typeName}";

// Add to type definitions if not already present
if (!typeDefinitions.ContainsKey(typeName))
{
var (properties, mapping) = AnalyzeType(propType);
typeDefinitions[typeName] = new TypeDefinition
{
Name = typeName,
Description = propType.GetCustomAttribute<System.ComponentModel.DescriptionAttribute>()?.Description,
Properties = properties,
PropertiesMapping = mapping
};
}

return new PropertyDefinition
{
Description = description,
Ref = typeRef,
Optional = isOptional
};
}

return null;
}

private static bool IsInputOrOutput(Type type, out Type unwrappedType)
{
unwrappedType = type;

if (type.IsGenericType)
{
var genericDef = type.GetGenericTypeDefinition();
if (genericDef == typeof(Input<>) || genericDef == typeof(Output<>))
{
unwrappedType = type.GetGenericArguments()[0];
return true;
}
}

return false;
}

private static bool IsBuiltinType(Type type, out string builtinType)
{
if (type == typeof(string))
{
builtinType = BuiltinTypeSpec.String;
return true;
}
if (type == typeof(int) || type == typeof(long))
{
builtinType = BuiltinTypeSpec.Integer;
return true;
}
if (type == typeof(double) || type == typeof(float) || type == typeof(decimal))
{
builtinType = BuiltinTypeSpec.Number;
return true;
}
if (type == typeof(bool))
{
builtinType = BuiltinTypeSpec.Boolean;
return true;
}

builtinType = BuiltinTypeSpec.Object;
return false;
}

private static bool IsOptionalProperty(PropertyInfo prop)
{
// Check if type is nullable
if (Nullable.GetUnderlyingType(prop.PropertyType) != null)
return true;

// Check if property has [Input] attribute with required=false
var inputAttr = prop.GetCustomAttribute<InputAttribute>();
if (inputAttr != null)
return !inputAttr.IsRequired;

// Default to optional
return true;
}

private static string GetSchemaPropertyName(PropertyInfo prop)
{
// Check for explicit name in Input attribute
var inputAttr = prop.GetCustomAttribute<InputAttribute>();
if (inputAttr != null && !string.IsNullOrEmpty(inputAttr.Name))
return inputAttr.Name;

// Convert to camelCase
var name = prop.Name;
return char.ToLowerInvariant(name[0]) + name.Substring(1);
}
}
}
110 changes: 110 additions & 0 deletions sdk/Pulumi/Provider/ComponentProviderImplementation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using System;
using System.Collections.Immutable;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Text.Json;
using System.Text.Json.Serialization;
using Pulumi.Utilities;
namespace Pulumi.Experimental.Provider
{
public class ComponentProviderImplementation : Provider
{
private readonly Assembly componentAssembly;
private readonly string packageName;
#pragma warning disable CS0618 // Type or member is obsolete
private readonly PropertyValueSerializer serializer;
#pragma warning restore CS0618 // Type or member is obsolete

public ComponentProviderImplementation(Assembly? componentAssembly, string? packageName)
{
this.componentAssembly = componentAssembly ?? Assembly.GetCallingAssembly();
this.packageName = packageName ?? this.componentAssembly.GetName().Name!.ToLower();
#pragma warning disable CS0618 // Type or member is obsolete
this.serializer = new PropertyValueSerializer();
#pragma warning restore CS0618 // Type or member is obsolete
}

public override Task<GetSchemaResponse> GetSchema(GetSchemaRequest request, CancellationToken ct)
{
var analyzer = new Analyzer(new Metadata
{
Name = packageName,
Version = "1.0.0" // This should probably come from the provider configuration
});

var (components, typeDefinitions) = analyzer.Analyze(componentAssembly);
var schema = PackageSpec.GenerateSchema(
metadata: new Metadata
{
Name = packageName,
Version = "1.0.0",
// You might want to add DisplayName from provider config
},
components: components,
typeDefinitions: typeDefinitions
);

// Serialize to JSON
var jsonSchema = JsonSerializer.Serialize(schema, new JsonSerializerOptions
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});

// throw new Exception(jsonSchema);

return Task.FromResult(new GetSchemaResponse
{
Schema = jsonSchema
});
}

public override async Task<ConstructResponse> Construct(ConstructRequest request, CancellationToken ct)
{
try {
// Parse type token
var parts = request.Type.Split(':');
if (parts.Length != 3 || parts[0] != packageName)
throw new ArgumentException($"Invalid resource type: {request.Type}");

var componentName = parts[2];
var componentType = componentAssembly.GetType(componentName)
?? throw new ArgumentException($"Component type not found: {componentName}");

// Create args instance by deserializing inputs
var argsType = GetArgsType(componentType);
var args = serializer.Deserialize(new PropertyValue(request.Inputs), argsType);

// Create component instance
var component = (ComponentResource)Activator.CreateInstance(componentType, request.Name, args, request.Options)!;

var urn = await OutputUtilities.GetValueAsync(component.Urn);
if (string.IsNullOrEmpty(urn))
{
throw new InvalidOperationException($"URN of resource {request.Name} is not known.");
}

var stateValue = await serializer.StateFromComponentResource(component);

return new ConstructResponse(new Experimental.Provider.Urn(urn), stateValue, ImmutableDictionary<string, ISet<Urn>>.Empty);
}
catch (Exception e)
{
throw new Exception($"Error constructing resource {request.Name}: {e.Message} {e.StackTrace}");
}
}

private static Type GetArgsType(Type componentType)
{
var constructor = componentType.GetConstructors().First();
var argsParameter = constructor.GetParameters()
.FirstOrDefault(p => p.Name == "args")
?? throw new ArgumentException($"Component {componentType.Name} must have an 'args' parameter in constructor");

return argsParameter.ParameterType;
}
}
}
Loading
Loading