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
23 changes: 13 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,16 +339,19 @@ Artifacts are stored in external S3 buckets. S3 Access can be configured in **tw

> NOTE: Environment variables(env/jvm/testomatio.properties) take precedence over server-provided credentials.

| Setting | Description | Default |
|-------------------------------|--------------------------------------------------|-------------|
| `testomatio.artifact.disable` | Completely disable artifact uploading | `false` |
| `testomatio.artifact.private` | Keep artifacts private (no public URLs) | `false` |
| `testomatio.step.artifacts.enabled` | Enables uploading artifacts for test steps | `false` |
| `s3.force-path-style` | Use path-style URLs for S3-compatible storage | `false` |
| `s3.endpoint` | Custom endpoint to be used with force-path-style | `false` |
| `s3.bucket` | Provides bucket name for configuration | |
| `s3.access-key-id` | Access key for the bucket | |
| `s3.region` | Bucket region | `us-west-1` |
| Setting | Description | Default |
|-------------------------------------|-------------------------------------------------------|-------------|
| `testomatio.artifact.disable` | Completely disable artifact uploading | `false` |
| `testomatio.artifact.private` | Keep artifacts private (no public URLs) | `false` |
| `testomatio.step.artifacts.enabled` | Enables uploading artifacts for test steps | `false` |
| `s3.force-path-style` | Use path-style URLs for S3-compatible storage | `false` |
| `s3.endpoint` | Custom endpoint to be used with force-path-style | `false` |
| `s3.bucket` | Provides bucket name for configuration | |
| `s3.access-key-id` | Access key for the bucket | |
| `s3.secret.access-key-id` | Secret access key for the bucket | |
| `s3.region` | Bucket region | `us-west-1` |
| `s3.assume.role.arn` | AWS IAM role ARN used for AssumeRole authentication | |
| `s3.assume.role.external.id` | External ID for AssumeRole authentication | |

**Note**: S3 credentials can be configured either in properties file or provided automatically on Testomat.io UI.
Environment variables take precedence over server-provided credentials.
Expand Down
6 changes: 5 additions & 1 deletion java-reporter-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

<groupId>io.testomat</groupId>
<artifactId>java-reporter-core</artifactId>
<version>0.12.1</version>
<version>0.13.0</version>
<packaging>jar</packaging>

<name>Testomat.io Reporter Core</name>
Expand Down Expand Up @@ -80,6 +80,10 @@
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sts</artifactId>
</dependency>

