pulumi/sdk/dotnet/Pulumi/Deployment/Deployment.cs
2021-11-12 14:58:34 -08:00

219 lines
8.8 KiB
C#

// Copyright 2016-2019, Pulumi Corporation
using System;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Pulumi.Testing;
using Pulumirpc;
using Serilog;
using Serilog.Events;
using Serilog.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Pulumi
{
/// <summary>
/// <see cref="Deployment"/> is the 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{TResult}"/> 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.
/// </summary>
public sealed partial class Deployment : IDeploymentInternal
{
private static readonly object _instanceLock = new object();
private static readonly AsyncLocal<DeploymentInstance?> _instance = new AsyncLocal<DeploymentInstance?>();
/// <summary>
/// The current running deployment instance. This is only available from inside the function
/// passed to <see cref="Deployment.RunAsync(Action)"/> (or its overloads).
/// </summary>
public static DeploymentInstance Instance
{
get => _instance.Value ?? throw new InvalidOperationException("Trying to acquire Deployment.Instance before 'Run' was called.");
internal set
{
lock (_instanceLock)
{
if (_instance.Value != null)
{
throw new InvalidOperationException("Deployment.Instance should only be set once at the beginning of a 'Run' call.");
}
_instance.Value = value;
}
}
}
internal static bool TryGetInternalInstance([NotNullWhen(true)] out IDeploymentInternal? instance)
{
if (_instance.Value != null)
{
instance = _instance.Value.Internal;
return true;
}
instance = null;
return false;
}
internal static IDeploymentInternal InternalInstance
=> Instance.Internal;
private static readonly bool _disableResourceReferences =
Environment.GetEnvironmentVariable("PULUMI_DISABLE_RESOURCE_REFERENCES") == "1" ||
string.Equals(Environment.GetEnvironmentVariable("PULUMI_DISABLE_RESOURCE_REFERENCES"), "TRUE", StringComparison.OrdinalIgnoreCase);
private readonly string _projectName;
private readonly string _stackName;
private readonly bool _isDryRun;
private readonly ConcurrentDictionary<string, bool> _featureSupport = new ConcurrentDictionary<string, bool>();
private readonly IEngineLogger _logger;
private readonly IRunner _runner;
internal IEngine Engine { get; }
internal IMonitor Monitor { get; }
internal Stack? _stack;
internal Stack Stack
{
get => _stack ?? throw new InvalidOperationException("Trying to acquire Deployment.Stack before 'Run' was called.");
set => _stack = (value ?? throw new ArgumentNullException(nameof(value)));
}
private Deployment()
{
// ReSharper disable UnusedVariable
var monitor = Environment.GetEnvironmentVariable("PULUMI_MONITOR");
var engine = Environment.GetEnvironmentVariable("PULUMI_ENGINE");
var project = Environment.GetEnvironmentVariable("PULUMI_PROJECT");
var stack = Environment.GetEnvironmentVariable("PULUMI_STACK");
var pwd = Environment.GetEnvironmentVariable("PULUMI_PWD");
var dryRun = Environment.GetEnvironmentVariable("PULUMI_DRY_RUN");
var queryMode = Environment.GetEnvironmentVariable("PULUMI_QUERY_MODE");
var parallel = Environment.GetEnvironmentVariable("PULUMI_PARALLEL");
var tracing = Environment.GetEnvironmentVariable("PULUMI_TRACING");
if (string.IsNullOrEmpty(monitor) ||
string.IsNullOrEmpty(engine) ||
string.IsNullOrEmpty(project) ||
string.IsNullOrEmpty(stack) ||
!bool.TryParse(dryRun, out var dryRunValue) ||
!bool.TryParse(queryMode, out var queryModeValue) ||
!int.TryParse(parallel, out var parallelValue))
{
throw new InvalidOperationException("Program run without the Pulumi engine available; re-run using the `pulumi` CLI");
}
// ReSharper restore UnusedVariable
_isDryRun = dryRunValue;
_stackName = stack;
_projectName = project;
var deploymentLogger = CreateDefaultLogger();
deploymentLogger.LogDebug("Creating deployment engine");
this.Engine = new GrpcEngine(engine);
deploymentLogger.LogDebug("Created deployment engine");
deploymentLogger.LogDebug("Creating deployment monitor");
this.Monitor = new GrpcMonitor(monitor);
deploymentLogger.LogDebug("Created deployment monitor");
_runner = new Runner(this, deploymentLogger);
_logger = new EngineLogger(this, deploymentLogger, this.Engine);
}
/// <summary>
/// This constructor is called from <see cref="TestAsync(IMocks, Func{IRunner, Task{int}}, TestOptions?)"/>
/// with a mocked monitor and dummy values for project and stack.
/// <para/>
/// This constructor is also used in deployment tests in order to
/// instantiate mock deployments.
/// </summary>
internal Deployment(IEngine engine, IMonitor monitor, TestOptions? options)
{
var deploymentLogger = CreateDefaultLogger();
_isDryRun = options?.IsPreview ?? true;
_stackName = options?.StackName ?? "stack";
_projectName = options?.ProjectName ?? "project";
this.Engine = engine;
this.Monitor = monitor;
_runner = new Runner(this, deploymentLogger);
_logger = new EngineLogger(this, deploymentLogger, this.Engine);
}
string IDeployment.ProjectName => _projectName;
string IDeployment.StackName => _stackName;
bool IDeployment.IsDryRun => _isDryRun;
IEngineLogger IDeploymentInternal.Logger => _logger;
IRunner IDeploymentInternal.Runner => _runner;
Stack IDeploymentInternal.Stack
{
get => Stack;
set => Stack = value;
}
private ILogger CreateDefaultLogger()
{
var logger = new LoggerConfiguration()
.MinimumLevel.Is(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("PULUMI_DOTNET_LOG_VERBOSE")) ? LogEventLevel.Verbose : LogEventLevel.Fatal)
.WriteTo.Console()
.CreateLogger();
var loggerFactory = new SerilogLoggerFactory(logger);
return loggerFactory.CreateLogger<Deployment>();
}
private async Task<bool> MonitorSupportsFeature(string feature)
{
if (!this._featureSupport.ContainsKey(feature))
{
var request = new SupportsFeatureRequest { Id = feature };
var response = await this.Monitor.SupportsFeatureAsync(request).ConfigureAwait(false);
this._featureSupport[feature] = response.HasSupport;
}
return this._featureSupport[feature];
}
internal Task<bool> MonitorSupportsResourceReferences()
{
return MonitorSupportsFeature("resourceReferences");
}
/// <summary>
/// Check if the monitor supports the "outputValues" feature.
/// </summary>
internal Task<bool> MonitorSupportsOutputValues()
{
return MonitorSupportsFeature("outputValues");
}
// Because the secrets feature predates the Pulumi .NET SDK, we assume
// that the monitor supports secrets.
}
}