Skip to content

Add support for S3 compatible storage#3188

Merged
havetisyan merged 2 commits intoAthenZ:masterfrom
gyakami:feature/s3compatible/20260130
Feb 1, 2026
Merged

Add support for S3 compatible storage#3188
havetisyan merged 2 commits intoAthenZ:masterfrom
gyakami:feature/s3compatible/20260130

Conversation

@gyakami
Copy link
Copy Markdown
Contributor

@gyakami gyakami commented Jan 30, 2026

Description

This PR enables support for S3-compatible storage services other than AWS S3.

Changes

Extended the implementation to allow configuring:

  • Custom endpoints
  • Custom CA certificates
  • Checksum validation

Backward Compatibility: This change does not affect existing AWS S3 configurations. The default settings maintain the original behavior.

Contribution Checklist:

  • The pull request does not introduce any breaking changes
  • I have read the contribution guidelines.
  • Create an issue and link to the pull request.

Attach Screenshots (Optional)

Signed-off-by: Go Yakami <gyakami@lycorp.co.jp>
Signed-off-by: Go Yakami <gyakami@lycorp.co.jp>
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @gyakami, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the flexibility of Athenz's S3 integration by enabling support for generic S3-compatible storage solutions. It achieves this by introducing configurable options for custom endpoints, custom CA certificates for secure connections, and optional checksum validation for data integrity. These additions broaden the deployment possibilities for Athenz components that rely on S3 for storage, without impacting existing AWS S3 setups.

Highlights

  • S3-Compatible Endpoint Configuration: Introduced the ability to configure a custom S3 endpoint URL, allowing the use of S3-compatible storage services beyond AWS S3. This is managed via the athenz.zts.aws_s3_endpoint system property for S3ChangeLogStore and aws_s3_endpoint config parameter for S3ClientFactory.
  • Custom CA Certificate Support: Added support for providing custom CA certificates for TLS verification when connecting to S3-compatible endpoints. This is configured using the athenz.zts.aws_s3_ca_cert system property for S3ChangeLogStore and aws_s3_ca_cert config parameter for S3ClientFactory.
  • Checksum Validation for S3 Operations: Implemented an option to enable request and response checksum validation for S3 operations in S3ClientFactory, configurable via the aws_s3_use_checksum_validation_when_required parameter. This enhances data integrity checks.
  • Backward Compatibility: The changes are designed to be backward compatible, ensuring that existing AWS S3 configurations continue to function without modification.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for S3-compatible storage by enabling custom endpoints and CA certificates, along with an option for checksum validation. The changes are well-implemented across S3ChangeLogStore and S3ClientFactory, with corresponding configuration updates and tests. My review focuses on improving maintainability by reducing code duplication and enhancing exception handling for better robustness.

Comment on lines +519 to +538
if (!StringUtil.isEmpty(s3CaCert)) {
try {
ApacheHttpClient.Builder httpClientBuilder = ApacheHttpClient.builder();
X509Certificate[] certs = Crypto.loadX509Certificates(s3CaCert);
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null); // Initialize empty keystore
int i = 0;
for (X509Certificate cert : certs) {
keyStore.setCertificateEntry("custom-ca-" + i++, cert);
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);

httpClientBuilder.tlsTrustManagersProvider(tmf::getTrustManagers);
s3ClientBuilder.httpClient(httpClientBuilder.build());
} catch (Exception ex) {
LOGGER.error("S3ChangeLogStore: unable to load custom ca cert: {}", s3CaCert, ex);
throw new RuntimeException("S3ChangeLogStore: unable to load custom ca cert");
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block of code for handling custom CA certificates is nearly identical to the one in S3ClientFactory.java. To improve maintainability and reduce code duplication, consider extracting this logic into a shared utility method. This method could take the caCertPath and return a configured TlsTrustManagersProvider or directly configure an ApacheHttpClient.Builder.

Comment on lines +87 to +99
if (!Config.isEmpty(caCertPath)) {
X509Certificate[] certs = Crypto.loadX509Certificates(caCertPath);
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null); // Initialize empty keystore
int i = 0;
for (X509Certificate cert : certs) {
keyStore.setCertificateEntry("custom-ca-" + i++, cert);
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);

httpClientBuilder.tlsTrustManagersProvider(tmf::getTrustManagers);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This logic for handling custom CA certificates is duplicated in S3ChangeLogStore.java. To avoid code duplication and improve maintainability, this logic should be extracted into a common utility method.

Comment on lines +308 to +474
@Test
public void testGetS3ClientWithChecksumValidationEnabled() throws Exception {
// Setup configuration
System.setProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_ROOT_PATH, TestUtils.TESTROOT);
System.setProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_AWS_BUCKET, "test-bucket");
System.setProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_AWS_S3_CHECKSUM_VALIDATION, "true");
System.setProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_AWS_KEY_ID, "test-key");
System.setProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_AWS_ACCESS_KEY, "test-secret");

