[auto/dotnet] - pulumi state delete & unprotect functionality (#8202)

Co-authored-by: Komal <komal@pulumi.com>
This commit is contained in:
Josh Studt 2021-10-18 19:26:45 -04:00 committed by GitHub
parent e710125885
commit caf5fcd525
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 428 additions and 1 deletions

View file

@ -9,5 +9,8 @@
SDK 3.15 or higher.
[#7899](https://github.com/pulumi/pulumi/pull/7899)
- [auto/dotnet] - Add `pulumi state delete` and `pulumi state unprotect` functionality
[#8202](https://github.com/pulumi/pulumi/pull/8202)
### Bug Fixes

View file

@ -1723,6 +1723,275 @@ namespace Pulumi.Automation.Tests
}
}
[Fact]
public async Task StateDelete()
{
const string type = "test:res";
var program = PulumiFn.Create(() =>
{
var config = new Config();
new ComponentResource(
type,
"a");
});
var stackName = RandomStackName();
var projectName = "test_state_delete";
using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program)
{
EnvironmentVariables = new Dictionary<string, string?>()
{
["PULUMI_CONFIG_PASSPHRASE"] = "test",
}
});
var jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
try
{
// pulumi up
var upResult = await stack.UpAsync();
Assert.Equal(UpdateKind.Update, upResult.Summary.Kind);
Assert.Equal(UpdateState.Succeeded, upResult.Summary.Result);
Assert.True(upResult.Summary.ResourceChanges!.TryGetValue(OperationType.Create, out var upCount));
Assert.Equal(2, upCount);
// export state
var exportResult = await stack.ExportStackAsync();
var state = JsonSerializer.Deserialize<StackState>(exportResult.Json.GetRawText(), jsonOptions);
Assert.Equal(2, state.Deployment.Resources.Count);
var resource = state.Deployment.Resources.Single(r => r.Urn.Contains(type));
// pulumi state delete
await stack.State.DeleteAsync(resource.Urn);
// test
exportResult = await stack.ExportStackAsync();
state = JsonSerializer.Deserialize<StackState>(exportResult.Json.GetRawText(), jsonOptions);
Assert.Single(state.Deployment.Resources);
}
finally
{
var destroyResult = await stack.DestroyAsync();
Assert.Equal(UpdateKind.Destroy, destroyResult.Summary.Kind);
Assert.Equal(UpdateState.Succeeded, destroyResult.Summary.Result);
await stack.Workspace.RemoveStackAsync(stackName);
}
}
[Fact]
public async Task StateDeleteForce()
{
const string type = "test:res";
var program = PulumiFn.Create(() =>
{
var config = new Config();
new ComponentResource(
type,
"a",
new ComponentResourceOptions
{
Protect = true,
});
});
var stackName = RandomStackName();
var projectName = "test_state_delete_force";
using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program)
{
EnvironmentVariables = new Dictionary<string, string?>()
{
["PULUMI_CONFIG_PASSPHRASE"] = "test",
}
});
var jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
try
{
// pulumi up
var upResult = await stack.UpAsync();
Assert.Equal(UpdateKind.Update, upResult.Summary.Kind);
Assert.Equal(UpdateState.Succeeded, upResult.Summary.Result);
Assert.True(upResult.Summary.ResourceChanges!.TryGetValue(OperationType.Create, out var upCount));
Assert.Equal(2, upCount);
// export state
var exportResult = await stack.ExportStackAsync();
var state = JsonSerializer.Deserialize<StackState>(exportResult.Json.GetRawText(), jsonOptions);
Assert.Equal(2, state.Deployment.Resources.Count);
var resource = state.Deployment.Resources.Single(r => r.Urn.Contains(type));
// pulumi state delete
await Assert.ThrowsAsync<CommandException>(() => stack.State.DeleteAsync(resource.Urn));
// pulumi state delete force
await stack.State.DeleteAsync(resource.Urn, force: true);
// test
exportResult = await stack.ExportStackAsync();
state = JsonSerializer.Deserialize<StackState>(exportResult.Json.GetRawText(), jsonOptions);
Assert.Single(state.Deployment.Resources);
}
finally
{
var destroyResult = await stack.DestroyAsync();
Assert.Equal(UpdateKind.Destroy, destroyResult.Summary.Kind);
Assert.Equal(UpdateState.Succeeded, destroyResult.Summary.Result);
await stack.Workspace.RemoveStackAsync(stackName);
}
}
[Fact]
public async Task StateUnprotect()
{
const string type = "test:res";
var program = PulumiFn.Create(() =>
{
var config = new Config();
new ComponentResource(
type,
"a",
new ComponentResourceOptions
{
Protect = true,
});
});
var stackName = RandomStackName();
var projectName = "test_state_unprotect";
using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program)
{
EnvironmentVariables = new Dictionary<string, string?>()
{
["PULUMI_CONFIG_PASSPHRASE"] = "test",
}
});
var jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
try
{
// pulumi up
var upResult = await stack.UpAsync();
Assert.Equal(UpdateKind.Update, upResult.Summary.Kind);
Assert.Equal(UpdateState.Succeeded, upResult.Summary.Result);
Assert.True(upResult.Summary.ResourceChanges!.TryGetValue(OperationType.Create, out var upCount));
Assert.Equal(2, upCount);
// export state
var exportResult = await stack.ExportStackAsync();
var state = JsonSerializer.Deserialize<StackState>(exportResult.Json.GetRawText(), jsonOptions);
Assert.Equal(2, state.Deployment.Resources.Count);
var resource = state.Deployment.Resources.Single(r => r.Urn.Contains(type));
Assert.True(resource.Protect);
// pulumi state unprotect
await stack.State.UnprotectAsync(resource.Urn);
// test
exportResult = await stack.ExportStackAsync();
state = JsonSerializer.Deserialize<StackState>(exportResult.Json.GetRawText(), jsonOptions);
Assert.Equal(2, state.Deployment.Resources.Count);
resource = state.Deployment.Resources.Single(r => r.Urn.Contains(type));
Assert.False(resource.Protect);
}
finally
{
await stack.State.UnprotectAllAsync();
var destroyResult = await stack.DestroyAsync();
Assert.Equal(UpdateKind.Destroy, destroyResult.Summary.Kind);
Assert.Equal(UpdateState.Succeeded, destroyResult.Summary.Result);
await stack.Workspace.RemoveStackAsync(stackName);
}
}
[Fact]
public async Task StateUnprotectAll()
{
const string type = "test:res";
var program = PulumiFn.Create(() =>
{
var config = new Config();
new ComponentResource(
type,
"a",
new ComponentResourceOptions
{
Protect = true,
});
new ComponentResource(
type,
"b",
new ComponentResourceOptions
{
Protect = true,
});
});
var stackName = RandomStackName();
var projectName = "test_state_unprotect_all";
using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program)
{
EnvironmentVariables = new Dictionary<string, string?>()
{
["PULUMI_CONFIG_PASSPHRASE"] = "test",
}
});
var jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
try
{
// pulumi up
var upResult = await stack.UpAsync();
Assert.Equal(UpdateKind.Update, upResult.Summary.Kind);
Assert.Equal(UpdateState.Succeeded, upResult.Summary.Result);
Assert.True(upResult.Summary.ResourceChanges!.TryGetValue(OperationType.Create, out var upCount));
Assert.Equal(3, upCount);
// export state
var exportResult = await stack.ExportStackAsync();
var state = JsonSerializer.Deserialize<StackState>(exportResult.Json.GetRawText(), jsonOptions);
Assert.Equal(3, state.Deployment.Resources.Count);
var resources = state.Deployment.Resources.Where(x => x.Urn.Contains(type)).ToList();
Assert.Equal(2, resources.Count);
Assert.True(resources.All(x => x.Protect));
// pulumi state unprotect
await stack.State.UnprotectAllAsync();
// test
exportResult = await stack.ExportStackAsync();
state = JsonSerializer.Deserialize<StackState>(exportResult.Json.GetRawText(), jsonOptions);
Assert.Equal(3, state.Deployment.Resources.Count);
resources = state.Deployment.Resources.Where(x => x.Urn.Contains(type)).ToList();
Assert.Equal(2, resources.Count);
Assert.DoesNotContain(resources, x => x.Protect);
}
finally
{
await stack.State.UnprotectAllAsync();
var destroyResult = await stack.DestroyAsync();
Assert.Equal(UpdateKind.Destroy, destroyResult.Summary.Kind);
Assert.Equal(UpdateState.Succeeded, destroyResult.Summary.Result);
await stack.Workspace.RemoveStackAsync(stackName);
}
}
private string ResourcePath(string path, [CallerFilePath] string pathBase = "LocalWorkspaceTests.cs")
{
var dir = Path.GetDirectoryName(pathBase) ?? ".";
@ -1749,5 +2018,22 @@ namespace Pulumi.Automation.Tests
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
=> _action();
}
private class StackState
{
public StackStateDeployment Deployment { get; set; } = new StackStateDeployment();
}
private class StackStateDeployment
{
public List<StackStateResource> Resources { get; set; } = new List<StackStateResource>();
}
private class StackStateResource
{
public string Urn { get; set; } = null!;
public bool Protect { get; set; }
}
}
}

