Skip to content

[Improvement] JDBC Connection Configuration with Flexible SSL and Property Injection #681

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

felicity3786
Copy link

@felicity3786 felicity3786 commented Apr 28, 2025

Description

Summary:
Today, the Trino Gateway only supports very basic JDBC connection initialization, assuming standard username/password settings. It lacks support for important real-world features like client-side certificates (mTLS), custom truststore setups, or fine-grained SSL configurations when connecting to databases (e.g., MySQL).
I would like to help contributing a more flexible, extensible JDBC connection management system.

Additional context and related issues

Right now, current gateway code hardcodes simple username/password based JDBC setup.
It is difficult to configure SSL/TLS for databases that require client certificates (e.g., SslMode=VERIFY_CA for MySQL). Gateway users with stricter security requirements (like us at LinkedIn) must patch or fork the code. And it is not cleanly extensible for new future other database types.

At LinkedIn, We already implemented the following internally and would like to contribute it upstream:

  • Introduce a JdbcPropertiesProvider interface for generating connection properties.
  • Add BasicJdbcPropertiesProvider (default simple username/password).
  • Add MySqlJdbcPropertiesProvider to handle MySQL-specific SSL properties:
  • Handles clientCertificateKeyStoreUrl, clientCertificateKeyStorePassword, etc.
  • Supports different SslMode settings (DISABLED, VERIFY_CA, etc.).
  • Introduce a JdbcPropertiesProviderFactory to pick the right provider automatically based on configuration.
  • Refactor JdbcConnectionManager to use these properties instead of hardcoded username/password.
  • Add Airlift-compliant @Singleton / Guice bindings for better dependency injection.

Benefits:
The changes will help

  • Secure and flexible database connections (support MySQL client cert auth, etc.).
  • Easily extensible for Oracle, or other databases in the future.
  • More separation of concerns: connection properties logic decoupled from connection management.
  • Retains full backward compatibility (H2, MySQL username/password still work out of the box).

Release notes

( ) This is not user-visible or is docs only, and no release notes are required.
( ) Release notes are required. Please propose a release note for me.
( ) Release notes are required, with the following suggested text:

* Fix some things. ({issue}`issuenumber`)

@cla-bot cla-bot bot added the cla-signed label Apr 28, 2025
Comment on lines 301 to 310

public boolean isOracleBackend()
{
return oracleBackend;
}

public void setOracleBackend(boolean oracleBackend)
{
this.oracleBackend = oracleBackend;
}
Copy link
Member

Choose a reason for hiding this comment

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

IIUC, with this change, now users will have to manually configure oracleBackend = true in addition to setting their JDBC URL to jdbc:oracle://..., is that right?

I have two ideas here.

First, in the interest of maintaining current behavior and reducing the burden on users, I would say we can just add a new method on DataStoreConfiguration like:

public boolean isOracleBackend() {
  return jdbcUrl.startsWith("jdbc:oracle:");
}

But, thinking on this more, I think we can do better. I am thinking we should create an enum for all of the supported backends:

enum DataStoreBackend {
  ORACLE,
  MYSQL,
  POSTGRES;
}

Then in DataStoreConfiguration we can default this to being set by the JDBC URL, but allow users to override if needed:

private DataStoreBackend dataStoreBackendType;

...

