Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG_PENDING.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
### Improvements

* Add `RemoveStackOptions` to Automation API

### Bug Fixes

* Fix generated 'plan' argument of 'preview' command
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,36 @@ public void selectStack(String stackName) throws AutomationException {
*/
@Override
public void removeStack(String stackName) throws AutomationException {
var args = List.of("stack", "rm", "--yes", Objects.requireNonNull(stackName));
removeStack(stackName, StackRemoveOptions.Empty);
}

/**
* {@inheritDoc}
*/
@Override
public void removeStack(String stackName, StackRemoveOptions options) throws AutomationException {
var args = new ArrayList<String>();
args.add("stack");
args.add("rm");
args.add("--yes");
args.add(Objects.requireNonNull(stackName));

if (options != null) {
if (options.force()) {
args.add("--force");
}
if (options.preserveConfig()) {
args.add("--preserve-config");
}
if (options.removeBackups()) {
if (!supportsCommand(Version.of(3, 188))) {
throw new IllegalStateException(
"The Pulumi CLI version does not support removeBackups. Please update the Pulumi CLI.");
}
args.add("--remove-backups");
}
}

runCommand(args);
}

Expand Down Expand Up @@ -1102,4 +1131,12 @@ public void close() throws Exception {
private interface WorkspaceStackFactory {
WorkspaceStack create(String name, Workspace workspace) throws AutomationException;
}

private boolean supportsCommand(Version minVersion) {
var version = cmd.version();
if (version == null) {
version = Version.of(3, 0);
}
return version.isHigherThanOrEquivalentTo(minVersion);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright 2025, Pulumi Corporation

package com.pulumi.automation;

/**
* Options for removing stacks.
*/
public final class StackRemoveOptions {
/**
* An empty set of options.
*/
public static final StackRemoveOptions Empty = StackRemoveOptions.builder().build();

private final boolean force;
private final boolean preserveConfig;
private final boolean removeBackups;

private StackRemoveOptions(Builder builder) {
this.force = builder.force;
this.preserveConfig = builder.preserveConfig;
this.removeBackups = builder.removeBackups;
}

/**
* Returns a new builder for {@link StackRemoveOptions}.
*
* @return the builder
*/
public static Builder builder() {
return new Builder();
}

/**
* Forces deletion of the stack, leaving behind any resources managed by the stack
*
* @return whether to force deletion
*/
public boolean force() {
return force;
}

/**
* Do not delete the corresponding {@code Pulumi.<stack-name>.yaml} configuration file for the stack
*
* @return whether to preserve the configuration
*/
public boolean preserveConfig() {
return preserveConfig;
}

/**
* Remove backups of the stack, if using the DIY backend
*
* @return whether to remove backups
*/
public boolean removeBackups() {
return removeBackups;
}

/**
* Builder for {@link StackRemoveOptions}.
*/
public static class Builder {
private boolean force;
private boolean preserveConfig;
private boolean removeBackups;

private Builder() {
}

/**
* Forces deletion of the stack, leaving behind any resources managed by the stack
*
* @param force whether to force deletion
* @return the builder
*/
public Builder force(boolean force) {
this.force = force;
return this;
}

/**
* Do not delete the corresponding {@code Pulumi.<stack-name>.yaml} configuration file for the stack
*
* @param preserveConfig whether to preserve the configuration
* @return the builder
*/
public Builder preserveConfig(boolean preserveConfig) {
this.preserveConfig = preserveConfig;
return this;
}

/**
* Remove backups of the stack, if using the DIY backend
*
* @param removeBackups whether to remove backups
* @return the builder
*/
public Builder removeBackups(boolean removeBackups) {
this.removeBackups = removeBackups;
return this;
}

/**
* Builds the {@link StackRemoveOptions}.
*
* @return the options
*/
public StackRemoveOptions build() {
return new StackRemoveOptions(this);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,21 @@ public Optional<StackSummary> getStack() throws AutomationException {
*/
public abstract void removeStack(String stackName) throws AutomationException;

/**
* Deletes the stack and all associated configuration and history.
*
* @param stackName the stack to remove
* @param options options for removing the stack
* @throws AutomationException if there was an issue removing the stack
*/
public void removeStack(String stackName, StackRemoveOptions options) throws AutomationException {
if (options != null && options != StackRemoveOptions.Empty) {
throw new UnsupportedOperationException(
"StackRemoveOptions are not supported in this workspace implementation");
}
removeStack(stackName);
}

/**
* Returns all stacks created under the current project.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,95 @@ void testInlineProgramDoesNotEmitWarning(@EnvVars Map<String, String> envVars) {
});
}

@Test
void testStackRemoveForce(@EnvVars Map<String, String> envVars) throws Exception {
var env = new HashMap<String, String>(envVars);
env.put("PULUMI_CONFIG_PASSPHRASE", "test");

final var type = "test:res";
Consumer<Context> program = ctx -> {
new ComponentResource(type, "a");
};

var stackName = randomStackName();
var projectName = "test_stack_remove_force";

try (var stack = LocalWorkspace.createStack(projectName, stackName, program, LocalWorkspaceOptions.builder()
.environmentVariables(env)
.build())) {

var upResult = stack.up();
assertThat(upResult.summary().kind()).isEqualTo(UpdateKind.UPDATE);
assertThat(upResult.summary().result()).isEqualTo(UpdateState.SUCCEEDED);
assertThat(upResult.summary().resourceChanges().get(OperationType.CREATE)).isEqualTo(2);

// We shouldn't be able to remove the stack that has resources without force.
assertThrows(AutomationException.class, () -> stack.workspace().removeStack(stackName));

// Force remove the stack.
stack.workspace().removeStack(stackName, StackRemoveOptions.builder().force(true).build());

// We shouldn't be able to select the stack after removing it.
assertThrows(StackNotFoundException.class, () -> stack.workspace().selectStack(stackName));
}
}

@Test
void testStackRemovePreserveConfig(@EnvVars Map<String, String> envVars) throws Exception {
var env = new HashMap<String, String>(envVars);
env.put("PULUMI_CONFIG_PASSPHRASE", "test");

Consumer<Context> program = ctx -> {
// Empty program
};

var stackName = randomStackName();
var projectName = "test_stack_remove_preserve_config";

try (var stack = LocalWorkspace.createStack(projectName, stackName, program, LocalWorkspaceOptions.builder()
.environmentVariables(env)
.build())) {

var upResult = stack.up();
assertThat(upResult.summary().kind()).isEqualTo(UpdateKind.UPDATE);
assertThat(upResult.summary().result()).isEqualTo(UpdateState.SUCCEEDED);

// Remove the stack, specifying preserveConfig.
stack.workspace().removeStack(stackName, StackRemoveOptions.builder().preserveConfig(true).build());

// We shouldn't be able to select the stack after removing it.
assertThrows(StackNotFoundException.class, () -> stack.workspace().selectStack(stackName));
}
}

@Test
void testStackRemoveRemoveBackups(@EnvVars Map<String, String> envVars) throws Exception {
var env = new HashMap<String, String>(envVars);
env.put("PULUMI_CONFIG_PASSPHRASE", "test");

Consumer<Context> program = ctx -> {
// Empty program
};

var stackName = randomStackName();
var projectName = "test_stack_remove_remove_backups";

try (var stack = LocalWorkspace.createStack(projectName, stackName, program, LocalWorkspaceOptions.builder()
.environmentVariables(env)
.build())) {

var upResult = stack.up();
assertThat(upResult.summary().kind()).isEqualTo(UpdateKind.UPDATE);
assertThat(upResult.summary().result()).isEqualTo(UpdateState.SUCCEEDED);

// Remove the stack, specifying removeBackups.
stack.workspace().removeStack(stackName, StackRemoveOptions.builder().removeBackups(true).build());

// We shouldn't be able to select the stack after removing it.
assertThrows(StackNotFoundException.class, () -> stack.workspace().selectStack(stackName));
}
}

void testPreviewDestroy(@EnvVars Map<String, String> envVars) {
assertDoesNotThrow(() -> {
var env = new HashMap<String, String>(envVars);
Expand Down
Loading