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
2 changes: 1 addition & 1 deletion VERSION-API
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
1.148.0
1.167.0
// Only first line of this file is read
// This version should be bumped to the minimum version where dependent API changes were introduced
// But never higher then the current Platform API Version deployed in Cloud Production: https://cloud.seqera.io/api/service-info
437 changes: 416 additions & 21 deletions conf/reflect-config.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ mockserverVersion = "5.15.0"
picocliVersion = "4.6.3"
shadowVersion = "9.4.1"
slf4jVersion = "2.0.17"
towerJavaSdkVersion = "1.150.0"
towerJavaSdkVersion = "1.167.0"
xzVersion = "1.10"

[libraries]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@
import io.seqera.tower.cli.exceptions.CredentialsNotFoundException;
import io.seqera.tower.cli.responses.CredentialsAdded;
import io.seqera.tower.cli.responses.Response;
import io.seqera.tower.model.AwsSecurityKeys;
import io.seqera.tower.model.CreateCredentialsRequest;
import io.seqera.tower.model.CreateCredentialsResponse;
import io.seqera.tower.model.Credentials;
import io.seqera.tower.model.DescribeCredentialsResponse;
import io.seqera.tower.model.SecurityKeys;
import picocli.CommandLine;
import picocli.CommandLine.Command;
Expand All @@ -48,19 +50,40 @@ public abstract class AbstractAddCmd<T extends SecurityKeys> extends AbstractCre
@Override
protected Response exec() throws ApiException, IOException {
Long wspId = workspaceId(workspace.workspace);
CredentialsProvider provider = getProvider();
boolean useExternalId = provider.useExternalId();

Credentials specs = new Credentials();
specs
.keys(getProvider().securityKeys())
.keys(provider.securityKeys())
.name(name)
.baseUrl(getProvider().baseUrl())
.provider(getProvider().type());
.baseUrl(provider.baseUrl())
.provider(provider.type());

if (overwrite) tryDeleteCredentials(name, wspId);

CreateCredentialsResponse resp = credentialsApi().createCredentials(new CreateCredentialsRequest().credentials(specs), wspId, getProvider().useExternalId());
CreateCredentialsResponse resp = credentialsApi().createCredentials(new CreateCredentialsRequest().credentials(specs), wspId, useExternalId);

return new CredentialsAdded(getProvider().type().name(), resp.getCredentialsId(), name, workspaceRef(wspId));
String externalId = null;
String setupSnippet = null;
if (useExternalId) {
try {
DescribeCredentialsResponse describe = credentialsApi().describeCredentials(resp.getCredentialsId(), wspId);
if (describe != null) {
if (describe.getCredentials() != null && describe.getCredentials().getKeys() instanceof AwsSecurityKeys aws) {
externalId = aws.getExternalId();
}
setupSnippet = describe.getSetupSnippet();
}
} catch (ApiException e) {
getSpec().commandLine().getErr().println(ansi(String.format(
"@|fg(yellow) Warning:|@ could not fetch credential details after creation: %s. The credential was created.",
e.getMessage())));
}
}

return new CredentialsAdded(provider.type().name(), resp.getCredentialsId(), name, workspaceRef(wspId),
externalId, setupSnippet);
}

protected abstract CredentialsProvider getProvider();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,12 @@ public AwsSecurityKeys securityKeys() {
}

