pulumi/sdk/dotnet/Pulumi/Deployment/Deployment_Run.cs
Josh Studt 963b5ab710
[Automation API] - C# Implementation (#5761)
* Init Workspace interface for C# Automation API

* fleshing out workspace interface and beginning of local workspace implementation

* initial run pulumi cmd implementation

* resolve issue with pulumi cmd cleanup wrapper task after testing

* flesh out local workspace implementation, flesh out stack implementation, cleanup run pulumi cmd implementation and make it an instance so it is mockable/testable, separate serialization in prep for custom converters

* project settings json serialization implemented

* Initial commit of language server

* Add deployment from language server

* Cleanup

* finish json serialization

* project runtime yaml serialization completed. just need stack config value yaml serialization

* Remove typed argument

* Limit concurrency

* Rename file for consistency

* final commit of a semi-working project settings & stack settings serialization so that it is in the commit history

* modify workspace API so that settings accessors aren't fully exposed since we are defering a complete serialization implementation until a later date

* yaml converters wrap any outgoing exceptions so resolve that

* getting the beginning of inline program GRPC communication set up

* stack lifecycle operations implemented, and switched to newtonsoft for JSON serialization

* change back to system.text.json with a custom object converter

* local workspace tests written, working on getting them passing

* fix the encoding on the GO files used for testing

* all tests passing except inline program, pulumi engine not available with inline

* inline program engine is now running as expecting, but inline program is not recognizing local stack config

* All tests passing, but no concurrency capability because of the singleton DeploymentInstance.

* cleanup unnecessary usings

* minor cleanup / changes after a quick review. Make sure ConfigureAwait is used where needed. Remove newtonsoft dependency from testing. Update workspace API to use existing PluginKind enum. Modify LanguageRuntimeService so that its semaphore operates process-wide.

* support for parallel execution of inline program, test included

* Update LocalWorkspaceTests.cs

remove some redundancy from the inline program parallel execution text

* flesh out some comments and make asynclocal instance readonly

* Strip out instance locking since it is no longer necessary with AsyncLocal wrapping the Deployment.Instance. Modify CreateRunner method such that we are ensuring there isn't a chance of delayed synchronous execution polluting the value of Deployment.Instance across calls to Deployment.RunAsync

* resolve conflicts with changes made to Deployment.TestAsync entrypoints

* update changelog

* write a test that fails if the CreateRunnerAndRunAsync method on Deployment is not marked async and fix test project data file ref

* make resource package state share the lifetime of the deployment so that their isn't cross deployment issues with resource packages, add support and tests for external resource packages (resource packages that aren't referenced by the executing assembly)

* enable parallel test collection execution in test suite, add some additional tests for deployment instance protection and ensuring that our first class stack exceptions are thrown when expected

* minor inline project name arg change, and re-add xunit json to build output (whoops)

* strip out concurrency changes since they are now in #6139, split automation into separate assembly, split automation tests into separate assembly

* add copyright to the top of each new file

* resolve some PR remarks

* inline program exception is now properly propagated to the caller on UpAsync and PreviewAsync

* modify PulumiFn to allow TStack and other delegate overloads without needing multiple first class delegates.

* whoops missing a copyright

* resolve getting TStack into IRunner so that outputs are registered correctly and so that there isn't 2 instances of Pulumi.Stack instantiated.

* resolve issue with propagation of TStack exceptions and add a test

* add support for a TStack PulumiFn resolved via IServiceProvider

* update automation API description

* fix comment and remove unnecessary TODOs

* disable packaging of automation api assembly

* re-name automation api documentation file appropriately

* add --limit support to dotnet automation api for stack history per #6257

* re-name XStack as WorkspaceStack

* replace --limit usage with --page-size and --page in dotnet automation api per #6292

Co-authored-by: evanboyle <evan@pulumi.com>
Co-authored-by: Josh Studt <josh.studt@figmarketing.com>
Co-authored-by: Dan Friedman <dan@thefriedmans.org>
Co-authored-by: David Ferretti <David.Ferretti@figmarketing.com>
Co-authored-by: Mikhail Shilkov <github@mikhail.io>
2021-02-18 11:36:21 +01:00

224 lines
10 KiB
C#