Config.getInstance().loadProperties();

// Mocks
try (MockedStatic<ApacheHttpClient> mockHttpClientStatic = mockStatic(ApacheHttpClient.class);
MockedStatic<S3Client> mockS3ClientStatic = mockStatic(S3Client.class)) {

// Mock ApacheHttpClient builder
ApacheHttpClient.Builder mockHttpBuilder = mock(ApacheHttpClient.Builder.class);
SdkHttpClient mockHttpClient = mock(SdkHttpClient.class);

mockHttpClientStatic.when(ApacheHttpClient::builder).thenReturn(mockHttpBuilder);
when(mockHttpBuilder.connectionTimeout(any())).thenReturn(mockHttpBuilder);
when(mockHttpBuilder.socketTimeout(any())).thenReturn(mockHttpBuilder);
when(mockHttpBuilder.build()).thenReturn(mockHttpClient);

// Mock S3Client builder
S3ClientBuilder mockS3Builder = mock(S3ClientBuilder.class);
S3Client mockS3Client = mock(S3Client.class);

mockS3ClientStatic.when(S3Client::builder).thenReturn(mockS3Builder);
when(mockS3Builder.httpClient(any(SdkHttpClient.class))).thenReturn(mockS3Builder);
when(mockS3Builder.region(any(Region.class))).thenReturn(mockS3Builder);
when(mockS3Builder.requestChecksumCalculation(any(RequestChecksumCalculation.class))).thenReturn(mockS3Builder);
when(mockS3Builder.responseChecksumValidation(any(ResponseChecksumValidation.class))).thenReturn(mockS3Builder);
when(mockS3Builder.credentialsProvider(any())).thenReturn(mockS3Builder);
when(mockS3Builder.build()).thenReturn(mockS3Client);

// Mock HeadBucket to pass verification
when(mockS3Client.headBucket(any(HeadBucketRequest.class))).thenReturn(HeadBucketResponse.builder().build());

// Execute
S3Client client = S3ClientFactory.getS3Client();

// Verify
assertNotNull(client);

// Verify checksum calculation and validation were enabled
verify(mockS3Builder).requestChecksumCalculation(RequestChecksumCalculation.WHEN_REQUIRED);
verify(mockS3Builder).responseChecksumValidation(ResponseChecksumValidation.WHEN_REQUIRED);
} finally {
// Cleanup
System.clearProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_ROOT_PATH);
System.clearProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_AWS_BUCKET);
System.clearProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_AWS_S3_CHECKSUM_VALIDATION);
System.clearProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_AWS_KEY_ID);
System.clearProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_AWS_ACCESS_KEY);
}
}

@Test
public void testGetS3ClientWithChecksumValidationDisabled() throws Exception {
// Setup configuration
System.setProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_ROOT_PATH, TestUtils.TESTROOT);
System.setProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_AWS_BUCKET, "test-bucket");
System.setProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_AWS_S3_CHECKSUM_VALIDATION, "false");
System.setProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_AWS_KEY_ID, "test-key");
System.setProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_AWS_ACCESS_KEY, "test-secret");

Config.getInstance().loadProperties();