<!-- Test dependencies -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ public class ArtifactPropertyNames {
public static final String SECRET_ACCESS_KEY_PROPERTY_NAME = "s3.secret.access-key-id";
public static final String REGION_PROPERTY_NAME = "s3.region";
public static final String ENDPOINT_PROPERTY_NAME = "s3.endpoint";
public static final String ASSUME_ROLE_ARN_PROPERTY_NAME = "s3.assume.role.arn";
public static final String ASSUME_ROLE_EXTERNAL_ID_PROPERTY_NAME = "s3.assume.role.external.id";

public static final String FORCE_PATH_PROPERTY_NAME = "s3.force-path-style";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.testomat.core.constants;

public class CommonConstants {
public static final String REPORTER_VERSION = "0.12.1";
public static final String REPORTER_VERSION = "0.13.0";

public static final String TESTS_STRING = "tests";
public static final String API_KEY_STRING = "api_key";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ public class CredentialConstants {
public static final String ACCESS_KEY_ID = "ACCESS_KEY_ID";
public static final String BUCKET = "BUCKET";
public static final String REGION = "REGION";
public static final String EXTERNAL_ID = "EXTERNAL_ID";
public static final String ARN = "ARN";
public static final String ENDPOINT = "ENDPOINT";
public static final String FORCE_PATH = "FORCE_PATH_STYLE";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,66 +10,137 @@
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.S3ClientBuilder;
import software.amazon.awssdk.services.s3.S3Configuration;
import software.amazon.awssdk.services.sts.StsClient;
import software.amazon.awssdk.services.sts.auth.StsAssumeRoleCredentialsProvider;

/**
* Factory for creating configured S3Client instances with custom endpoint support.
* Handles AWS credentials, regions, and S3-compatible storage configurations.
* Factory for creating configured S3Client instances with custom endpoint support. Handles AWS credentials, regions,
* and S3-compatible storage configurations.
*/
public class S3ClientFactory {

/**
* Creates a configured S3Client based on current credentials and settings.
* Priority:
* 1. IAM Role (if roleArn configured);
* 2. Static access key / secret key.
*
* @return configured S3Client instance
* @throws IllegalArgumentException if credentials are invalid or missing
*/
public S3Client createS3Client() {
S3Credentials s3Credentials = CredentialsManager.getCredentials();
S3Credentials s3 = CredentialsManager.getCredentials();

S3ClientBuilder builder = S3Client.builder();
Region region = resolveRegion(s3);

AwsCredentialsProvider credentialsProvider;
if (s3Credentials.getAccessKeyId() != null && s3Credentials.getSecretAccessKey() != null) {
if (s3Credentials.getAccessKeyId().trim().isEmpty() || s3Credentials.getSecretAccessKey().trim().isEmpty()) {
throw new IllegalArgumentException("Access key and secret access key cannot be empty");
}
AwsBasicCredentials credentials = AwsBasicCredentials.create(s3Credentials.getAccessKeyId().trim(), s3Credentials.getSecretAccessKey().trim());
credentialsProvider = StaticCredentialsProvider.create(credentials);
} else {
throw new IllegalArgumentException("S3 credentials (access key and secret access key) must be configured");
AwsCredentialsProvider provider =
buildCredentialsProvider(s3, region);

S3ClientBuilder builder = S3Client.builder()
.credentialsProvider(provider)
.region(region);

configureEndpoint(builder, s3);

return builder.build();
}

/**
* Builds AWS credentials provider.
*/
private AwsCredentialsProvider buildCredentialsProvider(S3Credentials s3, Region region) {
boolean useIamRole = s3.getRoleArn() != null && !s3.getRoleArn().isBlank();

if (useIamRole) {
return buildIamRoleProvider(s3, region);
}
builder.credentialsProvider(credentialsProvider);

if (s3Credentials.getRegion() != null && !s3Credentials.getRegion().trim().isEmpty()) {
try {
builder.region(Region.of(s3Credentials.getRegion().trim()));
} catch (Exception e) {
throw new IllegalArgumentException("Invalid region: " + s3Credentials.getRegion(), e);
return buildStaticCredentialsProvider(s3);
}

/**
* Creates AssumeRole credentials provider.
*/
private AwsCredentialsProvider buildIamRoleProvider(S3Credentials s3Credentials, Region region) {
AwsCredentialsProvider baseCredentials = buildStaticCredentialsProvider(s3Credentials);

StsClient stsClient = StsClient.builder()
.region(region)
.credentialsProvider(baseCredentials)
.build();

return StsAssumeRoleCredentialsProvider.builder()
.stsClient(stsClient)
.refreshRequest(request -> {
request.roleArn(s3Credentials.getRoleArn().trim());
request.roleSessionName("testomat-s3-upload");

if (s3Credentials.getExternalId() != null
&& !s3Credentials.getExternalId().isBlank()) {

request.externalId(s3Credentials.getExternalId().trim());
}
})
.build();
}

/**
* Creates static credentials provider.
*/
private AwsCredentialsProvider buildStaticCredentialsProvider(S3Credentials s3Credentials) {
if (s3Credentials.getAccessKeyId() == null || s3Credentials.getAccessKeyId().isBlank()) {
throw new IllegalArgumentException("AWS access key is missing");
}

if (s3Credentials.getSecretAccessKey() == null || s3Credentials.getSecretAccessKey().isBlank()) {
throw new IllegalArgumentException("AWS secret key is missing");
}

AwsBasicCredentials credentials =
AwsBasicCredentials.create(
s3Credentials.getAccessKeyId().trim(),
s3Credentials.getSecretAccessKey().trim());

return StaticCredentialsProvider.create(credentials);
}

/**
* Resolves AWS region.
*/
private Region resolveRegion(S3Credentials s3Credentials) {
try {
if (s3Credentials.getRegion() == null || s3Credentials.getRegion().isBlank()) {
return Region.US_EAST_1;
}
} else {
builder.region(Region.US_EAST_1);
return Region.of(s3Credentials.getRegion().trim());
} catch (Exception e) {
throw new IllegalArgumentException("Invalid AWS region: " + s3Credentials.getRegion(), e);
}
}

/**
* Configures custom endpoint and path-style access.
*/
private void configureEndpoint(S3ClientBuilder builder, S3Credentials s3Credentials) {
boolean hasCustomEndpoint =
s3Credentials.getCustomEndpoint() != null && !s3Credentials.getCustomEndpoint().isBlank();

if (s3Credentials.getCustomEndpoint() != null && !s3Credentials.getCustomEndpoint().trim().isEmpty()) {
if (hasCustomEndpoint) {
try {
builder.endpointOverride(URI.create(s3Credentials.getCustomEndpoint().trim()));
} catch (Exception e) {
throw new IllegalArgumentException("Invalid endpoint URL: " + s3Credentials.getCustomEndpoint(), e);
}

S3Configuration s3Config = S3Configuration.builder()
.pathStyleAccessEnabled(s3Credentials.isForcePath())
.build();
builder.serviceConfiguration(s3Config);
} else {
if (s3Credentials.isForcePath()) {
S3Configuration s3Config = S3Configuration.builder()
.pathStyleAccessEnabled(true)
.build();
builder.serviceConfiguration(s3Config);
}
}

return builder.build();
if (s3Credentials.isForcePath() || hasCustomEndpoint) {
builder.serviceConfiguration(
S3Configuration.builder()
.pathStyleAccessEnabled(
s3Credentials.isForcePath()
)
.build()
);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package io.testomat.core.facade.methods.artifact.credential;

import static io.testomat.core.constants.ArtifactPropertyNames.ACCESS_KEY_PROPERTY_NAME;
import static io.testomat.core.constants.ArtifactPropertyNames.ASSUME_ROLE_ARN_PROPERTY_NAME;
import static io.testomat.core.constants.ArtifactPropertyNames.BUCKET_PROPERTY_NAME;
import static io.testomat.core.constants.ArtifactPropertyNames.ENDPOINT_PROPERTY_NAME;
import static io.testomat.core.constants.ArtifactPropertyNames.ASSUME_ROLE_EXTERNAL_ID_PROPERTY_NAME;
import static io.testomat.core.constants.ArtifactPropertyNames.FORCE_PATH_PROPERTY_NAME;
import static io.testomat.core.constants.ArtifactPropertyNames.PRIVATE_ARTIFACTS_PROPERTY_NAME;
import static io.testomat.core.constants.ArtifactPropertyNames.REGION_PROPERTY_NAME;
import static io.testomat.core.constants.ArtifactPropertyNames.SECRET_ACCESS_KEY_PROPERTY_NAME;
import static io.testomat.core.constants.CredentialConstants.ACCESS_KEY_ID;
import static io.testomat.core.constants.CredentialConstants.ARN;
import static io.testomat.core.constants.CredentialConstants.BUCKET;
import static io.testomat.core.constants.CredentialConstants.ENDPOINT;
import static io.testomat.core.constants.CredentialConstants.EXTERNAL_ID;
import static io.testomat.core.constants.CredentialConstants.FORCE_PATH;
import static io.testomat.core.constants.CredentialConstants.IAM;
import static io.testomat.core.constants.CredentialConstants.PRESIGN;
Expand Down Expand Up @@ -77,6 +81,12 @@ public void populateCredentials(Map<String, Object> credsFromServer) {
populateCredentialField(REGION_PROPERTY_NAME, REGION, credsFromServer, "Region",
value -> credentials.setRegion(getStringValue(value)));

populateCredentialField(ASSUME_ROLE_ARN_PROPERTY_NAME, ARN, credsFromServer, "Arn",
value -> credentials.setRoleArn(getStringValue(value)));

populateCredentialField(ASSUME_ROLE_EXTERNAL_ID_PROPERTY_NAME, EXTERNAL_ID, credsFromServer, "ExternalId",
value -> credentials.setExternalId(getStringValue(value)));

credentials.setIam(getBooleanValue(credsFromServer.get(IAM)));
credentials.setShared(getBooleanValue(credsFromServer.get(SHARED)));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public class S3Credentials {
private String bucket;
private String region;
private String customEndpoint;
private String roleArn;
private String externalId;
private boolean forcePath = false;

public boolean isForcePath() {
Expand Down Expand Up @@ -86,4 +88,20 @@ public String getCustomEndpoint() {
public void setCustomEndpoint(String customEndpoint) {
this.customEndpoint = customEndpoint;
}

public String getExternalId() {
return externalId;
}

public void setExternalId(String externalId) {
this.externalId = externalId;
}

public String getRoleArn() {
return roleArn;
}

public void setRoleArn(String roleArn) {
this.roleArn = roleArn;
}
}
Loading
Loading