// Copyright 2016-2019, Pulumi Corporation
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Pulumi.Testing;
namespace Pulumi
{
public partial class Deployment
{
/// <summary>
/// <see cref="RunAsync(Func{Task{IDictionary{string, object}}}, StackOptions)"/> for more details.
/// </summary>
/// <param name="action">Callback that creates stack resources.</param>
public static Task<int> RunAsync(Action action)
=> RunAsync(() =>
{
action();
return ImmutableDictionary<string, object?>.Empty;
});
/// <summary>
/// <see cref="RunAsync(Func{Task{IDictionary{string, object}}}, StackOptions)"/> for more details.
/// </summary>
/// <param name="func">Callback that creates stack resources.</param>
/// <returns>A dictionary of stack outputs.</returns>
public static Task<int> RunAsync(Func<IDictionary<string, object?>> func)
=> RunAsync(() => Task.FromResult(func()));
/// <summary>
/// <see cref="RunAsync(Func{Task{IDictionary{string, object}}}, StackOptions)"/> for more details.
/// </summary>
/// <param name="func">Callback that creates stack resources.</param>
public static Task<int> RunAsync(Func<Task> func)
=> RunAsync(async () =>
{
await func();
return ImmutableDictionary<string, object?>.Empty;
});
/// <summary>
/// <see cref="RunAsync(Func{Task{IDictionary{string, object}}}, StackOptions)"/> 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&lt;int&gt; Main(string[] args)
/// {
/// // program initialization code ...
///
/// return Deployment.Run(async () =>
/// {
/// // Code that creates resources.
/// });
/// }
/// </c>
/// </para>
/// Importantly: Cloud resources cannot be created outside of the lambda passed to any of the
/// <see cref="Deployment.RunAsync(Action)"/> overloads. 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/>
/// The function passed to <see cref="RunAsync(Func{Task{IDictionary{string, object}}}, StackOptions)"/>
/// can optionally return an <see cref="IDictionary{TKey, TValue}"/>. The keys and values
/// in this dictionary will become the outputs for the Pulumi Stack that is created.
/// </summary>
/// <param name="func">Callback that creates stack resources.</param>
/// <param name="options">Stack options.</param>
public static Task<int> RunAsync(Func<Task<IDictionary<string, object?>>> func, StackOptions? options = null)
=> CreateRunner(() => new Deployment()).RunAsync(func, options);
/// <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&lt;int&gt; Main(string[] args) {// program
/// initialization code ...
///
/// return Deployment.Run&lt;MyStack&gt;();}
/// </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(() => new Deployment()).RunAsync<TStack>();
/// <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&lt;int&gt; Main(string[] args) {// program
/// initialization code ...
///
/// return Deployment.Run&lt;MyStack&gt;(serviceProvider);}
/// </c>
/// </para>
/// <para>
/// Deployment will instantiate a new stack instance based on the type
/// passed as TStack type parameter using the serviceProvider.
/// 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>(IServiceProvider serviceProvider) where TStack : Stack
=> CreateRunner(() => new Deployment()).RunAsync<TStack>(serviceProvider);
/// <summary>
/// Entry point to test a Pulumi application. Deployment will
/// instantiate a new stack instance based on the type passed as TStack
/// type parameter using the given service provider. This method creates
/// no real resources.
/// Note: Currently, unit tests that call
/// <see cref="TestWithServiceProviderAsync{TStack}(IMocks, IServiceProvider, TestOptions)"/>
/// must run serially; parallel execution is not supported.
/// </summary>
/// <param name="mocks">Hooks to mock the engine calls.</param>
/// <param name="serviceProvider"></param>
/// <param name="options">Optional settings for the test run.</param>
/// <typeparam name="TStack">The type of the stack to test.</typeparam>
/// <returns>Test result containing created resources and errors, if any.</returns>
public static Task<ImmutableArray<Resource>> TestWithServiceProviderAsync<TStack>(IMocks mocks, IServiceProvider serviceProvider, TestOptions? options = null)
where TStack : Stack
{
return TestAsync(mocks, (deployment) => deployment._runner.RunAsync<TStack>(serviceProvider), options);
}
/// <summary>
/// Entry point to test a Pulumi application. Deployment will
/// instantiate a new stack instance based on the type passed as TStack
/// type parameter. This method creates no real resources.
/// Note: Currently, unit tests that call <see cref="TestAsync{TStack}(IMocks, TestOptions)"/>
/// must run serially; parallel execution is not supported.
/// </summary>
/// <param name="mocks">Hooks to mock the engine calls.</param>
/// <param name="options">Optional settings for the test run.</param>
/// <typeparam name="TStack">The type of the stack to test.</typeparam>
/// <returns>Test result containing created resources and errors, if any.</returns>
public static Task<ImmutableArray<Resource>> TestAsync<TStack>(IMocks mocks, TestOptions? options = null)
where TStack : Stack, new()
{
return TestAsync(mocks, (deployment)=> deployment._runner.RunAsync<TStack>(), options);
}
private static async Task<ImmutableArray<Resource>> TestAsync(IMocks mocks, Func<Deployment, Task<int>> runAsync, TestOptions? options = null)
{
var engine = new MockEngine();
var monitor = new MockMonitor(mocks);
Deployment deployment;
lock (_instanceLock)
{
if (_instance != null)
throw new NotSupportedException($"Multiple executions of {nameof(TestAsync)} must run serially. Please configure your unit test suite to run tests one-by-one.");
deployment = new Deployment(engine, monitor, options);
Instance = new DeploymentInstance(deployment);
}
try
{
await runAsync(deployment);
return engine.Errors.Count switch
{
1 => throw new RunException(engine.Errors.Single()),
int v when v > 1 => throw new AggregateException(engine.Errors.Select(e => new RunException(e))),
_ => monitor.Resources.ToImmutableArray()
};
}
finally
{
lock (_instanceLock)
{
_instance = null;
}
}
}
private static IRunner CreateRunner(Func<Deployment> deploymentFactory)
{
// Serilog.Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console().CreateLogger();
Serilog.Log.Debug("Deployment.Run called.");
lock (_instanceLock)
{
if (_instance != null)
throw new NotSupportedException("Deployment.Run can only be called a single time.");
Serilog.Log.Debug("Creating new Deployment.");
var deployment = deploymentFactory();
Instance = new DeploymentInstance(deployment);
return deployment._runner;
}
}
}
}