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
15 changes: 12 additions & 3 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -713,10 +713,19 @@ $ tw data-links browse -w seqeralabs/showcase -i v1-user-6d8f44c239e2a098b3e02e9

These commands act on an organization (`-o`/`--organization`). Most require the organization `OWNER` role.

### Custom roles

Run `tw roles -h` to view the supported operations: `list`, `view`, `add`, `update`, `delete`, and `permissions`. `tw participants update --role` accepts custom role names.

```bash
tw roles permissions -o my-org
tw roles add -o my-org -n launcher -d "Launch only" -p <permission>,<permission>
```

### Teams and quotas

- `tw teams view|update|workspaces`: view a team, update its name or description, and list its workspaces.
- `tw organizations quotas`: view organization quotas.
- `tw organizations quotas` and `tw members roles -u <user>`: view organization quotas and a member's workspace roles.

## Personal access

Expand Down Expand Up @@ -756,9 +765,9 @@ tw managed-identities credentials -o my-org -n hpc1 add -l my_linux_user -k ~/.s
- `tw launch`, `tw pipelines add|update`, `tw runs relaunch`, and `tw actions add|update`: `--tower-config`.
- `tw runs star|unstar`, `tw runs view log [--next <cursor>]`, and `tw runs delete -i <id>,<id>`.
- `tw pipelines schema`: show a pipeline's parameter schema.
- `tw studios extend|rename-checkpoint`.
- `tw studios extend|rename-checkpoint`. `--mount-data-uris` also accepts a folder inside a data link.
- `tw data-links delete-content|download-script`.
- `tw datasets disable-version`.
- `tw datasets add --url`, `tw datasets update --url`, and `tw datasets disable-version`.

