diff --git a/.ci/cloudbuild.yaml b/.ci/cloudbuild.yaml index 4b5fc9dec..cdc0886f4 100644 --- a/.ci/cloudbuild.yaml +++ b/.ci/cloudbuild.yaml @@ -18,57 +18,87 @@ steps: env: - "IP_TYPE=${_IP_TYPE}" - "JOB_TYPE=integration" - secretEnv: ["MYSQL_CONNECTION_NAME", "MYSQL_USER", "MYSQL_IAM_USER", "MYSQL_PASS", "MYSQL_DB", "POSTGRES_CONNECTION_NAME", "POSTGRES_USER", "POSTGRES_IAM_USER", "POSTGRES_PASS", "POSTGRES_DB", "POSTGRES_CAS_CONNECTION_NAME", "POSTGRES_CAS_PASS", "POSTGRES_CUSTOMER_CAS_CONNECTION_NAME", "POSTGRES_CUSTOMER_CAS_PASS", "POSTGRES_CUSTOMER_CAS_PASS_VALID_DOMAIN_NAME","POSTGRES_CUSTOMER_CAS_PASS_INVALID_DOMAIN_NAME","SQLSERVER_CONNECTION_NAME", "SQLSERVER_USER", "SQLSERVER_PASS", "SQLSERVER_DB", "QUOTA_PROJECT", "IMPERSONATED_USER"] + secretEnv: + [ + "MYSQL_CONNECTION_NAME", + "MYSQL_USER", + "MYSQL_IAM_USER", + "MYSQL_PASS", + "MYSQL_DB", + "MYSQL_MCP_CONNECTION_NAME", + "MYSQL_MCP_PASS", + "POSTGRES_CONNECTION_NAME", + "POSTGRES_USER", + "POSTGRES_IAM_USER", + "POSTGRES_PASS", + "POSTGRES_DB", + "POSTGRES_CAS_CONNECTION_NAME", + "POSTGRES_CAS_PASS", + "POSTGRES_CUSTOMER_CAS_CONNECTION_NAME", + "POSTGRES_CUSTOMER_CAS_PASS", + "POSTGRES_CUSTOMER_CAS_PASS_VALID_DOMAIN_NAME", + "POSTGRES_CUSTOMER_CAS_PASS_INVALID_DOMAIN_NAME", + "SQLSERVER_CONNECTION_NAME", + "SQLSERVER_USER", + "SQLSERVER_PASS", + "SQLSERVER_DB", + "QUOTA_PROJECT", + "IMPERSONATED_USER" + ] args: - "-c" - | ./.github/scripts/run_tests.sh availableSecrets: secretManager: - - versionName: 'projects/$PROJECT_ID/secrets/MYSQL_CONNECTION_NAME/versions/latest' - env: 'MYSQL_CONNECTION_NAME' - - versionName: 'projects/$PROJECT_ID/secrets/MYSQL_USER/versions/latest' - env: 'MYSQL_USER' - - versionName: 'projects/$PROJECT_ID/secrets/CLOUD_BUILD_MYSQL_IAM_USER/versions/latest' - env: 'MYSQL_IAM_USER' - - versionName: 'projects/$PROJECT_ID/secrets/MYSQL_PASS/versions/latest' - env: 'MYSQL_PASS' - - versionName: 'projects/$PROJECT_ID/secrets/MYSQL_DB/versions/latest' - env: 'MYSQL_DB' - - versionName: 'projects/$PROJECT_ID/secrets/POSTGRES_CONNECTION_NAME/versions/latest' - env: 'POSTGRES_CONNECTION_NAME' - - versionName: 'projects/$PROJECT_ID/secrets/POSTGRES_USER/versions/latest' - env: 'POSTGRES_USER' - - versionName: 'projects/$PROJECT_ID/secrets/CLOUD_BUILD_POSTGRES_IAM_USER/versions/latest' - env: 'POSTGRES_IAM_USER' - - versionName: 'projects/$PROJECT_ID/secrets/POSTGRES_PASS/versions/latest' - env: 'POSTGRES_PASS' - - versionName: 'projects/$PROJECT_ID/secrets/POSTGRES_DB/versions/latest' - env: 'POSTGRES_DB' - - versionName: 'projects/$PROJECT_ID/secrets/POSTGRES_CAS_CONNECTION_NAME/versions/latest' - env: 'POSTGRES_CAS_CONNECTION_NAME' - - versionName: 'projects/$PROJECT_ID/secrets/POSTGRES_CAS_PASS/versions/latest' - env: 'POSTGRES_CAS_PASS' - - versionName: 'projects/$PROJECT_ID/secrets/POSTGRES_CUSTOMER_CAS_CONNECTION_NAME/versions/latest' - env: 'POSTGRES_CUSTOMER_CAS_CONNECTION_NAME' - - versionName: 'projects/$PROJECT_ID/secrets/POSTGRES_CUSTOMER_CAS_PASS/versions/latest' - env: 'POSTGRES_CUSTOMER_CAS_PASS' - - versionName: 'projects/$PROJECT_ID/secrets/POSTGRES_CUSTOMER_CAS_PASS_VALID_DOMAIN_NAME/versions/latest' - env: 'POSTGRES_CUSTOMER_CAS_PASS_VALID_DOMAIN_NAME' - - versionName: 'projects/$PROJECT_ID/secrets/POSTGRES_CUSTOMER_CAS_PASS_INVALID_DOMAIN_NAME/versions/latest' - env: 'POSTGRES_CUSTOMER_CAS_PASS_INVALID_DOMAIN_NAME' - - versionName: 'projects/$PROJECT_ID/secrets/SQLSERVER_CONNECTION_NAME/versions/latest' - env: 'SQLSERVER_CONNECTION_NAME' - - versionName: 'projects/$PROJECT_ID/secrets/SQLSERVER_USER/versions/latest' - env: 'SQLSERVER_USER' - - versionName: 'projects/$PROJECT_ID/secrets/SQLSERVER_PASS/versions/latest' - env: 'SQLSERVER_PASS' - - versionName: 'projects/$PROJECT_ID/secrets/SQLSERVER_DB/versions/latest' - env: 'SQLSERVER_DB' - - versionName: 'projects/$PROJECT_ID/secrets/QUOTA_PROJECT/versions/latest' - env: 'QUOTA_PROJECT' - - versionName: 'projects/$PROJECT_ID/secrets/CLOUD_BUILD_SA/versions/latest' - env: 'IMPERSONATED_USER' + - versionName: "projects/$PROJECT_ID/secrets/MYSQL_CONNECTION_NAME/versions/latest" + env: "MYSQL_CONNECTION_NAME" + - versionName: "projects/$PROJECT_ID/secrets/MYSQL_USER/versions/latest" + env: "MYSQL_USER" + - versionName: "projects/$PROJECT_ID/secrets/CLOUD_BUILD_MYSQL_IAM_USER/versions/latest" + env: "MYSQL_IAM_USER" + - versionName: "projects/$PROJECT_ID/secrets/MYSQL_PASS/versions/latest" + env: "MYSQL_PASS" + - versionName: "projects/$PROJECT_ID/secrets/MYSQL_DB/versions/latest" + env: "MYSQL_DB" + - versionName: "projects/$PROJECT_ID/secrets/MYSQL_MCP_CONNECTION_NAME/versions/latest" + env: "MYSQL_MCP_CONNECTION_NAME" + - versionName: "projects/$PROJECT_ID/secrets/MYSQL_MCP_PASS/versions/latest" + env: "MYSQL_MCP_PASS" + - versionName: "projects/$PROJECT_ID/secrets/POSTGRES_CONNECTION_NAME/versions/latest" + env: "POSTGRES_CONNECTION_NAME" + - versionName: "projects/$PROJECT_ID/secrets/POSTGRES_USER/versions/latest" + env: "POSTGRES_USER" + - versionName: "projects/$PROJECT_ID/secrets/CLOUD_BUILD_POSTGRES_IAM_USER/versions/latest" + env: "POSTGRES_IAM_USER" + - versionName: "projects/$PROJECT_ID/secrets/POSTGRES_PASS/versions/latest" + env: "POSTGRES_PASS" + - versionName: "projects/$PROJECT_ID/secrets/POSTGRES_DB/versions/latest" + env: "POSTGRES_DB" + - versionName: "projects/$PROJECT_ID/secrets/POSTGRES_CAS_CONNECTION_NAME/versions/latest" + env: "POSTGRES_CAS_CONNECTION_NAME" + - versionName: "projects/$PROJECT_ID/secrets/POSTGRES_CAS_PASS/versions/latest" + env: "POSTGRES_CAS_PASS" + - versionName: "projects/$PROJECT_ID/secrets/POSTGRES_CUSTOMER_CAS_CONNECTION_NAME/versions/latest" + env: "POSTGRES_CUSTOMER_CAS_CONNECTION_NAME" + - versionName: "projects/$PROJECT_ID/secrets/POSTGRES_CUSTOMER_CAS_PASS/versions/latest" + env: "POSTGRES_CUSTOMER_CAS_PASS" + - versionName: "projects/$PROJECT_ID/secrets/POSTGRES_CUSTOMER_CAS_PASS_VALID_DOMAIN_NAME/versions/latest" + env: "POSTGRES_CUSTOMER_CAS_PASS_VALID_DOMAIN_NAME" + - versionName: "projects/$PROJECT_ID/secrets/POSTGRES_CUSTOMER_CAS_PASS_INVALID_DOMAIN_NAME/versions/latest" + env: "POSTGRES_CUSTOMER_CAS_PASS_INVALID_DOMAIN_NAME" + - versionName: "projects/$PROJECT_ID/secrets/SQLSERVER_CONNECTION_NAME/versions/latest" + env: "SQLSERVER_CONNECTION_NAME" + - versionName: "projects/$PROJECT_ID/secrets/SQLSERVER_USER/versions/latest" + env: "SQLSERVER_USER" + - versionName: "projects/$PROJECT_ID/secrets/SQLSERVER_PASS/versions/latest" + env: "SQLSERVER_PASS" + - versionName: "projects/$PROJECT_ID/secrets/SQLSERVER_DB/versions/latest" + env: "SQLSERVER_DB" + - versionName: "projects/$PROJECT_ID/secrets/QUOTA_PROJECT/versions/latest" + env: "QUOTA_PROJECT" + - versionName: "projects/$PROJECT_ID/secrets/CLOUD_BUILD_SA/versions/latest" + env: "IMPERSONATED_USER" substitutions: _MAVEN_VERSION: ${_VERSION} _IP_TYPE: ${_IP_TYPE} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5ad020ba9..f20b48693 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -130,6 +130,8 @@ jobs: MYSQL_USER:${{ vars.GOOGLE_CLOUD_PROJECT }}/MYSQL_USER MYSQL_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/MYSQL_PASS MYSQL_DB:${{ vars.GOOGLE_CLOUD_PROJECT }}/MYSQL_DB + MYSQL_MCP_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT}}/MYSQL_MCP_CONNECTION_NAME + MYSQL_MCP_PASS:${{ vars.GOOGLE_CLOUD_PROJECT}}/MYSQL_MCP_PASS MYSQL_IAM_USER_JAVA:${{ vars.GOOGLE_CLOUD_PROJECT }}/MYSQL_USER_IAM_JAVA POSTGRES_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CONNECTION_NAME POSTGRES_USER:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_USER @@ -154,6 +156,8 @@ jobs: MYSQL_USER: "${{ steps.secrets.outputs.MYSQL_USER }}" MYSQL_PASS: "${{ steps.secrets.outputs.MYSQL_PASS }}" MYSQL_DB: "${{ steps.secrets.outputs.MYSQL_DB }}" + MYSQL_MCP_CONNECTION_NAME: "${{ steps.secrets.outputs.MYSQL_MCP_CONNECTION_NAME }}" + MYSQL_MCP_PASS: "${{ steps.secrets.outputs.MYSQL_MCP_PASS }}" MYSQL_IAM_USER: "${{ steps.secrets.outputs.MYSQL_IAM_USER_JAVA }}" POSTGRES_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CONNECTION_NAME }}" POSTGRES_USER: "${{ steps.secrets.outputs.POSTGRES_USER }}" @@ -237,6 +241,8 @@ jobs: MYSQL_USER:${{ vars.GOOGLE_CLOUD_PROJECT }}/MYSQL_USER MYSQL_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/MYSQL_PASS MYSQL_DB:${{ vars.GOOGLE_CLOUD_PROJECT }}/MYSQL_DB + MYSQL_MCP_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT}}/MYSQL_MCP_CONNECTION_NAME + MYSQL_MCP_PASS:${{ vars.GOOGLE_CLOUD_PROJECT}}/MYSQL_MCP_PASS MYSQL_IAM_USER_JAVA:${{ vars.GOOGLE_CLOUD_PROJECT }}/MYSQL_USER_IAM_JAVA POSTGRES_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CONNECTION_NAME POSTGRES_USER:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_USER @@ -262,6 +268,8 @@ jobs: MYSQL_USER: "${{ steps.secrets.outputs.MYSQL_USER }}" MYSQL_PASS: "${{ steps.secrets.outputs.MYSQL_PASS }}" MYSQL_DB: "${{ steps.secrets.outputs.MYSQL_DB }}" + MYSQL_MCP_CONNECTION_NAME: "${{ steps.secrets.outputs.MYSQL_MCP_CONNECTION_NAME }}" + MYSQL_MCP_PASS: "${{ steps.secrets.outputs.MYSQL_MCP_PASS }}" MYSQL_IAM_USER: "${{ steps.secrets.outputs.MYSQL_IAM_USER_JAVA }}" POSTGRES_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CONNECTION_NAME }}" POSTGRES_USER: "${{ steps.secrets.outputs.POSTGRES_USER }}" diff --git a/jdbc/mysql-j-8/src/test/java/com/google/cloud/sql/mysql/JdbcMysqlJ8McpIamAuthIntegrationTests.java b/jdbc/mysql-j-8/src/test/java/com/google/cloud/sql/mysql/JdbcMysqlJ8McpIamAuthIntegrationTests.java new file mode 100644 index 000000000..a0c119fbd --- /dev/null +++ b/jdbc/mysql-j-8/src/test/java/com/google/cloud/sql/mysql/JdbcMysqlJ8McpIamAuthIntegrationTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.sql.mysql; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import com.google.common.collect.ImmutableList; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.Timeout; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class JdbcMysqlJ8McpIamAuthIntegrationTests { + + private static final String CONNECTION_NAME = System.getenv("MYSQL_MCP_CONNECTION_NAME"); + private static final String DB_NAME = System.getenv("MYSQL_DB"); + private static final String DB_USER = System.getenv("MYSQL_IAM_USER"); + private static final String IP_TYPE = + System.getenv("IP_TYPE") == null ? "PUBLIC" : System.getenv("IP_TYPE"); + + private static final ImmutableList requiredEnvVars = + ImmutableList.of("MYSQL_IAM_USER", "MYSQL_DB", "MYSQL_MCP_CONNECTION_NAME"); + @Rule public Timeout globalTimeout = new Timeout(80, TimeUnit.SECONDS); + private HikariDataSource connectionPool; + + @BeforeClass + public static void checkEnvVars() { + // Check that required env vars are set + requiredEnvVars.forEach( + (varName) -> + assertWithMessage( + String.format( + "Environment variable '%s' must be set to perform these tests.", varName)) + .that(System.getenv(varName)) + .isNotEmpty()); + } + + @Before + public void setUpPool() throws SQLException { + // Set up URL parameters + String jdbcURL = String.format("jdbc:mysql:///%s", DB_NAME); + Properties connProps = new Properties(); + connProps.setProperty("user", DB_USER); + connProps.setProperty("sslmode", "disable"); + connProps.setProperty("socketFactory", "com.google.cloud.sql.mysql.SocketFactory"); + connProps.setProperty("cloudSqlInstance", CONNECTION_NAME); + connProps.setProperty("ipTypes", IP_TYPE); + connProps.setProperty("enableIamAuth", "true"); + + // Initialize connection pool + HikariConfig config = new HikariConfig(); + config.setJdbcUrl(jdbcURL); + config.setDataSourceProperties(connProps); + config.setConnectionTimeout(10000); // 10s + + this.connectionPool = new HikariDataSource(config); + } + + @Test + public void pooledConnectionTest() throws SQLException { + + List rows = new ArrayList<>(); + try (Connection conn = connectionPool.getConnection()) { + try (PreparedStatement selectStmt = conn.prepareStatement("SELECT NOW() as TS")) { + ResultSet rs = selectStmt.executeQuery(); + while (rs.next()) { + rows.add(rs.getTimestamp("TS")); + } + } + } + assertThat(rows.size()).isEqualTo(1); + } +} diff --git a/jdbc/mysql-j-8/src/test/java/com/google/cloud/sql/mysql/JdbcMysqlJ8McpIntegrationTests.java b/jdbc/mysql-j-8/src/test/java/com/google/cloud/sql/mysql/JdbcMysqlJ8McpIntegrationTests.java new file mode 100644 index 000000000..3b6b00481 --- /dev/null +++ b/jdbc/mysql-j-8/src/test/java/com/google/cloud/sql/mysql/JdbcMysqlJ8McpIntegrationTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.sql.mysql; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import com.google.common.collect.ImmutableList; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.Timeout; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class JdbcMysqlJ8McpIntegrationTests { + + private static final String CONNECTION_NAME = System.getenv("MYSQL_MCP_CONNECTION_NAME"); + private static final String DB_NAME = System.getenv("MYSQL_DB"); + private static final String DB_USER = System.getenv("MYSQL_USER"); + private static final String DB_PASSWORD = System.getenv("MYSQL_MCP_PASS"); + private static final String IP_TYPE = + System.getenv("IP_TYPE") == null ? "PUBLIC" : System.getenv("IP_TYPE"); + private static final ImmutableList requiredEnvVars = + ImmutableList.of("MYSQL_USER", "MYSQL_MCP_PASS", "MYSQL_DB", "MYSQL_MCP_CONNECTION_NAME"); + @Rule public Timeout globalTimeout = new Timeout(80, TimeUnit.SECONDS); + private HikariDataSource connectionPool; + + @BeforeClass + public static void checkEnvVars() { + // Check that required env vars are set + requiredEnvVars.forEach( + (varName) -> + assertWithMessage( + String.format( + "Environment variable '%s' must be set to perform these tests.", varName)) + .that(System.getenv(varName)) + .isNotEmpty()); + } + + @Before + public void setUpPool() throws SQLException { + // Set up URL parameters + String jdbcURL = String.format("jdbc:mysql://db.example.com/%s", DB_NAME); + Properties connProps = new Properties(); + connProps.setProperty("user", DB_USER); + connProps.setProperty("password", DB_PASSWORD); + connProps.setProperty("socketFactory", "com.google.cloud.sql.mysql.SocketFactory"); + connProps.setProperty("ipTypes", IP_TYPE); + connProps.setProperty("cloudSqlInstance", CONNECTION_NAME); + + // Initialize connection pool + HikariConfig config = new HikariConfig(); + config.setJdbcUrl(jdbcURL); + config.setDataSourceProperties(connProps); + config.setConnectionTimeout(10000); // 10s + + this.connectionPool = new HikariDataSource(config); + } + + @Test + public void pooledConnectionTest() throws SQLException { + + List rows = new ArrayList<>(); + try (Connection conn = connectionPool.getConnection()) { + try (PreparedStatement selectStmt = conn.prepareStatement("SELECT NOW() as TS")) { + ResultSet rs = selectStmt.executeQuery(); + while (rs.next()) { + rows.add(rs.getTimestamp("TS")); + } + } + } + assertThat(rows.size()).isEqualTo(1); + } +}