View file

@ -356,8 +356,13 @@ Pulumi.Automation.WorkspaceStack.RemoveAllConfigAsync(System.Collections.Generic
Pulumi.Automation.WorkspaceStack.RemoveConfigAsync(string key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task
Pulumi.Automation.WorkspaceStack.SetAllConfigAsync(System.Collections.Generic.IDictionary<string, Pulumi.Automation.ConfigValue> configMap, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task
Pulumi.Automation.WorkspaceStack.SetConfigAsync(string key, Pulumi.Automation.ConfigValue value, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task
Pulumi.Automation.WorkspaceStack.State.get -> Pulumi.Automation.WorkspaceStackState
Pulumi.Automation.WorkspaceStack.UpAsync(Pulumi.Automation.UpOptions options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Pulumi.Automation.UpResult>
Pulumi.Automation.WorkspaceStack.Workspace.get -> Pulumi.Automation.Workspace
Pulumi.Automation.WorkspaceStackState
Pulumi.Automation.WorkspaceStackState.DeleteAsync(string urn, bool force = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task
Pulumi.Automation.WorkspaceStackState.UnprotectAllAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task
Pulumi.Automation.WorkspaceStackState.UnprotectAsync(string urn, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task
abstract Pulumi.Automation.Workspace.CreateStackAsync(string stackName, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task
abstract Pulumi.Automation.Workspace.EnvironmentVariables.get -> System.Collections.Generic.IDictionary<string, string>
abstract Pulumi.Automation.Workspace.EnvironmentVariables.set -> void

View file

@ -1198,6 +1198,11 @@
The Workspace the Stack was created from.
</summary>
</member>
<member name="P:Pulumi.Automation.WorkspaceStack.State">
<summary>
A module for editing the Stack's state.
</summary>
</member>
<member name="M:Pulumi.Automation.WorkspaceStack.CreateAsync(System.String,Pulumi.Automation.Workspace,System.Threading.CancellationToken)">
<summary>
Creates a new stack using the given workspace, and stack name.
@ -1348,5 +1353,59 @@
supported for local backends.
</summary>
</member>
<member name="T:Pulumi.Automation.WorkspaceStackState">
<summary>
Module class for manipulating stack state for a given <see cref="T:Pulumi.Automation.WorkspaceStack"/>.
</summary>
</member>
<member name="M:Pulumi.Automation.WorkspaceStackState.DeleteAsync(System.String,System.Boolean,System.Threading.CancellationToken)">
<summary>
This command deletes a resource from a stacks state, as long as it is safe to do so.
The resource is specified by its Pulumi URN.
<para/>
Resources cant be deleted if there exist other resources that depend on it or are parented to it.
Protected resources will not be deleted unless it is specifically requested using the <paramref name="force"/> flag.
</summary>
<param name="urn">The Pulumi URN of the resource to be deleted.</param>
<param name="force">A boolean indicating whether the deletion should be forced.</param>
<param name="cancellationToken">A cancellation token.</param>
</member>
<member name="M:Pulumi.Automation.WorkspaceStackState.UnprotectAsync(System.String)">
<summary>
Unprotect a resource in a stack's state.
This command clears the protect bit on the provided resource <paramref name="urn"/>, allowing the resource to be deleted.
</summary>
<param name="urn">The Pulumi URN to be unprotected.</param>
</member>
<member name="M:Pulumi.Automation.WorkspaceStackState.UnprotectAsync(System.String,System.Threading.CancellationToken)">
<summary>
Unprotect a resource in a stack's state.
This command clears the protect bit on the provided resource <paramref name="urn"/>, allowing the resource to be deleted.
</summary>
<param name="urn">The Pulumi URN to be unprotected.</param>
<param name="cancellationToken">A cancellation token.</param>
</member>
<member name="M:Pulumi.Automation.WorkspaceStackState.UnprotectAsync(System.Collections.Generic.IEnumerable{System.String})">
<summary>
Unprotect resources in a stack's state.
This command clears the protect bit on the provided resource <paramref name="urns"/>, allowing those resources to be deleted.
</summary>
<param name="urns">The Pulumi URNs to be unprotected.</param>
</member>
<member name="M:Pulumi.Automation.WorkspaceStackState.UnprotectAsync(System.Collections.Generic.IEnumerable{System.String},System.Threading.CancellationToken)">
<summary>
Unprotect resources in a stack's state.
This command clears the protect bit on the provided resource <paramref name="urns"/>, allowing those resources to be deleted.
</summary>
<param name="urns">The Pulumi URNs to be unprotected.</param>
<param name="cancellationToken">A cancellation token.</param>
</member>
<member name="M:Pulumi.Automation.WorkspaceStackState.UnprotectAllAsync(System.Threading.CancellationToken)">
<summary>
Unprotect all resources in a stack's state.
This command clears the protect bit on all resources in the stack, allowing those resources to be deleted.
</summary>
<param name="cancellationToken">A cancellation token.</param>
</member>
</members>
</doc>

View file

@ -52,6 +52,11 @@ namespace Pulumi.Automation
/// </summary>
public Workspace Workspace { get; }
/// <summary>
/// A module for editing the Stack's state.
/// </summary>
public WorkspaceStackState State { get; }
/// <summary>
/// Creates a new stack using the given workspace, and stack name.
/// It fails if a stack with that name already exists.
@ -115,6 +120,7 @@ namespace Pulumi.Automation
{
this.Name = name;
this.Workspace = workspace;
this.State = new WorkspaceStackState(this);
this._readyTask = mode switch
{
@ -644,7 +650,7 @@ namespace Pulumi.Automation
public async Task CancelAsync(CancellationToken cancellationToken = default)
=> await this.Workspace.RunCommandAsync(new[] { "cancel", "--stack", this.Name, "--yes" }, cancellationToken).ConfigureAwait(false);
private async Task<CommandResult> RunCommandAsync(
internal async Task<CommandResult> RunCommandAsync(
IList<string> args,
Action<string>? onStandardOutput,
Action<string>? onStandardError,

View file

@ -0,0 +1,68 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Pulumi.Automation
{
/// <summary>
/// Module class for manipulating stack state for a given <see cref="WorkspaceStack"/>.
/// </summary>
public sealed class WorkspaceStackState
{
private readonly WorkspaceStack _workspaceStack;
internal WorkspaceStackState(WorkspaceStack workspaceStack)
{
this._workspaceStack = workspaceStack;
}
/// <summary>
/// This command deletes a resource from a stacks state, as long as it is safe to do so.
/// The resource is specified by its Pulumi URN.
/// <para/>
/// Resources cant be deleted if there exist other resources that depend on it or are parented to it.
/// Protected resources will not be deleted unless it is specifically requested using the <paramref name="force"/> flag.
/// </summary>
/// <param name="urn">The Pulumi URN of the resource to be deleted.</param>
/// <param name="force">A boolean indicating whether the deletion should be forced.</param>
/// <param name="cancellationToken">A cancellation token.</param>
public Task DeleteAsync(string urn, bool force = false, CancellationToken cancellationToken = default)
{
var args = new List<string>()
{
"state",
"delete",
urn,
};
if (force)
args.Add("--force");
return this._workspaceStack.RunCommandAsync(args, null, null, null, cancellationToken);
}
/// <summary>
/// Unprotect a resource in a stack's state.
/// This command clears the protect bit on the provided resource <paramref name="urn"/>, allowing the resource to be deleted.
/// </summary>
/// <param name="urn">The Pulumi URN to be unprotected.</param>
/// <param name="cancellationToken">A cancellation token.</param>
public Task UnprotectAsync(string urn, CancellationToken cancellationToken = default)
{
var args = new string[] { "state", "unprotect", urn }.ToList();
return this._workspaceStack.RunCommandAsync(args, null, null, null, cancellationToken);
}
/// <summary>
/// Unprotect all resources in a stack's state.
/// This command clears the protect bit on all resources in the stack, allowing those resources to be deleted.
/// </summary>
/// <param name="cancellationToken">A cancellation token.</param>
public Task UnprotectAllAsync(CancellationToken cancellationToken = default)
{
var args = new string[] { "state", "unprotect", "--all", }.ToList();
return this._workspaceStack.RunCommandAsync(args, null, null, null, cancellationToken);
}
}
}