[compute-envs]: https://docs.seqera.io/platform/latest/compute-envs/overview
[credentials]: https://docs.seqera.io/platform/latest/credentials/overview
Expand Down
99 changes: 99 additions & 0 deletions conf/reflect-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,12 @@
"allDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"io.seqera.tower.cli.commands.RolesCmd",
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"io.seqera.tower.cli.commands.RunsCmd",
"allDeclaredFields":true,
Expand Down Expand Up @@ -1614,6 +1620,13 @@
"allDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"io.seqera.tower.cli.commands.members.RolesCmd",
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"fields":[{"name":"organizationRef"}, {"name":"user"}],
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"io.seqera.tower.cli.commands.members.UpdateCmd",
"allDeclaredFields":true,
Expand Down Expand Up @@ -1859,6 +1872,48 @@
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"io.seqera.tower.cli.commands.roles.AddCmd",
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"fields":[{"name":"description"}, {"name":"name"}, {"name":"organizationRef"}, {"name":"permissions"}],
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"io.seqera.tower.cli.commands.roles.DeleteCmd",
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"fields":[{"name":"name"}, {"name":"organizationRef"}],
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"io.seqera.tower.cli.commands.roles.ListCmd",
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"fields":[{"name":"filter"}, {"name":"organizationRef"}, {"name":"paginationOptions"}, {"name":"type"}],
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"io.seqera.tower.cli.commands.roles.PermissionsCmd",
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"fields":[{"name":"organizationRef"}],
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"io.seqera.tower.cli.commands.roles.UpdateCmd",
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"fields":[{"name":"description"}, {"name":"name"}, {"name":"newName"}, {"name":"organizationRef"}, {"name":"permissions"}],
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"io.seqera.tower.cli.commands.roles.ViewCmd",
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"fields":[{"name":"name"}, {"name":"organizationRef"}],
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"io.seqera.tower.cli.commands.runs.AbstractRunsCmd",
"allDeclaredFields":true,
Expand Down Expand Up @@ -2741,6 +2796,14 @@
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
},
{
"name":"io.seqera.tower.cli.responses.members.MemberRolesList",
"allDeclaredFields":true,
"allDeclaredMethods":true,
"allDeclaredConstructors":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
},
{
"name":"io.seqera.tower.cli.responses.members.MembersAdded",
"allDeclaredFields":true,
Expand Down Expand Up @@ -2907,6 +2970,42 @@
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
},
{
"name":"io.seqera.tower.cli.responses.roles.RoleAdded",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
},
{
"name":"io.seqera.tower.cli.responses.roles.RoleDeleted",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
},
{
"name":"io.seqera.tower.cli.responses.roles.RolePermissionsList",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
},
{
"name":"io.seqera.tower.cli.responses.roles.RoleUpdated",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
},
{
"name":"io.seqera.tower.cli.responses.roles.RoleView",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
},
{
"name":"io.seqera.tower.cli.responses.roles.RolesList",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
},
{
"name":"io.seqera.tower.cli.responses.runs.RunCanceled",
"allDeclaredFields":true,
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/io/seqera/tower/cli/Tower.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import io.seqera.tower.cli.commands.ParticipantsCmd;
import io.seqera.tower.cli.commands.PipelinesCmd;
import io.seqera.tower.cli.commands.PipelineSchemasCmd;
import io.seqera.tower.cli.commands.RolesCmd;
import io.seqera.tower.cli.commands.RunsCmd;
import io.seqera.tower.cli.commands.SecretsCmd;
import io.seqera.tower.cli.commands.SshKeysCmd;
Expand Down Expand Up @@ -77,6 +78,7 @@
ParticipantsCmd.class,
PipelinesCmd.class,
PipelineSchemasCmd.class,
RolesCmd.class,
RunsCmd.class,
SshKeysCmd.class,
TeamsCmd.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import io.seqera.tower.api.PipelineVersionsApi;
import io.seqera.tower.api.PipelinesApi;
import io.seqera.tower.api.PlatformsApi;
import io.seqera.tower.api.RolesApi;
import io.seqera.tower.api.ServiceInfoApi;
import io.seqera.tower.api.SshKeysApi;
import io.seqera.tower.api.StudiosApi;
Expand Down Expand Up @@ -136,6 +137,7 @@ public abstract class AbstractApiCmd extends AbstractCmd {
private PipelineSecretsApi pipelineSecretsApi;
private PipelineVersionsApi pipelineVersionsApi;
private PlatformsApi platformsApi;
private RolesApi rolesApi;
private ServiceInfoApi serviceInfoApi;
private SshKeysApi sshKeysApi;
private StudiosApi studiosApi;
Expand Down Expand Up @@ -267,6 +269,10 @@ protected PlatformsApi platformsApi() throws ApiException {
return platformsApi == null ? new PlatformsApi(apiClient()) : platformsApi;
}

protected RolesApi rolesApi() throws ApiException {
return rolesApi == null ? new RolesApi(apiClient()) : rolesApi;
}

protected ServiceInfoApi serviceInfoApi() throws ApiException {
return serviceInfoApi == null ? new ServiceInfoApi(apiClient()) : serviceInfoApi;
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/io/seqera/tower/cli/commands/MembersCmd.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.seqera.tower.cli.commands.members.DeleteCmd;
import io.seqera.tower.cli.commands.members.LeaveCmd;
import io.seqera.tower.cli.commands.members.ListCmd;
import io.seqera.tower.cli.commands.members.RolesCmd;
import io.seqera.tower.cli.commands.members.UpdateCmd;
import picocli.CommandLine;

Expand All @@ -32,6 +33,7 @@
DeleteCmd.class,
UpdateCmd.class,
LeaveCmd.class,
RolesCmd.class,
}
)
public class MembersCmd extends AbstractRootCmd {
Expand Down
40 changes: 40 additions & 0 deletions src/main/java/io/seqera/tower/cli/commands/RolesCmd.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2021-2026, 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;

import io.seqera.tower.cli.commands.roles.AddCmd;
import io.seqera.tower.cli.commands.roles.DeleteCmd;
import io.seqera.tower.cli.commands.roles.ListCmd;
import io.seqera.tower.cli.commands.roles.PermissionsCmd;
import io.seqera.tower.cli.commands.roles.UpdateCmd;
import io.seqera.tower.cli.commands.roles.ViewCmd;
import picocli.CommandLine.Command;

@Command(
name = "roles",
description = "Manage workspace roles. Custom roles are not available for Seqera Cloud Basic organizations.",
subcommands = {
ListCmd.class,
ViewCmd.class,
AddCmd.class,
UpdateCmd.class,
DeleteCmd.class,
PermissionsCmd.class,
}
)
public class RolesCmd extends AbstractRootCmd {
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@

package io.seqera.tower.cli.commands.data.links;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;

Expand All @@ -30,6 +30,7 @@
import io.seqera.tower.cli.exceptions.TowerRuntimeException;
import io.seqera.tower.cli.utils.ResponseHelper;
import io.seqera.tower.model.DataLinkDto;
import io.seqera.tower.model.MountData;

public class DataLinkService {

Expand Down Expand Up @@ -114,10 +115,10 @@ public DataLinkDto getDataLink(DataLinkRefOptions.DataLinkRef dataLinkRef, Long
return null;
}

public List<String> getDataLinkIds(io.seqera.tower.cli.commands.studios.DataLinkRefOptions.DataLinkRef dataLinkRef, Long wspId) {
public List<MountData> getMountData(io.seqera.tower.cli.commands.studios.DataLinkRefOptions.DataLinkRef dataLinkRef, Long wspId) {
// if DataLink IDs are supplied - use those directly
if (dataLinkRef.getMountDataIds() != null) {
return dataLinkRef.getMountDataIds();
return dataLinkRef.getMountDataIds().stream().map(id -> new MountData().dataLinkId(id)).collect(Collectors.toList());
}

// Check and wait if DataLinks are still being fetched
Expand All @@ -126,21 +127,41 @@ public List<String> getDataLinkIds(io.seqera.tower.cli.commands.studios.DataLink
throw new TowerRuntimeException("Failed to fetch datalinks for mountData - please retry.");
}

List<String> dataLinkIds = new ArrayList<>();

if (dataLinkRef.getMountDataNames() != null) {
dataLinkIds = dataLinkRef.getMountDataNames().stream()
.map(name -> getDataLinkByName(wspId, null, name).getId())
return dataLinkRef.getMountDataNames().stream()
.map(name -> new MountData().dataLinkId(getDataLinkByName(wspId, null, name).getId()))
.collect(Collectors.toList());
}

if (dataLinkRef.getMountDataUris() != null) {
dataLinkIds = dataLinkRef.getMountDataUris().stream()
.map(resourceRef -> getDataLinkByResourceRef(wspId, null, resourceRef).getId())
.collect(Collectors.toList());
return dataLinkRef.getMountDataUris().stream()
.map(uri -> getMountDataByUri(wspId, uri))
.collect(Collectors.toList());
}

/**
* Resolves a URI to the data link whose resource reference is the URI itself or its closest parent,
* so that a folder inside a data link can be mounted (the remainder becomes the mount path).
*/
private MountData getMountDataByUri(Long wspId, String uri) {
String target = stripTrailingSlash(uri);
int rootEnd = target.indexOf("://") + 2;
String candidate = target;
while (true) {
Optional<DataLinkDto> dataLink = findDataLinkByResourceRef(wspId, candidate);
if (dataLink.isPresent()) {
MountData mountData = new MountData().dataLinkId(dataLink.get().getId());
return candidate.equals(target) ? mountData : mountData.path(target.substring(candidate.length() + 1));
}
int parentEnd = candidate.lastIndexOf('/');
if (parentEnd <= rootEnd) {
throw new DataLinkNotFoundException(getResourceRefKeywordParam(uri), wspId);
}
candidate = candidate.substring(0, parentEnd);
}
}

return dataLinkIds;
private static String stripTrailingSlash(String ref) {
return ref != null && ref.endsWith("/") ? ref.substring(0, ref.length() - 1) : ref;
}

private DataLinkDto getDataLinkByName(Long wspId, String credId, String name) {
Expand All @@ -151,21 +172,27 @@ private DataLinkDto getDataLinkByResourceRef(Long wspId, String credId, String r
return getDataLinkBySearchAndFindExactMatch(wspId, getResourceRefKeywordParam(resourceRef), credId, datalink -> resourceRef.equals(datalink.getResourceRef()));
}

private Optional<DataLinkDto> findDataLinkByResourceRef(Long wspId, String resourceRef) {
// Data links created before Platform normalised resource refs may still be stored with a trailing slash
return findDataLinkBySearchAndExactMatch(wspId, getResourceRefKeywordParam(resourceRef), null, datalink -> resourceRef.equals(stripTrailingSlash(datalink.getResourceRef())));
}

private DataLinkDto getDataLinkBySearchAndFindExactMatch(Long wspId, String search, String credId, Predicate<DataLinkDto> filter) {
return findDataLinkBySearchAndExactMatch(wspId, search, credId, filter)
.orElseThrow(() -> new DataLinkNotFoundException(search, wspId));
}

private Optional<DataLinkDto> findDataLinkBySearchAndExactMatch(Long wspId, String search, String credId, Predicate<DataLinkDto> filter) {
var datalinks = getDataLinksBySearchCriteria(wspId, search, credId).stream()
.filter(filter)
.collect(Collectors.toList());

if (datalinks.isEmpty()) {
throw new DataLinkNotFoundException(search, wspId);
}

if (datalinks.size() > 1) {
var dataLinkIds = datalinks.stream().map(DataLinkDto::getId).collect(Collectors.toList());
throw new MultipleDataLinksFoundException(search, wspId, dataLinkIds);
}

return datalinks.get(0);
return datalinks.stream().findFirst();
}

private DataLinkDto getDataLinkById(String dataLinkId, Long wspId, String credId) {
Expand Down
Loading
Loading