Skip to content

[SpannerToSourceDb] Reverse replication Integration test with SSL based connection - #4192

Open
darshan-sj wants to merge 3 commits into
GoogleCloudPlatform:mainfrom
darshan-sj:rr-ssl
Open

[SpannerToSourceDb] Reverse replication Integration test with SSL based connection#4192
darshan-sj wants to merge 3 commits into
GoogleCloudPlatform:mainfrom
darshan-sj:rr-ssl

Conversation

@darshan-sj

Copy link
Copy Markdown
Contributor

No description provided.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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 enhances the integration testing framework for the Spanner-to-SourceDB migration template by adding support for SSL-encrypted connections to MySQL. It includes a new resource manager that handles the automated generation and conversion of SSL certificates into Java KeyStore (JKS) format, allowing the pipeline to securely connect to the database during tests. The changes also involve updating existing test infrastructure to support these secure connections and improving code flexibility by decoupling test utilities from specific MySQL implementations.

Highlights

  • SSL Support for MySQL Integration Tests: Introduced SSLMySQLResourceManager to enable SSL connections in MySQL integration tests by dynamically generating and extracting JKS keystores and truststores from the container.
  • Integration Test Updates: Updated SpannerToSourceDbIT to utilize the new SSL resource manager, configure SSL connection properties, and stage the necessary truststore files for the Dataflow pipeline.
  • Refactoring Resource Management: Generalized method signatures in SpannerToSourceDbITBase and SpannerGeneratedColumnUtils to accept the base JDBCResourceManager instead of the concrete MySQLResourceManager.
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 the 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 counterproductive. 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.

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.

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

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.

Code Review

This pull request introduces SSLMySQLResourceManager to enable SSL support for MySQL test containers, dynamically extracting generated certificates into JKS keystores and truststores. It also updates the SpannerToSourceDbIT integration test to utilize this new resource manager and refactors several test utilities to use the generic JDBCResourceManager interface. Feedback focuses on improving process execution robustness and resource cleanup in SSLMySQLResourceManager, using existing GCS path helpers, and updating URL encoding to use the modern Charset-based API.

Comment on lines +106 to +155
ProcessBuilder pbTrust =
new ProcessBuilder(
keytool,
"-importcert",
"-alias",
"mysqlca",
"-file",
tempDir.resolve("ca.pem").toString(),
"-keystore",
truststore.toString(),
"-storepass",
password,
"-noprompt");
Process trustProcess = pbTrust.start();
trustProcess.waitFor();
if (trustProcess.exitValue() != 0) {
throw new RuntimeException("Failed to generate truststore.jks");
}

// Convert PKCS12 to Keystore JKS
Path keystore = tempDir.resolve("keystore.jks");
LOG.info("Generating keystore.jks");
ProcessBuilder pbKey =
new ProcessBuilder(
keytool,
"-importkeystore",
"-srckeystore",
tempDir.resolve("keystore.p12").toString(),
"-srcstoretype",
"PKCS12",
"-srcstorepass",
password,
"-destkeystore",
keystore.toString(),
"-deststoretype",
"JKS",
"-deststorepass",
password,
"-noprompt");
Process keyProcess = pbKey.start();
keyProcess.waitFor();
if (keyProcess.exitValue() != 0) {
throw new RuntimeException("Failed to generate keystore.jks");
}

this.keystorePath = keystore.toAbsolutePath().toString();
this.truststorePath = truststore.toAbsolutePath().toString();
LOG.info(
"Successfully generated Keystore: {} and Truststore: {}", keystorePath, truststorePath);
}

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 process execution for keytool is duplicated, does not capture the error stream (making failures hard to debug), and does not guarantee process destruction in a finally block if interrupted or if an exception is thrown.

We can refactor this into a robust helper method runProcess that redirects the error stream, captures the output on failure, and ensures the process is destroyed.

    ProcessBuilder pbTrust = 
        new ProcessBuilder(
            keytool,
            "-importcert",
            "-alias",
            "mysqlca",
            "-file",
            tempDir.resolve("ca.pem").toString(),
            "-keystore",
            truststore.toString(),
            "-storepass",
            password,
            "-noprompt");
    runProcess(pbTrust, "generate truststore.jks");

    // Convert PKCS12 to Keystore JKS
    Path keystore = tempDir.resolve("keystore.jks");
    LOG.info("Generating keystore.jks");
    ProcessBuilder pbKey = 
        new ProcessBuilder(
            keytool,
            "-importkeystore",
            "-srckeystore",
            tempDir.resolve("keystore.p12").toString(),
            "-srcstoretype",
            "PKCS12",
            "-srcstorepass",
            password,
            "-destkeystore",
            keystore.toString(),
            "-deststoretype",
            "JKS",
            "-deststorepass",
            password,
            "-noprompt");
    runProcess(pbKey, "generate keystore.jks");

    this.keystorePath = keystore.toAbsolutePath().toString();
    this.truststorePath = truststore.toAbsolutePath().toString();
    LOG.info(
        "Successfully generated Keystore: {} and Truststore: {}", keystorePath, truststorePath);
  }

  private void runProcess(ProcessBuilder pb, String stepName) throws IOException, InterruptedException {
    pb.redirectErrorStream(true);
    Process process = pb.start();
    try {
      int exitCode = process.waitFor();
      if (exitCode != 0) {
        String output;
        try (java.io.InputStream is = process.getInputStream()) {
          output = new String(is.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);
        } catch (Exception e) {
          output = "Could not read process output";
        }
        throw new RuntimeException("Failed to " + stepName + ". Exit code: " + exitCode + ". Output: " + output);
      }
    } finally {
      process.destroy();
    }
  }