@Override
public Boolean useExternalId() {
public boolean useExternalId() {
AwsCredentialsMode mode = getMode();
if (mode == AwsCredentialsMode.role) {
return true;
}
if (generateExternalId && assumeRoleArn != null) {
return true;
}
return null;
return generateExternalId && assumeRoleArn != null;
}

private AwsCredentialsMode getMode() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public interface CredentialsProvider {

SecurityKeys securityKeys() throws IOException, ApiException;

default Boolean useExternalId() {
return null;
default boolean useExternalId() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,44 @@ public class CredentialsAdded extends Response {
public final String provider;
public final String name;
public final String workspaceRef;
public final String externalId;
public final String setupSnippet;

public CredentialsAdded(String provider, String id, String name, String workspaceRef) {
this(provider, id, name, workspaceRef, null, null);
}

public CredentialsAdded(String provider, String id, String name, String workspaceRef,
String externalId, String setupSnippet) {
this.provider = provider;
this.id = id;
this.name = name;
this.workspaceRef = workspaceRef;
this.externalId = externalId;
this.setupSnippet = setupSnippet;
}

@Override
public String toString() {
return ansi(String.format("%n @|yellow New %S credentials '%s (%s)' added at %s workspace|@%n", provider, name, id, workspaceRef));
StringBuilder out = new StringBuilder();
out.append(ansi(String.format("%n @|yellow New %S credentials '%s (%s)' added at %s workspace|@%n",
provider, name, id, workspaceRef)));
if (externalId != null && !externalId.isEmpty()) {
out.append(ansi(String.format("%n @|bold External ID:|@ %s%n", externalId)));
}
if (setupSnippet != null && !setupSnippet.isEmpty()) {
out.append(ansi(String.format("%n @|bold Trust policy|@ (paste this into your IAM role's trust relationship):%n%n%s%n",
indent(setupSnippet, " "))));
}
return out.toString();
}

private static String indent(String text, String prefix) {
String nl = String.format("%n");
StringBuilder sb = new StringBuilder();
for (String line : text.split("\\R", -1)) {
sb.append(prefix).append(line).append(nl);
}
return sb.toString();
}
}
4 changes: 2 additions & 2 deletions src/test/java/io/seqera/tower/cli/InfoCmdTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ void testInfo(OutputType format, MockServerClient mock) throws IOException {
Map<String, String> opts = new HashMap<>();
opts.put("cliVersion", getCliVersion() );
opts.put("cliApiVersion", getCliApiVersion());
opts.put("towerApiVersion", "1.148.0");
opts.put("towerApiVersion", "1.167.0");
opts.put("towerVersion", "22.3.0-torricelli");
opts.put("towerApiEndpoint", "http://localhost:"+mock.getPort());
opts.put("userName", "jordi");
Expand Down Expand Up @@ -86,7 +86,7 @@ void testInfoStatusTokenFail(MockServerClient mock) throws IOException {
Map<String, String> opts = new HashMap<>();
opts.put("cliVersion", getCliVersion() );
opts.put("cliApiVersion", getCliApiVersion());
opts.put("towerApiVersion", "1.148.0");
opts.put("towerApiVersion", "1.167.0");
opts.put("towerVersion", "22.3.0-torricelli");
opts.put("towerApiEndpoint", "http://localhost:"+mock.getPort());
opts.put("userName", null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ void testAddWithRoleMode(OutputType format, MockServerClient mock) {
response().withStatusCode(200).withBody("{\"credentialsId\":\"3cz5A8cuBkB5iJliCwJCFU\"}").withContentType(MediaType.APPLICATION_JSON)
);

mock.when(
request().withMethod("GET").withPath("/credentials/3cz5A8cuBkB5iJliCwJCFU"), exactly(1)
).respond(
response().withStatusCode(200).withBody("{\"credentials\":{\"id\":\"3cz5A8cuBkB5iJliCwJCFU\",\"name\":\"aws-role\",\"provider\":\"aws\",\"keys\":{\"discriminator\":\"aws\",\"mode\":\"role\",\"assumeRoleArn\":\"arn:aws:iam::123456789012:role/MyRole\"}}}").withContentType(MediaType.APPLICATION_JSON)
);

ExecOut out = exec(format, mock, "credentials", "add", "aws", "-n", "aws-role", "--mode=role", "-r", "arn:aws:iam::123456789012:role/MyRole");
assertOutput(format, out, new CredentialsAdded("AWS", "3cz5A8cuBkB5iJliCwJCFU", "aws-role", USER_WORKSPACE_NAME));
}
Expand All @@ -133,10 +139,42 @@ void testAddKeysModeWithGenerateExternalId(OutputType format, MockServerClient m
response().withStatusCode(200).withBody("{\"credentialsId\":\"4cz5A8cuBkB5iJliCwJCFU\"}").withContentType(MediaType.APPLICATION_JSON)
);

mock.when(
request().withMethod("GET").withPath("/credentials/4cz5A8cuBkB5iJliCwJCFU"), exactly(1)
).respond(
response().withStatusCode(200).withBody("{\"credentials\":{\"id\":\"4cz5A8cuBkB5iJliCwJCFU\",\"name\":\"aws-ext\",\"provider\":\"aws\",\"keys\":{\"discriminator\":\"aws\",\"accessKey\":\"access_key\",\"assumeRoleArn\":\"arn_role\"}}}").withContentType(MediaType.APPLICATION_JSON)
);

ExecOut out = exec(format, mock, "credentials", "add", "aws", "-n", "aws-ext", "-a", "access_key", "-s", "secret_key", "-r", "arn_role", "--generate-external-id");
assertOutput(format, out, new CredentialsAdded("AWS", "4cz5A8cuBkB5iJliCwJCFU", "aws-ext", USER_WORKSPACE_NAME));
}

@ParameterizedTest
@EnumSource(OutputType.class)
void testAddRoleModeSurfacesExternalIdAndTrustPolicy(OutputType format, MockServerClient mock) {

mock.when(
request()
.withMethod("POST")
.withPath("/credentials")
.withQueryStringParameter("useExternalId", "true")
.withBody(json("{\"credentials\":{\"keys\":{\"mode\":\"role\",\"assumeRoleArn\":\"arn:aws:iam::222222222222:role/CustomerRole\"},\"name\":\"aws-role-jump\",\"provider\":\"aws\"}}")),
exactly(1)
).respond(
response().withStatusCode(200).withBody("{\"credentialsId\":\"5cz5A8cuBkB5iJliCwJCFU\"}").withContentType(MediaType.APPLICATION_JSON)
);

mock.when(
request().withMethod("GET").withPath("/credentials/5cz5A8cuBkB5iJliCwJCFU"), exactly(1)
).respond(
response().withStatusCode(200).withBody("{\"credentials\":{\"id\":\"5cz5A8cuBkB5iJliCwJCFU\",\"name\":\"aws-role-jump\",\"provider\":\"aws\",\"keys\":{\"discriminator\":\"aws\",\"mode\":\"role\",\"assumeRoleArn\":\"arn:aws:iam::222222222222:role/CustomerRole\",\"externalId\":\"a1b2c3d4-e5f6\"}},\"setupSnippet\":\"{ \\\"Version\\\": \\\"2012-10-17\\\" }\"}").withContentType(MediaType.APPLICATION_JSON)
);

ExecOut out = exec(format, mock, "credentials", "add", "aws", "-n", "aws-role-jump", "--mode=role", "-r", "arn:aws:iam::222222222222:role/CustomerRole");
assertOutput(format, out, new CredentialsAdded("AWS", "5cz5A8cuBkB5iJliCwJCFU", "aws-role-jump", USER_WORKSPACE_NAME,
"a1b2c3d4-e5f6", "{ \"Version\": \"2012-10-17\" }"));
}

@Test
void testAddRoleModeRejectsAccessKeys(MockServerClient mock) {

Expand Down
2 changes: 1 addition & 1 deletion src/test/resources/runcmd/info/service-info.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"serviceInfo": {
"version": "22.3.0-torricelli",
"apiVersion": "1.148.0",
"apiVersion": "1.167.0",
"commitId": "3f04bfd4",
"authTypes": [
"github",
Expand Down
Loading