public DataStoreBackend getDataStoreBackendType() {
  if (dataStoreBackendType != null) {
    return dataStoreBackendType;
  }
  if (jdbcUrl.startsWith("jdbc:oracle") {
    return ORACLE;
  } else if (...) {
    ..
  }
}

And then we can pass around this nice enum instead of a boolean. I suspect that over time we're going to need more DB-specific modifications throughout the system. WDYT?

Copy link
Author

Choose a reason for hiding this comment

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

Yep I think the enum DataStoreBackend is better, updated in the new commit. Thanks!

Comment on lines 19 to 20

public class BasicJdbcPropertiesProvider
Copy link
Member

Choose a reason for hiding this comment

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

Maybe add Javadoc explaining that this is the fallback / default behavior in case no DB-specific prop provider is available, and that this should always be the last one in the list?

@xkrogen
Copy link
Member

xkrogen commented May 1, 2025

Current diff LGTM, cc @willmostly

*/
package io.trino.gateway.ha.config;

public enum DataStoreBackend {
Copy link
Member

Choose a reason for hiding this comment

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

DataStoreBackend -> DataStoreType

this.mysqlConfiguration = mysqlConfig;
}

public DataStoreBackend getDataStoreBackendType()
Copy link
Member

Choose a reason for hiding this comment

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

Please consider using java.sql.Driver#acceptsURL instead.

/**
* Configuration for MySQL SSL (client cert and truststore settings).
*/
public class MysqlConfiguration
Copy link
Member

Choose a reason for hiding this comment

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

Mysql -> MySql

Comment on lines 23 to 28
private SslMode sslMode = SslMode.DISABLED;
private String clientCertificateKeyStoreUrl;
private String clientCertificateKeyStorePassword;
private String clientCertificateKeyStoreType;
private String trustCertificateKeyStoreUrl;
private String trustCertificateKeyStorePassword;
Copy link
Member

Choose a reason for hiding this comment

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

Rename these fields for consistency with Trino:

  • tlsEnabled
  • keystorePath
  • keystorePassword
  • keystoreType
  • truststorePath
  • truststorePassword

Copy link
Author

Choose a reason for hiding this comment

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

The naming is from the mysql server: https://github.com/mysql/mysql-connector-j/blob/20ef8ee9eb4294a03858acccea0ddad3525f1ff9/src/main/core-api/java/com/mysql/cj/conf/PropertyKey.java#L82-L84 so the driver can easily pick it up without translating it. I felt it might be easier keeping it this way, I can add some comments for the trino concept mapping though, WDYT?

Copy link
Member

@xkrogen xkrogen May 7, 2025

Choose a reason for hiding this comment

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

+1 with @felicity3786 -- we previously discussed that if we had these as generic, Gateway-level configs, we should use the names consistent with Trino like @ebyhr said. But currently these are MySQL-specific configs, so we should be consistent with the MySQL naming convention.

{
return this.routingManager;
return new QueryCountBasedRouter(gatewayBackendManager, queryHistoryManager);
Copy link
Member

Choose a reason for hiding this comment

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

Add requireNonNull

@@ -54,12 +61,18 @@ public Jdbi getJdbi(@Nullable String routingGroupDatabase)
if (routingGroupDatabase == null) {
return jdbi;
}
Properties props = jdbcPropertiesProvider.getProperties(configuration);
Copy link
Member

Choose a reason for hiding this comment

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

props -> properties

@Inject
public JdbcPropertiesProviderFactory(List<JdbcPropertiesProvider> orderedProviders)
{
this.providers = requireNonNull(orderedProviders, "providers is null");
Copy link
Member

Choose a reason for hiding this comment

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

Remove requireNonNull and add immutable copy.

public JdbcPropertiesProvider forConfig(DataStoreConfiguration config)
{
return providers.stream()
.filter(p -> p.supports(config))
Copy link
Member

Choose a reason for hiding this comment

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


import static org.assertj.core.api.Assertions.assertThat;

class TestJdbcPropertiesProviderFactory
Copy link
Member

Choose a reason for hiding this comment

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

final

{
private JdbcPropertiesProviderFactory factory;

@BeforeEach
Copy link
Member

Choose a reason for hiding this comment

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

Comment on lines 109 to 111
var cfg = makeMySqlConfig(SslMode.VERIFY_CA);
var factory = factoryFor(cfg);
var mySqlConfiguration = cfg.getMySqlConfiguration();
Copy link
Member

Choose a reason for hiding this comment

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

Avoid var per Trino guidelines (here and elsewhere): https://github.com/trinodb/trino/blob/master/.github/DEVELOPMENT.md#avoid-var


import java.util.Properties;

import static org.assertj.core.api.Assertions.assertThat;

class TestJdbcPropertiesProviderFactory
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need this?

Copy link
Author

Choose a reason for hiding this comment

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

Hmm I guess we don't really need it given I have remove all the @BeforeAll, removed it

@@ -30,13 +29,13 @@ public class JdbcPropertiesProviderFactory
@Inject
public JdbcPropertiesProviderFactory(List<JdbcPropertiesProvider> orderedProviders)
{
this.providers = requireNonNull(orderedProviders, "providers is null");
this.providers = List.copyOf(orderedProviders);
Copy link
Member

Choose a reason for hiding this comment

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

For Trino we typically use the Guava one i.e. ImmutableList.copyOf() and we should still have requireNonNull here:

Suggested change
this.providers = List.copyOf(orderedProviders);
this.providers = ImmutableList.copyOf(requireNonNull(orderedProviders, "providers is null"));

Copy link
Member

Choose a reason for hiding this comment

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

requireNonNull is redundant when we use ImmutableList.copyOf.

Copy link
Member

@xkrogen xkrogen May 9, 2025

Choose a reason for hiding this comment

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

Comment on lines 122 to 123
if (jdbcUrl == null) {
throw new IllegalStateException("jdbcUrl must be set");
Copy link
Member

Choose a reason for hiding this comment

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

If jdbcUrl is required, let's add the non-null validation to getJdbcUrl(), and then just use getJdbcUrl() here, to keep that validation consistent

Comment on lines 131 to 134
String driverClass = driver.getClass().getName();
if (driverClass.contains("oracle")) {
return DataStoreType.ORACLE;
}
Copy link
Member

Choose a reason for hiding this comment

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

This fuzzy string match against class name doesn't seem right to me. We have all of the driver classes on the classpath, can we do an actual match against the class name? Like adding a field to DataStoreType which tracks the expected instance of Class<?> for that DataStoreType. Then this iteration can become something like:

while (drivers.hasMoreElements()) {
                Driver driver = drivers.nextElement();
                if (driver.acceptsURL(jdbcUrl)) {
                    for (DataStoreType dataStoreType : DataStoreType.values() {
                      if (dataStoreType.driverClass() == driver.getClass()) {
                        return dataStoreType;
                      }
                    }
                    break;
                 }
}

return clientCertificateKeyStoreUrl;
}

public MySqlConfiguration setClientCertificateKeyStoreUrl(String url)
Copy link
Member

Choose a reason for hiding this comment

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

Please rename the parameter. Same for others.

Copy link
Author

Choose a reason for hiding this comment

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

Hi @ebyhr, @xkrogen and I both think it's would make more sense to keep the mysql naming convention as we discussed in #681 (comment), wdyt?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

4 participants