From fc8262bad0b1316dc18f8e05f2e0ce93029f3e07 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Thu, 1 Apr 2021 15:27:24 -0400 Subject: [PATCH] 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 * Update sdk/dotnet/Pulumi.Automation/ProjectRuntime.cs Co-authored-by: Ville Penttinen * Update sdk/dotnet/Pulumi.Automation/ProjectTemplateConfigValue.cs Co-authored-by: Ville Penttinen * Update sdk/dotnet/Pulumi.Automation/ProjectTemplate.cs Co-authored-by: Ville Penttinen * Update sdk/dotnet/Pulumi.Automation/ProjectRuntimeOptions.cs Co-authored-by: Ville Penttinen * Update sdk/dotnet/Pulumi.Automation/ProjectBackend.cs Co-authored-by: Ville Penttinen * Update sdk/dotnet/Pulumi.Automation/ProjectSettings.cs Co-authored-by: Ville Penttinen * 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 Co-authored-by: Ville Penttinen --- CHANGELOG_PENDING.md | 6 +- .../Data/correct_project/Pulumi.yaml | 3 + .../LocalWorkspaceTests.cs | 65 +++++++++++++-- .../Collections/DictionaryContentsComparer.cs | 58 ++++++++++++++ .../ProjectSettingsConflictException.cs | 39 +++++++++ .../Pulumi.Automation/LocalWorkspace.cs | 80 +++++++++++++------ .../Pulumi.Automation/ProjectBackend.cs | 33 ++++++++ .../Pulumi.Automation/ProjectRuntime.cs | 36 +++++++++ .../ProjectRuntimeOptions.cs | 33 ++++++++ .../Pulumi.Automation/ProjectSettings.cs | 60 ++++++++++++++ .../Pulumi.Automation/ProjectTemplate.cs | 42 ++++++++++ .../ProjectTemplateConfigValue.cs | 33 ++++++++ .../Pulumi.Automation/PublicAPI.Unshipped.txt | 2 + .../Pulumi.Automation/Pulumi.Automation.xml | 28 +++++++ sdk/dotnet/README.md | 20 +++++ 15 files changed, 503 insertions(+), 35 deletions(-) create mode 100644 sdk/dotnet/Pulumi.Automation.Tests/Data/correct_project/Pulumi.yaml create mode 100644 sdk/dotnet/Pulumi.Automation/Collections/DictionaryContentsComparer.cs create mode 100644 sdk/dotnet/Pulumi.Automation/Exceptions/ProjectSettingsConflictException.cs diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 6cb553ebc..c0ec419c2 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -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) diff --git a/sdk/dotnet/Pulumi.Automation.Tests/Data/correct_project/Pulumi.yaml b/sdk/dotnet/Pulumi.Automation.Tests/Data/correct_project/Pulumi.yaml new file mode 100644 index 000000000..6eff69341 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation.Tests/Data/correct_project/Pulumi.yaml @@ -0,0 +1,3 @@ +description: This is a description +name: correct_project +runtime: dotnet diff --git a/sdk/dotnet/Pulumi.Automation.Tests/LocalWorkspaceTests.cs b/sdk/dotnet/Pulumi.Automation.Tests/LocalWorkspaceTests.cs index 1308e45a9..b5f854c9a 100644 --- a/sdk/dotnet/Pulumi.Automation.Tests/LocalWorkspaceTests.cs +++ b/sdk/dotnet/Pulumi.Automation.Tests/LocalWorkspaceTests.cs @@ -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() @@ -622,7 +621,7 @@ namespace Pulumi.Automation.Tests } static async Task RunCommand(Func> func, string command) - where TOptions: UpdateOptions, new() + where TOptions : UpdateOptions, new() { var events = new List(); @@ -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(); + + 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(); + + 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 + }) + ); + } + + private string ResourcePath(string path, [CallerFilePath] string pathBase = "LocalWorkspaceTests.cs") + { + var dir = Path.GetDirectoryName(pathBase) ?? "."; + return Path.Combine(dir, path); + } } } diff --git a/sdk/dotnet/Pulumi.Automation/Collections/DictionaryContentsComparer.cs b/sdk/dotnet/Pulumi.Automation/Collections/DictionaryContentsComparer.cs new file mode 100644 index 000000000..8e0e6d71f --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Collections/DictionaryContentsComparer.cs @@ -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 : IEqualityComparer> where K : notnull + { + private readonly IEqualityComparer _keyComparer; + private readonly IEqualityComparer _valueComparer; + + public DictionaryContentsComparer(IEqualityComparer keyComparer, IEqualityComparer valueComparer) + { + this._keyComparer = keyComparer; + this._valueComparer = valueComparer; + } + + bool IEqualityComparer>.Equals(IDictionary? x, IDictionary? 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(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>.GetHashCode(IDictionary obj) + { + return 0; // inefficient but correct + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Exceptions/ProjectSettingsConflictException.cs b/sdk/dotnet/Pulumi.Automation/Exceptions/ProjectSettingsConflictException.cs new file mode 100644 index 000000000..12c11dc48 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Exceptions/ProjectSettingsConflictException.cs @@ -0,0 +1,39 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; + +namespace Pulumi.Automation.Exceptions +{ + /// + /// + /// 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 + /// + /// + public class ProjectSettingsConflictException : Exception + { + + /// + /// + /// FullPath of the Pulumi.yaml (or Pulumi.yml, Pulumi.json) + /// settings file found on disk. + /// + /// + public string SettingsFileLocation { get; } + + internal ProjectSettingsConflictException(string settingsFileLocation) + : base($"Custom {nameof(ProjectSettings)} passed in code conflict with settings found on disk: {settingsFileLocation}") + { + SettingsFileLocation = settingsFileLocation; + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/LocalWorkspace.cs b/sdk/dotnet/Pulumi.Automation/LocalWorkspace.cs index d41acb16a..a3f160f2a 100644 --- a/sdk/dotnet/Pulumi.Automation/LocalWorkspace.cs +++ b/sdk/dotnet/Pulumi.Automation/LocalWorkspace.cs @@ -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 /// public override async Task 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(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(content); var model = this._serializer.DeserializeYaml(content); return model.Convert(); } - - return null; } /// 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 /// public override async Task SetConfigAsync(string stackName, IDictionary configMap, CancellationToken cancellationToken = default) { - var args = new List{"config", "set-all", "--stack", stackName}; - foreach (var (key, value) in configMap) + var args = new List { "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 /// public override async Task RemoveConfigAsync(string stackName, IEnumerable keys, CancellationToken cancellationToken = default) { - var args = new List{"config", "rm-all", "--stack", stackName}; + var args = new List { "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.Empty; - + var stacks = this._serializer.DeserializeJson>(result.StandardOutput); return stacks.ToImmutableList(); } diff --git a/sdk/dotnet/Pulumi.Automation/ProjectBackend.cs b/sdk/dotnet/Pulumi.Automation/ProjectBackend.cs index c1efa4063..d47e2289c 100644 --- a/sdk/dotnet/Pulumi.Automation/ProjectBackend.cs +++ b/sdk/dotnet/Pulumi.Automation/ProjectBackend.cs @@ -1,5 +1,8 @@ // Copyright 2016-2021, Pulumi Corporation +using System; +using System.Collections.Generic; + namespace Pulumi.Automation { /// @@ -7,6 +10,36 @@ namespace Pulumi.Automation /// public class ProjectBackend { + internal static IEqualityComparer Comparer { get; } = new ProjectBackendComparer(); + public string? Url { get; set; } + + private sealed class ProjectBackendComparer : IEqualityComparer + { + bool IEqualityComparer.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.GetHashCode(ProjectBackend obj) + { + return HashCode.Combine(obj.Url); + } + } } } diff --git a/sdk/dotnet/Pulumi.Automation/ProjectRuntime.cs b/sdk/dotnet/Pulumi.Automation/ProjectRuntime.cs index 9f23a2b45..4bfd946e1 100644 --- a/sdk/dotnet/Pulumi.Automation/ProjectRuntime.cs +++ b/sdk/dotnet/Pulumi.Automation/ProjectRuntime.cs @@ -1,5 +1,8 @@ // Copyright 2016-2021, Pulumi Corporation +using System; +using System.Collections.Generic; + namespace Pulumi.Automation { /// @@ -7,6 +10,8 @@ namespace Pulumi.Automation /// public class ProjectRuntime { + internal static IEqualityComparer 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 + { + bool IEqualityComparer.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.GetHashCode(ProjectRuntime obj) + { + return HashCode.Combine( + obj.Name, + obj.Options != null ? ProjectRuntimeOptions.Comparer.GetHashCode(obj.Options) : 0 + ); + } + } } } diff --git a/sdk/dotnet/Pulumi.Automation/ProjectRuntimeOptions.cs b/sdk/dotnet/Pulumi.Automation/ProjectRuntimeOptions.cs index 9405eb74e..cabed4199 100644 --- a/sdk/dotnet/Pulumi.Automation/ProjectRuntimeOptions.cs +++ b/sdk/dotnet/Pulumi.Automation/ProjectRuntimeOptions.cs @@ -1,5 +1,8 @@ // Copyright 2016-2021, Pulumi Corporation +using System; +using System.Collections.Generic; + namespace Pulumi.Automation { /// @@ -7,6 +10,8 @@ namespace Pulumi.Automation /// public class ProjectRuntimeOptions { + internal static IEqualityComparer Comparer { get; } = new ProjectRuntimeOptionsComparer(); + /// /// Applies to NodeJS projects only. /// @@ -29,5 +34,33 @@ namespace Pulumi.Automation /// A string that specifies the path to a virtual environment to use when running the program. /// public string? VirtualEnv { get; set; } + + private sealed class ProjectRuntimeOptionsComparer : IEqualityComparer + { + bool IEqualityComparer.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.GetHashCode(ProjectRuntimeOptions obj) + { + return HashCode.Combine(obj.TypeScript, obj.Binary, obj.VirtualEnv); + } + } } } diff --git a/sdk/dotnet/Pulumi.Automation/ProjectSettings.cs b/sdk/dotnet/Pulumi.Automation/ProjectSettings.cs index 67c3b41c9..31a6f571b 100644 --- a/sdk/dotnet/Pulumi.Automation/ProjectSettings.cs +++ b/sdk/dotnet/Pulumi.Automation/ProjectSettings.cs @@ -1,5 +1,8 @@ // Copyright 2016-2021, Pulumi Corporation +using System; +using System.Collections.Generic; + namespace Pulumi.Automation { /// @@ -7,6 +10,8 @@ namespace Pulumi.Automation /// public class ProjectSettings { + internal static IEqualityComparer 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 + { + bool IEqualityComparer.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.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 + ); + } + } } } diff --git a/sdk/dotnet/Pulumi.Automation/ProjectTemplate.cs b/sdk/dotnet/Pulumi.Automation/ProjectTemplate.cs index d8a58f6a3..60d882fcd 100644 --- a/sdk/dotnet/Pulumi.Automation/ProjectTemplate.cs +++ b/sdk/dotnet/Pulumi.Automation/ProjectTemplate.cs @@ -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 /// public class ProjectTemplate { + internal static IEqualityComparer Comparer { get; } = new ProjectTemplateComparer(); + public string? Description { get; set; } public string? QuickStart { get; set; } @@ -16,5 +20,43 @@ namespace Pulumi.Automation public IDictionary? Config { get; set; } public bool? Important { get; set; } + + private sealed class ProjectTemplateComparer : IEqualityComparer + { + + private IEqualityComparer> _configComparer = + new DictionaryContentsComparer( + EqualityComparer.Default, + ProjectTemplateConfigValue.Comparer); + + bool IEqualityComparer.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.GetHashCode(ProjectTemplate obj) + { + // omit hashing Config dict for efficiency + return HashCode.Combine(obj.Description, obj.QuickStart, obj.Important); + } + } } } diff --git a/sdk/dotnet/Pulumi.Automation/ProjectTemplateConfigValue.cs b/sdk/dotnet/Pulumi.Automation/ProjectTemplateConfigValue.cs index 8a63fffcd..4b609ca88 100644 --- a/sdk/dotnet/Pulumi.Automation/ProjectTemplateConfigValue.cs +++ b/sdk/dotnet/Pulumi.Automation/ProjectTemplateConfigValue.cs @@ -1,5 +1,8 @@ // Copyright 2016-2021, Pulumi Corporation +using System; +using System.Collections.Generic; + namespace Pulumi.Automation { /// @@ -7,10 +10,40 @@ namespace Pulumi.Automation /// public class ProjectTemplateConfigValue { + internal static IEqualityComparer Comparer { get; } = new ProjectTemplateConfigValueComparer(); + public string? Description { get; set; } public string? Default { get; set; } public bool? Secret { get; set; } + + private sealed class ProjectTemplateConfigValueComparer : IEqualityComparer + { + bool IEqualityComparer.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.GetHashCode(ProjectTemplateConfigValue obj) + { + return HashCode.Combine(obj.Description, obj.Default, obj.Secret); + } + } } } diff --git a/sdk/dotnet/Pulumi.Automation/PublicAPI.Unshipped.txt b/sdk/dotnet/Pulumi.Automation/PublicAPI.Unshipped.txt index e0c3a08e1..4668ac088 100644 --- a/sdk/dotnet/Pulumi.Automation/PublicAPI.Unshipped.txt +++ b/sdk/dotnet/Pulumi.Automation/PublicAPI.Unshipped.txt @@ -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 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.Exceptions.ProjectSettingsConflictException +Pulumi.Automation.Exceptions.ProjectSettingsConflictException.SettingsFileLocation.get -> string diff --git a/sdk/dotnet/Pulumi.Automation/Pulumi.Automation.xml b/sdk/dotnet/Pulumi.Automation/Pulumi.Automation.xml index 971446711..605509cc0 100644 --- a/sdk/dotnet/Pulumi.Automation/Pulumi.Automation.xml +++ b/sdk/dotnet/Pulumi.Automation/Pulumi.Automation.xml @@ -4,6 +4,9 @@ Pulumi.Automation + + Compares two dictionaries for equality by content, as F# maps would. + Options controlling the behavior of an operation. @@ -274,6 +277,31 @@ PolicyPacks run during update. Maps PolicyPackName -> version. + + + + 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 + + + + + + + FullPath of the Pulumi.yaml (or Pulumi.yml, Pulumi.json) + settings file found on disk. + + + Options controlling the behavior of a operation. diff --git a/sdk/dotnet/README.md b/sdk/dotnet/README.md index 988eebffc..603a969e0 100644 --- a/sdk/dotnet/README.md +++ b/sdk/dotnet/README.md @@ -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.