diff --git a/src/main/java/io/seqera/tower/cli/commands/computeenvs/DeleteCmd.java b/src/main/java/io/seqera/tower/cli/commands/computeenvs/DeleteCmd.java index d40d647a..e18bbf33 100644 --- a/src/main/java/io/seqera/tower/cli/commands/computeenvs/DeleteCmd.java +++ b/src/main/java/io/seqera/tower/cli/commands/computeenvs/DeleteCmd.java @@ -17,14 +17,19 @@ package io.seqera.tower.cli.commands.computeenvs; import io.seqera.tower.ApiException; +import io.seqera.tower.cli.commands.enums.OutputType; import io.seqera.tower.cli.commands.global.WorkspaceOptionalOptions; import io.seqera.tower.cli.exceptions.ComputeEnvNotFoundException; import io.seqera.tower.cli.responses.Response; import io.seqera.tower.cli.responses.computeenvs.ComputeEnvDeleted; import io.seqera.tower.model.ComputeEnvResponseDto; +import io.seqera.tower.model.ComputeEnvStatus; import picocli.CommandLine; import picocli.CommandLine.Command; +import java.util.Collections; +import java.util.concurrent.TimeUnit; + @Command( name = "delete", description = "Delete a compute environment." @@ -37,6 +42,12 @@ public class DeleteCmd extends AbstractComputeEnvCmd { @CommandLine.Mixin public WorkspaceOptionalOptions workspace; + @CommandLine.Option(names = {"--wait"}, description = "Wait until the compute environment is fully deleted.") + public boolean wait; + + private String deletedId; + private Long deletedWspId; + @Override protected Response exec() throws ApiException { Long wspId = workspaceId(workspace.workspace); @@ -51,6 +62,8 @@ protected Response exec() throws ApiException { try { computeEnvsApi().deleteComputeEnv(id, wspId); + deletedId = id; + deletedWspId = wspId; return new ComputeEnvDeleted(id, workspaceRef(wspId)); } catch (ApiException e) { if (e.getCode() == 403) { @@ -60,4 +73,52 @@ protected Response exec() throws ApiException { throw e; } } + + @Override + protected Integer onBeforeExit(int exitCode, Response response) { + if (exitCode != 0 || !wait || response == null) { + return exitCode; + } + + boolean showProgress = app().output != OutputType.json; + + try { + long sleepMillis = 2000; + while (true) { + ComputeEnvStatus status = checkComputeEnvStatus(deletedId, deletedWspId); + + if (status == null) { + // CE is gone (404) - deletion succeeded + if (showProgress) { + app().getOut().println(); + } + return CommandLine.ExitCode.OK; + } + + if (status == ComputeEnvStatus.ERRORED) { + app().getErr().println("ERROR: Compute environment disposal failed (status: ERRORED). AWS resources may not have been cleaned up."); + return CommandLine.ExitCode.SOFTWARE; + } + + if (showProgress) { + app().getOut().print("."); + app().getOut().flush(); + } + + TimeUnit.MILLISECONDS.sleep(sleepMillis); + sleepMillis = Math.min(sleepMillis + 1000, 120_000); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return exitCode; + } + } + + private ComputeEnvStatus checkComputeEnvStatus(String computeEnvId, Long workspaceId) { + try { + return computeEnvsApi().describeComputeEnv(computeEnvId, workspaceId, Collections.emptyList()).getComputeEnv().getStatus(); + } catch (ApiException | NullPointerException e) { + return null; + } + } } diff --git a/src/test/java/io/seqera/tower/cli/computeenvs/ComputeEnvsCmdTest.java b/src/test/java/io/seqera/tower/cli/computeenvs/ComputeEnvsCmdTest.java index 10f30240..315ea51f 100644 --- a/src/test/java/io/seqera/tower/cli/computeenvs/ComputeEnvsCmdTest.java +++ b/src/test/java/io/seqera/tower/cli/computeenvs/ComputeEnvsCmdTest.java @@ -675,4 +675,53 @@ void testUpdateInvalidName(OutputType format, MockServerClient mock) { assertEquals("", out.stdOut); assertEquals(1, out.exitCode); } + + @Test + void testDeleteWaitHappyPath(MockServerClient mock) { + // DELETE returns 204 + mock.when( + request().withMethod("DELETE").withPath("/compute-envs/vYOK4vn7spw7bHHWBDXZ2"), exactly(1) + ).respond( + response().withStatusCode(204) + ); + + // First DESCRIBE returns DELETING + mock.when( + request().withMethod("GET").withPath("/compute-envs/vYOK4vn7spw7bHHWBDXZ2"), exactly(1) + ).respond( + response().withStatusCode(200).withBody("{\"computeEnv\":{\"id\":\"vYOK4vn7spw7bHHWBDXZ2\",\"name\":\"demo\",\"platform\":\"aws-batch\",\"status\":\"DELETING\"}}").withContentType(MediaType.APPLICATION_JSON) + ); + + // Second DESCRIBE returns 404 (CE has been deleted) + mock.when( + request().withMethod("GET").withPath("/compute-envs/vYOK4vn7spw7bHHWBDXZ2"), exactly(1) + ).respond( + response().withStatusCode(404) + ); + + ExecOut out = exec(mock, "compute-envs", "delete", "-i", "vYOK4vn7spw7bHHWBDXZ2", "--wait"); + + assertEquals(0, out.exitCode); + } + + @Test + void testDeleteWaitErrored(MockServerClient mock) { + // DELETE returns 204 + mock.when( + request().withMethod("DELETE").withPath("/compute-envs/vYOK4vn7spw7bHHWBDXZ2"), exactly(1) + ).respond( + response().withStatusCode(204) + ); + + // DESCRIBE returns ERRORED + mock.when( + request().withMethod("GET").withPath("/compute-envs/vYOK4vn7spw7bHHWBDXZ2"), exactly(1) + ).respond( + response().withStatusCode(200).withBody("{\"computeEnv\":{\"id\":\"vYOK4vn7spw7bHHWBDXZ2\",\"name\":\"demo\",\"platform\":\"aws-batch\",\"status\":\"ERRORED\"}}").withContentType(MediaType.APPLICATION_JSON) + ); + + ExecOut out = exec(mock, "compute-envs", "delete", "-i", "vYOK4vn7spw7bHHWBDXZ2", "--wait"); + + assertEquals(1, out.exitCode); + } }