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:
Anton Tayanovskyy 2021-04-01 15:27:24 -04:00 committed by GitHub
parent 55ecf7a81e
commit fc8262bad0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 503 additions and 35 deletions

View file

@ -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)

View file

@ -0,0 +1,3 @@
description: This is a description
name: correct_project
runtime: dotnet

View file

@ -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);
}
}
}

View file

@ -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
}
}
}

View file

@ -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;
}
}
}

View file

@ -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();
}

View file

@ -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);
}
}
}
}

View file

@ -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
);
}
}
}
}

View file

@ -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);
}
}
}
}

View file

@ -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
);
}
}
}
}

View file

@ -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);
}
}
}
}

View file

@ -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);
}
}
}
}

View file

@ -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

View file

@ -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.

View file

@ -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.