First-class Stack component for .NET (#3618)
First-class Stack component for .NET
This commit is contained in:
parent
6525d17b7b
commit
66de4a48b7
|
@ -18,6 +18,8 @@ CHANGELOG
|
|||
|
||||
- Support for using `Config`, `getProject()`, `getStack()`, and `isDryRun()` from Policy Packs.
|
||||
[#3612](https://github.com/pulumi/pulumi/pull/3612)
|
||||
- Top-level Stack component in the .NET SDK.
|
||||
[#3618](https://github.com/pulumi/pulumi/pull/3618)
|
||||
|
||||
- Add the .NET Core 3.0 runtime to the `pulumi/pulumi` container. [#3616](https://github.com/pulumi/pulumi/pull/3616)
|
||||
|
||||
|
|
113
sdk/dotnet/Pulumi.Tests/StackTests.cs
Normal file
113
sdk/dotnet/Pulumi.Tests/StackTests.cs
Normal file
|
@ -0,0 +1,113 @@
|
|||
// Copyright 2016-2019, Pulumi Corporation
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Moq;
|
||||
using Pulumi.Serialization;
|
||||
using Xunit;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace Pulumi.Tests
|
||||
{
|
||||
public class StackTests
|
||||
{
|
||||
private class ValidStack : Stack
|
||||
{
|
||||
[Output("foo")]
|
||||
public Output<string> ExplicitName { get; }
|
||||
|
||||
[Output]
|
||||
public Output<string> ImplicitName { get; }
|
||||
|
||||
public ValidStack()
|
||||
{
|
||||
this.ExplicitName = Output.Create("bar");
|
||||
this.ImplicitName = Output.Create("buzz");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidStackInstantiationSucceeds()
|
||||
{
|
||||
var (stack, outputs) = await Run<ValidStack>();
|
||||
Assert.Equal(2, outputs.Count);
|
||||
Assert.Same(stack.ExplicitName, outputs["foo"]);
|
||||
Assert.Same(stack.ImplicitName, outputs["ImplicitName"]);
|
||||
}
|
||||
|
||||
private class NullOutputStack : Stack
|
||||
{
|
||||
[Output("foo")]
|
||||
public Output<string>? Foo { get; }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StackWithNullOutputsThrows()
|
||||
{
|
||||
try
|
||||
{
|
||||
var (stack, outputs) = await Run<NullOutputStack>();
|
||||
}
|
||||
catch (RunException ex)
|
||||
{
|
||||
Assert.Contains("foo", ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new XunitException("Should not come here");
|
||||
}
|
||||
|
||||
private class InvalidOutputTypeStack : Stack
|
||||
{
|
||||
[Output("foo")]
|
||||
public string Foo { get; }
|
||||
|
||||
public InvalidOutputTypeStack()
|
||||
{
|
||||
this.Foo = "bar";
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StackWithInvalidOutputTypeThrows()
|
||||
{
|
||||
try
|
||||
{
|
||||
var (stack, outputs) = await Run<InvalidOutputTypeStack>();
|
||||
}
|
||||
catch (RunException ex)
|
||||
{
|
||||
Assert.Contains("foo", ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new XunitException("Should not come here");
|
||||
}
|
||||
|
||||
private async Task<(T, IDictionary<string, object?>)> Run<T>() where T : Stack, new()
|
||||
{
|
||||
// Arrange
|
||||
Output<IDictionary<string, object?>>? outputs = null;
|
||||
|
||||
var mock = new Mock<IDeploymentInternal>(MockBehavior.Strict);
|
||||
mock.Setup(d => d.ProjectName).Returns("TestProject");
|
||||
mock.Setup(d => d.StackName).Returns("TestStack");
|
||||
mock.SetupSet(content => content.Stack = It.IsAny<Stack>());
|
||||
mock.Setup(d => d.ReadOrRegisterResource(It.IsAny<Stack>(), It.IsAny<ResourceArgs>(), It.IsAny<ResourceOptions>()));
|
||||
mock.Setup(d => d.RegisterResourceOutputs(It.IsAny<Stack>(), It.IsAny<Output<IDictionary<string, object?>>>()))
|
||||
.Callback((Resource _, Output<IDictionary<string, object?>> o) => outputs = o);
|
||||
|
||||
Deployment.Instance = mock.Object;
|
||||
|
||||
// Act
|
||||
var stack = new T();
|
||||
stack.RegisterPropertyOutputs();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(outputs);
|
||||
var values = await outputs!.DataTask;
|
||||
return (stack, values.Value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Pulumi.Tests")]
|
||||
[assembly: InternalsVisibleTo("Pulumi.Tests")]
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq tests
|
||||
|
|
|
@ -15,6 +15,23 @@ namespace Pulumi
|
|||
public Runner(IDeploymentInternal deployment)
|
||||
=> _deployment = deployment;
|
||||
|
||||
public Task<int> RunAsync<TStack>() where TStack : Stack, new()
|
||||
{
|
||||
try
|
||||
{
|
||||
var stack = new TStack();
|
||||
// Stack doesn't call RegisterOutputs, so we register them on its behalf.
|
||||
stack.RegisterPropertyOutputs();
|
||||
RegisterTask("User program code.", stack.Outputs.DataTask);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return HandleExceptionAsync(ex);
|
||||
}
|
||||
|
||||
return WhileRunningAsync();
|
||||
}
|
||||
|
||||
public Task<int> RunAsync(Func<Task<IDictionary<string, object?>>> func)
|
||||
{
|
||||
var stack = new Stack(func);
|
||||
|
|
|
@ -9,6 +9,7 @@ namespace Pulumi
|
|||
public partial class Deployment
|
||||
{
|
||||
private Task<string>? _rootResource;
|
||||
private object _rootResourceLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Returns a root resource URN that will automatically become the default parent of all
|
||||
|
@ -23,21 +24,18 @@ namespace Pulumi
|
|||
if (type == Stack._rootPulumiStackTypeName)
|
||||
return null;
|
||||
|
||||
if (_rootResource == null)
|
||||
throw new InvalidOperationException($"Calling {nameof(GetRootResourceAsync)} before the root resource was registered!");
|
||||
lock (_rootResourceLock)
|
||||
{
|
||||
if (_rootResource == null)
|
||||
{
|
||||
var stack = InternalInstance.Stack ?? throw new InvalidOperationException($"Calling {nameof(GetRootResourceAsync)} before the stack was registered!");
|
||||
_rootResource = SetRootResourceWorkerAsync(stack);
|
||||
}
|
||||
}
|
||||
|
||||
return await _rootResource.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Task IDeploymentInternal.SetRootResourceAsync(Stack stack)
|
||||
{
|
||||
if (_rootResource != null)
|
||||
throw new InvalidOperationException("Tried to set the root resource more than once!");
|
||||
|
||||
_rootResource = SetRootResourceWorkerAsync(stack);
|
||||
return _rootResource;
|
||||
}
|
||||
|
||||
private async Task<string> SetRootResourceWorkerAsync(Stack stack)
|
||||
{
|
||||
var resUrn = await stack.Urn.GetValueAsync().ConfigureAwait(false);
|
||||
|
|
|
@ -28,7 +28,7 @@ namespace Pulumi
|
|||
=> RunAsync(() => Task.FromResult(func()));
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="RunAsync(Func{Task{IDictionary{string, object}}})"/> is the
|
||||
/// <see cref="RunAsync(Func{Task{IDictionary{string, object}}})"/> is an
|
||||
/// entry-point to a Pulumi application. .NET applications should perform all startup logic
|
||||
/// they need in their <c>Main</c> method and then end with:
|
||||
/// <para>
|
||||
|
@ -36,7 +36,7 @@ namespace Pulumi
|
|||
/// static Task<int> Main(string[] args)
|
||||
/// {
|
||||
/// // program initialization code ...
|
||||
///
|
||||
///
|
||||
/// return Deployment.Run(async () =>
|
||||
/// {
|
||||
/// // Code that creates resources.
|
||||
|
@ -56,6 +56,38 @@ namespace Pulumi
|
|||
/// in this dictionary will become the outputs for the Pulumi Stack that is created.
|
||||
/// </summary>
|
||||
public static Task<int> RunAsync(Func<Task<IDictionary<string, object?>>> func)
|
||||
=> CreateRunner().RunAsync(func);
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="RunAsync{TStack}()"/> is an entry-point to a Pulumi
|
||||
/// application. .NET applications should perform all startup logic they
|
||||
/// need in their <c>Main</c> method and then end with:
|
||||
/// <para>
|
||||
/// <c>
|
||||
/// static Task<int> Main(string[] args) {// program
|
||||
/// initialization code ...
|
||||
///
|
||||
/// return Deployment.Run<MyStack>();}
|
||||
/// </c>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Deployment will instantiate a new stack instance based on the type
|
||||
/// passed as TStack type parameter. Importantly, cloud resources cannot
|
||||
/// be created outside of the <see cref="Stack"/> component.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Because cloud Resource construction is inherently asynchronous, the
|
||||
/// result of this function is a <see cref="Task{T}"/> which should then
|
||||
/// be returned or awaited. This will ensure that any problems that are
|
||||
/// encountered during the running of the program are properly reported.
|
||||
/// Failure to do this may lead to the program ending early before all
|
||||
/// resources are properly registered.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static Task<int> RunAsync<TStack>() where TStack : Stack, new()
|
||||
=> CreateRunner().RunAsync<TStack>();
|
||||
|
||||
private static IRunner CreateRunner()
|
||||
{
|
||||
// Serilog.Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console().CreateLogger();
|
||||
|
||||
|
@ -68,7 +100,7 @@ namespace Pulumi
|
|||
Serilog.Log.Debug("Creating new Deployment.");
|
||||
var deployment = new Deployment();
|
||||
Instance = deployment;
|
||||
return deployment._runner.RunAsync(func);
|
||||
return deployment._runner;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,6 @@ namespace Pulumi
|
|||
ILogger Logger { get; }
|
||||
IRunner Runner { get; }
|
||||
|
||||
Task SetRootResourceAsync(Stack stack);
|
||||
|
||||
void ReadOrRegisterResource(Resource resource, ResourceArgs args, ResourceOptions opts);
|
||||
void RegisterResourceOutputs(Resource resource, Output<IDictionary<string, object?>> outputs);
|
||||
}
|
||||
|
|
|
@ -10,5 +10,6 @@ namespace Pulumi
|
|||
{
|
||||
void RegisterTask(string description, Task task);
|
||||
Task<int> RunAsync(Func<Task<IDictionary<string, object?>>> func);
|
||||
Task<int> RunAsync<TStack>() where TStack : Stack, new();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -165,11 +165,13 @@ Pulumi.Serialization.InputAttribute
|
|||
Pulumi.Serialization.InputAttribute.InputAttribute(string name, bool required = false, bool json = false) -> void
|
||||
Pulumi.Serialization.OutputAttribute
|
||||
Pulumi.Serialization.OutputAttribute.Name.get -> string
|
||||
Pulumi.Serialization.OutputAttribute.OutputAttribute(string name) -> void
|
||||
Pulumi.Serialization.OutputAttribute.OutputAttribute(string name = null) -> void
|
||||
Pulumi.Serialization.OutputConstructorAttribute
|
||||
Pulumi.Serialization.OutputConstructorAttribute.OutputConstructorAttribute() -> void
|
||||
Pulumi.Serialization.OutputTypeAttribute
|
||||
Pulumi.Serialization.OutputTypeAttribute.OutputTypeAttribute() -> void
|
||||
Pulumi.Stack
|
||||
Pulumi.Stack.Stack() -> void
|
||||
Pulumi.StackReference
|
||||
Pulumi.StackReference.GetOutput(Pulumi.Input<string> name) -> Pulumi.Output<object>
|
||||
Pulumi.StackReference.GetValueAsync(Pulumi.Input<string> name) -> System.Threading.Tasks.Task<object>
|
||||
|
@ -208,6 +210,7 @@ static Pulumi.Deployment.Instance.get -> Pulumi.IDeployment
|
|||
static Pulumi.Deployment.RunAsync(System.Action action) -> System.Threading.Tasks.Task<int>
|
||||
static Pulumi.Deployment.RunAsync(System.Func<System.Collections.Generic.IDictionary<string, object>> func) -> System.Threading.Tasks.Task<int>
|
||||
static Pulumi.Deployment.RunAsync(System.Func<System.Threading.Tasks.Task<System.Collections.Generic.IDictionary<string, object>>> func) -> System.Threading.Tasks.Task<int>
|
||||
static Pulumi.Deployment.RunAsync<TStack>() -> System.Threading.Tasks.Task<int>
|
||||
static Pulumi.Input<T>.implicit operator Pulumi.Input<T>(Pulumi.Output<T> value) -> Pulumi.Input<T>
|
||||
static Pulumi.Input<T>.implicit operator Pulumi.Input<T>(T value) -> Pulumi.Input<T>
|
||||
static Pulumi.Input<T>.implicit operator Pulumi.Output<T>(Pulumi.Input<T> input) -> Pulumi.Output<T>
|
||||
|
|
|
@ -11,9 +11,9 @@ namespace Pulumi.Serialization
|
|||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class OutputAttribute : Attribute
|
||||
{
|
||||
public string Name { get; }
|
||||
public string? Name { get; }
|
||||
|
||||
public OutputAttribute(string name)
|
||||
public OutputAttribute(string? name = null)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
|
|
@ -92,7 +92,9 @@ namespace Pulumi.Serialization
|
|||
var completionSource = (IOutputCompletionSource)ocsContructor.Invoke(new[] { resource });
|
||||
|
||||
setMethod.Invoke(resource, new[] { completionSource.Output });
|
||||
result.Add(attr.Name, completionSource);
|
||||
|
||||
var outputName = attr.Name ?? prop.Name;
|
||||
result.Add(outputName, completionSource);
|
||||
}
|
||||
|
||||
Log.Debug("Fields to assign: " + JsonSerializer.Serialize(result.Keys), resource);
|
||||
|
|
|
@ -3,18 +3,18 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Pulumi.Serialization;
|
||||
|
||||
namespace Pulumi
|
||||
{
|
||||
/// <summary>
|
||||
/// Stack is the root resource for a Pulumi stack. Before invoking the <c>init</c> callback, it
|
||||
/// registers itself as the root resource with the Pulumi engine.
|
||||
///
|
||||
/// An instance of this will be automatically created when any <see
|
||||
/// cref="Deployment.RunAsync(Action)"/> overload is called.
|
||||
/// Stack is the root resource for a Pulumi stack. Derive from this class to create your
|
||||
/// stack definitions.
|
||||
/// </summary>
|
||||
internal sealed class Stack : ComponentResource
|
||||
public class Stack : ComponentResource
|
||||
{
|
||||
/// <summary>
|
||||
/// Constant to represent the 'root stack' resource for a Pulumi application. The purpose
|
||||
|
@ -31,7 +31,7 @@ namespace Pulumi
|
|||
/// may look a bit confusing and may incorrectly look like something that could be removed
|
||||
/// without changing semantics.
|
||||
/// </summary>
|
||||
public static readonly Resource? Root = null;
|
||||
internal static readonly Resource? Root = null;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="_rootPulumiStackTypeName"/> is the type name that should be used to construct
|
||||
|
@ -44,14 +44,25 @@ namespace Pulumi
|
|||
/// <summary>
|
||||
/// The outputs of this stack, if the <c>init</c> callback exited normally.
|
||||
/// </summary>
|
||||
public readonly Output<IDictionary<string, object?>> Outputs =
|
||||
internal Output<IDictionary<string, object?>> Outputs =
|
||||
Output.Create<IDictionary<string, object?>>(ImmutableDictionary<string, object?>.Empty);
|
||||
|
||||
internal Stack(Func<Task<IDictionary<string, object?>>> init)
|
||||
/// <summary>
|
||||
/// Create a Stack with stack resources defined in derived class constructor.
|
||||
/// </summary>
|
||||
public Stack()
|
||||
: base(_rootPulumiStackTypeName, $"{Deployment.Instance.ProjectName}-{Deployment.Instance.StackName}")
|
||||
{
|
||||
Deployment.InternalInstance.Stack = this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Stack with stack resources created by the <c>init</c> callback.
|
||||
/// An instance of this will be automatically created when any <see
|
||||
/// cref="Deployment.RunAsync(Action)"/> overload is called.
|
||||
/// </summary>
|
||||
internal Stack(Func<Task<IDictionary<string, object?>>> init) : this()
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Outputs = Output.Create(RunInitAsync(init));
|
||||
|
@ -62,12 +73,46 @@ namespace Pulumi
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inspect all public properties of the stack to find outputs. Validate the values and register them as stack outputs.
|
||||
/// </summary>
|
||||
internal void RegisterPropertyOutputs()
|
||||
{
|
||||
var outputs = (from property in this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
|
||||
let attr = property.GetCustomAttribute<OutputAttribute>()
|
||||
where attr != null
|
||||
let name = attr.Name ?? property.Name
|
||||
select new KeyValuePair<string, object?>(name, property.GetValue(this))).ToList();
|
||||
|
||||
// Check that none of the values are null: catch unassigned outputs
|
||||
var nulls = (from kv in outputs
|
||||
where kv.Value == null
|
||||
select kv.Key).ToList();
|
||||
if (nulls.Any())
|
||||
{
|
||||
var message = $"Output(s) '{string.Join(", ", nulls)}' have no value assigned. [Output] attributed properties must be assigned inside Stack constructor.";
|
||||
throw new RunException(message);
|
||||
}
|
||||
|
||||
// Check that all the values are Output<T>
|
||||
var wrongTypes = (from kv in outputs
|
||||
let type = kv.Value.GetType()
|
||||
let isOutput = type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Output<>)
|
||||
where !isOutput
|
||||
select kv.Key).ToList();
|
||||
if (wrongTypes.Any())
|
||||
{
|
||||
var message = $"Output(s) '{string.Join(", ", wrongTypes)}' have incorrect type. [Output] attributed properties must be instances of Output<T>.";
|
||||
throw new RunException(message);
|
||||
}
|
||||
|
||||
IDictionary<string, object?> dict = new Dictionary<string, object?>(outputs);
|
||||
this.Outputs = Output.Create(dict);
|
||||
this.RegisterOutputs(this.Outputs);
|
||||
}
|
||||
|
||||
private async Task<IDictionary<string, object?>> RunInitAsync(Func<Task<IDictionary<string, object?>>> init)
|
||||
{
|
||||
// Ensure we are known as the root resource. This is needed before we execute any user
|
||||
// code as many codepaths will request the root resource.
|
||||
await Deployment.InternalInstance.SetRootResourceAsync(this).ConfigureAwait(false);
|
||||
|
||||
var dictionary = await init().ConfigureAwait(false);
|
||||
return dictionary == null
|
||||
? ImmutableDictionary<string, object?>.Empty
|
||||
|
|
|
@ -505,6 +505,29 @@ func TestStackDependencyGraph(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
// TestStackComponentDotNet tests the programming model of defining a stack as an explicit top-level component.
|
||||
func TestStackComponentDotNet(t *testing.T) {
|
||||
integration.ProgramTest(t, &integration.ProgramTestOptions{
|
||||
Dir: filepath.Join("stack_component", "dotnet"),
|
||||
Dependencies: []string{"Pulumi"},
|
||||
Quick: true,
|
||||
ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) {
|
||||
// Ensure the checkpoint contains a single resource, the Stack, with two outputs.
|
||||
fmt.Printf("Deployment: %v", stackInfo.Deployment)
|
||||
assert.NotNil(t, stackInfo.Deployment)
|
||||
if assert.Equal(t, 1, len(stackInfo.Deployment.Resources)) {
|
||||
stackRes := stackInfo.Deployment.Resources[0]
|
||||
assert.NotNil(t, stackRes)
|
||||
assert.Equal(t, resource.RootStackType, stackRes.URN.Type())
|
||||
assert.Equal(t, 0, len(stackRes.Inputs))
|
||||
assert.Equal(t, 2, len(stackRes.Outputs))
|
||||
assert.Equal(t, "ABC", stackRes.Outputs["abc"])
|
||||
assert.Equal(t, float64(42), stackRes.Outputs["Foo"])
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// TestConfigSave ensures that config commands in the Pulumi CLI work as expected.
|
||||
func TestConfigSave(t *testing.T) {
|
||||
e := ptesting.NewEnvironment(t)
|
||||
|
|
5
tests/integration/stack_component/dotnet/.gitignore
vendored
Normal file
5
tests/integration/stack_component/dotnet/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
/.pulumi/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
|
||||
|
30
tests/integration/stack_component/dotnet/Program.cs
Normal file
30
tests/integration/stack_component/dotnet/Program.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
// Copyright 2016-2019, Pulumi Corporation. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Pulumi;
|
||||
using Pulumi.Serialization;
|
||||
|
||||
class MyStack : Stack
|
||||
{
|
||||
[Output("abc")]
|
||||
public Output<string> Abc { get; private set; }
|
||||
|
||||
[Output]
|
||||
public Output<int> Foo { get; private set; }
|
||||
|
||||
// This should NOT be exported as stack output due to the missing attribute
|
||||
public Output<string> Bar { get; private set; }
|
||||
|
||||
public MyStack()
|
||||
{
|
||||
this.Abc = Output.Create("ABC");
|
||||
this.Foo = Output.Create(42);
|
||||
this.Bar = Output.Create("this should not come to output");
|
||||
}
|
||||
}
|
||||
|
||||
class Program
|
||||
{
|
||||
static Task<int> Main(string[] args) => Deployment.RunAsync<MyStack>();
|
||||
}
|
3
tests/integration/stack_component/dotnet/Pulumi.yaml
Normal file
3
tests/integration/stack_component/dotnet/Pulumi.yaml
Normal file
|
@ -0,0 +1,3 @@
|
|||
name: stack_compoennt
|
||||
description: A program that is defined as a Stack component.
|
||||
runtime: dotnet
|
|
@ -0,0 +1,8 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
Loading…
Reference in a new issue