// Mocks
try (MockedStatic<ApacheHttpClient> mockHttpClientStatic = mockStatic(ApacheHttpClient.class);
MockedStatic<S3Client> mockS3ClientStatic = mockStatic(S3Client.class)) {

// Mock ApacheHttpClient builder
ApacheHttpClient.Builder mockHttpBuilder = mock(ApacheHttpClient.Builder.class);
SdkHttpClient mockHttpClient = mock(SdkHttpClient.class);

mockHttpClientStatic.when(ApacheHttpClient::builder).thenReturn(mockHttpBuilder);
when(mockHttpBuilder.connectionTimeout(any())).thenReturn(mockHttpBuilder);
when(mockHttpBuilder.socketTimeout(any())).thenReturn(mockHttpBuilder);
when(mockHttpBuilder.build()).thenReturn(mockHttpClient);

// Mock S3Client builder
S3ClientBuilder mockS3Builder = mock(S3ClientBuilder.class);
S3Client mockS3Client = mock(S3Client.class);

mockS3ClientStatic.when(S3Client::builder).thenReturn(mockS3Builder);
when(mockS3Builder.httpClient(any(SdkHttpClient.class))).thenReturn(mockS3Builder);
when(mockS3Builder.region(any(Region.class))).thenReturn(mockS3Builder);
when(mockS3Builder.credentialsProvider(any())).thenReturn(mockS3Builder);
when(mockS3Builder.build()).thenReturn(mockS3Client);

// Mock HeadBucket to pass verification
when(mockS3Client.headBucket(any(HeadBucketRequest.class))).thenReturn(HeadBucketResponse.builder().build());

// Execute
S3Client client = S3ClientFactory.getS3Client();

// Verify
assertNotNull(client);

// Verify checksum calculation and validation were NOT called
verify(mockS3Builder, never()).requestChecksumCalculation(any());
verify(mockS3Builder, never()).responseChecksumValidation(any());
} finally {
// Cleanup
System.clearProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_ROOT_PATH);
System.clearProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_AWS_BUCKET);
System.clearProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_AWS_S3_CHECKSUM_VALIDATION);
System.clearProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_AWS_KEY_ID);
System.clearProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_AWS_ACCESS_KEY);
}
}

@Test
public void testGetS3ClientWithChecksumValidationNotConfigured() throws Exception {
// Setup configuration without checksum validation parameter
System.setProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_ROOT_PATH, TestUtils.TESTROOT);
System.setProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_AWS_BUCKET, "test-bucket");
System.setProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_AWS_KEY_ID, "test-key");
System.setProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_AWS_ACCESS_KEY, "test-secret");

Config.getInstance().loadProperties();

// Mocks
try (MockedStatic<ApacheHttpClient> mockHttpClientStatic = mockStatic(ApacheHttpClient.class);
MockedStatic<S3Client> mockS3ClientStatic = mockStatic(S3Client.class)) {

// Mock ApacheHttpClient builder
ApacheHttpClient.Builder mockHttpBuilder = mock(ApacheHttpClient.Builder.class);
SdkHttpClient mockHttpClient = mock(SdkHttpClient.class);

mockHttpClientStatic.when(ApacheHttpClient::builder).thenReturn(mockHttpBuilder);
when(mockHttpBuilder.connectionTimeout(any())).thenReturn(mockHttpBuilder);
when(mockHttpBuilder.socketTimeout(any())).thenReturn(mockHttpBuilder);
when(mockHttpBuilder.build()).thenReturn(mockHttpClient);

// Mock S3Client builder
S3ClientBuilder mockS3Builder = mock(S3ClientBuilder.class);
S3Client mockS3Client = mock(S3Client.class);

mockS3ClientStatic.when(S3Client::builder).thenReturn(mockS3Builder);
when(mockS3Builder.httpClient(any(SdkHttpClient.class))).thenReturn(mockS3Builder);
when(mockS3Builder.region(any(Region.class))).thenReturn(mockS3Builder);
when(mockS3Builder.credentialsProvider(any())).thenReturn(mockS3Builder);
when(mockS3Builder.build()).thenReturn(mockS3Client);

// Mock HeadBucket to pass verification
when(mockS3Client.headBucket(any(HeadBucketRequest.class))).thenReturn(HeadBucketResponse.builder().build());

// Execute
S3Client client = S3ClientFactory.getS3Client();

// Verify
assertNotNull(client);

// Verify checksum calculation and validation were NOT called (default behavior)
verify(mockS3Builder, never()).requestChecksumCalculation(any());
verify(mockS3Builder, never()).responseChecksumValidation(any());
} finally {
// Cleanup
System.clearProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_ROOT_PATH);
System.clearProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_AWS_BUCKET);
System.clearProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_AWS_KEY_ID);
System.clearProperty(Config.PROP_PREFIX + Config.SYNC_CFG_PARAM_AWS_ACCESS_KEY);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The three new tests for checksum validation (testGetS3ClientWithChecksumValidationEnabled, testGetS3ClientWithChecksumValidationDisabled, testGetS3ClientWithChecksumValidationNotConfigured) contain a lot of duplicated code for setting up system properties and mocks. This can be refactored into a private helper method to reduce duplication and improve test readability and maintainability. The helper method could take the checksum configuration value and a Consumer<S3ClientBuilder> for verification logic as parameters.

@havetisyan havetisyan merged commit d1f75b6 into AthenZ:master Feb 1, 2026
3 checks passed
@gyakami gyakami deleted the feature/s3compatible/20260130 branch February 1, 2026 10:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants