// Copyright 2016-2021, Pulumi Corporation using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Pulumi.Automation.Commands.Exceptions; using Pulumi.Automation.Events; using Pulumi.Automation.Exceptions; using Semver; using Serilog; using Serilog.Extensions.Logging; using Xunit; using Xunit.Abstractions; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Pulumi.Automation.Tests { public class LocalWorkspaceTests { private static readonly string _pulumiOrg = GetTestOrg(); private static string GetTestSuffix() { var random = new Random(); var result = 100000 + random.Next(0, 900000); return result.ToString(); } private static string RandomStackName() { const string chars = "abcdefghijklmnopqrstuvwxyz"; return new string(Enumerable.Range(1, 8).Select(_ => chars[new Random().Next(chars.Length)]).ToArray()); } private static string GetTestOrg() => Environment.GetEnvironmentVariable("PULUMI_TEST_ORG") ?? "pulumi-test"; private static string FullyQualifiedStackName(string org, string project, string stack) => $"{org}/{project}/{stack}"; private static string NormalizeConfigKey(string key, string projectName) { var parts = key.Split(":"); if (parts.Length < 2) return $"{projectName}:{key}"; return string.Empty; } private ILogger TestLogger { get; } public LocalWorkspaceTests(ITestOutputHelper output) { var logger = new LoggerConfiguration() .MinimumLevel.Verbose() .WriteTo.TestOutput(output, outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}") .CreateLogger(); var loggerFactory = new SerilogLoggerFactory(logger); TestLogger = loggerFactory.CreateLogger(); } [Theory] [InlineData("yaml")] [InlineData("yml")] [InlineData("json")] public async Task GetProjectSettings(string extension) { var workingDir = ResourcePath(Path.Combine("Data", extension)); using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions { WorkDir = workingDir, }); var settings = await workspace.GetProjectSettingsAsync(); Assert.NotNull(settings); Assert.Equal("testproj", settings!.Name); Assert.Equal(ProjectRuntimeName.Go, settings.Runtime.Name); Assert.Equal("A minimal Go Pulumi program", settings.Description); } [Theory] [InlineData("yaml")] [InlineData("yml")] [InlineData("json")] public async Task GetStackSettings(string extension) { var workingDir = ResourcePath(Path.Combine("Data", extension)); using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions { WorkDir = workingDir, }); var settings = await workspace.GetStackSettingsAsync("dev"); Assert.NotNull(settings); Assert.Equal("abc", settings!.SecretsProvider); Assert.NotNull(settings.Config); Assert.True(settings.Config!.TryGetValue("plain", out var plainValue)); Assert.Equal("plain", plainValue!.Value); Assert.False(plainValue.IsSecure); Assert.True(settings.Config.TryGetValue("secure", out var secureValue)); Assert.Equal("secret", secureValue!.Value); Assert.True(secureValue.IsSecure); } [Fact] public async Task AddRemoveListPlugins() { using var workspace = await LocalWorkspace.CreateAsync(); var plugins = await workspace.ListPluginsAsync(); if (plugins.Any(p => p.Name == "aws" && p.Version == "3.0.0")) { await workspace.RemovePluginAsync("aws", "3.0.0"); plugins = await workspace.ListPluginsAsync(); Assert.DoesNotContain(plugins, p => p.Name == "aws" && p.Version == "3.0.0"); } await workspace.InstallPluginAsync("aws", "v3.0.0"); plugins = await workspace.ListPluginsAsync(); var aws = plugins.FirstOrDefault(p => p.Name == "aws" && p.Version == "3.0.0"); Assert.NotNull(aws); await workspace.RemovePluginAsync("aws", "3.0.0"); plugins = await workspace.ListPluginsAsync(); Assert.DoesNotContain(plugins, p => p.Name == "aws" && p.Version == "3.0.0"); } [Fact] public async Task CreateSelectRemoveStack() { var projectSettings = new ProjectSettings("create_select_remove_stack_test", ProjectRuntimeName.NodeJS); using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions { ProjectSettings = projectSettings, EnvironmentVariables = new Dictionary { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); var stackName = $"{RandomStackName()}"; var stacks = await workspace.ListStacksAsync(); if (stacks.Any(s => s.Name == stackName)) { await workspace.RemoveStackAsync(stackName); stacks = await workspace.ListStacksAsync(); Assert.DoesNotContain(stacks, s => s.Name == stackName); } await workspace.CreateStackAsync(stackName); stacks = await workspace.ListStacksAsync(); var newStack = stacks.FirstOrDefault(s => s.Name == stackName); Assert.NotNull(newStack); Assert.True(newStack!.IsCurrent); await workspace.SelectStackAsync(stackName); await workspace.RemoveStackAsync(stackName); stacks = await workspace.ListStacksAsync(); Assert.DoesNotContain(stacks, s => s.Name == stackName); } [Fact] public async Task ImportExportStack() { var workingDir = ResourcePath(Path.Combine("Data", "testproj")); var projectSettings = new ProjectSettings("testproj", ProjectRuntimeName.Go) { Description = "A minimal Go Pulumi program" }; using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions { WorkDir = workingDir, ProjectSettings = projectSettings, EnvironmentVariables = new Dictionary() { ["PULUMI_CONFIG_PASSPHRASE"] = "test" } }); var stackName = $"{RandomStackName()}"; try { var stack = await WorkspaceStack.CreateAsync(stackName, workspace); var upResult = await stack.UpAsync(); Assert.Equal(UpdateKind.Update, upResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, upResult.Summary.Result); Assert.Equal(3, upResult.Outputs.Count); var deployment = await workspace.ExportStackAsync(stackName); Assert.True(deployment.Version > 0); var previewBeforeDestroy = await stack.PreviewAsync(); Assert.Equal(1, previewBeforeDestroy.ChangeSummary[OperationType.Same]); await stack.DestroyAsync(); var previewAfterDestroy = await stack.PreviewAsync(); Assert.Equal(1, previewAfterDestroy.ChangeSummary[OperationType.Create]); await workspace.ImportStackAsync(stackName, deployment); // After we imported before-destroy deployment, // preview is back to reporting the before-destroy // state. var previewAfterImport = await stack.PreviewAsync(); Assert.Equal(1, previewAfterImport.ChangeSummary[OperationType.Same]); } finally { await workspace.RemoveStackAsync(stackName); } } [Fact] public async Task ManipulateConfig() { var projectName = "manipulate_config_test"; var projectSettings = new ProjectSettings(projectName, ProjectRuntimeName.NodeJS); using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions { ProjectSettings = projectSettings, EnvironmentVariables = new Dictionary { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); var stackName = $"{RandomStackName()}"; var stack = await WorkspaceStack.CreateAsync(stackName, workspace); var plainKey = NormalizeConfigKey("plain", projectName); var secretKey = NormalizeConfigKey("secret", projectName); try { await Assert.ThrowsAsync( () => stack.GetConfigAsync(plainKey)); var values = await stack.GetAllConfigAsync(); Assert.Empty(values); var config = new Dictionary { ["plain"] = new ConfigValue("abc"), ["secret"] = new ConfigValue("def", isSecret: true), }; await stack.SetAllConfigAsync(config); values = await stack.GetAllConfigAsync(); Assert.True(values.TryGetValue(plainKey, out var plainValue)); Assert.Equal("abc", plainValue!.Value); Assert.False(plainValue.IsSecret); Assert.True(values.TryGetValue(secretKey, out var secretValue)); Assert.Equal("def", secretValue!.Value); Assert.True(secretValue.IsSecret); // Get individual configuration values plainValue = await stack.GetConfigAsync(plainKey); Assert.Equal("abc", plainValue!.Value); Assert.False(plainValue.IsSecret); secretValue = await stack.GetConfigAsync(secretKey); Assert.Equal("def", secretValue!.Value); Assert.True(secretValue.IsSecret); await stack.RemoveConfigAsync("plain"); values = await stack.GetAllConfigAsync(); Assert.Single(values); await stack.SetConfigAsync("foo", new ConfigValue("bar")); values = await stack.GetAllConfigAsync(); Assert.Equal(2, values.Count); } finally { await workspace.RemoveStackAsync(stackName); } } [Fact] public async Task ListStackAndCurrentlySelected() { var projectSettings = new ProjectSettings( $"node_list_test{GetTestSuffix()}", ProjectRuntimeName.NodeJS); using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions { ProjectSettings = projectSettings, EnvironmentVariables = new Dictionary { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); var stackNames = new List(); try { for (var i = 0; i < 2; i++) { var stackName = GetStackName(); await WorkspaceStack.CreateAsync(stackName, workspace); stackNames.Add(stackName); var summary = await workspace.GetStackAsync(); Assert.NotNull(summary); Assert.True(summary!.IsCurrent); var stacks = await workspace.ListStacksAsync(); Assert.Equal(i + 1, stacks.Count); } } finally { foreach (var name in stackNames) await workspace.RemoveStackAsync(name); } static string GetStackName() => $"int_test{GetTestSuffix()}"; } [Fact] public async Task CheckStackStatus() { var projectSettings = new ProjectSettings("check_stack_status_test", ProjectRuntimeName.NodeJS); using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions { ProjectSettings = projectSettings, EnvironmentVariables = new Dictionary { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); var stackName = $"{RandomStackName()}"; var stack = await WorkspaceStack.CreateAsync(stackName, workspace); try { var history = await stack.GetHistoryAsync(); Assert.Empty(history); var info = await stack.GetInfoAsync(); Assert.Null(info); } finally { await workspace.RemoveStackAsync(stackName); } } [Fact] public async Task StackLifecycleLocalProgram() { var stackName = $"{RandomStackName()}"; var workingDir = ResourcePath(Path.Combine("Data", "testproj")); using var stack = await LocalWorkspace.CreateStackAsync(new LocalProgramArgs(stackName, workingDir) { EnvironmentVariables = new Dictionary { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); try { var config = new Dictionary { ["bar"] = new ConfigValue("abc"), ["buzz"] = new ConfigValue("secret", isSecret: true), }; await stack.SetAllConfigAsync(config); // pulumi up var upResult = await stack.UpAsync(); Assert.Equal(UpdateKind.Update, upResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, upResult.Summary.Result); Assert.Equal(3, upResult.Outputs.Count); // exp_static Assert.True(upResult.Outputs.TryGetValue("exp_static", out var expStaticValue)); Assert.Equal("foo", expStaticValue!.Value); Assert.False(expStaticValue.IsSecret); // exp_cfg Assert.True(upResult.Outputs.TryGetValue("exp_cfg", out var expConfigValue)); Assert.Equal("abc", expConfigValue!.Value); Assert.False(expConfigValue.IsSecret); // exp_secret Assert.True(upResult.Outputs.TryGetValue("exp_secret", out var expSecretValue)); Assert.Equal("secret", expSecretValue!.Value); Assert.True(expSecretValue.IsSecret); // pulumi preview var previewResult = await stack.PreviewAsync(); Assert.True(previewResult.ChangeSummary.TryGetValue(OperationType.Same, out var sameCount)); Assert.Equal(1, sameCount); // pulumi refresh var refreshResult = await stack.RefreshAsync(); Assert.Equal(UpdateKind.Refresh, refreshResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, refreshResult.Summary.Result); // pulumi destroy var destroyResult = await stack.DestroyAsync(); Assert.Equal(UpdateKind.Destroy, destroyResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, destroyResult.Summary.Result); } finally { await stack.Workspace.RemoveStackAsync(stackName); } } [Fact] public async Task StackLifecycleInlineProgram() { var program = PulumiFn.Create(() => { var config = new Config(); return new Dictionary { ["exp_static"] = "foo", ["exp_cfg"] = config.Get("bar"), ["exp_secret"] = config.GetSecret("buzz"), }; }); Assert.IsType(program); var stackName = $"{RandomStackName()}"; var projectName = "inline_node"; using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program) { EnvironmentVariables = new Dictionary { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); try { var config = new Dictionary { ["bar"] = new ConfigValue("abc"), ["buzz"] = new ConfigValue("secret", isSecret: true), }; await stack.SetAllConfigAsync(config); // pulumi up var upResult = await stack.UpAsync(); Assert.Equal(UpdateKind.Update, upResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, upResult.Summary.Result); Assert.Equal(3, upResult.Outputs.Count); // exp_static Assert.True(upResult.Outputs.TryGetValue("exp_static", out var expStaticValue)); Assert.Equal("foo", expStaticValue!.Value); Assert.False(expStaticValue.IsSecret); // exp_cfg Assert.True(upResult.Outputs.TryGetValue("exp_cfg", out var expConfigValue)); Assert.Equal("abc", expConfigValue!.Value); Assert.False(expConfigValue.IsSecret); // exp_secret Assert.True(upResult.Outputs.TryGetValue("exp_secret", out var expSecretValue)); Assert.Equal("secret", expSecretValue!.Value); Assert.True(expSecretValue.IsSecret); // pulumi preview var previewResult = await stack.PreviewAsync(); Assert.True(previewResult.ChangeSummary.TryGetValue(OperationType.Same, out var sameCount)); Assert.Equal(1, sameCount); // pulumi refresh var refreshResult = await stack.RefreshAsync(); Assert.Equal(UpdateKind.Refresh, refreshResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, refreshResult.Summary.Result); // pulumi destroy var destroyResult = await stack.DestroyAsync(); Assert.Equal(UpdateKind.Destroy, destroyResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, destroyResult.Summary.Result); } finally { await stack.Workspace.RemoveStackAsync(stackName); } } [Fact] public async Task SupportsStackOutputs() { var program = PulumiFn.Create(() => { var config = new Config(); return new Dictionary { ["exp_static"] = "foo", ["exp_cfg"] = config.Get("bar"), ["exp_secret"] = config.GetSecret("buzz"), }; }); var stackName = $"{RandomStackName()}"; var projectName = "inline_node"; using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program) { EnvironmentVariables = new Dictionary { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); try { var config = new Dictionary { ["bar"] = new ConfigValue("abc"), ["buzz"] = new ConfigValue("secret", isSecret: true), }; await stack.SetAllConfigAsync(config); var initialOutputs = await stack.GetOutputsAsync(); Assert.Empty(initialOutputs); // pulumi up var upResult = await stack.UpAsync(); Assert.Equal(UpdateKind.Update, upResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, upResult.Summary.Result); AssertOutputs(upResult.Outputs); var outputsAfterUp = await stack.GetOutputsAsync(); AssertOutputs(outputsAfterUp); // pulumi destroy var destroyResult = await stack.DestroyAsync(); Assert.Equal(UpdateKind.Destroy, destroyResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, destroyResult.Summary.Result); var outputsAfterDestroy = await stack.GetOutputsAsync(); Assert.Empty(outputsAfterDestroy); } finally { await stack.Workspace.RemoveStackAsync(stack.Name); } static void AssertOutputs(IImmutableDictionary outputs) { Assert.Equal(3, outputs.Count); // exp_static Assert.True(outputs.TryGetValue("exp_static", out var expStaticValue)); Assert.Equal("foo", expStaticValue!.Value); Assert.False(expStaticValue.IsSecret); // exp_cfg Assert.True(outputs.TryGetValue("exp_cfg", out var expConfigValue)); Assert.Equal("abc", expConfigValue!.Value); Assert.False(expConfigValue.IsSecret); // exp_secret Assert.True(outputs.TryGetValue("exp_secret", out var expSecretValue)); Assert.Equal("secret", expSecretValue!.Value); Assert.True(expSecretValue.IsSecret); } } [Fact(Skip = "Breaking builds")] public async Task StackReferenceDestroyDiscardsWithTwoInlinePrograms() { var programA = PulumiFn.Create(() => new Dictionary { ["exp_static"] = "foo", }); var programB = PulumiFn.Create(() => { var config = new Config(); var stackRef = new StackReference(config.Require("Ref")); return new Dictionary { ["exp_static"] = stackRef.GetOutput("exp_static"), }; }); var stackNameA = $"{RandomStackName()}"; var stackNameB = $"{RandomStackName()}"; var projectName = "inline_stack_reference"; var stackA = await SetupStack(projectName, stackNameA, programA, new Dictionary()); var stackB = await SetupStack(projectName, stackNameB, programB, new Dictionary { ["Ref"] = new ConfigValue(FullyQualifiedStackName(_pulumiOrg, projectName, stackNameA)), }); try { // Update the first stack { var upResult = await stackA.UpAsync(); Assert.Equal(UpdateKind.Update, upResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, upResult.Summary.Result); Assert.Equal(1, upResult.Outputs.Count); // exp_static Assert.True(upResult.Outputs.TryGetValue("exp_static", out var expStaticValue)); Assert.Equal("foo", expStaticValue!.Value); Assert.False(expStaticValue.IsSecret); } // Update the second stack which references the first { var upResult = await stackB.UpAsync(); Assert.Equal(UpdateKind.Update, upResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, upResult.Summary.Result); Assert.Equal(1, upResult.Outputs.Count); // exp_static Assert.True(upResult.Outputs.TryGetValue("exp_static", out var expStaticValue)); Assert.Equal("foo", expStaticValue!.Value); Assert.False(expStaticValue.IsSecret); } // Destroy stacks in reverse order { var destroyResult = await stackB.DestroyAsync(); Assert.Equal(UpdateKind.Destroy, destroyResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, destroyResult.Summary.Result); } { var destroyResult = await stackA.DestroyAsync(); Assert.Equal(UpdateKind.Destroy, destroyResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, destroyResult.Summary.Result); } } // Ensure stacks are deleted even if some of the operations fail finally { await stackA.Workspace.RemoveStackAsync(stackNameA); await stackB.Workspace.RemoveStackAsync(stackNameB); } static async Task SetupStack(string project, string stackName, PulumiFn program, Dictionary configMap) { var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(project, stackName, program) { EnvironmentVariables = new Dictionary { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); await stack.SetAllConfigAsync(configMap); return stack; } } [Fact] public async Task OutputStreamAndDelegateIsWritten() { var program = PulumiFn.Create(() => new Dictionary { ["test"] = "test", }); var stackName = $"{RandomStackName()}"; var projectName = "inline_output"; using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program) { EnvironmentVariables = new Dictionary { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); try { // pulumi preview var outputCalled = false; var previewResult = await stack.PreviewAsync(new PreviewOptions { OnStandardOutput = str => outputCalled = true }); Assert.False(string.IsNullOrEmpty(previewResult.StandardOutput)); Assert.True(outputCalled); // pulumi up outputCalled = false; var upResult = await stack.UpAsync(new UpOptions { OnStandardOutput = str => outputCalled = true }); Assert.False(string.IsNullOrEmpty(upResult.StandardOutput)); Assert.True(outputCalled); // pulumi refresh outputCalled = false; var refreshResult = await stack.RefreshAsync(new RefreshOptions { OnStandardOutput = str => outputCalled = true }); Assert.False(string.IsNullOrEmpty(refreshResult.StandardOutput)); Assert.True(outputCalled); // pulumi destroy outputCalled = false; var destroyResult = await stack.DestroyAsync(new DestroyOptions { OnStandardOutput = str => outputCalled = true }); Assert.False(string.IsNullOrEmpty(destroyResult.StandardOutput)); Assert.True(outputCalled); } finally { await stack.Workspace.RemoveStackAsync(stack.Name); } } [Fact] public async Task HandlesEvents() { var program = PulumiFn.Create(() => new Dictionary { ["exp_static"] = "foo", }); var projectName = "event_test"; var stackName = $"inline_events{GetTestSuffix()}"; using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program) { EnvironmentVariables = new Dictionary { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); try { // pulumi preview var previewResult = await RunCommand(stack.PreviewAsync, "preview", new PreviewOptions()); Assert.True(previewResult.ChangeSummary.TryGetValue(OperationType.Create, out var createCount)); Assert.Equal(1, createCount); // pulumi up var upResult = await RunCommand(stack.UpAsync, "up", new UpOptions()); Assert.Equal(UpdateKind.Update, upResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, upResult.Summary.Result); // pulumi preview var previewResultAgain = await RunCommand(stack.PreviewAsync, "preview", new PreviewOptions()); Assert.True(previewResultAgain.ChangeSummary.TryGetValue(OperationType.Same, out var sameCount)); Assert.Equal(1, sameCount); // pulumi refresh var refreshResult = await RunCommand(stack.RefreshAsync, "refresh", new RefreshOptions()); Assert.Equal(UpdateKind.Refresh, refreshResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, refreshResult.Summary.Result); // pulumi destroy var destroyResult = await RunCommand(stack.DestroyAsync, "destroy", new DestroyOptions()); Assert.Equal(UpdateKind.Destroy, destroyResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, destroyResult.Summary.Result); } finally { await stack.Workspace.RemoveStackAsync(stackName); } static async Task RunCommand(Func> func, string command, TOptions options) where TOptions : UpdateOptions, new() { var events = new List(); options.OnEvent = events.Add; var result = await func(options, CancellationToken.None); var seenSummaryEvent = events.Any(@event => @event.SummaryEvent != null); var seenCancelEvent = events.Any(@event => @event.CancelEvent != null); Assert.True(events.Any(), $"No Events found for '{command}'"); Assert.True(events.SequenceEqual(events.OrderBy(@event => @event.Sequence)), $"Events should be received in the sequence order for '{command}'"); Assert.True(seenSummaryEvent, $"No SummaryEvent for '{command}'"); Assert.True(seenCancelEvent, $"No CancelEvent for '{command}'"); return result; } } // TODO[pulumi/pulumi#7127]: Re-enable the warning. [Fact(Skip="Temporarily skipping test until we've re-enabled the warning - pulumi/pulumi#7127")] public async Task ConfigSecretWarnings() { var program = PulumiFn.Create(() => { var config = new Config(); config.Get("plainstr1"); config.Require("plainstr2"); config.GetSecret("plainstr3"); config.RequireSecret("plainstr4"); config.GetBoolean("plainbool1"); config.RequireBoolean("plainbool2"); config.GetSecretBoolean("plainbool3"); config.RequireSecretBoolean("plainbool4"); config.GetInt32("plainint1"); config.RequireInt32("plainint2"); config.GetSecretInt32("plainint3"); config.RequireSecretInt32("plainint4"); config.GetObject("plainobj1"); config.RequireObject("plainobj2"); config.GetSecretObject("plainobj3"); config.RequireSecretObject("plainobj4"); config.Get("str1"); config.Require("str2"); config.GetSecret("str3"); config.RequireSecret("str4"); config.GetBoolean("bool1"); config.RequireBoolean("bool2"); config.GetSecretBoolean("bool3"); config.RequireSecretBoolean("bool4"); config.GetInt32("int1"); config.RequireInt32("int2"); config.GetSecretInt32("int3"); config.RequireSecretInt32("int4"); config.GetObject("obj1"); config.RequireObject("obj2"); config.GetSecretObject("obj3"); config.RequireSecretObject("obj4"); }); var projectName = "inline_dotnet"; var stackName = $"inline_dotnet{GetTestSuffix()}"; using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program) { EnvironmentVariables = new Dictionary { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); try { var config = new Dictionary { { "plainstr1", new ConfigValue("1") }, { "plainstr2", new ConfigValue("2") }, { "plainstr3", new ConfigValue("3") }, { "plainstr4", new ConfigValue("4") }, { "plainbool1", new ConfigValue("true") }, { "plainbool2", new ConfigValue("true") }, { "plainbool3", new ConfigValue("true") }, { "plainbool4", new ConfigValue("true") }, { "plainint1", new ConfigValue("1") }, { "plainint2", new ConfigValue("2") }, { "plainint3", new ConfigValue("3") }, { "plainint4", new ConfigValue("4") }, { "plainobj1", new ConfigValue("{}") }, { "plainobj2", new ConfigValue("{}") }, { "plainobj3", new ConfigValue("{}") }, { "plainobj4", new ConfigValue("{}") }, { "str1", new ConfigValue("1", isSecret: true) }, { "str2", new ConfigValue("2", isSecret: true) }, { "str3", new ConfigValue("3", isSecret: true) }, { "str4", new ConfigValue("4", isSecret: true) }, { "bool1", new ConfigValue("true", isSecret: true) }, { "bool2", new ConfigValue("true", isSecret: true) }, { "bool3", new ConfigValue("true", isSecret: true) }, { "bool4", new ConfigValue("true", isSecret: true) }, { "int1", new ConfigValue("1", isSecret: true) }, { "int2", new ConfigValue("2", isSecret: true) }, { "int3", new ConfigValue("3", isSecret: true) }, { "int4", new ConfigValue("4", isSecret: true) }, { "obj1", new ConfigValue("{}", isSecret: true) }, { "obj2", new ConfigValue("{}", isSecret: true) }, { "obj3", new ConfigValue("{}", isSecret: true) }, { "obj4", new ConfigValue("{}", isSecret: true) }, }; await stack.SetAllConfigAsync(config); // pulumi preview await RunCommand(stack.PreviewAsync, "preview", new PreviewOptions()); // pulumi up await RunCommand(stack.UpAsync, "up", new UpOptions()); } finally { await stack.Workspace.RemoveStackAsync(stackName); } static async Task RunCommand(Func> func, string command, TOptions options) where TOptions : UpdateOptions, new() { var expectedWarnings = new[] { "Configuration 'inline_dotnet:str1' value is a secret; use `GetSecret` instead of `Get`", "Configuration 'inline_dotnet:str2' value is a secret; use `RequireSecret` instead of `Require`", "Configuration 'inline_dotnet:bool1' value is a secret; use `GetSecretBoolean` instead of `GetBoolean`", "Configuration 'inline_dotnet:bool2' value is a secret; use `RequireSecretBoolean` instead of `RequireBoolean`", "Configuration 'inline_dotnet:int1' value is a secret; use `GetSecretInt32` instead of `GetInt32`", "Configuration 'inline_dotnet:int2' value is a secret; use `RequireSecretInt32` instead of `RequireInt32`", "Configuration 'inline_dotnet:obj1' value is a secret; use `GetSecretObject` instead of `GetObject`", "Configuration 'inline_dotnet:obj2' value is a secret; use `RequireSecretObject` instead of `RequireObject`", }; // These keys should not be in any warning messages. var unexpectedWarnings = new[] { "plainstr1", "plainstr2", "plainstr3", "plainstr4", "plainbool1", "plainbool2", "plainbool3", "plainbool4", "plainint1", "plainint2", "plainint3", "plainint4", "plainobj1", "plainobj2", "plainobj3", "plainobj4", "str3", "str4", "bool3", "bool4", "int3", "int4", "obj3", "obj4", }; var events = new List(); options.OnEvent = @event => { if (@event.DiagnosticEvent?.Severity == "warning") { events.Add(@event.DiagnosticEvent); } }; var result = await func(options, CancellationToken.None); foreach (var expected in expectedWarnings) { Assert.Contains(events, @event => @event.Message.Contains(expected)); } foreach (var unexpected in unexpectedWarnings) { Assert.DoesNotContain(events, @event => @event.Message.Contains(unexpected)); } return result; } } private class ValidStack : Stack { [Output("exp_static")] public Output ExpStatic { get; set; } [Output("exp_cfg")] public Output ExpConfig { get; set; } [Output("exp_secret")] public Output ExpSecret { get; set; } public ValidStack() { var config = new Config(); this.ExpStatic = Output.Create("foo"); this.ExpConfig = Output.Create(config.Get("bar")!); this.ExpSecret = config.GetSecret("buzz")!; } } [Fact] public async Task StackLifecycleInlineProgramWithTStack() { var program = PulumiFn.Create(); Assert.IsType>(program); var stackName = $"{RandomStackName()}"; var projectName = "inline_tstack_node"; using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program) { EnvironmentVariables = new Dictionary { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); try { var config = new Dictionary { ["bar"] = new ConfigValue("abc"), ["buzz"] = new ConfigValue("secret", isSecret: true), }; await stack.SetAllConfigAsync(config); // pulumi up var upResult = await stack.UpAsync(); Assert.Equal(UpdateKind.Update, upResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, upResult.Summary.Result); Assert.Equal(3, upResult.Outputs.Count); // exp_static Assert.True(upResult.Outputs.TryGetValue("exp_static", out var expStaticValue)); Assert.Equal("foo", expStaticValue!.Value); Assert.False(expStaticValue.IsSecret); // exp_cfg Assert.True(upResult.Outputs.TryGetValue("exp_cfg", out var expConfigValue)); Assert.Equal("abc", expConfigValue!.Value); Assert.False(expConfigValue.IsSecret); // exp_secret Assert.True(upResult.Outputs.TryGetValue("exp_secret", out var expSecretValue)); Assert.Equal("secret", expSecretValue!.Value); Assert.True(expSecretValue.IsSecret); // pulumi preview var previewResult = await stack.PreviewAsync(); Assert.True(previewResult.ChangeSummary.TryGetValue(OperationType.Same, out var sameCount)); Assert.Equal(1, sameCount); // pulumi refresh var refreshResult = await stack.RefreshAsync(); Assert.Equal(UpdateKind.Refresh, refreshResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, refreshResult.Summary.Result); // pulumi destroy var destroyResult = await stack.DestroyAsync(); Assert.Equal(UpdateKind.Destroy, destroyResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, destroyResult.Summary.Result); } finally { await stack.Workspace.RemoveStackAsync(stackName); } } [Fact] public async Task StackLifecycleInlineProgramWithServiceProvider() { await using var provider = new ServiceCollection() .AddTransient() // must be transient so it is instantiated each time .BuildServiceProvider(); var program = PulumiFn.Create(provider); Assert.IsType(program); var stackName = $"{RandomStackName()}"; var projectName = "inline_serviceprovider_node"; using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program) { EnvironmentVariables = new Dictionary { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); try { var config = new Dictionary { ["bar"] = new ConfigValue("abc"), ["buzz"] = new ConfigValue("secret", isSecret: true), }; await stack.SetAllConfigAsync(config); // pulumi up var upResult = await stack.UpAsync(); Assert.Equal(UpdateKind.Update, upResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, upResult.Summary.Result); Assert.Equal(3, upResult.Outputs.Count); // exp_static Assert.True(upResult.Outputs.TryGetValue("exp_static", out var expStaticValue)); Assert.Equal("foo", expStaticValue!.Value); Assert.False(expStaticValue.IsSecret); // exp_cfg Assert.True(upResult.Outputs.TryGetValue("exp_cfg", out var expConfigValue)); Assert.Equal("abc", expConfigValue!.Value); Assert.False(expConfigValue.IsSecret); // exp_secret Assert.True(upResult.Outputs.TryGetValue("exp_secret", out var expSecretValue)); Assert.Equal("secret", expSecretValue!.Value); Assert.True(expSecretValue.IsSecret); // pulumi preview var previewResult = await stack.PreviewAsync(); Assert.True(previewResult.ChangeSummary.TryGetValue(OperationType.Same, out var sameCount)); Assert.Equal(1, sameCount); // pulumi refresh var refreshResult = await stack.RefreshAsync(); Assert.Equal(UpdateKind.Refresh, refreshResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, refreshResult.Summary.Result); // pulumi destroy var destroyResult = await stack.DestroyAsync(); Assert.Equal(UpdateKind.Destroy, destroyResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, destroyResult.Summary.Result); } finally { await stack.Workspace.RemoveStackAsync(stackName); } } [Fact] public async Task InlineProgramExceptionPropagatesToCaller() { const string projectName = "exception_inline_node"; var program = PulumiFn.Create((Action)(() => throw new FileNotFoundException())); Assert.IsType(program); using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, RandomStackName(), program) { EnvironmentVariables = new Dictionary { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); var previewTask = stack.PreviewAsync(); await Assert.ThrowsAsync( () => previewTask); var upTask = stack.UpAsync(); await Assert.ThrowsAsync( () => upTask); // verify also propagates for output delegates var outputTask = Task.Run((Func)(() => throw new FileNotFoundException())); var programWithOutput = PulumiFn.Create(() => { var output = Output.Create(outputTask); return new Dictionary { ["output"] = output, }; }); Assert.IsType(programWithOutput); using var stackWithOutput = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, RandomStackName(), programWithOutput) { EnvironmentVariables = new Dictionary { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); var previewTaskWithOutput = stackWithOutput.PreviewAsync(); await Assert.ThrowsAsync( () => previewTaskWithOutput); var upTaskWithOutput = stackWithOutput.UpAsync(); await Assert.ThrowsAsync( () => upTaskWithOutput); } private class FileNotFoundStack : Stack { public FileNotFoundStack() { throw new FileNotFoundException(); } } private class FileNotFoundOutputStack : Stack { public FileNotFoundOutputStack() { var outputTask = Task.Run((Func)(() => throw new FileNotFoundException())); var output = Output.Create(outputTask); this.RegisterOutputs(new Dictionary { ["output"] = output, }); } } [Fact] public async Task InlineProgramExceptionPropagatesToCallerWithTStack() { const string projectName = "exception_inline_tstack_node"; var program = PulumiFn.Create(); Assert.IsType>(program); using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, RandomStackName(), program) { EnvironmentVariables = new Dictionary { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); var previewTask = stack.PreviewAsync(); await Assert.ThrowsAsync( () => previewTask); var upTask = stack.UpAsync(); await Assert.ThrowsAsync( () => upTask); // verify also propagates for output delegates var programWithOutput = PulumiFn.Create(); Assert.IsType>(programWithOutput); using var stackWithOutput = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, RandomStackName(), programWithOutput) { EnvironmentVariables = new Dictionary { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); var previewTaskWithOutput = stackWithOutput.PreviewAsync(); await Assert.ThrowsAsync( () => previewTaskWithOutput); var upTaskWithOutput = stackWithOutput.UpAsync(); await Assert.ThrowsAsync( () => upTaskWithOutput); } [Fact] public async Task InlineProgramExceptionPropagatesToCallerWithServiceProvider() { await using var provider = new ServiceCollection() .AddTransient() // must be transient so it is instantiated each time .AddTransient() .BuildServiceProvider(); const string projectName = "exception_inline_serviceprovider_node"; var program = PulumiFn.Create(provider); Assert.IsType(program); using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, RandomStackName(), program) { EnvironmentVariables = new Dictionary { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); var previewTask = stack.PreviewAsync(); await Assert.ThrowsAsync( () => previewTask); var upTask = stack.UpAsync(); await Assert.ThrowsAsync( () => upTask); // verify also propagates for output delegates var programWithOutput = PulumiFn.Create(provider); Assert.IsType(programWithOutput); using var stackWithOutput = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, RandomStackName(), programWithOutput) { EnvironmentVariables = new Dictionary { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); var previewTaskWithOutput = stackWithOutput.PreviewAsync(); await Assert.ThrowsAsync( () => previewTaskWithOutput); var upTaskWithOutput = stackWithOutput.UpAsync(); await Assert.ThrowsAsync( () => upTaskWithOutput); } [Fact] public async Task InlineProgramAllowsParallelExecution() { const string projectNameOne = "parallel_inline_node1"; const string projectNameTwo = "parallel_inline_node2"; var stackNameOne = $"{RandomStackName()}"; var stackNameTwo = $"{RandomStackName()}"; var hasReachedSemaphoreOne = false; using var semaphoreOne = new SemaphoreSlim(0, 1); var programOne = PulumiFn.Create(() => { // we want to assert before and after each interaction with // the semaphore because we want to alternately stutter // programOne and programTwo so we can assert they aren't // touching eachothers instances var config = new Config(); Assert.Equal(projectNameOne, Deployment.Instance.ProjectName); Assert.Equal(stackNameOne, Deployment.Instance.StackName); hasReachedSemaphoreOne = true; // ReSharper disable once AccessToDisposedClosure semaphoreOne.Wait(); Assert.Equal(projectNameOne, Deployment.Instance.ProjectName); Assert.Equal(stackNameOne, Deployment.Instance.StackName); return new Dictionary { ["exp_static"] = "1", ["exp_cfg"] = config.Get("bar"), ["exp_secret"] = config.GetSecret("buzz"), }; }); var hasReachedSemaphoreTwo = false; using var semaphoreTwo = new SemaphoreSlim(0, 1); var programTwo = PulumiFn.Create(() => { var config = new Config(); Assert.Equal(projectNameTwo, Deployment.Instance.ProjectName); Assert.Equal(stackNameTwo, Deployment.Instance.StackName); hasReachedSemaphoreTwo = true; // ReSharper disable once AccessToDisposedClosure semaphoreTwo.Wait(); Assert.Equal(projectNameTwo, Deployment.Instance.ProjectName); Assert.Equal(stackNameTwo, Deployment.Instance.StackName); return new Dictionary { ["exp_static"] = "2", ["exp_cfg"] = config.Get("bar"), ["exp_secret"] = config.GetSecret("buzz"), }; }); using var stackOne = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectNameOne, stackNameOne, programOne) { EnvironmentVariables = new Dictionary { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); using var stackTwo = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectNameTwo, stackNameTwo, programTwo) { EnvironmentVariables = new Dictionary { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); await stackOne.SetAllConfigAsync(new Dictionary { ["bar"] = new ConfigValue("1"), ["buzz"] = new ConfigValue("1", isSecret: true), }); await stackTwo.SetAllConfigAsync(new Dictionary { ["bar"] = new ConfigValue("2"), ["buzz"] = new ConfigValue("2", isSecret: true), }); var upTaskOne = stackOne.UpAsync(); // wait until we hit semaphore one while (!hasReachedSemaphoreOne) { await Task.Delay(TimeSpan.FromSeconds(2)); if (upTaskOne.IsFaulted) throw upTaskOne.Exception!; if (upTaskOne.IsCompleted) throw new Exception("Never hit semaphore in first UP task."); } var upTaskTwo = stackTwo.UpAsync(); // wait until we hit semaphore two while (!hasReachedSemaphoreTwo) { await Task.Delay(TimeSpan.FromSeconds(2)); if (upTaskTwo.IsFaulted) throw upTaskTwo.Exception!; if (upTaskTwo.IsCompleted) throw new Exception("Never hit semaphore in second UP task."); } // alternately allow them to progress semaphoreOne.Release(); var upResultOne = await upTaskOne; semaphoreTwo.Release(); var upResultTwo = await upTaskTwo; AssertUpResult(upResultOne, "1"); AssertUpResult(upResultTwo, "2"); static void AssertUpResult(UpResult upResult, string value) { Assert.Equal(UpdateKind.Update, upResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, upResult.Summary.Result); Assert.Equal(3, upResult.Outputs.Count); // exp_static Assert.True(upResult.Outputs.TryGetValue("exp_static", out var expStaticValue)); Assert.Equal(value, expStaticValue!.Value); Assert.False(expStaticValue.IsSecret); // exp_cfg Assert.True(upResult.Outputs.TryGetValue("exp_cfg", out var expConfigValue)); Assert.Equal(value, expConfigValue!.Value); Assert.False(expConfigValue.IsSecret); // exp_secret Assert.True(upResult.Outputs.TryGetValue("exp_secret", out var expSecretValue)); Assert.Equal(value, expSecretValue!.Value); Assert.True(expSecretValue.IsSecret); } } [Fact(Skip = "Flakey test - https://github.com/pulumi/pulumi/issues/7467")] public async Task WorkspaceStackSupportsCancel() { var workingDir = ResourcePath(Path.Combine("Data", "testproj")); using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions { WorkDir = workingDir }); var stackName = $"{RandomStackName()}"; var stack = await WorkspaceStack.CreateAsync(stackName, workspace); try { await stack.UpAsync(); // Race destroy and cancel. We do not know for sure // which one will win. var destroyTask = stack.DestroyAsync(); // Empirically 1s delay makes cancel task win // sometimes. YMMV. await Task.Delay(1000); var cancelTask = stack.CancelAsync(); try { Task.WaitAll(destroyTask, cancelTask); } catch (AggregateException) { } if (destroyTask.IsFaulted && !cancelTask.IsFaulted) { // This is what we want to happen at least // sometimes to test Cancel functionality. } else if (cancelTask.IsFaulted && !destroyTask.IsFaulted) { // This may happen if destroyTask wins the race. } else { // This should not happen. Assert.True(destroyTask.IsFaulted, "destroyTask must fail"); Assert.True(!cancelTask.IsFaulted, "cancelTask must not fail"); } } finally { await workspace.RemoveStackAsync(stackName); } } [Fact] public async Task PulumiVersionTest() { using var workspace = await LocalWorkspace.CreateAsync(); Assert.Matches("(\\d+\\.)(\\d+\\.)(\\d+)(-.*)?", workspace.PulumiVersion); } [Theory] [InlineData("100.0.0", true, false)] [InlineData("1.0.0", true, false)] [InlineData("2.22.0", false, false)] [InlineData("2.1.0", true, false)] [InlineData("2.21.2", false, false)] [InlineData("2.21.1", false, false)] [InlineData("2.21.0", true, false)] // Note that prerelease < release so this case should error [InlineData("2.21.1-alpha.1234", true, false)] [InlineData("2.20.0", false, true)] [InlineData("2.22.0", false, true)] // Invalid version check [InlineData("invalid", false, true)] [InlineData("invalid", true, false)] public void ValidVersionTheory(string currentVersion, bool errorExpected, bool optOut) { var testMinVersion = SemVersion.Parse("2.21.1"); if (errorExpected) { void ValidatePulumiVersion() => LocalWorkspace.ParseAndValidatePulumiVersion(testMinVersion, currentVersion, optOut); Assert.Throws(ValidatePulumiVersion); } else { LocalWorkspace.ParseAndValidatePulumiVersion(testMinVersion, currentVersion, optOut); } } [Fact] public async Task RespectsProjectSettingsTest() { var program = PulumiFn.Create(); var stackName = $"{RandomStackName()}"; var projectName = "project_was_overwritten"; var workdir = ResourcePath(Path.Combine("Data", "correct_project")); var stack = await LocalWorkspace.CreateStackAsync( new InlineProgramArgs(projectName, stackName, program) { WorkDir = workdir, EnvironmentVariables = new Dictionary() { ["PULUMI_CONFIG_PASSPHRASE"] = "test" } }); var settings = await stack.Workspace.GetProjectSettingsAsync(); Assert.Equal("correct_project", settings!.Name); Assert.Equal("This is a description", settings.Description); } [Fact] public async Task DetectsProjectSettingConflictTest() { var program = PulumiFn.Create(); var stackName = $"{RandomStackName()}"; var projectName = "project_was_overwritten"; var workdir = ResourcePath(Path.Combine("Data", "correct_project")); var projectSettings = ProjectSettings.Default(projectName); projectSettings.Description = "non-standard description"; await Assert.ThrowsAsync(() => LocalWorkspace.CreateStackAsync( new InlineProgramArgs(projectName, stackName, program) { WorkDir = workdir, ProjectSettings = projectSettings }) ); } [Fact] public async Task InlineProgramLoggerCanBeOverridden() { var program = PulumiFn.Create(() => { Log.Debug("test"); }); var loggerWasInvoked = false; var logger = new CustomLogger(() => loggerWasInvoked = true); var stackName = $"{RandomStackName()}"; var projectName = "inline_logger_override"; using var stack = await LocalWorkspace.CreateOrSelectStackAsync( new InlineProgramArgs(projectName, stackName, program) { Logger = logger, EnvironmentVariables = new Dictionary() { ["PULUMI_CONFIG_PASSPHRASE"] = "test" } }); // make sure workspace logger is used await stack.PreviewAsync(); Assert.True(loggerWasInvoked); // preview logger is used loggerWasInvoked = false; stack.Workspace.Logger = null; await stack.PreviewAsync(new PreviewOptions { Logger = logger, }); Assert.True(loggerWasInvoked); // up logger is used loggerWasInvoked = false; await stack.UpAsync(new UpOptions { Logger = logger, }); Assert.True(loggerWasInvoked); await stack.DestroyAsync(); } [Fact] public async Task InlineProgramLoggerCanRedirectToTestOutput() { var program = PulumiFn.Create(() => { Log.Info("Pulumi.Log calls appear in test output"); }); var stackName = $"{RandomStackName()}"; var projectName = "inline_logger_test_output"; using var stack = await LocalWorkspace.CreateOrSelectStackAsync( new InlineProgramArgs(projectName, stackName, program) { Logger = TestLogger, EnvironmentVariables = new Dictionary() { ["PULUMI_CONFIG_PASSPHRASE"] = "test" } }); TestLogger.LogInformation("Previewing stack..."); await stack.PreviewAsync(); TestLogger.LogInformation("Updating stack..."); await stack.UpAsync(); TestLogger.LogInformation("Destroying stack..."); await stack.DestroyAsync(); } [Fact] public async Task InlineProgramExceptionDuringUpShouldNotDeleteResources() { var program = PulumiFn.Create(() => { var config = new Config(); new ComponentResource("test:res:a", "a"); if (config.GetBoolean("ShouldFail") == true) throw new FileNotFoundException("ShouldFail"); new ComponentResource("test:res:b", "b"); new ComponentResource("test:res:c", "c"); }); Assert.IsType(program); var stackName = $"{RandomStackName()}"; var projectName = "test_optionally_failing_stack"; using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program) { EnvironmentVariables = new Dictionary() { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); try { var config = new Dictionary { ["ShouldFail"] = new ConfigValue("false"), }; await stack.SetAllConfigAsync(config); // pulumi up var upResult = await stack.UpAsync(); Assert.Equal(UpdateKind.Update, upResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, upResult.Summary.Result); Assert.True(upResult.Summary.ResourceChanges!.TryGetValue(OperationType.Create, out var upCount)); Assert.Equal(4, upCount); config["ShouldFail"] = new ConfigValue("true"); await stack.SetAllConfigAsync(config); var sb = new System.Text.StringBuilder(); await Assert.ThrowsAsync( () => stack.UpAsync(new UpOptions() { OnStandardOutput = msg => sb.AppendLine(msg), })); var upOutput = sb.ToString(); Assert.DoesNotContain("test:res:b b deleted", upOutput); Assert.DoesNotContain("test:res:c c deleted", upOutput); config["ShouldFail"] = new ConfigValue("false"); await stack.SetAllConfigAsync(config); // pulumi preview var previewResult = await stack.PreviewAsync(new PreviewOptions() { OnStandardOutput = Console.WriteLine }); Assert.True(previewResult.ChangeSummary.TryGetValue(OperationType.Same, out var sameCount)); Assert.Equal(4, sameCount); } finally { var destroyResult = await stack.DestroyAsync(); Assert.Equal(UpdateKind.Destroy, destroyResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, destroyResult.Summary.Result); await stack.Workspace.RemoveStackAsync(stackName); } } [Fact] public async Task StateDelete() { const string type = "test:res"; var program = PulumiFn.Create(() => { var config = new Config(); new ComponentResource( type, "a"); }); var stackName = RandomStackName(); var projectName = "test_state_delete"; using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program) { EnvironmentVariables = new Dictionary() { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; try { // pulumi up var upResult = await stack.UpAsync(); Assert.Equal(UpdateKind.Update, upResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, upResult.Summary.Result); Assert.True(upResult.Summary.ResourceChanges!.TryGetValue(OperationType.Create, out var upCount)); Assert.Equal(2, upCount); // export state var exportResult = await stack.ExportStackAsync(); var state = JsonSerializer.Deserialize(exportResult.Json.GetRawText(), jsonOptions); Assert.Equal(2, state.Deployment.Resources.Count); var resource = state.Deployment.Resources.Single(r => r.Urn.Contains(type)); // pulumi state delete await stack.State.DeleteAsync(resource.Urn); // test exportResult = await stack.ExportStackAsync(); state = JsonSerializer.Deserialize(exportResult.Json.GetRawText(), jsonOptions); Assert.Single(state.Deployment.Resources); } finally { var destroyResult = await stack.DestroyAsync(); Assert.Equal(UpdateKind.Destroy, destroyResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, destroyResult.Summary.Result); await stack.Workspace.RemoveStackAsync(stackName); } } [Fact] public async Task StateDeleteForce() { const string type = "test:res"; var program = PulumiFn.Create(() => { var config = new Config(); new ComponentResource( type, "a", new ComponentResourceOptions { Protect = true, }); }); var stackName = RandomStackName(); var projectName = "test_state_delete_force"; using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program) { EnvironmentVariables = new Dictionary() { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; try { // pulumi up var upResult = await stack.UpAsync(); Assert.Equal(UpdateKind.Update, upResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, upResult.Summary.Result); Assert.True(upResult.Summary.ResourceChanges!.TryGetValue(OperationType.Create, out var upCount)); Assert.Equal(2, upCount); // export state var exportResult = await stack.ExportStackAsync(); var state = JsonSerializer.Deserialize(exportResult.Json.GetRawText(), jsonOptions); Assert.Equal(2, state.Deployment.Resources.Count); var resource = state.Deployment.Resources.Single(r => r.Urn.Contains(type)); // pulumi state delete await Assert.ThrowsAsync(() => stack.State.DeleteAsync(resource.Urn)); // pulumi state delete force await stack.State.DeleteAsync(resource.Urn, force: true); // test exportResult = await stack.ExportStackAsync(); state = JsonSerializer.Deserialize(exportResult.Json.GetRawText(), jsonOptions); Assert.Single(state.Deployment.Resources); } finally { var destroyResult = await stack.DestroyAsync(); Assert.Equal(UpdateKind.Destroy, destroyResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, destroyResult.Summary.Result); await stack.Workspace.RemoveStackAsync(stackName); } } [Fact] public async Task StateUnprotect() { const string type = "test:res"; var program = PulumiFn.Create(() => { var config = new Config(); new ComponentResource( type, "a", new ComponentResourceOptions { Protect = true, }); }); var stackName = RandomStackName(); var projectName = "test_state_unprotect"; using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program) { EnvironmentVariables = new Dictionary() { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; try { // pulumi up var upResult = await stack.UpAsync(); Assert.Equal(UpdateKind.Update, upResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, upResult.Summary.Result); Assert.True(upResult.Summary.ResourceChanges!.TryGetValue(OperationType.Create, out var upCount)); Assert.Equal(2, upCount); // export state var exportResult = await stack.ExportStackAsync(); var state = JsonSerializer.Deserialize(exportResult.Json.GetRawText(), jsonOptions); Assert.Equal(2, state.Deployment.Resources.Count); var resource = state.Deployment.Resources.Single(r => r.Urn.Contains(type)); Assert.True(resource.Protect); // pulumi state unprotect await stack.State.UnprotectAsync(resource.Urn); // test exportResult = await stack.ExportStackAsync(); state = JsonSerializer.Deserialize(exportResult.Json.GetRawText(), jsonOptions); Assert.Equal(2, state.Deployment.Resources.Count); resource = state.Deployment.Resources.Single(r => r.Urn.Contains(type)); Assert.False(resource.Protect); } finally { await stack.State.UnprotectAllAsync(); var destroyResult = await stack.DestroyAsync(); Assert.Equal(UpdateKind.Destroy, destroyResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, destroyResult.Summary.Result); await stack.Workspace.RemoveStackAsync(stackName); } } [Fact] public async Task StateUnprotectAll() { const string type = "test:res"; var program = PulumiFn.Create(() => { var config = new Config(); new ComponentResource( type, "a", new ComponentResourceOptions { Protect = true, }); new ComponentResource( type, "b", new ComponentResourceOptions { Protect = true, }); }); var stackName = RandomStackName(); var projectName = "test_state_unprotect_all"; using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program) { EnvironmentVariables = new Dictionary() { ["PULUMI_CONFIG_PASSPHRASE"] = "test", } }); var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; try { // pulumi up var upResult = await stack.UpAsync(); Assert.Equal(UpdateKind.Update, upResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, upResult.Summary.Result); Assert.True(upResult.Summary.ResourceChanges!.TryGetValue(OperationType.Create, out var upCount)); Assert.Equal(3, upCount); // export state var exportResult = await stack.ExportStackAsync(); var state = JsonSerializer.Deserialize(exportResult.Json.GetRawText(), jsonOptions); Assert.Equal(3, state.Deployment.Resources.Count); var resources = state.Deployment.Resources.Where(x => x.Urn.Contains(type)).ToList(); Assert.Equal(2, resources.Count); Assert.True(resources.All(x => x.Protect)); // pulumi state unprotect await stack.State.UnprotectAllAsync(); // test exportResult = await stack.ExportStackAsync(); state = JsonSerializer.Deserialize(exportResult.Json.GetRawText(), jsonOptions); Assert.Equal(3, state.Deployment.Resources.Count); resources = state.Deployment.Resources.Where(x => x.Urn.Contains(type)).ToList(); Assert.Equal(2, resources.Count); Assert.DoesNotContain(resources, x => x.Protect); } finally { await stack.State.UnprotectAllAsync(); var destroyResult = await stack.DestroyAsync(); Assert.Equal(UpdateKind.Destroy, destroyResult.Summary.Kind); Assert.Equal(UpdateState.Succeeded, destroyResult.Summary.Result); await stack.Workspace.RemoveStackAsync(stackName); } } private string ResourcePath(string path, [CallerFilePath] string pathBase = "LocalWorkspaceTests.cs") { var dir = Path.GetDirectoryName(pathBase) ?? "."; return Path.Combine(dir, path); } private class CustomLogger : ILogger { private readonly Action _action; public CustomLogger(Action action) { _action = action; } public IDisposable BeginScope(TState state) { throw new NotImplementedException(); } public bool IsEnabled(LogLevel logLevel) => true; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) => _action(); } private class StackState { public StackStateDeployment Deployment { get; set; } = new StackStateDeployment(); } private class StackStateDeployment { public List Resources { get; set; } = new List(); } private class StackStateResource { public string Urn { get; set; } = null!; public bool Protect { get; set; } } } }