Avoid overriding dotnet proj settings accidentally (#6670)
* Add failing test * Guard against overrding project settings accidentally * Throw exception in case of conflct * Update sdk/dotnet/Pulumi.Automation/DictionaryContentsComparer.cs Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com> * Update sdk/dotnet/Pulumi.Automation/ProjectRuntime.cs Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com> * Update sdk/dotnet/Pulumi.Automation/ProjectTemplateConfigValue.cs Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com> * Update sdk/dotnet/Pulumi.Automation/ProjectTemplate.cs Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com> * Update sdk/dotnet/Pulumi.Automation/ProjectRuntimeOptions.cs Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com> * Update sdk/dotnet/Pulumi.Automation/ProjectBackend.cs Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com> * Update sdk/dotnet/Pulumi.Automation/ProjectSettings.cs Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com> * Reduce nesting * Make the new exception public * Introduce a CHANGELOG entry since we add to pub API * Stricter check before throwing * Address PR feedback, round 1 * Use Reference.Equals check * Move DictionaryContentsComparer out of top-level Co-authored-by: Komal Ali <komal@pulumi.com> Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>
This commit is contained in:
parent
55ecf7a81e
commit
fc8262bad0
|
@ -23,9 +23,13 @@
|
|||
|
||||
- [automation/python] Fix passing of additional environment variables.
|
||||
[#6639](https://github.com/pulumi/pulumi/pull/6639)
|
||||
|
||||
|
||||
- [sdk/python] Make exceptions raised by calls to provider functions (e.g. data sources) catchable.
|
||||
[#6504](https://github.com/pulumi/pulumi/pull/6504)
|
||||
|
||||
- [automation/go,python,nodejs] Respect pre-existing Pulumi.yaml for inline programs.
|
||||
[#6655](https://github.com/pulumi/pulumi/pull/6655)
|
||||
|
||||
- [sdk/dotnet] Respect pre-existing Pulumi.yaml for inline programs,
|
||||
introduce ProjectSettingsConflictException.
|
||||
[6670](https://github.com/pulumi/pulumi/pull/6670)
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
description: This is a description
|
||||
name: correct_project
|
||||
runtime: dotnet
|
|
@ -6,8 +6,10 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Pulumi.Automation.Exceptions;
|
||||
using Pulumi.Automation.Commands.Exceptions;
|
||||
using Pulumi.Automation.Events;
|
||||
using Xunit;
|
||||
|
@ -16,9 +18,6 @@ namespace Pulumi.Automation.Tests
|
|||
{
|
||||
public class LocalWorkspaceTests
|
||||
{
|
||||
private static readonly string _dataDirectory =
|
||||
Path.Combine(new FileInfo(Assembly.GetExecutingAssembly().Location).DirectoryName, "Data");
|
||||
|
||||
private static readonly string _pulumiOrg = GetTestOrg();
|
||||
|
||||
private static string GetTestSuffix()
|
||||
|
@ -55,7 +54,7 @@ namespace Pulumi.Automation.Tests
|
|||
[InlineData("json")]
|
||||
public async Task GetProjectSettings(string extension)
|
||||
{
|
||||
var workingDir = Path.Combine(_dataDirectory, extension);
|
||||
var workingDir = ResourcePath(Path.Combine("Data", extension));
|
||||
using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions
|
||||
{
|
||||
WorkDir = workingDir,
|
||||
|
@ -74,7 +73,7 @@ namespace Pulumi.Automation.Tests
|
|||
[InlineData("json")]
|
||||
public async Task GetStackSettings(string extension)
|
||||
{
|
||||
var workingDir = Path.Combine(_dataDirectory, extension);
|
||||
var workingDir = ResourcePath(Path.Combine("Data", extension));
|
||||
using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions
|
||||
{
|
||||
WorkDir = workingDir,
|
||||
|
@ -283,7 +282,7 @@ namespace Pulumi.Automation.Tests
|
|||
public async Task StackLifecycleLocalProgram()
|
||||
{
|
||||
var stackName = $"{RandomStackName()}";
|
||||
var workingDir = Path.Combine(_dataDirectory, "testproj");
|
||||
var workingDir = ResourcePath(Path.Combine("Data", "testproj"));
|
||||
using var stack = await LocalWorkspace.CreateStackAsync(new LocalProgramArgs(stackName, workingDir)
|
||||
{
|
||||
EnvironmentVariables = new Dictionary<string, string>()
|
||||
|
@ -622,7 +621,7 @@ namespace Pulumi.Automation.Tests
|
|||
}
|
||||
|
||||
static async Task<T> RunCommand<T, TOptions>(Func<TOptions, CancellationToken, Task<T>> func, string command)
|
||||
where TOptions: UpdateOptions, new()
|
||||
where TOptions : UpdateOptions, new()
|
||||
{
|
||||
var events = new List<EngineEvent>();
|
||||
|
||||
|
@ -908,7 +907,7 @@ namespace Pulumi.Automation.Tests
|
|||
Assert.True(expSecretValue.IsSecret);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task PulumiVersionTest()
|
||||
{
|
||||
|
@ -939,5 +938,55 @@ namespace Pulumi.Automation.Tests
|
|||
LocalWorkspace.ValidatePulumiVersion(testMinVersion, currentVersion);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RespectsProjectSettingsTest()
|
||||
{
|
||||
var program = PulumiFn.Create<ValidStack>();
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
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<ValidStack>();
|
||||
|
||||
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<ProjectSettingsConflictException>(() =>
|
||||
LocalWorkspace.CreateStackAsync(
|
||||
new InlineProgramArgs(projectName, stackName, program)
|
||||
{
|
||||
WorkDir = workdir,
|
||||
ProjectSettings = projectSettings
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private string ResourcePath(string path, [CallerFilePath] string pathBase = "LocalWorkspaceTests.cs")
|
||||
{
|
||||
var dir = Path.GetDirectoryName(pathBase) ?? ".";
|
||||
return Path.Combine(dir, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
// Copyright 2016-2019, Pulumi Corporation
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Pulumi.Automation.Collections
|
||||
{
|
||||
/// Compares two dictionaries for equality by content, as F# maps would.
|
||||
internal sealed class DictionaryContentsComparer<K, V> : IEqualityComparer<IDictionary<K, V>> where K : notnull
|
||||
{
|
||||
private readonly IEqualityComparer<K> _keyComparer;
|
||||
private readonly IEqualityComparer<V> _valueComparer;
|
||||
|
||||
public DictionaryContentsComparer(IEqualityComparer<K> keyComparer, IEqualityComparer<V> valueComparer)
|
||||
{
|
||||
this._keyComparer = keyComparer;
|
||||
this._valueComparer = valueComparer;
|
||||
}
|
||||
|
||||
bool IEqualityComparer<IDictionary<K, V>>.Equals(IDictionary<K, V>? x, IDictionary<K, V>? y)
|
||||
{
|
||||
if (x == null)
|
||||
{
|
||||
return y == null;
|
||||
}
|
||||
if (y == null)
|
||||
{
|
||||
return x == null;
|
||||
}
|
||||
if (ReferenceEquals(x, y))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (x.Count != y.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var y2 = new Dictionary<K, V>(y, this._keyComparer);
|
||||
foreach (var pair in x)
|
||||
{
|
||||
if (!y2.ContainsKey(pair.Key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this._valueComparer.Equals(pair.Value, y2[pair.Key]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int IEqualityComparer<IDictionary<K, V>>.GetHashCode(IDictionary<K, V> obj)
|
||||
{
|
||||
return 0; // inefficient but correct
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright 2016-2021, Pulumi Corporation
|
||||
|
||||
using System;
|
||||
|
||||
namespace Pulumi.Automation.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// Thrown when creating a Workspace detects a conflict between
|
||||
/// project settings found on disk (such as Pulumi.yaml) and a
|
||||
/// ProjectSettings object passed to the Create API.
|
||||
///
|
||||
/// There are two resolutions:
|
||||
///
|
||||
/// (A) to use the ProjectSettings, delete the Pulumi.yaml file
|
||||
/// from WorkDir or use a different WorkDir
|
||||
///
|
||||
/// (B) to use the exiting Pulumi.yaml from WorkDir, avoid
|
||||
/// customizing the ProjectSettings
|
||||
///
|
||||
/// </summary>
|
||||
public class ProjectSettingsConflictException : Exception
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// FullPath of the Pulumi.yaml (or Pulumi.yml, Pulumi.json)
|
||||
/// settings file found on disk.
|
||||
///
|
||||
/// </summary>
|
||||
public string SettingsFileLocation { get; }
|
||||
|
||||
internal ProjectSettingsConflictException(string settingsFileLocation)
|
||||
: base($"Custom {nameof(ProjectSettings)} passed in code conflict with settings found on disk: {settingsFileLocation}")
|
||||
{
|
||||
SettingsFileLocation = settingsFileLocation;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -328,9 +328,10 @@ namespace Pulumi.Automation
|
|||
|
||||
readyTasks.Add(this.PopulatePulumiVersionAsync(cancellationToken));
|
||||
|
||||
// these are after working dir is set because they start immediately
|
||||
if (options?.ProjectSettings != null)
|
||||
readyTasks.Add(this.SaveProjectSettingsAsync(options.ProjectSettings, cancellationToken));
|
||||
{
|
||||
readyTasks.Add(this.InitializeProjectSettingsAsync(options.ProjectSettings, cancellationToken));
|
||||
}
|
||||
|
||||
if (options?.StackSettings != null && options.StackSettings.Any())
|
||||
{
|
||||
|
@ -341,6 +342,26 @@ namespace Pulumi.Automation
|
|||
this._readyTask = Task.WhenAll(readyTasks);
|
||||
}
|
||||
|
||||
private async Task InitializeProjectSettingsAsync(ProjectSettings projectSettings,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// If given project settings, we want to write them out to
|
||||
// the working dir. We do not want to override existing
|
||||
// settings with default settings though.
|
||||
|
||||
var existingSettings = await this.GetProjectSettingsAsync(cancellationToken);
|
||||
if (existingSettings == null)
|
||||
{
|
||||
await this.SaveProjectSettingsAsync(projectSettings, cancellationToken);
|
||||
}
|
||||
else if (!projectSettings.IsDefault &&
|
||||
!ProjectSettings.Comparer.Equals(projectSettings, existingSettings))
|
||||
{
|
||||
var path = this.FindSettingsFile();
|
||||
throw new Exceptions.ProjectSettingsConflictException(path);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly string[] SettingsExtensions = new string[] { ".yaml", ".yml", ".json" };
|
||||
|
||||
private async Task PopulatePulumiVersionAsync(CancellationToken cancellationToken)
|
||||
|
@ -358,10 +379,12 @@ namespace Pulumi.Automation
|
|||
|
||||
internal static void ValidatePulumiVersion(SemVersion minVersion, SemVersion currentVersion)
|
||||
{
|
||||
if (minVersion.Major < currentVersion.Major) {
|
||||
if (minVersion.Major < currentVersion.Major)
|
||||
{
|
||||
throw new InvalidOperationException($"Major version mismatch. You are using Pulumi CLI version {currentVersion} with Automation SDK v{minVersion.Major}. Please update the SDK.");
|
||||
}
|
||||
if (minVersion > currentVersion) {
|
||||
if (minVersion > currentVersion)
|
||||
{
|
||||
throw new InvalidOperationException($"Minimum version requirement failed. The minimum CLI version requirement is {minVersion}, your current CLI version is {currentVersion}. Please update the Pulumi CLI.");
|
||||
}
|
||||
}
|
||||
|
@ -369,41 +392,46 @@ namespace Pulumi.Automation
|
|||
/// <inheritdoc/>
|
||||
public override async Task<ProjectSettings?> GetProjectSettingsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
foreach (var ext in SettingsExtensions)
|
||||
var path = this.FindSettingsFile();
|
||||
var isJson = Path.GetExtension(path) == ".json";
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var content = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false);
|
||||
if (isJson)
|
||||
{
|
||||
return this._serializer.DeserializeJson<ProjectSettings>(content);
|
||||
}
|
||||
else
|
||||
{
|
||||
var isJson = ext == ".json";
|
||||
var path = Path.Combine(this.WorkDir, $"Pulumi{ext}");
|
||||
if (!File.Exists(path))
|
||||
continue;
|
||||
|
||||
var content = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false);
|
||||
if (isJson)
|
||||
return this._serializer.DeserializeJson<ProjectSettings>(content);
|
||||
|
||||
var model = this._serializer.DeserializeYaml<ProjectSettingsModel>(content);
|
||||
return model.Convert();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Task SaveProjectSettingsAsync(ProjectSettings settings, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var foundExt = ".yaml";
|
||||
var path = this.FindSettingsFile();
|
||||
var ext = Path.GetExtension(path);
|
||||
var content = ext == ".json" ? this._serializer.SerializeJson(settings) : this._serializer.SerializeYaml(settings);
|
||||
return File.WriteAllTextAsync(path, content, cancellationToken);
|
||||
}
|
||||
|
||||
private string FindSettingsFile()
|
||||
{
|
||||
foreach (var ext in SettingsExtensions)
|
||||
{
|
||||
var testPath = Path.Combine(this.WorkDir, $"Pulumi{ext}");
|
||||
if (File.Exists(testPath))
|
||||
{
|
||||
foundExt = ext;
|
||||
break;
|
||||
return testPath;
|
||||
}
|
||||
}
|
||||
|
||||
var path = Path.Combine(this.WorkDir, $"Pulumi{foundExt}");
|
||||
var content = foundExt == ".json" ? this._serializer.SerializeJson(settings) : this._serializer.SerializeYaml(settings);
|
||||
return File.WriteAllTextAsync(path, content, cancellationToken);
|
||||
var defaultPath = Path.Combine(this.WorkDir, "Pulumi.yaml");
|
||||
return defaultPath;
|
||||
}
|
||||
|
||||
private static string GetStackSettingsName(string stackName)
|
||||
|
@ -498,8 +526,8 @@ namespace Pulumi.Automation
|
|||
/// <inheritdoc/>
|
||||
public override async Task SetConfigAsync(string stackName, IDictionary<string, ConfigValue> configMap, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var args = new List<string>{"config", "set-all", "--stack", stackName};
|
||||
foreach (var (key, value) in configMap)
|
||||
var args = new List<string> { "config", "set-all", "--stack", stackName };
|
||||
foreach (var (key, value) in configMap)
|
||||
{
|
||||
var secretArg = value.IsSecret ? "--secret" : "--plaintext";
|
||||
args.Add(secretArg);
|
||||
|
@ -524,7 +552,7 @@ namespace Pulumi.Automation
|
|||
/// <inheritdoc/>
|
||||
public override async Task RemoveConfigAsync(string stackName, IEnumerable<string> keys, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var args = new List<string>{"config", "rm-all", "--stack", stackName};
|
||||
var args = new List<string> { "config", "rm-all", "--stack", stackName };
|
||||
args.AddRange(keys);
|
||||
await this.RunCommandAsync(args, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
@ -574,7 +602,7 @@ namespace Pulumi.Automation
|
|||
var result = await this.RunCommandAsync(new[] { "stack", "ls", "--json" }, cancellationToken).ConfigureAwait(false);
|
||||
if (string.IsNullOrWhiteSpace(result.StandardOutput))
|
||||
return ImmutableList<StackSummary>.Empty;
|
||||
|
||||
|
||||
var stacks = this._serializer.DeserializeJson<List<StackSummary>>(result.StandardOutput);
|
||||
return stacks.ToImmutableList();
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
// Copyright 2016-2021, Pulumi Corporation
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Pulumi.Automation
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -7,6 +10,36 @@ namespace Pulumi.Automation
|
|||
/// </summary>
|
||||
public class ProjectBackend
|
||||
{
|
||||
internal static IEqualityComparer<ProjectBackend> Comparer { get; } = new ProjectBackendComparer();
|
||||
|
||||
public string? Url { get; set; }
|
||||
|
||||
private sealed class ProjectBackendComparer : IEqualityComparer<ProjectBackend>
|
||||
{
|
||||
bool IEqualityComparer<ProjectBackend>.Equals(ProjectBackend? x, ProjectBackend? y)
|
||||
{
|
||||
if (x == null)
|
||||
{
|
||||
return y == null;
|
||||
}
|
||||
|
||||
if (y == null)
|
||||
{
|
||||
return x == null;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(x, y))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return x.Url == y.Url;
|
||||
}
|
||||
|
||||
int IEqualityComparer<ProjectBackend>.GetHashCode(ProjectBackend obj)
|
||||
{
|
||||
return HashCode.Combine(obj.Url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
// Copyright 2016-2021, Pulumi Corporation
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Pulumi.Automation
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -7,6 +10,8 @@ namespace Pulumi.Automation
|
|||
/// </summary>
|
||||
public class ProjectRuntime
|
||||
{
|
||||
internal static IEqualityComparer<ProjectRuntime> Comparer { get; } = new ProjectRuntimeComparer();
|
||||
|
||||
public ProjectRuntimeName Name { get; set; }
|
||||
|
||||
public ProjectRuntimeOptions? Options { get; set; }
|
||||
|
@ -15,5 +20,36 @@ namespace Pulumi.Automation
|
|||
{
|
||||
this.Name = name;
|
||||
}
|
||||
|
||||
private sealed class ProjectRuntimeComparer : IEqualityComparer<ProjectRuntime>
|
||||
{
|
||||
bool IEqualityComparer<ProjectRuntime>.Equals(ProjectRuntime? x, ProjectRuntime? y)
|
||||
{
|
||||
if (x == null)
|
||||
{
|
||||
return y == null;
|
||||
}
|
||||
|
||||
if (y == null)
|
||||
{
|
||||
return x == null;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(x, y))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return x.Name == y.Name && ProjectRuntimeOptions.Comparer.Equals(x.Options, y.Options);
|
||||
}
|
||||
|
||||
int IEqualityComparer<ProjectRuntime>.GetHashCode(ProjectRuntime obj)
|
||||
{
|
||||
return HashCode.Combine(
|
||||
obj.Name,
|
||||
obj.Options != null ? ProjectRuntimeOptions.Comparer.GetHashCode(obj.Options) : 0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
// Copyright 2016-2021, Pulumi Corporation
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Pulumi.Automation
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -7,6 +10,8 @@ namespace Pulumi.Automation
|
|||
/// </summary>
|
||||
public class ProjectRuntimeOptions
|
||||
{
|
||||
internal static IEqualityComparer<ProjectRuntimeOptions> Comparer { get; } = new ProjectRuntimeOptionsComparer();
|
||||
|
||||
/// <summary>
|
||||
/// Applies to NodeJS projects only.
|
||||
/// <para/>
|
||||
|
@ -29,5 +34,33 @@ namespace Pulumi.Automation
|
|||
/// A string that specifies the path to a virtual environment to use when running the program.
|
||||
/// </summary>
|
||||
public string? VirtualEnv { get; set; }
|
||||
|
||||
private sealed class ProjectRuntimeOptionsComparer : IEqualityComparer<ProjectRuntimeOptions>
|
||||
{
|
||||
bool IEqualityComparer<ProjectRuntimeOptions>.Equals(ProjectRuntimeOptions? x, ProjectRuntimeOptions? y)
|
||||
{
|
||||
if (x == null)
|
||||
{
|
||||
return y == null;
|
||||
}
|
||||
|
||||
if (y == null)
|
||||
{
|
||||
return x == null;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(x, y))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return x.TypeScript == y.TypeScript && x.Binary == y.Binary && x.VirtualEnv == y.VirtualEnv;
|
||||
}
|
||||
|
||||
int IEqualityComparer<ProjectRuntimeOptions>.GetHashCode(ProjectRuntimeOptions obj)
|
||||
{
|
||||
return HashCode.Combine(obj.TypeScript, obj.Binary, obj.VirtualEnv);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
// Copyright 2016-2021, Pulumi Corporation
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Pulumi.Automation
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -7,6 +10,8 @@ namespace Pulumi.Automation
|
|||
/// </summary>
|
||||
public class ProjectSettings
|
||||
{
|
||||
internal static IEqualityComparer<ProjectSettings> Comparer { get; } = new ProjectSettingsComparer();
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public ProjectRuntime Runtime { get; set; }
|
||||
|
@ -44,5 +49,60 @@ namespace Pulumi.Automation
|
|||
|
||||
internal static ProjectSettings Default(string name)
|
||||
=> new ProjectSettings(name, new ProjectRuntime(ProjectRuntimeName.NodeJS));
|
||||
|
||||
internal bool IsDefault
|
||||
{
|
||||
get
|
||||
{
|
||||
return ProjectSettings.Comparer.Equals(this, ProjectSettings.Default(this.Name));
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ProjectSettingsComparer : IEqualityComparer<ProjectSettings>
|
||||
{
|
||||
bool IEqualityComparer<ProjectSettings>.Equals(ProjectSettings? x, ProjectSettings? y)
|
||||
{
|
||||
if (x == null)
|
||||
{
|
||||
return y == null;
|
||||
}
|
||||
|
||||
if (y == null)
|
||||
{
|
||||
return x == null;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(x, y))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return x.Name == y.Name &&
|
||||
ProjectRuntime.Comparer.Equals(x.Runtime, y.Runtime) &&
|
||||
x.Main == y.Main &&
|
||||
x.Description == y.Description &&
|
||||
x.Author == y.Author &&
|
||||
x.Website == y.Website &&
|
||||
x.License == y.License &&
|
||||
x.Config == y.Config &&
|
||||
ProjectTemplate.Comparer.Equals(x.Template, y.Template) &&
|
||||
ProjectBackend.Comparer.Equals(x.Backend, y.Backend);
|
||||
}
|
||||
|
||||
int IEqualityComparer<ProjectSettings>.GetHashCode(ProjectSettings obj)
|
||||
{
|
||||
// fields with custom Comparer skipped for efficiency
|
||||
return HashCode.Combine(
|
||||
obj.Name,
|
||||
obj.Main,
|
||||
obj.Description,
|
||||
obj.Author,
|
||||
obj.Website,
|
||||
obj.License,
|
||||
obj.Config,
|
||||
obj.Backend
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright 2016-2021, Pulumi Corporation
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Pulumi.Automation.Collections;
|
||||
|
||||
namespace Pulumi.Automation
|
||||
{
|
||||
|
@ -9,6 +11,8 @@ namespace Pulumi.Automation
|
|||
/// </summary>
|
||||
public class ProjectTemplate
|
||||
{
|
||||
internal static IEqualityComparer<ProjectTemplate> Comparer { get; } = new ProjectTemplateComparer();
|
||||
|
||||
public string? Description { get; set; }
|
||||
|
||||
public string? QuickStart { get; set; }
|
||||
|
@ -16,5 +20,43 @@ namespace Pulumi.Automation
|
|||
public IDictionary<string, ProjectTemplateConfigValue>? Config { get; set; }
|
||||
|
||||
public bool? Important { get; set; }
|
||||
|
||||
private sealed class ProjectTemplateComparer : IEqualityComparer<ProjectTemplate>
|
||||
{
|
||||
|
||||
private IEqualityComparer<IDictionary<string, ProjectTemplateConfigValue>> _configComparer =
|
||||
new DictionaryContentsComparer<string, ProjectTemplateConfigValue>(
|
||||
EqualityComparer<string>.Default,
|
||||
ProjectTemplateConfigValue.Comparer);
|
||||
|
||||
bool IEqualityComparer<ProjectTemplate>.Equals(ProjectTemplate? x, ProjectTemplate? y)
|
||||
{
|
||||
if (x == null)
|
||||
{
|
||||
return y == null;
|
||||
}
|
||||
|
||||
if (y == null)
|
||||
{
|
||||
return x == null;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(x, y))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return x.Description == y.Description
|
||||
&& x.QuickStart == y.QuickStart
|
||||
&& x.Important == y.Important
|
||||
&& _configComparer.Equals(x.Config, y.Config);
|
||||
}
|
||||
|
||||
int IEqualityComparer<ProjectTemplate>.GetHashCode(ProjectTemplate obj)
|
||||
{
|
||||
// omit hashing Config dict for efficiency
|
||||
return HashCode.Combine(obj.Description, obj.QuickStart, obj.Important);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
// Copyright 2016-2021, Pulumi Corporation
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Pulumi.Automation
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -7,10 +10,40 @@ namespace Pulumi.Automation
|
|||
/// </summary>
|
||||
public class ProjectTemplateConfigValue
|
||||
{
|
||||
internal static IEqualityComparer<ProjectTemplateConfigValue> Comparer { get; } = new ProjectTemplateConfigValueComparer();
|
||||
|
||||
public string? Description { get; set; }
|
||||
|
||||
public string? Default { get; set; }
|
||||
|
||||
public bool? Secret { get; set; }
|
||||
|
||||
private sealed class ProjectTemplateConfigValueComparer : IEqualityComparer<ProjectTemplateConfigValue>
|
||||
{
|
||||
bool IEqualityComparer<ProjectTemplateConfigValue>.Equals(ProjectTemplateConfigValue? x, ProjectTemplateConfigValue? y)
|
||||
{
|
||||
if (x == null)
|
||||
{
|
||||
return y == null;
|
||||
}
|
||||
|
||||
if (y == null)
|
||||
{
|
||||
return x == null;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(x, y))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return x.Description == y.Description && x.Default == y.Default && x.Secret == y.Secret;
|
||||
}
|
||||
|
||||
int IEqualityComparer<ProjectTemplateConfigValue>.GetHashCode(ProjectTemplateConfigValue obj)
|
||||
{
|
||||
return HashCode.Combine(obj.Description, obj.Default, obj.Secret);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -429,3 +429,5 @@ static Pulumi.Automation.WorkspaceStack.CreateOrSelectAsync(string name, Pulumi.
|
|||
static Pulumi.Automation.WorkspaceStack.SelectAsync(string name, Pulumi.Automation.Workspace workspace, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Pulumi.Automation.WorkspaceStack>
|
||||
virtual Pulumi.Automation.Workspace.Dispose() -> void
|
||||
virtual Pulumi.Automation.Workspace.GetStackAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Pulumi.Automation.StackSummary>
|
||||
Pulumi.Automation.Exceptions.ProjectSettingsConflictException
|
||||
Pulumi.Automation.Exceptions.ProjectSettingsConflictException.SettingsFileLocation.get -> string
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
<name>Pulumi.Automation</name>
|
||||
</assembly>
|
||||
<members>
|
||||
<member name="T:Pulumi.Automation.Collections.DictionaryContentsComparer`2">
|
||||
Compares two dictionaries for equality by content, as F# maps would.
|
||||
</member>
|
||||
<member name="T:Pulumi.Automation.DestroyOptions">
|
||||
<summary>
|
||||
Options controlling the behavior of an <see cref="M:Pulumi.Automation.WorkspaceStack.DestroyAsync(Pulumi.Automation.DestroyOptions,System.Threading.CancellationToken)"/> operation.
|
||||
|
@ -274,6 +277,31 @@
|
|||
PolicyPacks run during update. Maps PolicyPackName -> version.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:Pulumi.Automation.Exceptions.ProjectSettingsConflictException">
|
||||
<summary>
|
||||
|
||||
Thrown when creating a Workspace detects a conflict between
|
||||
project settings found on disk (such as Pulumi.yaml) and a
|
||||
ProjectSettings object passed to the Create API.
|
||||
|
||||
There are two resolutions:
|
||||
|
||||
(A) to use the ProjectSettings, delete the Pulumi.yaml file
|
||||
from WorkDir or use a different WorkDir
|
||||
|
||||
(B) to use the exiting Pulumi.yaml from WorkDir, avoid
|
||||
customizing the ProjectSettings
|
||||
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:Pulumi.Automation.Exceptions.ProjectSettingsConflictException.SettingsFileLocation">
|
||||
<summary>
|
||||
|
||||
FullPath of the Pulumi.yaml (or Pulumi.yml, Pulumi.json)
|
||||
settings file found on disk.
|
||||
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:Pulumi.Automation.HistoryOptions">
|
||||
<summary>
|
||||
Options controlling the behavior of a <see cref="M:Pulumi.Automation.WorkspaceStack.GetHistoryAsync(Pulumi.Automation.HistoryOptions,System.Threading.CancellationToken)"/> operation.
|
||||
|
|
|
@ -77,3 +77,23 @@ $ pulumi config set aws:region us-west-2
|
|||
```
|
||||
|
||||
And finally, preview and update as you would any other Pulumi project.
|
||||
|
||||
|
||||
## Public API Changes
|
||||
|
||||
When making changes to the code you may get the following compilation
|
||||
error:
|
||||
|
||||
```
|
||||
error RS0016: Symbol XYZ' is not part of the declared API.
|
||||
```
|
||||
|
||||
This indicates a change in public API. If you are developing a change
|
||||
and this is intentional, add the new API elements to
|
||||
`PublicAPI.Unshipped.txt` corresponding to your project (some IDEs
|
||||
will do this automatically for you, but manual additions are fine as
|
||||
well).
|
||||
|
||||
Project maintainers will move API elements from
|
||||
`PublicAPI.Unshipped.txt` to `PublicAPI.Shipped.txt` when cutting a
|
||||
release.
|
||||
|
|
Loading…
Reference in a new issue