diff --git a/build.gradle b/build.gradle index 11c74bd2..4af234bc 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ dependencies { implementation 'org.slf4j:slf4j-api:1.7.36' implementation 'ch.qos.logback:logback-core:1.2.11' implementation 'ch.qos.logback:logback-classic:1.2.11' - implementation 'io.seqera.tower:tower-java-sdk:1.9.9' + implementation 'io.seqera.tower:tower-java-sdk:1.9.10' implementation 'info.picocli:picocli:4.6.3' implementation 'org.apache.commons:commons-compress:1.22' implementation 'org.tukaani:xz:1.9' diff --git a/conf/reflect-config.json b/conf/reflect-config.json index 809cda4f..15495642 100644 --- a/conf/reflect-config.json +++ b/conf/reflect-config.json @@ -1124,6 +1124,12 @@ "allDeclaredFields":true, "queryAllDeclaredMethods":true }, +{ + "name":"io.seqera.tower.cli.commands.datastudios.DataStudioConfigurationOptions", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, { "name":"io.seqera.tower.cli.commands.datastudios.DataStudioRefOptions", "allDeclaredFields":true, @@ -1142,6 +1148,12 @@ "queryAllDeclaredMethods":true, "methods":[{"name":"","parameterTypes":[] }] }, +{ + "name":"io.seqera.tower.cli.commands.datastudios.StartCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, { "name":"io.seqera.tower.cli.commands.datastudios.ViewCmd", "allDeclaredFields":true, @@ -1835,6 +1847,12 @@ "queryAllDeclaredMethods":true, "queryAllDeclaredConstructors":true }, +{ + "name":"io.seqera.tower.cli.responses.datastudios.DataStudioStartSubmitted", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, { "name":"io.seqera.tower.cli.responses.datastudios.DataStudiosList", "allDeclaredFields":true, @@ -2639,7 +2657,8 @@ "name":"io.seqera.tower.model.DataStudioDtoParentCheckpoint", "allDeclaredFields":true, "queryAllDeclaredMethods":true, - "queryAllDeclaredConstructors":true + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"getCheckpointId","parameterTypes":[] }, {"name":"getCheckpointName","parameterTypes":[] }, {"name":"getSessionId","parameterTypes":[] }, {"name":"getStudioName","parameterTypes":[] }, {"name":"setCheckpointId","parameterTypes":["java.lang.Long"] }, {"name":"setCheckpointName","parameterTypes":["java.lang.String"] }, {"name":"setSessionId","parameterTypes":["java.lang.String"] }, {"name":"setStudioName","parameterTypes":["java.lang.String"] }] }, { "name":"io.seqera.tower.model.DataStudioListResponse", @@ -2661,6 +2680,20 @@ "queryAllDeclaredMethods":true, "methods":[{"name":"fromValue","parameterTypes":["java.lang.String"] }, {"name":"getValue","parameterTypes":[] }, {"name":"setValue","parameterTypes":["java.lang.String"] }] }, +{ + "name":"io.seqera.tower.model.DataStudioStartRequest", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"getConfiguration_JsonNullable","parameterTypes":[] }, {"name":"getDescription_JsonNullable","parameterTypes":[] }] +}, +{ + "name":"io.seqera.tower.model.DataStudioStartResponse", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setJobSubmitted","parameterTypes":["java.lang.Boolean"] }, {"name":"setSessionId","parameterTypes":["java.lang.String"] }, {"name":"setStatusInfo","parameterTypes":["io.seqera.tower.model.DataStudioStatusInfo"] }] +}, { "name":"io.seqera.tower.model.DataStudioStatus", "allDeclaredFields":true, diff --git a/src/main/java/io/seqera/tower/cli/commands/DataStudiosCmd.java b/src/main/java/io/seqera/tower/cli/commands/DataStudiosCmd.java index 948aa4ad..675ea035 100644 --- a/src/main/java/io/seqera/tower/cli/commands/DataStudiosCmd.java +++ b/src/main/java/io/seqera/tower/cli/commands/DataStudiosCmd.java @@ -18,6 +18,7 @@ package io.seqera.tower.cli.commands; import io.seqera.tower.cli.commands.datastudios.ListCmd; +import io.seqera.tower.cli.commands.datastudios.StartCmd; import io.seqera.tower.cli.commands.datastudios.ViewCmd; import picocli.CommandLine; @@ -27,6 +28,7 @@ subcommands = { ViewCmd.class, ListCmd.class, + StartCmd.class, } ) public class DataStudiosCmd extends AbstractRootCmd { diff --git a/src/main/java/io/seqera/tower/cli/commands/datastudios/AbstractStudiosCmd.java b/src/main/java/io/seqera/tower/cli/commands/datastudios/AbstractStudiosCmd.java index 69b6f2ff..0a9069fe 100644 --- a/src/main/java/io/seqera/tower/cli/commands/datastudios/AbstractStudiosCmd.java +++ b/src/main/java/io/seqera/tower/cli/commands/datastudios/AbstractStudiosCmd.java @@ -17,13 +17,56 @@ package io.seqera.tower.cli.commands.datastudios; +import java.util.Optional; +import java.util.function.Supplier; + import io.seqera.tower.ApiException; import io.seqera.tower.cli.commands.AbstractApiCmd; import io.seqera.tower.model.DataStudioDto; +import io.seqera.tower.model.DataStudioProgressStep; + +import static io.seqera.tower.model.DataStudioProgressStepStatus.ERRORED; +import static io.seqera.tower.model.DataStudioProgressStepStatus.IN_PROGRESS; public class AbstractStudiosCmd extends AbstractApiCmd { protected DataStudioDto fetchDataStudio(DataStudioRefOptions dataStudioRefOptions, Long wspId) throws ApiException { return api().describeDataStudio(dataStudioRefOptions.dataStudio.sessionId, wspId); } + + public class ProgressStepMessageSupplier implements Supplier { + + private final String sessionId; + private final Long workspaceId; + private DataStudioProgressStep currentProgressStep; + + public ProgressStepMessageSupplier(String sessionId, Long workspaceId) { + this.sessionId = sessionId; + this.workspaceId = workspaceId; + this.currentProgressStep = new DataStudioProgressStep(); + } + + @Override + public String get() { + + try { + DataStudioDto dataStudioDto = api().describeDataStudio(sessionId, workspaceId); + + Optional inProgressStep = dataStudioDto.getProgress().stream() + .filter(step -> step.getStatus() == IN_PROGRESS || step.getStatus() == ERRORED) + .findFirst(); + + if (inProgressStep.isPresent() && !inProgressStep.get().equals(currentProgressStep)) { + currentProgressStep = inProgressStep.get(); + return currentProgressStep.getStatus() != ERRORED + ? String.format("\n %s", currentProgressStep.getMessage()) + : String.format("\n %s - Error encountered: %s", currentProgressStep.getMessage(), dataStudioDto.getStatusInfo().getMessage()); + } + + return null; + } catch (Exception e) { + return null; + } + } + } } diff --git a/src/main/java/io/seqera/tower/cli/commands/datastudios/DataStudioConfigurationOptions.java b/src/main/java/io/seqera/tower/cli/commands/datastudios/DataStudioConfigurationOptions.java new file mode 100644 index 00000000..66d779e8 --- /dev/null +++ b/src/main/java/io/seqera/tower/cli/commands/datastudios/DataStudioConfigurationOptions.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021-2023, Seqera. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.seqera.tower.cli.commands.datastudios; + +import java.util.List; + +import picocli.CommandLine; + +public class DataStudioConfigurationOptions { + + @CommandLine.Option(names = {"-g", "--gpu"}, description = "Optional configuration override for 'gpu' setting (Integer representing number of cores)") + public Integer gpu; + + @CommandLine.Option(names = {"-c", "--cpu"}, description = "Optional configuration override for 'cpu' setting (Integer representing number of cores)") + public Integer cpu; + + @CommandLine.Option(names = {"-m", "--memory"}, description = "Optional configuration override for 'memory' setting (Integer representing memory in MBs)") + public Integer memory; + + @CommandLine.Option(names = {"--mount-data"}, description = "Optional configuration override for 'mountData' setting (comma separate list of datalinkIds)", split = ",") + public List mountData; + + @CommandLine.Option(names = {"--conda-env"}, description = "Optional configuration override for 'condaEnvironment' setting (YAML conda packages configurations)") + public String condaEnvironment; + +} diff --git a/src/main/java/io/seqera/tower/cli/commands/datastudios/StartCmd.java b/src/main/java/io/seqera/tower/cli/commands/datastudios/StartCmd.java new file mode 100644 index 00000000..73c1071d --- /dev/null +++ b/src/main/java/io/seqera/tower/cli/commands/datastudios/StartCmd.java @@ -0,0 +1,155 @@ +/* + * Copyright 2021-2023, Seqera. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.seqera.tower.cli.commands.datastudios; + +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.DataStudioNotFoundException; +import io.seqera.tower.cli.exceptions.TowerException; +import io.seqera.tower.cli.responses.Response; +import io.seqera.tower.cli.responses.datastudios.DataStudioStartSubmitted; +import io.seqera.tower.model.DataStudioConfiguration; +import io.seqera.tower.model.DataStudioDto; +import io.seqera.tower.model.DataStudioStartRequest; +import io.seqera.tower.model.DataStudioStartResponse; +import io.seqera.tower.model.DataStudioStatus; +import picocli.CommandLine; + +import static io.seqera.tower.cli.utils.ResponseHelper.waitStatus; +import static java.lang.Boolean.FALSE; + +@CommandLine.Command( + name = "start", + description = "Start a data studio." +) +public class StartCmd extends AbstractStudiosCmd { + + @CommandLine.Mixin + public WorkspaceOptionalOptions workspace; + + @CommandLine.Mixin + public DataStudioRefOptions dataStudioRefOptions; + + @CommandLine.Mixin + public DataStudioConfigurationOptions dataStudioConfigOptions; + + @CommandLine.Option(names = {"--wait"}, description = "Wait until given status or fail. Valid options: ${COMPLETION-CANDIDATES}.") + public DataStudioStatus wait; + + @CommandLine.Option(names = {"--description"}, description = "Optional configuration override for 'description'.") + public String description; + + @Override + protected Response exec() throws ApiException { + Long wspId = workspaceId(workspace.workspace); + + try { + DataStudioDto dataStudioDto = fetchDataStudio(dataStudioRefOptions, wspId); + + DataStudioStartRequest request = getStartRequestWithOverridesApplied(dataStudioDto); + + DataStudioStartResponse response = api().startDataStudio(dataStudioRefOptions.dataStudio.sessionId, request, wspId); + + return new DataStudioStartSubmitted(dataStudioRefOptions.dataStudio.sessionId, wspId, workspaceRef(wspId), dataStudioDto.getStudioUrl(), response.getJobSubmitted()); + } catch (ApiException e) { + if (e.getCode() == 404) { + throw new DataStudioNotFoundException(dataStudioRefOptions.dataStudio.sessionId, wspId); + } + if (e.getCode() == 403) { + throw new TowerException(String.format("User not entitled to view studio '%s' at %s workspace", dataStudioRefOptions.dataStudio.sessionId, wspId)); + } + throw e; + } + } + + @Override + protected Integer onBeforeExit(int exitCode, Response response) { + + if (exitCode != 0 || wait == null || response == null) { + return exitCode; + } + + DataStudioStartSubmitted submitted = (DataStudioStartSubmitted) response; + + // If response declares job failed to submit, don't wait and exit early. + if (FALSE.equals(submitted.jobSubmitted)) { + return exitCode; + } + + boolean showProgress = app().output != OutputType.json; + + try { + return waitStatus( + app().getOut(), + showProgress, + new ProgressStepMessageSupplier(submitted.sessionId, submitted.workspaceId), + wait, + DataStudioStatus.values(), + () -> checkDataStudioStatus(submitted.sessionId, submitted.workspaceId), + DataStudioStatus.STOPPED, DataStudioStatus.ERRORED, DataStudioStatus.RUNNING + ); + } catch (InterruptedException e) { + return exitCode; + } + } + + private DataStudioStatus checkDataStudioStatus(String sessionId, Long workspaceId) { + try { + return api().describeDataStudio(sessionId, workspaceId).getStatusInfo().getStatus(); + } catch (ApiException | NullPointerException e) { + return null; + } + } + + private DataStudioStartRequest getStartRequestWithOverridesApplied(DataStudioDto dataStudioDto) { + DataStudioConfiguration dataStudioConfiguration = dataStudioDto.getConfiguration() == null + ? new DataStudioConfiguration() + : dataStudioDto.getConfiguration(); + + dataStudioConfiguration.setGpu(dataStudioConfigOptions.gpu == null + ? dataStudioConfiguration.getGpu() + : dataStudioConfigOptions.gpu); + dataStudioConfiguration.setCpu(dataStudioConfigOptions.cpu == null + ? dataStudioConfiguration.getCpu() + : dataStudioConfigOptions.cpu); + dataStudioConfiguration.setMemory(dataStudioConfigOptions.memory == null + ? dataStudioConfiguration.getMemory() + : dataStudioConfigOptions.memory); + dataStudioConfiguration.setMountData(dataStudioConfigOptions.mountData == null + ? dataStudioConfiguration.getMountData() + : dataStudioConfigOptions.mountData); + dataStudioConfiguration.setCondaEnvironment(dataStudioConfigOptions.condaEnvironment == null + ? dataStudioConfiguration.getCondaEnvironment() + : dataStudioConfigOptions.condaEnvironment); + + String appliedDescription = description == null + ? dataStudioDto.getDescription() + : description; + + DataStudioStartRequest request = new DataStudioStartRequest(); + + request.setConfiguration(dataStudioConfiguration); + request.setDescription(appliedDescription); + + return request; + } + + + +} \ No newline at end of file diff --git a/src/main/java/io/seqera/tower/cli/responses/datastudios/DataStudioStartSubmitted.java b/src/main/java/io/seqera/tower/cli/responses/datastudios/DataStudioStartSubmitted.java new file mode 100644 index 00000000..f437f4fb --- /dev/null +++ b/src/main/java/io/seqera/tower/cli/responses/datastudios/DataStudioStartSubmitted.java @@ -0,0 +1,46 @@ +/* + * Copyright 2021-2023, Seqera. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.seqera.tower.cli.responses.datastudios; + +import io.seqera.tower.cli.responses.Response; + +public class DataStudioStartSubmitted extends Response { + + public final String sessionId; + + public final String studioUrl; + + public final Long workspaceId; + public final String workspaceRef; + public final Boolean jobSubmitted; + + public DataStudioStartSubmitted(String sessionId, Long workspaceId, String workspaceRef, String studioUrl, Boolean jobSubmitted) { + this.sessionId = sessionId; + this.workspaceId = workspaceId; + this.workspaceRef = workspaceRef; + this.studioUrl = studioUrl; + this.jobSubmitted = jobSubmitted; + } + + @Override + public String toString() { + String isSuccess = jobSubmitted ? "successfully submitted" : "failed to submit"; + return ansi(String.format("%n @|yellow Data Studio %s START %s at %s workspace.|@%n%n @|bold %s|@%n", sessionId, isSuccess, workspaceRef, studioUrl)); + } + +} diff --git a/src/main/java/io/seqera/tower/cli/utils/ResponseHelper.java b/src/main/java/io/seqera/tower/cli/utils/ResponseHelper.java index a79354d7..b5bd5851 100644 --- a/src/main/java/io/seqera/tower/cli/utils/ResponseHelper.java +++ b/src/main/java/io/seqera/tower/cli/utils/ResponseHelper.java @@ -135,6 +135,10 @@ private static String decodeMessage(ApiException ex) { } public static > Integer waitStatus(PrintWriter out, boolean showProgress, S targetStatus, S[] allStates, Supplier checkStatus, S... endStates ) throws InterruptedException { + return waitStatus(out, showProgress, null, targetStatus, allStates, checkStatus, endStates); + } + + public static > Integer waitStatus(PrintWriter out, boolean showProgress, Supplier additionalProgressSteps, S targetStatus, S[] allStates, Supplier checkStatus, S... endStates ) throws InterruptedException { Map positions = new HashMap<>(); for (int i=0; i < allStates.length; i++) { @@ -162,9 +166,18 @@ public static > Integer waitStatus(PrintWriter out, boolean sh if (showProgress) { out.print('.'); if (lastReported != status) { + if(additionalProgressSteps != null) { + out.print("\n Status transitioned to: "); + } out.print(String.format("%s", status)); lastReported = status; } + if (additionalProgressSteps != null) { + String progressStep = additionalProgressSteps.get(); + if (progressStep != null) { + out.print(progressStep); + } + } out.flush(); } if (secondsToSleep < maxSecondsToSleep) { @@ -173,6 +186,12 @@ public static > Integer waitStatus(PrintWriter out, boolean sh } while (currentPos < targetPos && !immutableStates.contains(status)); if (showProgress) { + if (additionalProgressSteps != null) { + String progressStep = additionalProgressSteps.get(); + if (progressStep != null) { + out.print(progressStep); + } + } out.print(currentPos == targetPos ? " [DONE]\n\n" : " [ERROR]\n\n"); out.flush(); } diff --git a/src/test/java/io/seqera/tower/cli/datastudios/DataStudiosCmdTest.java b/src/test/java/io/seqera/tower/cli/datastudios/DataStudiosCmdTest.java index fff4b3ec..49bb516e 100644 --- a/src/test/java/io/seqera/tower/cli/datastudios/DataStudiosCmdTest.java +++ b/src/test/java/io/seqera/tower/cli/datastudios/DataStudiosCmdTest.java @@ -20,10 +20,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import io.seqera.tower.cli.BaseCmdTest; import io.seqera.tower.cli.commands.enums.OutputType; +import io.seqera.tower.cli.responses.datastudios.DataStudioStartSubmitted; import io.seqera.tower.cli.responses.datastudios.DataStudiosList; import io.seqera.tower.cli.responses.datastudios.DataStudiosView; import io.seqera.tower.cli.utils.PaginationInfo; import io.seqera.tower.model.DataStudioDto; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -32,14 +34,21 @@ import org.mockserver.client.MockServerClient; import org.mockserver.model.MediaType; +import org.mockserver.verify.VerificationTimes; import static io.seqera.tower.cli.utils.JsonHelper.parseJson; import static org.mockserver.matchers.Times.exactly; import static org.mockserver.model.HttpRequest.request; import static org.mockserver.model.HttpResponse.response; +import static org.mockserver.model.JsonBody.json; public class DataStudiosCmdTest extends BaseCmdTest { + @BeforeEach + void init(MockServerClient mock) { + mock.reset(); + } + @ParameterizedTest @EnumSource(OutputType.class) void testView(OutputType format, MockServerClient mock) throws JsonProcessingException { @@ -63,57 +72,70 @@ void testView(OutputType format, MockServerClient mock) throws JsonProcessingExc ExecOut out = exec(format, mock, "studios", "view", "-w", "75887156211589", "-i" ,"3e8370e7"); - assertOutput(format, out, new DataStudiosView(parseJson("{\n" + - " \"sessionId\": \"3e8370e7\",\n" + - " \"workspaceId\": 75887156211589,\n" + - " \"user\": {\n" + - " \"id\": 2345,\n" + - " \"userName\": \"John Doe\",\n" + - " \"email\": \"john@seqera.io\",\n" + - " \"avatar\": null\n" + - " },\n" + - " \"name\": \"studio-a66d\",\n" + - " \"description\": \"my first studio\",\n" + - " \"studioUrl\": \"https://a3e8370e7.dev-tower.com\",\n" + - " \"computeEnv\": {\n" + - " \"id\": \"3xkkzYH2nbD3nZjrzKm0oR\",\n" + - " \"name\": \"ce1\",\n" + - " \"platform\": \"aws-batch\",\n" + - " \"region\": \"us-east-2\"\n" + - " },\n" + - " \"template\": {\n" + - " \"repository\": \"cr.seqera.io/public/data-studio-vscode:1.93.1-snapshot\",\n" + - " \"icon\": \"vscode\"\n" + - " },\n" + - " \"configuration\": {\n" + - " \"gpu\": 0,\n" + - " \"cpu\": 2,\n" + - " \"memory\": 8192,\n" + - " \"mountData\": [\n" + - " \"v1-user-1ccf131810375d303bf0402dd8423433\"\n" + - " ],\n" + - " \"condaEnvironment\":null\n" + - " },\n" + - " \"dateCreated\": \"2024-12-19T06:49:24.893122+01:00\",\n" + - " \"lastUpdated\": \"2024-12-19T06:52:50.686822+01:00\",\n" + - " \"statusInfo\": {\n" + - " \"status\": \"running\",\n" + - " \"message\": \"\",\n" + - " \"lastUpdate\": \"2024-12-19T05:52:41.823Z\"\n" + - " },\n" + - " \"waveBuildUrl\": null,\n" + - " \"baseImage\": \"cr.seqera.io/public/data-studio-jupyter:4.2.5-snapshot\",\n" + - " \"mountedDataLinks\": [\n" + - " {\n" + - " \"id\": \"v1-user-1ccf131810375d303bf0402dd8423433\",\n" + - " \"name\": \"aaa-my-bucket\",\n" + - " \"resourceRef\": \"s3://aaa-my-bucket\",\n" + - " \"type\": \"bucket\",\n" + - " \"provider\": \"aws\",\n" + - " \"region\": \"us-east-2\"\n" + - " }\n" + - " ]\n" + - "}", DataStudioDto.class), "[organization1 / workspace1]" )); + assertOutput(format, out, new DataStudiosView(parseJson(""" + { + "sessionId": "3e8370e7", + "workspaceId": 75887156211589, + "user": { + "id": 2345, + "userName": "John Doe", + "email": "john@seqera.io", + "avatar": null + }, + "name": "studio-a66d", + "description": "my first studio", + "studioUrl": "https://a3e8370e7.dev-tower.com", + "computeEnv": { + "id": "3xkkzYH2nbD3nZjrzKm0oR", + "name": "ce1", + "platform": "aws-batch", + "region": "us-east-2" + }, + "template": { + "repository": "cr.seqera.io/public/data-studio-vscode:1.93.1-snapshot", + "icon": "vscode" + }, + "configuration": { + "gpu": 0, + "cpu": 2, + "memory": 8192, + "mountData": [ + "v1-user-1ccf131810375d303bf0402dd8423433" + ], + "condaEnvironment":null + }, + "dateCreated": "2024-12-19T06:49:24.893122+01:00", + "lastUpdated": "2024-12-19T06:52:50.686822+01:00", + "statusInfo": { + "status": "running", + "message": "", + "lastUpdate": "2024-12-19T05:52:41.823Z" + }, + "waveBuildUrl": null, + "baseImage": "cr.seqera.io/public/data-studio-jupyter:4.2.5-snapshot", + "mountedDataLinks": [ + { + "id": "v1-user-1ccf131810375d303bf0402dd8423433", + "name": "aaa-my-bucket", + "resourceRef": "s3://aaa-my-bucket", + "type": "bucket", + "provider": "aws", + "region": "us-east-2" + } + ], + "progress": [ + { + "status": "succeeded", + "message": "Provisioning compute resources", + "warnings": null + }, + { + "status": "succeeded", + "message": "Mounting checkpoints", + "warnings": null + } + ] + }""", DataStudioDto.class), "[organization1 / workspace1]" )); } @ParameterizedTest @@ -139,92 +161,95 @@ void testList(OutputType format, MockServerClient mock) throws JsonProcessingExc ExecOut out = exec(format, mock, "studios", "list", "-w", "75887156211589"); - assertOutput(format, out, new DataStudiosList("[organization1 / workspace1]", Arrays.asList(parseJson(" {\n" + - " \"sessionId\": \"ddfd5e14\",\n" + - " \"workspaceId\": 75887156211589,\n" + - " \"parentCheckpoint\": null,\n" + - " \"user\": {\n" + - " \"id\": 1,\n" + - " \"userName\": \"samurai-jack\",\n" + - " \"email\": \"jack@seqera.io\",\n" + - " \"avatar\": null\n" + - " },\n" + - " \"name\": \"studio-7728\",\n" + - " \"description\": \"Local studio\",\n" + - " \"studioUrl\": \"http://addfd5e14.studio.localhost:9191\",\n" + - " \"computeEnv\": {\n" + - " \"id\": \"16esMgELkyQ3QPcHGNTiXQ\",\n" + - " \"name\": \"my-other-local-ce\",\n" + - " \"platform\": \"local-platform\",\n" + - " \"region\": null\n" + - " },\n" + - " \"template\": {\n" + - " \"repository\": \"cr.seqera.io/public/data-studio-jupyter:4.2.5-snapshot\",\n" + - " \"icon\": \"jupyter\"\n" + - " },\n" + - " \"configuration\": {\n" + - " \"gpu\": 0,\n" + - " \"cpu\": 2,\n" + - " \"memory\": 8192,\n" + - " \"mountData\": [],\n" + - " \"condaEnvironment\": null\n" + - " },\n" + - " \"dateCreated\": \"2025-01-14T11:51:05.393498Z\",\n" + - " \"lastUpdated\": \"2025-01-15T09:10:30.016752Z\",\n" + - " \"activeConnections\": [],\n" + - " \"statusInfo\": {\n" + - " \"status\": \"running\",\n" + - " \"message\": \"\",\n" + - " \"lastUpdate\": \"2025-01-15T09:10:30.016588Z\"\n" + - " },\n" + - " \"waveBuildUrl\": null,\n" + - " \"baseImage\": null,\n" + - " \"customImage\": false,\n" + - " \"progress\": null\n" + - " }", DataStudioDto.class), - parseJson("{\n" + - " \"sessionId\": \"c779bf09\",\n" + - " \"workspaceId\": 75887156211589,\n" + - " \"parentCheckpoint\": null,\n" + - " \"user\": {\n" + - " \"id\": 1,\n" + - " \"userName\": \"johnny-bravo\",\n" + - " \"email\": \"johnny@seqera.io\",\n" + - " \"avatar\": null\n" + - " },\n" + - " \"name\": \"studio-d456\",\n" + - " \"description\": null,\n" + - " \"studioUrl\": \"http://ac779bf09.studio.localhost:9191\",\n" + - " \"computeEnv\": {\n" + - " \"id\": \"61DYXYj3XQAYbJIHrI1XSg\",\n" + - " \"name\": \"my-local-ce\",\n" + - " \"platform\": \"local-platform\",\n" + - " \"region\": null\n" + - " },\n" + - " \"template\": {\n" + - " \"repository\": \"cr.seqera.io/public/data-studio-vscode:1.93.1-snapshot\",\n" + - " \"icon\": \"vscode\"\n" + - " },\n" + - " \"configuration\": {\n" + - " \"gpu\": 0,\n" + - " \"cpu\": 2,\n" + - " \"memory\": 8192,\n" + - " \"mountData\": [],\n" + - " \"condaEnvironment\": null\n" + - " },\n" + - " \"dateCreated\": \"2025-01-10T17:26:36.83703Z\",\n" + - " \"lastUpdated\": \"2025-01-12T03:00:30.014415Z\",\n" + - " \"activeConnections\": [],\n" + - " \"statusInfo\": {\n" + - " \"status\": \"errored\",\n" + - " \"message\": \"\",\n" + - " \"lastUpdate\": \"2025-01-12T03:00:30.010738Z\"\n" + - " },\n" + - " \"waveBuildUrl\": null,\n" + - " \"baseImage\": null,\n" + - " \"customImage\": false,\n" + - " \"progress\": null\n" + - " }", DataStudioDto.class) + assertOutput(format, out, new DataStudiosList("[organization1 / workspace1]", Arrays.asList(parseJson(""" + { + "sessionId": "ddfd5e14", + "workspaceId": 75887156211589, + "parentCheckpoint": null, + "user": { + "id": 1, + "userName": "samurai-jack", + "email": "jack@seqera.io", + "avatar": null + }, + "name": "studio-7728", + "description": "Local studio", + "studioUrl": "http://addfd5e14.studio.localhost:9191", + "computeEnv": { + "id": "16esMgELkyQ3QPcHGNTiXQ", + "name": "my-other-local-ce", + "platform": "local-platform", + "region": null + }, + "template": { + "repository": "cr.seqera.io/public/data-studio-jupyter:4.2.5-snapshot", + "icon": "jupyter" + }, + "configuration": { + "gpu": 0, + "cpu": 2, + "memory": 8192, + "mountData": [], + "condaEnvironment": null + }, + "dateCreated": "2025-01-14T11:51:05.393498Z", + "lastUpdated": "2025-01-15T09:10:30.016752Z", + "activeConnections": [], + "statusInfo": { + "status": "running", + "message": "", + "lastUpdate": "2025-01-15T09:10:30.016588Z" + }, + "waveBuildUrl": null, + "baseImage": null, + "customImage": false, + "progress": null + }\ + """, DataStudioDto.class), + parseJson(""" + { + "sessionId": "c779bf09", + "workspaceId": 75887156211589, + "parentCheckpoint": null, + "user": { + "id": 1, + "userName": "johnny-bravo", + "email": "johnny@seqera.io", + "avatar": null + }, + "name": "studio-d456", + "description": null, + "studioUrl": "http://ac779bf09.studio.localhost:9191", + "computeEnv": { + "id": "61DYXYj3XQAYbJIHrI1XSg", + "name": "my-local-ce", + "platform": "local-platform", + "region": null + }, + "template": { + "repository": "cr.seqera.io/public/data-studio-vscode:1.93.1-snapshot", + "icon": "vscode" + }, + "configuration": { + "gpu": 0, + "cpu": 2, + "memory": 8192, + "mountData": [], + "condaEnvironment": null + }, + "dateCreated": "2025-01-10T17:26:36.83703Z", + "lastUpdated": "2025-01-12T03:00:30.014415Z", + "activeConnections": [], + "statusInfo": { + "status": "errored", + "message": "", + "lastUpdate": "2025-01-12T03:00:30.010738Z" + }, + "waveBuildUrl": null, + "baseImage": null, + "customImage": false, + "progress": null + }""", DataStudioDto.class) ), null)); } @@ -254,92 +279,95 @@ void testListWithOffset(OutputType format, MockServerClient mock) throws JsonPro ExecOut out = exec(format, mock, "studios", "list", "-w", "75887156211589", "--offset", "1", "--max", "2"); - assertOutput(format, out, new DataStudiosList("[organization1 / workspace1]", Arrays.asList(parseJson(" {\n" + - " \"sessionId\": \"ddfd5e14\",\n" + - " \"workspaceId\": 75887156211589,\n" + - " \"parentCheckpoint\": null,\n" + - " \"user\": {\n" + - " \"id\": 1,\n" + - " \"userName\": \"samurai-jack\",\n" + - " \"email\": \"jack@seqera.io\",\n" + - " \"avatar\": null\n" + - " },\n" + - " \"name\": \"studio-7728\",\n" + - " \"description\": \"Local studio\",\n" + - " \"studioUrl\": \"http://addfd5e14.studio.localhost:9191\",\n" + - " \"computeEnv\": {\n" + - " \"id\": \"16esMgELkyQ3QPcHGNTiXQ\",\n" + - " \"name\": \"my-other-local-ce\",\n" + - " \"platform\": \"local-platform\",\n" + - " \"region\": null\n" + - " },\n" + - " \"template\": {\n" + - " \"repository\": \"cr.seqera.io/public/data-studio-jupyter:4.2.5-snapshot\",\n" + - " \"icon\": \"jupyter\"\n" + - " },\n" + - " \"configuration\": {\n" + - " \"gpu\": 0,\n" + - " \"cpu\": 2,\n" + - " \"memory\": 8192,\n" + - " \"mountData\": [],\n" + - " \"condaEnvironment\": null\n" + - " },\n" + - " \"dateCreated\": \"2025-01-14T11:51:05.393498Z\",\n" + - " \"lastUpdated\": \"2025-01-15T09:10:30.016752Z\",\n" + - " \"activeConnections\": [],\n" + - " \"statusInfo\": {\n" + - " \"status\": \"running\",\n" + - " \"message\": \"\",\n" + - " \"lastUpdate\": \"2025-01-15T09:10:30.016588Z\"\n" + - " },\n" + - " \"waveBuildUrl\": null,\n" + - " \"baseImage\": null,\n" + - " \"customImage\": false,\n" + - " \"progress\": null\n" + - " }", DataStudioDto.class), - parseJson("{\n" + - " \"sessionId\": \"c779bf09\",\n" + - " \"workspaceId\": 75887156211589,\n" + - " \"parentCheckpoint\": null,\n" + - " \"user\": {\n" + - " \"id\": 1,\n" + - " \"userName\": \"johnny-bravo\",\n" + - " \"email\": \"johnny@seqera.io\",\n" + - " \"avatar\": null\n" + - " },\n" + - " \"name\": \"studio-d456\",\n" + - " \"description\": null,\n" + - " \"studioUrl\": \"http://ac779bf09.studio.localhost:9191\",\n" + - " \"computeEnv\": {\n" + - " \"id\": \"61DYXYj3XQAYbJIHrI1XSg\",\n" + - " \"name\": \"my-local-ce\",\n" + - " \"platform\": \"local-platform\",\n" + - " \"region\": null\n" + - " },\n" + - " \"template\": {\n" + - " \"repository\": \"cr.seqera.io/public/data-studio-vscode:1.93.1-snapshot\",\n" + - " \"icon\": \"vscode\"\n" + - " },\n" + - " \"configuration\": {\n" + - " \"gpu\": 0,\n" + - " \"cpu\": 2,\n" + - " \"memory\": 8192,\n" + - " \"mountData\": [],\n" + - " \"condaEnvironment\": null\n" + - " },\n" + - " \"dateCreated\": \"2025-01-10T17:26:36.83703Z\",\n" + - " \"lastUpdated\": \"2025-01-12T03:00:30.014415Z\",\n" + - " \"activeConnections\": [],\n" + - " \"statusInfo\": {\n" + - " \"status\": \"errored\",\n" + - " \"message\": \"\",\n" + - " \"lastUpdate\": \"2025-01-12T03:00:30.010738Z\"\n" + - " },\n" + - " \"waveBuildUrl\": null,\n" + - " \"baseImage\": null,\n" + - " \"customImage\": false,\n" + - " \"progress\": null\n" + - " }", DataStudioDto.class) + assertOutput(format, out, new DataStudiosList("[organization1 / workspace1]", Arrays.asList(parseJson(""" + { + "sessionId": "ddfd5e14", + "workspaceId": 75887156211589, + "parentCheckpoint": null, + "user": { + "id": 1, + "userName": "samurai-jack", + "email": "jack@seqera.io", + "avatar": null + }, + "name": "studio-7728", + "description": "Local studio", + "studioUrl": "http://addfd5e14.studio.localhost:9191", + "computeEnv": { + "id": "16esMgELkyQ3QPcHGNTiXQ", + "name": "my-other-local-ce", + "platform": "local-platform", + "region": null + }, + "template": { + "repository": "cr.seqera.io/public/data-studio-jupyter:4.2.5-snapshot", + "icon": "jupyter" + }, + "configuration": { + "gpu": 0, + "cpu": 2, + "memory": 8192, + "mountData": [], + "condaEnvironment": null + }, + "dateCreated": "2025-01-14T11:51:05.393498Z", + "lastUpdated": "2025-01-15T09:10:30.016752Z", + "activeConnections": [], + "statusInfo": { + "status": "running", + "message": "", + "lastUpdate": "2025-01-15T09:10:30.016588Z" + }, + "waveBuildUrl": null, + "baseImage": null, + "customImage": false, + "progress": null + }\ + """, DataStudioDto.class), + parseJson(""" + { + "sessionId": "c779bf09", + "workspaceId": 75887156211589, + "parentCheckpoint": null, + "user": { + "id": 1, + "userName": "johnny-bravo", + "email": "johnny@seqera.io", + "avatar": null + }, + "name": "studio-d456", + "description": null, + "studioUrl": "http://ac779bf09.studio.localhost:9191", + "computeEnv": { + "id": "61DYXYj3XQAYbJIHrI1XSg", + "name": "my-local-ce", + "platform": "local-platform", + "region": null + }, + "template": { + "repository": "cr.seqera.io/public/data-studio-vscode:1.93.1-snapshot", + "icon": "vscode" + }, + "configuration": { + "gpu": 0, + "cpu": 2, + "memory": 8192, + "mountData": [], + "condaEnvironment": null + }, + "dateCreated": "2025-01-10T17:26:36.83703Z", + "lastUpdated": "2025-01-12T03:00:30.014415Z", + "activeConnections": [], + "statusInfo": { + "status": "errored", + "message": "", + "lastUpdate": "2025-01-12T03:00:30.010738Z" + }, + "waveBuildUrl": null, + "baseImage": null, + "customImage": false, + "progress": null + }""", DataStudioDto.class) ), PaginationInfo.from(1, 2, null, 2L))); } @@ -370,49 +398,51 @@ void testListWithPage(OutputType format, MockServerClient mock) throws JsonProce ExecOut out = exec(format, mock, "studios", "list", "-w", "75887156211589", "--page", "1", "--max", "1"); - assertOutput(format, out, new DataStudiosList("[organization1 / workspace1]", Arrays.asList(parseJson(" {\n" + - " \"sessionId\": \"ddfd5e14\",\n" + - " \"workspaceId\": 75887156211589,\n" + - " \"parentCheckpoint\": null,\n" + - " \"user\": {\n" + - " \"id\": 1,\n" + - " \"userName\": \"samurai-jack\",\n" + - " \"email\": \"jack@seqera.io\",\n" + - " \"avatar\": null\n" + - " },\n" + - " \"name\": \"studio-7728\",\n" + - " \"description\": \"Local studio\",\n" + - " \"studioUrl\": \"http://addfd5e14.studio.localhost:9191\",\n" + - " \"computeEnv\": {\n" + - " \"id\": \"16esMgELkyQ3QPcHGNTiXQ\",\n" + - " \"name\": \"my-other-local-ce\",\n" + - " \"platform\": \"local-platform\",\n" + - " \"region\": null\n" + - " },\n" + - " \"template\": {\n" + - " \"repository\": \"cr.seqera.io/public/data-studio-jupyter:4.2.5-snapshot\",\n" + - " \"icon\": \"jupyter\"\n" + - " },\n" + - " \"configuration\": {\n" + - " \"gpu\": 0,\n" + - " \"cpu\": 2,\n" + - " \"memory\": 8192,\n" + - " \"mountData\": [],\n" + - " \"condaEnvironment\": null\n" + - " },\n" + - " \"dateCreated\": \"2025-01-14T11:51:05.393498Z\",\n" + - " \"lastUpdated\": \"2025-01-15T09:10:30.016752Z\",\n" + - " \"activeConnections\": [],\n" + - " \"statusInfo\": {\n" + - " \"status\": \"running\",\n" + - " \"message\": \"\",\n" + - " \"lastUpdate\": \"2025-01-15T09:10:30.016588Z\"\n" + - " },\n" + - " \"waveBuildUrl\": null,\n" + - " \"baseImage\": null,\n" + - " \"customImage\": false,\n" + - " \"progress\": null\n" + - " }", DataStudioDto.class) + assertOutput(format, out, new DataStudiosList("[organization1 / workspace1]", Collections.singletonList(parseJson(""" + { + "sessionId": "ddfd5e14", + "workspaceId": 75887156211589, + "parentCheckpoint": null, + "user": { + "id": 1, + "userName": "samurai-jack", + "email": "jack@seqera.io", + "avatar": null + }, + "name": "studio-7728", + "description": "Local studio", + "studioUrl": "http://addfd5e14.studio.localhost:9191", + "computeEnv": { + "id": "16esMgELkyQ3QPcHGNTiXQ", + "name": "my-other-local-ce", + "platform": "local-platform", + "region": null + }, + "template": { + "repository": "cr.seqera.io/public/data-studio-jupyter:4.2.5-snapshot", + "icon": "jupyter" + }, + "configuration": { + "gpu": 0, + "cpu": 2, + "memory": 8192, + "mountData": [], + "condaEnvironment": null + }, + "dateCreated": "2025-01-14T11:51:05.393498Z", + "lastUpdated": "2025-01-15T09:10:30.016752Z", + "activeConnections": [], + "statusInfo": { + "status": "running", + "message": "", + "lastUpdate": "2025-01-15T09:10:30.016588Z" + }, + "waveBuildUrl": null, + "baseImage": null, + "customImage": false, + "progress": null + }\ + """, DataStudioDto.class) ), PaginationInfo.from(null, 1, 1, 2L))); } @@ -441,49 +471,214 @@ void testListWithFilter(OutputType format, MockServerClient mock) throws JsonPro ExecOut out = exec(format, mock, "studios", "list", "-w", "75887156211589", "--filter", "status:running"); - assertOutput(format, out, new DataStudiosList("[organization1 / workspace1]", Collections.singletonList(parseJson(" {\n" + - " \"sessionId\": \"ddfd5e14\",\n" + - " \"workspaceId\": 75887156211589,\n" + - " \"parentCheckpoint\": null,\n" + - " \"user\": {\n" + - " \"id\": 1,\n" + - " \"userName\": \"samurai-jack\",\n" + - " \"email\": \"jack@seqera.io\",\n" + - " \"avatar\": null\n" + - " },\n" + - " \"name\": \"studio-7728\",\n" + - " \"description\": \"Local studio\",\n" + - " \"studioUrl\": \"http://addfd5e14.studio.localhost:9191\",\n" + - " \"computeEnv\": {\n" + - " \"id\": \"16esMgELkyQ3QPcHGNTiXQ\",\n" + - " \"name\": \"my-other-local-ce\",\n" + - " \"platform\": \"local-platform\",\n" + - " \"region\": null\n" + - " },\n" + - " \"template\": {\n" + - " \"repository\": \"cr.seqera.io/public/data-studio-jupyter:4.2.5-snapshot\",\n" + - " \"icon\": \"jupyter\"\n" + - " },\n" + - " \"configuration\": {\n" + - " \"gpu\": 0,\n" + - " \"cpu\": 2,\n" + - " \"memory\": 8192,\n" + - " \"mountData\": [],\n" + - " \"condaEnvironment\": null\n" + - " },\n" + - " \"dateCreated\": \"2025-01-14T11:51:05.393498Z\",\n" + - " \"lastUpdated\": \"2025-01-15T09:10:30.016752Z\",\n" + - " \"activeConnections\": [],\n" + - " \"statusInfo\": {\n" + - " \"status\": \"running\",\n" + - " \"message\": \"\",\n" + - " \"lastUpdate\": \"2025-01-15T09:10:30.016588Z\"\n" + - " },\n" + - " \"waveBuildUrl\": null,\n" + - " \"baseImage\": null,\n" + - " \"customImage\": false,\n" + - " \"progress\": null\n" + - " }", DataStudioDto.class) + assertOutput(format, out, new DataStudiosList("[organization1 / workspace1]", Collections.singletonList(parseJson(""" + { + "sessionId": "ddfd5e14", + "workspaceId": 75887156211589, + "parentCheckpoint": null, + "user": { + "id": 1, + "userName": "samurai-jack", + "email": "jack@seqera.io", + "avatar": null + }, + "name": "studio-7728", + "description": "Local studio", + "studioUrl": "http://addfd5e14.studio.localhost:9191", + "computeEnv": { + "id": "16esMgELkyQ3QPcHGNTiXQ", + "name": "my-other-local-ce", + "platform": "local-platform", + "region": null + }, + "template": { + "repository": "cr.seqera.io/public/data-studio-jupyter:4.2.5-snapshot", + "icon": "jupyter" + }, + "configuration": { + "gpu": 0, + "cpu": 2, + "memory": 8192, + "mountData": [], + "condaEnvironment": null + }, + "dateCreated": "2025-01-14T11:51:05.393498Z", + "lastUpdated": "2025-01-15T09:10:30.016752Z", + "activeConnections": [], + "statusInfo": { + "status": "running", + "message": "", + "lastUpdate": "2025-01-15T09:10:30.016588Z" + }, + "waveBuildUrl": null, + "baseImage": null, + "customImage": false, + "progress": null + }\ + """, DataStudioDto.class) ), null)); } + + @ParameterizedTest + @EnumSource(OutputType.class) + void testStart(OutputType format, MockServerClient mock) { + + mock.when( + request().withMethod("GET").withPath("/user-info"), exactly(1) + ).respond( + response().withStatusCode(200).withBody(loadResource("user")).withContentType(MediaType.APPLICATION_JSON) + ); + + mock.when( + request().withMethod("GET").withPath("/user/1264/workspaces"), exactly(1) + ).respond( + response().withStatusCode(200).withBody(loadResource("workspaces/workspaces_list")).withContentType(MediaType.APPLICATION_JSON) + ); + + mock.when( + request().withMethod("GET").withPath("/studios/3e8370e7").withQueryStringParameter("workspaceId", "75887156211589"), exactly(1) + ).respond( + response().withStatusCode(200).withBody(loadResource("datastudios/datastudios_view_response_studio_stopped")).withContentType(MediaType.APPLICATION_JSON) + ); + + mock.when( + request().withMethod("PUT").withPath("/studios/3e8370e7/start").withQueryStringParameter("workspaceId", "75887156211589").withBody(json(""" + { + "configuration": { + "gpu": 0, + "cpu": 2, + "memory": 8192, + "mountData": [ + "v1-user-1ccf131810375d303bf0402dd8423433" + ] + }, + "description": "my first studio" + } + """ + ) + + ), exactly(1) + ).respond( + response().withStatusCode(200).withBody(loadResource("datastudios/datastudios_start_response")).withContentType(MediaType.APPLICATION_JSON) + ); + + + ExecOut out = exec(format, mock, "studios", "start", "-w", "75887156211589", "-i" ,"3e8370e7"); + + assertOutput(format, out, new DataStudioStartSubmitted("3e8370e7", 75887156211589L, + "[organization1 / workspace1]", "https://a3e8370e7.dev-tower.com", true)); + } + + @ParameterizedTest + @EnumSource(OutputType.class) + void testStartWithConfigOverride(OutputType format, MockServerClient mock) { + mock.when( + request().withMethod("GET").withPath("/user-info"), exactly(1) + ).respond( + response().withStatusCode(200).withBody(loadResource("user")).withContentType(MediaType.APPLICATION_JSON) + ); + + mock.when( + request().withMethod("GET").withPath("/user/1264/workspaces"), exactly(1) + ).respond( + response().withStatusCode(200).withBody(loadResource("workspaces/workspaces_list")).withContentType(MediaType.APPLICATION_JSON) + ); + + mock.when( + request().withMethod("GET").withPath("/studios/3e8370e7").withQueryStringParameter("workspaceId", "75887156211589"), exactly(1) + ).respond( + response().withStatusCode(200).withBody(loadResource("datastudios/datastudios_view_response_studio_stopped")).withContentType(MediaType.APPLICATION_JSON) + ); + + mock.when( + request().withMethod("PUT").withPath("/studios/3e8370e7/start").withQueryStringParameter("workspaceId", "75887156211589").withBody(json(""" + { + "configuration": { + "gpu": 0, + "cpu": 4, + "memory": 8192, + "mountData": [ + "v1-user-1ccf131810375d303bf0402dd8423433" + ] + }, + "description": "Override description" + } + """ + ) + + ), exactly(1) + ).respond( + response().withStatusCode(200).withBody(loadResource("datastudios/datastudios_start_response")).withContentType(MediaType.APPLICATION_JSON) + ); + + + ExecOut out = exec(format, mock, "studios", "start", "-w", "75887156211589", "-i" ,"3e8370e7", "-c", "4", "--description", "Override description"); + + assertOutput(format, out, new DataStudioStartSubmitted("3e8370e7", 75887156211589L, + "[organization1 / workspace1]", "https://a3e8370e7.dev-tower.com", true)); + } + + // Only run this test in json output format, since extra stdout output is printed out to console with --wait flag + @ParameterizedTest + @EnumSource(value = OutputType.class, names = {"json"}) + void testStartWithWait(OutputType format, MockServerClient mock) { + mock.when( + request().withMethod("GET").withPath("/user-info"), exactly(1) + ).respond( + response().withStatusCode(200).withBody(loadResource("user")).withContentType(MediaType.APPLICATION_JSON) + ); + + mock.when( + request().withMethod("GET").withPath("/user/1264/workspaces"), exactly(1) + ).respond( + response().withStatusCode(200).withBody(loadResource("workspaces/workspaces_list")).withContentType(MediaType.APPLICATION_JSON) + ); + + mock.when( + request().withMethod("GET").withPath("/studios/3e8370e7").withQueryStringParameter("workspaceId", "75887156211589"), exactly(1) + ).respond( + response().withStatusCode(200).withBody(loadResource("datastudios/datastudios_view_response_studio_stopped")).withContentType(MediaType.APPLICATION_JSON) + ); + + mock.when( + request().withMethod("PUT").withPath("/studios/3e8370e7/start").withQueryStringParameter("workspaceId", "75887156211589").withBody(json(""" + { + "configuration": { + "gpu": 0, + "cpu": 2, + "memory": 8192, + "mountData": [ + "v1-user-1ccf131810375d303bf0402dd8423433" + ] + }, + "description": "my first studio" + } + """ + ) + + ), exactly(1) + ).respond( + response().withStatusCode(200).withBody(loadResource("datastudios/datastudios_start_response")).withContentType(MediaType.APPLICATION_JSON) + ); + + mock.when( + request().withMethod("GET").withPath("/studios/3e8370e7").withQueryStringParameter("workspaceId", "75887156211589"), exactly(2) + ).respond( + response().withStatusCode(200).withBody(loadResource("datastudios/datastudios_view_response_studio_starting")).withContentType(MediaType.APPLICATION_JSON) + ); + + mock.when( + request().withMethod("GET").withPath("/studios/3e8370e7").withQueryStringParameter("workspaceId", "75887156211589"), exactly(1) + ).respond( + response().withStatusCode(200).withBody(loadResource("datastudios/datastudios_view_response")).withContentType(MediaType.APPLICATION_JSON) + ); + + ExecOut out = exec(format, mock, "studios", "start", "-w", "75887156211589", "-i" ,"3e8370e7", "--wait", "RUNNING"); + + assertOutput(format, out, new DataStudioStartSubmitted("3e8370e7", 75887156211589L, + "[organization1 / workspace1]", "https://a3e8370e7.dev-tower.com", true)); + + // verify the API has been polled additionally for the status + mock.verify(request().withMethod("GET").withPath("/studios/3e8370e7"), VerificationTimes.exactly(4)); + } } diff --git a/src/test/resources/runcmd/datastudios/datastudios_start_response.json b/src/test/resources/runcmd/datastudios/datastudios_start_response.json new file mode 100644 index 00000000..d41c7d86 --- /dev/null +++ b/src/test/resources/runcmd/datastudios/datastudios_start_response.json @@ -0,0 +1,9 @@ +{ + "jobSubmitted": true, + "sessionId": "3e8370e7", + "statusInfo": { + "status": "starting", + "message": "", + "lastUpdate": "2025-01-22T15:16:11.508692Z" + } +} \ No newline at end of file diff --git a/src/test/resources/runcmd/datastudios/datastudios_view_response.json b/src/test/resources/runcmd/datastudios/datastudios_view_response.json index 419023eb..177de513 100644 --- a/src/test/resources/runcmd/datastudios/datastudios_view_response.json +++ b/src/test/resources/runcmd/datastudios/datastudios_view_response.json @@ -47,5 +47,17 @@ "provider": "aws", "region": "us-east-2" } + ], + "progress": [ + { + "status": "succeeded", + "message": "Provisioning compute resources", + "warnings": null + }, + { + "status": "succeeded", + "message": "Mounting checkpoints", + "warnings": null + } ] } \ No newline at end of file diff --git a/src/test/resources/runcmd/datastudios/datastudios_view_response_studio_starting.json b/src/test/resources/runcmd/datastudios/datastudios_view_response_studio_starting.json new file mode 100644 index 00000000..78d5fe9f --- /dev/null +++ b/src/test/resources/runcmd/datastudios/datastudios_view_response_studio_starting.json @@ -0,0 +1,63 @@ +{ + "sessionId": "3e8370e7", + "workspaceId": 75887156211589, + "user": { + "id": 2345, + "userName": "John Doe", + "email": "john@seqera.io", + "avatar": null + }, + "name": "studio-a66d", + "description": "my first studio", + "studioUrl": "https://a3e8370e7.dev-tower.com", + "computeEnv": { + "id": "3xkkzYH2nbD3nZjrzKm0oR", + "name": "ce1", + "platform": "aws-batch", + "region": "us-east-2" + }, + "template": { + "repository": "cr.seqera.io/public/data-studio-vscode:1.93.1-snapshot", + "icon": "vscode" + }, + "configuration": { + "gpu": 0, + "cpu": 2, + "memory": 8192, + "mountData": [ + "v1-user-1ccf131810375d303bf0402dd8423433" + ], + "condaEnvironment":null + }, + "dateCreated": "2024-12-19T06:49:24.893122+01:00", + "lastUpdated": "2024-12-19T06:52:50.686822+01:00", + "statusInfo": { + "status": "starting", + "message": "", + "lastUpdate": "2024-12-19T05:52:41.823Z" + }, + "waveBuildUrl": null, + "baseImage": "cr.seqera.io/public/data-studio-jupyter:4.2.5-snapshot", + "mountedDataLinks": [ + { + "id": "v1-user-1ccf131810375d303bf0402dd8423433", + "name": "aaa-my-bucket", + "resourceRef": "s3://aaa-my-bucket", + "type": "bucket", + "provider": "aws", + "region": "us-east-2" + } + ], + "progress": [ + { + "status": "in-progress", + "message": "Provisioning compute resources", + "warnings": null + }, + { + "status": "pending", + "message": "Mounting checkpoints", + "warnings": null + } + ] +} \ No newline at end of file diff --git a/src/test/resources/runcmd/datastudios/datastudios_view_response_studio_stopped.json b/src/test/resources/runcmd/datastudios/datastudios_view_response_studio_stopped.json new file mode 100644 index 00000000..59700fc2 --- /dev/null +++ b/src/test/resources/runcmd/datastudios/datastudios_view_response_studio_stopped.json @@ -0,0 +1,51 @@ +{ + "sessionId": "3e8370e7", + "workspaceId": 75887156211589, + "user": { + "id": 2345, + "userName": "John Doe", + "email": "john@seqera.io", + "avatar": null + }, + "name": "studio-a66d", + "description": "my first studio", + "studioUrl": "https://a3e8370e7.dev-tower.com", + "computeEnv": { + "id": "3xkkzYH2nbD3nZjrzKm0oR", + "name": "ce1", + "platform": "aws-batch", + "region": "us-east-2" + }, + "template": { + "repository": "cr.seqera.io/public/data-studio-vscode:1.93.1-snapshot", + "icon": "vscode" + }, + "configuration": { + "gpu": 0, + "cpu": 2, + "memory": 8192, + "mountData": [ + "v1-user-1ccf131810375d303bf0402dd8423433" + ], + "condaEnvironment":null + }, + "dateCreated": "2024-12-19T06:49:24.893122+01:00", + "lastUpdated": "2024-12-19T06:52:50.686822+01:00", + "statusInfo": { + "status": "stopped", + "message": "", + "lastUpdate": "2024-12-19T05:52:41.823Z" + }, + "waveBuildUrl": null, + "baseImage": "cr.seqera.io/public/data-studio-jupyter:4.2.5-snapshot", + "mountedDataLinks": [ + { + "id": "v1-user-1ccf131810375d303bf0402dd8423433", + "name": "aaa-my-bucket", + "resourceRef": "s3://aaa-my-bucket", + "type": "bucket", + "provider": "aws", + "region": "us-east-2" + } + ] +} \ No newline at end of file