Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import io.seqera.tower.cli.commands.AbstractApiCmd;
import io.seqera.tower.cli.commands.data.links.DataLinkService;
import io.seqera.tower.cli.commands.labels.Label;
import io.seqera.tower.cli.exceptions.MemberNotFoundException;
import io.seqera.tower.cli.exceptions.StudioNotFoundException;
import io.seqera.tower.model.DataStudioConfiguration;
import io.seqera.tower.cli.commands.enums.OutputType;
Expand All @@ -35,6 +36,8 @@
import io.seqera.tower.model.DataStudioStatusInfo;
import io.seqera.tower.model.DataStudioTemplate;
import io.seqera.tower.model.DataStudioTemplatesListResponse;
import io.seqera.tower.model.ListMembersResponse;
import io.seqera.tower.model.MemberDbDto;

import static io.seqera.tower.cli.utils.ResponseHelper.waitStatus;
import static io.seqera.tower.model.DataStudioProgressStepStatus.ERRORED;
Expand All @@ -46,19 +49,16 @@ public class AbstractStudiosCmd extends AbstractApiCmd {
protected String getSessionId(StudioRefOptions studioRefOptions, Long wspId) throws ApiException {
return studioRefOptions.studio.sessionId != null
? studioRefOptions.studio.sessionId
: fetchStudio(studioRefOptions, wspId).getSessionId();
: getStudioByName(wspId, studioRefOptions.studio.studioName).getSessionId();
}

protected DataStudioDto fetchStudio(StudioRefOptions studioRefOptions, Long wspId) throws ApiException {
DataStudioDto studio;

if (studioRefOptions.studio.sessionId != null) {
studio = getStudioById(wspId, studioRefOptions.studio.sessionId);
} else {
studio = getStudioByName(wspId, studioRefOptions.studio.studioName);
return getStudioById(wspId, studioRefOptions.studio.sessionId);
}

return studio;
String sessionId = getStudioByName(wspId, studioRefOptions.studio.studioName).getSessionId();
return getStudioById(wspId, sessionId);
}

protected String getParentStudioSessionId(ParentStudioRefOptions parentStudioRefOptions, Long wspId) throws ApiException {
Expand Down Expand Up @@ -92,6 +92,44 @@ private DataStudioDto getStudioById(Long wspId, String sessionId) throws ApiExce
return studiosApi().describeDataStudio(sessionId, wspId);
}

/**
* Resolves each {@code --allow-user} value (a numeric user ID, username, or email) to a numeric
* user ID. Returns {@code null} when nothing was provided, so the allow list is left untouched.
*/
protected List<Long> resolveAllowedUserIds(String allowUser, Long wspId) throws ApiException {
if (allowUser == null) {
return null;
}
// The platform currently permits a single additional user, so the CLI exposes one value; the
// API field is a list for forward-compatibility, hence the resolved id is wrapped in a list.
return List.of(resolveUserId(allowUser, wspId));
}

private Long resolveUserId(String userToAllow, Long wspId) throws ApiException {
// A numeric value is treated as a user ID directly.
if (userToAllow != null && !userToAllow.isEmpty() && userToAllow.chars().allMatch(Character::isDigit)) {
return Long.parseLong(userToAllow);
}
// Resolve the username/email to a user ID. A workspace participant may be a full organization
// member or a collaborator (role=collaborator, which listOrganizationMembers excludes), so check
// both. The backend still enforces that the resolved user is a participant of the workspace.
Long organizationId = orgId(wspId);
MemberDbDto member = firstMember(orgsApi().listOrganizationMembers(organizationId, null, null, userToAllow));
if (member == null) {
member = firstMember(orgsApi().listOrganizationCollaborators(organizationId, null, null, userToAllow));
}
if (member == null || member.getUserId() == null) {
throw new MemberNotFoundException(organizationId, userToAllow);
}
return member.getUserId();
}

private static MemberDbDto firstMember(ListMembersResponse response) {
return response == null || response.getMembers() == null
? null
: response.getMembers().stream().findFirst().orElse(null);
}

protected Integer onBeforeExit(int exitCode, String sessionId, Long workspaceId, DataStudioStatus targetStatus) {
boolean showProgress = app().output != OutputType.json;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ public class AddAsNewCmd extends AbstractStudiosCmd{
@CommandLine.Option(names = {"--private"}, description = "Create a private studio that only you can access or manage (default: false)", defaultValue = "false")
public Boolean isPrivate;

@CommandLine.Option(names = {"--allow-user"}, description = "User (numeric ID, username, or email), besides the creator, allowed to connect to and start this studio when it is private.")
public String allowedUser;

@CommandLine.Option(names = {"--labels"}, description = "Comma-separated list of labels", split = ",", converter = Label.StudioResourceLabelsConverter.class)
public List<Label> labels;

Expand Down Expand Up @@ -99,6 +102,7 @@ DataStudioCreateRequest prepareRequest(DataStudioDto parentDataStudio, String pa
}
request.setLabelIds(getLabelIds(labels, wspId));
request.setIsPrivate(isPrivate);
request.setAllowedUserIds(resolveAllowedUserIds(allowedUser, wspId));
if (parentCheckpointId == null) {
DataStudioListCheckpointsResponse response = studiosApi().listDataStudioCheckpoints(parentDataStudio.getSessionId(), parentDataStudio.getWorkspaceId(), null, 1, null);
if (!response.getCheckpoints().isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ public class AddCmd extends AbstractStudiosCmd{
@CommandLine.Option(names = {"--ssh"}, description = "Enable SSH connectivity to the studio (default: false).")
public Boolean ssh;

@CommandLine.Option(names = {"--allow-user"}, description = "User (numeric ID, username, or email), besides the creator, allowed to connect to and start this studio when it is private.")
public String allowedUser;

@CommandLine.Option(names = {"--labels"}, description = "Comma-separated list of labels", split = ",", converter = Label.StudioResourceLabelsConverter.class)
public List<Label> labels;

Expand Down Expand Up @@ -131,6 +134,7 @@ DataStudioCreateRequest prepareRequest(Long wspId) throws ApiException {
if (description != null && !description.isEmpty()) {request.description(description);}
request.setLabelIds(getLabelIds(labels, wspId));
request.setIsPrivate(isPrivate);
request.setAllowedUserIds(resolveAllowedUserIds(allowedUser, wspId));
if (spot != null) {
request.setSpot(spot);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ public class StartCmd extends AbstractStudiosCmd {
@CommandLine.Option(names = {"--ssh"}, description = "Optional override to enable SSH connectivity to the studio.")
public Boolean ssh;

@CommandLine.Option(names = {"--allow-user"}, description = "Override the user (numeric ID, username, or email), besides the creator, allowed to connect to and start this studio when it is private. Omit to leave the allow list unchanged. Only the studio creator may change it.")
public String allowedUser;

@Override
protected Response exec() throws ApiException {
Long wspId = workspaceId(workspace.workspace);
Expand Down Expand Up @@ -115,6 +118,7 @@ private DataStudioStartRequest getStartRequestWithOverridesApplied(DataStudioDto
request.setConfiguration(newConfig);
request.setDescription(appliedDescription);
request.setLabelIds(getLabelIds(labels, studioDto.getWorkspaceId()));
request.setAllowedUserIds(resolveAllowedUserIds(allowedUser, studioDto.getWorkspaceId()));
if (spot != null) {
request.setSpot(spot);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ public void toString(PrintWriter out){
table.addRow("Description", formatDescription(studio.getDescription(), 100));
table.addRow("Created on", formatTime(studio.getDateCreated()));
table.addRow("Created by", studioUser == null ? "NA" : String.format("%s | %s",studioUser.getUserName(), studioUser.getEmail()));
table.addRow("Allowed users", studio.getAllowedUsers() == null || studio.getAllowedUsers().isEmpty() ? "NA" : studio.getAllowedUsers()
.stream().map(u -> String.format("%s | %s", u.getUserName(), u.getEmail())).collect(java.util.stream.Collectors.joining(", ")));
table.addRow("Template", studio.getTemplate() == null ? "NA" : studio.getTemplate().getRepository());
table.addRow("Mounted data", studio.getMountedDataLinks() == null ? "NA" : studio.getMountedDataLinks()
.stream().map(DataLinkDto::getResourceRef).collect(java.util.stream.Collectors.joining(", ")));
Expand Down
Loading
Loading