From e1f705d9918a7cb01a86d07268fd9b75880b609d Mon Sep 17 00:00:00 2001 From: Justin Van Patten Date: Tue, 5 Aug 2025 13:02:06 -0700 Subject: [PATCH] Add `RemoveStackOptions` to Automation API This change adds `RemoveStackOptions` to Automation API, with support for `force`, `preserveConfig`, and `removeBackups` options. `removeBackups` requires v3.188.0 or later of the Pulumi CLI. --- CHANGELOG_PENDING.md | 2 + .../com/pulumi/automation/LocalWorkspace.java | 39 +++++- .../pulumi/automation/StackRemoveOptions.java | 113 ++++++++++++++++++ .../java/com/pulumi/automation/Workspace.java | 15 +++ .../pulumi/automation/LocalWorkspaceTest.java | 89 ++++++++++++++ 5 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 sdk/java/pulumi/src/main/java/com/pulumi/automation/StackRemoveOptions.java diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 8ceb7c0cd68..b37052cf68a 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -1,5 +1,7 @@ ### Improvements +* Add `RemoveStackOptions` to Automation API + ### Bug Fixes * Fix generated 'plan' argument of 'preview' command \ No newline at end of file diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/LocalWorkspace.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/LocalWorkspace.java index 428aabfb8a6..eb87fae3290 100644 --- a/sdk/java/pulumi/src/main/java/com/pulumi/automation/LocalWorkspace.java +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/LocalWorkspace.java @@ -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(); + 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); } @@ -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); + } } diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/StackRemoveOptions.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/StackRemoveOptions.java new file mode 100644 index 00000000000..661b4aa1c04 --- /dev/null +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/StackRemoveOptions.java @@ -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..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..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); + } + } +} diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/Workspace.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/Workspace.java index 88b4ad09dc4..9e3a6ef3aa0 100644 --- a/sdk/java/pulumi/src/main/java/com/pulumi/automation/Workspace.java +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/Workspace.java @@ -411,6 +411,21 @@ public Optional 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. *

diff --git a/sdk/java/pulumi/src/test/java/com/pulumi/automation/LocalWorkspaceTest.java b/sdk/java/pulumi/src/test/java/com/pulumi/automation/LocalWorkspaceTest.java index 82934b375d7..dcd7dc33d6a 100644 --- a/sdk/java/pulumi/src/test/java/com/pulumi/automation/LocalWorkspaceTest.java +++ b/sdk/java/pulumi/src/test/java/com/pulumi/automation/LocalWorkspaceTest.java @@ -583,6 +583,95 @@ void testInlineProgramDoesNotEmitWarning(@EnvVars Map envVars) { }); } + @Test + void testStackRemoveForce(@EnvVars Map envVars) throws Exception { + var env = new HashMap(envVars); + env.put("PULUMI_CONFIG_PASSPHRASE", "test"); + + final var type = "test:res"; + Consumer 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 envVars) throws Exception { + var env = new HashMap(envVars); + env.put("PULUMI_CONFIG_PASSPHRASE", "test"); + + Consumer 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 envVars) throws Exception { + var env = new HashMap(envVars); + env.put("PULUMI_CONFIG_PASSPHRASE", "test"); + + Consumer 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 envVars) { assertDoesNotThrow(() -> { var env = new HashMap(envVars);