Comment on lines +161 to +163
public String getTruststorePath() {
return truststorePath;
}

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 temporary directory created via Files.createTempDirectory is never cleaned up, which can lead to disk space leaks on the test runners. Override the cleanup() method to delete the temporary directory and its contents when the resource manager is closed.

  public String getTruststorePath() {
    return truststorePath;
  }

  @Override
  public void cleanup() {
    try {
      super.cleanup();
    } finally {
      if (keystorePath != null) {
        Path tempDir = Paths.get(keystorePath).getParent();
        if (tempDir != null && Files.exists(tempDir)) {
          try (java.util.stream.Stream<Path> walk = Files.walk(tempDir)) {
            walk.sorted(java.util.Comparator.reverseOrder())
                .map(Path::toFile)
                .forEach(java.io.File::delete);
          } catch (IOException e) {
            LOG.warn("Failed to delete temporary SSL directory: {}", tempDir, e);
          }
        }
      }
    }
  }

Comment on lines +116 to +123
String truststoreGcsUrl =
"gs://"
+ gcsResourceManager.getBucket()
+ "/"
+ gcsResourceManager
.uploadArtifact(
"input/truststore_Shard1.jks", jdbcResourceManager.getTruststorePath())
.name();

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

Instead of manually constructing the GCS URI by concatenating the bucket name and the uploaded artifact name, use the existing getGcsPath helper method. This is cleaner, less error-prone, and consistent with how other files (like session.json) are handled in this test.

        gcsResourceManager.uploadArtifact(
            "input/truststore_Shard1.jks", jdbcResourceManager.getTruststorePath());
        String truststoreGcsUrl = getGcsPath("input/truststore_Shard1.jks", gcsResourceManager);
References
  1. In template integration tests, getGcsPath has an overloaded signature that accepts a GcsResourceManager as a second argument, in addition to the single-argument version.

Comment on lines +127 to +131
String props =
String.format(
"sslMode=VERIFY_CA&allowPublicKeyRetrieval=true&trustCertificateKeyStoreUrl=%s&trustCertificateKeyStorePassword=%s",
URLEncoder.encode(truststoreLocalUrl, "UTF-8"),
URLEncoder.encode(jdbcResourceManager.getPassword(), "UTF-8"));

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

Using URLEncoder.encode(String, String) with a hardcoded string "UTF-8" is discouraged in modern Java as it throws a checked UnsupportedEncodingException. Use URLEncoder.encode(String, Charset) with StandardCharsets.UTF_8 instead.

Suggested change
String props =
String.format(
"sslMode=VERIFY_CA&allowPublicKeyRetrieval=true&trustCertificateKeyStoreUrl=%s&trustCertificateKeyStorePassword=%s",
URLEncoder.encode(truststoreLocalUrl, "UTF-8"),
URLEncoder.encode(jdbcResourceManager.getPassword(), "UTF-8"));
String props =
String.format(
"sslMode=VERIFY_CA&allowPublicKeyRetrieval=true&trustCertificateKeyStoreUrl=%s&trustCertificateKeyStorePassword=%s",
URLEncoder.encode(truststoreLocalUrl, java.nio.charset.StandardCharsets.UTF_8),
URLEncoder.encode(jdbcResourceManager.getPassword(), java.nio.charset.StandardCharsets.UTF_8));

@codecov

codecov Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 55.92%. Comparing base (3515682) to head (928e88d).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #4192      +/-   ##
============================================
- Coverage     55.94%   55.92%   -0.03%     
+ Complexity     7485     7028     -457     
============================================
  Files          1134     1134              
  Lines         70175    70175              
  Branches       8023     8023              
============================================
- Hits          39261    39244      -17     
- Misses        28367    28381      +14     
- Partials       2547     2550       +3     
Components Coverage Δ
spanner-templates 84.71% <ø> (ø)
spanner-import-export 68.87% <ø> (-0.17%) ⬇️
spanner-live-forward-migration 88.69% <ø> (ø)
spanner-live-reverse-replication 81.40% <ø> (ø)
spanner-bulk-migration 89.08% <ø> (ø)
gcs-spanner-dv 87.92% <ø> (ø)
see 7 files with indirect coverage changes
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant