pulumi/sdk/dotnet/Pulumi.Automation.Tests/LocalWorkspaceTests.cs
2021-02-18 14:03:27 +01:00

646 lines
25 KiB
C#

// Copyright 2016-2021, Pulumi Corporation
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Pulumi.Automation.Commands.Exceptions;
using Xunit;
namespace Pulumi.Automation.Tests
{
public class LocalWorkspaceTests
{
private static readonly string _dataDirectory =
Path.Combine(new FileInfo(Assembly.GetExecutingAssembly().Location).DirectoryName, "Data");
private static string GetTestSuffix()
{
var random = new Random();
var result = 100000 + random.Next(0, 900000);
return result.ToString();
}
private static string NormalizeConfigKey(string key, string projectName)
{
var parts = key.Split(":");
if (parts.Length < 2)
return $"{projectName}:{key}";
return string.Empty;
}
[Theory]
[InlineData("yaml")]
[InlineData("yml")]
[InlineData("json")]
public async Task GetProjectSettings(string extension)
{
var workingDir = Path.Combine(_dataDirectory, 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 = Path.Combine(_dataDirectory, 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(Skip = "Failing due to workspace already existing with some stacks from previous runs")]
public async Task CreateSelectRemoveStack()
{
var projectSettings = new ProjectSettings("node_test", ProjectRuntimeName.NodeJS);
using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions
{
ProjectSettings = projectSettings,
EnvironmentVariables = new Dictionary<string, string>()
{
["PULUMI_CONFIG_PASSPHRASE"] = "test",
}
});
var stackName = $"int_test{GetTestSuffix()}";
var stacks = await workspace.ListStacksAsync();
Assert.Empty(stacks);
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.Empty(stacks);
}
[Fact]
public async Task ManipulateConfig()
{
var projectName = "node_test";
var projectSettings = new ProjectSettings(projectName, ProjectRuntimeName.NodeJS);
using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions
{
ProjectSettings = projectSettings,
EnvironmentVariables = new Dictionary<string, string>()
{
["PULUMI_CONFIG_PASSPHRASE"] = "test",
}
});
var stackName = $"int_test{GetTestSuffix()}";
var stack = await WorkspaceStack.CreateAsync(stackName, workspace);
var config = new Dictionary<string, ConfigValue>()
{
["plain"] = new ConfigValue("abc"),
["secret"] = new ConfigValue("def", isSecret: true),
};
var plainKey = NormalizeConfigKey("plain", projectName);
var secretKey = NormalizeConfigKey("secret", projectName);
await Assert.ThrowsAsync<CommandException>(
() => stack.GetConfigValueAsync(plainKey));
var values = await stack.GetConfigAsync();
Assert.Empty(values);
await stack.SetConfigAsync(config);
values = await stack.GetConfigAsync();
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);
await stack.RemoveConfigValueAsync("plain");
values = await stack.GetConfigAsync();
Assert.Single(values);
await stack.SetConfigValueAsync("foo", new ConfigValue("bar"));
values = await stack.GetConfigAsync();
Assert.Equal(2, values.Count);
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<string, string>()
{
["PULUMI_CONFIG_PASSPHRASE"] = "test",
}
});
var stackNames = new List<string>();
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("node_test", ProjectRuntimeName.NodeJS);
using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions
{
ProjectSettings = projectSettings,
EnvironmentVariables = new Dictionary<string, string>()
{
["PULUMI_CONFIG_PASSPHRASE"] = "test",
}
});
var stackName = $"int_test{GetTestSuffix()}";
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 = $"int_test{GetTestSuffix()}";
var workingDir = Path.Combine(_dataDirectory, "testproj");
using var stack = await LocalWorkspace.CreateStackAsync(new LocalProgramArgs(stackName, workingDir)
{
EnvironmentVariables = new Dictionary<string, string>()
{
["PULUMI_CONFIG_PASSPHRASE"] = "test",
}
});
var config = new Dictionary<string, ConfigValue>()
{
["bar"] = new ConfigValue("abc"),
["buzz"] = new ConfigValue("secret", isSecret: true),
};
await stack.SetConfigAsync(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
await stack.PreviewAsync();
// TODO: update assertions when we have structured output
// 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);
await stack.Workspace.RemoveStackAsync(stackName);
}
[Fact]
public async Task StackLifecycleInlineProgram()
{
var program = PulumiFn.Create(() =>
{
var config = new Pulumi.Config();
return new Dictionary<string, object?>
{
["exp_static"] = "foo",
["exp_cfg"] = config.Get("bar"),
["exp_secret"] = config.GetSecret("buzz"),
};
});
var stackName = $"int_test{GetTestSuffix()}";
var projectName = "inline_node";
using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program)
{
EnvironmentVariables = new Dictionary<string, string>()
{
["PULUMI_CONFIG_PASSPHRASE"] = "test",
}
});
var config = new Dictionary<string, ConfigValue>()
{
["bar"] = new ConfigValue("abc"),
["buzz"] = new ConfigValue("secret", isSecret: true),
};
await stack.SetConfigAsync(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
await stack.PreviewAsync();
// TODO: update assertions when we have structured output
// 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);
await stack.Workspace.RemoveStackAsync(stackName);
}
private class ValidStack : Stack
{
[Output("exp_static")]
public Output<string> ExpStatic { get; set; }
[Output("exp_cfg")]
public Output<string> ExpConfig { get; set; }
[Output("exp_secret")]
public Output<string> ExpSecret { get; set; }
public ValidStack()
{
var config = new Pulumi.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<ValidStack>();
var stackName = $"int_test{GetTestSuffix()}";
var projectName = "inline_tstack_node";
using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program)
{
EnvironmentVariables = new Dictionary<string, string>()
{
["PULUMI_CONFIG_PASSPHRASE"] = "test",
}
});
var config = new Dictionary<string, ConfigValue>()
{
["bar"] = new ConfigValue("abc"),
["buzz"] = new ConfigValue("secret", isSecret: true),
};
await stack.SetConfigAsync(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
await stack.PreviewAsync();
// TODO: update assertions when we have structured output
// 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);
await stack.Workspace.RemoveStackAsync(stackName);
}
[Fact]
public async Task InlineProgramExceptionPropagatesToCaller()
{
const string projectName = "exception_inline_node";
var stackName = $"int_test_{GetTestSuffix()}";
var program = PulumiFn.Create((Action)(() => throw new FileNotFoundException()));
using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program)
{
EnvironmentVariables = new Dictionary<string, string>()
{
["PULUMI_CONFIG_PASSPHRASE"] = "test",
}
});
var upTask = stack.UpAsync();
await Assert.ThrowsAsync<FileNotFoundException>(
() => upTask);
}
private class FileNotFoundStack : Pulumi.Stack
{
public FileNotFoundStack()
{
throw new FileNotFoundException();
}
}
[Fact]
public async Task InlineProgramExceptionPropagatesToCallerWithTStack()
{
const string projectName = "exception_inline_tstack_node";
var stackName = $"int_test_{GetTestSuffix()}";
var program = PulumiFn.Create<FileNotFoundStack>();
using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program)
{
EnvironmentVariables = new Dictionary<string, string>()
{
["PULUMI_CONFIG_PASSPHRASE"] = "test",
}
});
var upTask = stack.UpAsync();
await Assert.ThrowsAsync<FileNotFoundException>(
() => upTask);
}
[Fact(Skip = "Parallel execution is not supported in this first version.")]
public async Task InlineProgramAllowsParallelExecution()
{
const string projectNameOne = "parallel_inline_node1";
const string projectNameTwo = "parallel_inline_node2";
var stackNameOne = $"int_test1_{GetTestSuffix()}";
var stackNameTwo = $"int_test2_{GetTestSuffix()}";
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 Pulumi.Config();
Assert.Equal(projectNameOne, Deployment.Instance.ProjectName);
Assert.Equal(stackNameOne, Deployment.Instance.StackName);
hasReachedSemaphoreOne = true;
semaphoreOne.Wait();
Assert.Equal(projectNameOne, Deployment.Instance.ProjectName);
Assert.Equal(stackNameOne, Deployment.Instance.StackName);
return new Dictionary<string, object?>
{
["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 Pulumi.Config();
Assert.Equal(projectNameTwo, Deployment.Instance.ProjectName);
Assert.Equal(stackNameTwo, Deployment.Instance.StackName);
hasReachedSemaphoreTwo = true;
semaphoreTwo.Wait();
Assert.Equal(projectNameTwo, Deployment.Instance.ProjectName);
Assert.Equal(stackNameTwo, Deployment.Instance.StackName);
return new Dictionary<string, object?>
{
["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<string, string>()
{
["PULUMI_CONFIG_PASSPHRASE"] = "test",
}
});
using var stackTwo = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectNameTwo, stackNameTwo, programTwo)
{
EnvironmentVariables = new Dictionary<string, string>()
{
["PULUMI_CONFIG_PASSPHRASE"] = "test",
}
});
await stackOne.SetConfigAsync(new Dictionary<string, ConfigValue>()
{
["bar"] = new ConfigValue("1"),
["buzz"] = new ConfigValue("1", isSecret: true),
});
await stackTwo.SetConfigAsync(new Dictionary<string, ConfigValue>()
{
["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!;
else 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!;
else 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);
}
}
}
}