This is an automated email from the ASF dual-hosted git repository.
jshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 116e5ae15 [#4107] feat(all): Add testConnection API for catalog (#4108)
116e5ae15 is described below
commit 116e5ae1591feca15b8eef20b3d77fcb3a99e8a8
Author: mchades <[email protected]>
AuthorDate: Tue Jul 16 18:54:29 2024 +0800
[#4107] feat(all): Add testConnection API for catalog (#4108)
### What changes were proposed in this pull request?
Add testConnection API for catalog
### Why are the changes needed?
Fix: #4107
### Does this PR introduce _any_ user-facing change?
yes, add a new API
### How was this patch tested?
tests added
---
.../org/apache/gravitino/SupportsCatalogs.java | 18 ++++
.../exceptions/ConnectionFailedException.java | 49 +++++++++
build.gradle.kts | 4 +-
.../catalog/hadoop/HadoopCatalogOperations.java | 21 ++++
.../hadoop/TestHadoopCatalogOperations.java | 14 +++
.../hadoop/integration/test/HadoopCatalogIT.java | 3 +-
.../test/HadoopUserImpersonationIT.java | 3 +-
.../catalog/hive/HiveCatalogOperations.java | 27 +++++
.../catalog/hive/TestHiveCatalogOperations.java | 28 +++++
.../catalog/jdbc/JdbcCatalogOperations.java | 20 ++++
.../jdbc/converter/JdbcExceptionConverter.java | 4 +-
.../catalog/jdbc/TestJdbcCatalogOperations.java | 54 ++++++++++
.../jdbc/operation/SqliteDatabaseOperations.java | 16 ++-
.../catalog/kafka/KafkaCatalogOperations.java | 23 +++-
.../catalog/kafka/TestKafkaCatalogOperations.java | 15 +++
.../kafka/integration/test/CatalogKafkaIT.java | 28 ++++-
.../iceberg/IcebergCatalogOperations.java | 26 +++++
.../iceberg/TestIcebergCatalogOperations.java | 45 ++++++++
.../lakehouse/paimon/PaimonCatalogOperations.java | 25 +++++
.../lakehouse/paimon/TestPaimonCatalog.java | 13 +++
.../org/apache/gravitino/client/ErrorHandlers.java | 4 +
.../apache/gravitino/client/GravitinoClient.java | 22 ++++
.../apache/gravitino/client/GravitinoMetalake.java | 43 ++++++++
.../gravitino/client/TestGravitinoClient.java | 65 ++++++++++++
.../gravitino/dto/responses/ErrorConstants.java | 3 +
.../gravitino/dto/responses/ErrorResponse.java | 26 +++++
.../org/apache/gravitino/StringIdentifier.java | 3 +
.../apache/gravitino/catalog/CatalogManager.java | 116 +++++++++++++++++----
.../catalog/CatalogNormalizeDispatcher.java | 12 +++
.../apache/gravitino/catalog/SupportsCatalogs.java | 18 ++++
.../gravitino/connector/CatalogOperations.java | 20 ++++
.../gravitino/listener/CatalogEventDispatcher.java | 12 +++
.../gravitino/catalog/DummyCatalogOperations.java | 11 ++
.../gravitino/catalog/TestCatalogManager.java | 22 +++-
.../gravitino/connector/TestCatalogOperations.java | 16 +++
docs/open-api/catalogs.yaml | 70 +++++++++++++
docs/open-api/openapi.yaml | 3 +
.../integration/test/client/CatalogIT.java | 20 +++-
.../integration/test/client/MetalakeIT.java | 3 +-
.../org/apache/gravitino/server/web/Utils.java | 14 +++
.../server/web/rest/CatalogOperations.java | 39 +++++++
.../server/web/rest/ExceptionHandlers.java | 31 ++++++
.../org/apache/gravitino/server/web/TestUtils.java | 10 ++
.../gravitino/server/web/rest/TestCatalog.java | 10 ++
.../server/web/rest/TestCatalogOperations.java | 55 ++++++++--
45 files changed, 1038 insertions(+), 46 deletions(-)
diff --git a/api/src/main/java/org/apache/gravitino/SupportsCatalogs.java
b/api/src/main/java/org/apache/gravitino/SupportsCatalogs.java
index c3805edec..8644430bc 100644
--- a/api/src/main/java/org/apache/gravitino/SupportsCatalogs.java
+++ b/api/src/main/java/org/apache/gravitino/SupportsCatalogs.java
@@ -114,4 +114,22 @@ public interface SupportsCatalogs {
* @return True if the catalog was dropped, false otherwise.
*/
boolean dropCatalog(String catalogName);
+
+ /**
+ * Test whether the catalog with specified parameters can be connected to
before creating it.
+ *
+ * @param catalogName the name of the catalog.
+ * @param type the type of the catalog.
+ * @param provider the provider of the catalog.
+ * @param comment the comment of the catalog.
+ * @param properties the properties of the catalog.
+ * @throws Exception if the test failed.
+ */
+ void testConnection(
+ String catalogName,
+ Catalog.Type type,
+ String provider,
+ String comment,
+ Map<String, String> properties)
+ throws Exception;
}
diff --git
a/api/src/main/java/org/apache/gravitino/exceptions/ConnectionFailedException.java
b/api/src/main/java/org/apache/gravitino/exceptions/ConnectionFailedException.java
new file mode 100644
index 000000000..711a3ea2f
--- /dev/null
+++
b/api/src/main/java/org/apache/gravitino/exceptions/ConnectionFailedException.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * http://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 org.apache.gravitino.exceptions;
+
+import com.google.errorprone.annotations.FormatMethod;
+import com.google.errorprone.annotations.FormatString;
+
+/** An exception thrown when connect to catalog failed. */
+public class ConnectionFailedException extends GravitinoRuntimeException {
+ /**
+ * Constructs a new exception with the specified detail message.
+ *
+ * @param cause the cause.
+ * @param errorMessageTemplate the detail message.
+ * @param args the arguments to the message.
+ */
+ @FormatMethod
+ public ConnectionFailedException(
+ Throwable cause, @FormatString String errorMessageTemplate, Object...
args) {
+ super(cause, errorMessageTemplate, args);
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message.
+ *
+ * @param errorMessageTemplate the detail message.
+ * @param args the arguments to the message.
+ */
+ @FormatMethod
+ public ConnectionFailedException(@FormatString String errorMessageTemplate,
Object... args) {
+ super(errorMessageTemplate, args);
+ }
+}
diff --git a/build.gradle.kts b/build.gradle.kts
index ead9f0167..7db69dcd5 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -490,7 +490,9 @@ tasks.rat {
"clients/client-python/.venv/*",
"clients/client-python/gravitino.egg-info/*",
"clients/client-python/gravitino/utils/exceptions.py",
- "clients/client-python/gravitino/utils/http_client.py"
+ "clients/client-python/gravitino/utils/http_client.py",
+ "clients/client-python/tests/unittests/htmlcov/*",
+ "clients/client-python/tests/integration/htmlcov/*"
)
// Add .gitignore excludes to the Apache Rat exclusion list.
diff --git
a/catalogs/catalog-hadoop/src/main/java/org/apache/gravitino/catalog/hadoop/HadoopCatalogOperations.java
b/catalogs/catalog-hadoop/src/main/java/org/apache/gravitino/catalog/hadoop/HadoopCatalogOperations.java
index eb77ac4fb..b82eaa359 100644
---
a/catalogs/catalog-hadoop/src/main/java/org/apache/gravitino/catalog/hadoop/HadoopCatalogOperations.java
+++
b/catalogs/catalog-hadoop/src/main/java/org/apache/gravitino/catalog/hadoop/HadoopCatalogOperations.java
@@ -34,6 +34,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.Catalog;
import org.apache.gravitino.Entity;
import org.apache.gravitino.EntityStore;
import org.apache.gravitino.GravitinoEnv;
@@ -538,6 +539,26 @@ public class HadoopCatalogOperations implements
CatalogOperations, SupportsSchem
}
}
+ /**
+ * Since the Hadoop catalog was completely managed by Gravitino, we don't
need to test the
+ * connection
+ *
+ * @param catalogIdent the name of the catalog.
+ * @param type the type of the catalog.
+ * @param provider the provider of the catalog.
+ * @param comment the comment of the catalog.
+ * @param properties the properties of the catalog.
+ */
+ @Override
+ public void testConnection(
+ NameIdentifier catalogIdent,
+ Catalog.Type type,
+ String provider,
+ String comment,
+ Map<String, String> properties) {
+ // Do nothing
+ }
+
@Override
public void close() throws IOException {}
diff --git
a/catalogs/catalog-hadoop/src/test/java/org/apache/gravitino/catalog/hadoop/TestHadoopCatalogOperations.java
b/catalogs/catalog-hadoop/src/test/java/org/apache/gravitino/catalog/hadoop/TestHadoopCatalogOperations.java
index 73b0c5dfc..284070f0b 100644
---
a/catalogs/catalog-hadoop/src/test/java/org/apache/gravitino/catalog/hadoop/TestHadoopCatalogOperations.java
+++
b/catalogs/catalog-hadoop/src/test/java/org/apache/gravitino/catalog/hadoop/TestHadoopCatalogOperations.java
@@ -50,6 +50,7 @@ import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.io.FileUtils;
+import org.apache.gravitino.Catalog;
import org.apache.gravitino.Config;
import org.apache.gravitino.EntityStore;
import org.apache.gravitino.EntityStoreFactory;
@@ -703,6 +704,19 @@ public class TestHadoopCatalogOperations {
}
}
+ @Test
+ public void testTestConnection() {
+ HadoopCatalogOperations catalogOperations = new
HadoopCatalogOperations(store);
+ Assertions.assertDoesNotThrow(
+ () ->
+ catalogOperations.testConnection(
+ NameIdentifier.of("metalake", "catalog"),
+ Catalog.Type.FILESET,
+ "hadoop",
+ "comment",
+ ImmutableMap.of()));
+ }
+
private static Stream<Arguments> locationArguments() {
return Stream.of(
// Honor the catalog location
diff --git
a/catalogs/catalog-hadoop/src/test/java/org/apache/gravitino/catalog/hadoop/integration/test/HadoopCatalogIT.java
b/catalogs/catalog-hadoop/src/test/java/org/apache/gravitino/catalog/hadoop/integration/test/HadoopCatalogIT.java
index 35786080a..3079e9203 100644
---
a/catalogs/catalog-hadoop/src/test/java/org/apache/gravitino/catalog/hadoop/integration/test/HadoopCatalogIT.java
+++
b/catalogs/catalog-hadoop/src/test/java/org/apache/gravitino/catalog/hadoop/integration/test/HadoopCatalogIT.java
@@ -31,6 +31,7 @@ import org.apache.gravitino.Namespace;
import org.apache.gravitino.Schema;
import org.apache.gravitino.client.GravitinoMetalake;
import org.apache.gravitino.exceptions.FilesetAlreadyExistsException;
+import org.apache.gravitino.exceptions.IllegalNameIdentifierException;
import org.apache.gravitino.exceptions.NoSuchFilesetException;
import org.apache.gravitino.file.Fileset;
import org.apache.gravitino.file.FilesetChange;
@@ -187,7 +188,7 @@ public class HadoopCatalogIT extends AbstractIT {
// create fileset with null fileset name
Assertions.assertThrows(
- IllegalArgumentException.class,
+ IllegalNameIdentifierException.class,
() ->
createFileset(
null,
diff --git
a/catalogs/catalog-hadoop/src/test/java/org/apache/gravitino/catalog/hadoop/integration/test/HadoopUserImpersonationIT.java
b/catalogs/catalog-hadoop/src/test/java/org/apache/gravitino/catalog/hadoop/integration/test/HadoopUserImpersonationIT.java
index 5edab4766..248b8d54f 100644
---
a/catalogs/catalog-hadoop/src/test/java/org/apache/gravitino/catalog/hadoop/integration/test/HadoopUserImpersonationIT.java
+++
b/catalogs/catalog-hadoop/src/test/java/org/apache/gravitino/catalog/hadoop/integration/test/HadoopUserImpersonationIT.java
@@ -43,6 +43,7 @@ import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Schema;
import org.apache.gravitino.client.GravitinoMetalake;
import org.apache.gravitino.exceptions.FilesetAlreadyExistsException;
+import org.apache.gravitino.exceptions.IllegalNameIdentifierException;
import org.apache.gravitino.file.Fileset;
import org.apache.gravitino.integration.test.util.AbstractIT;
import org.apache.gravitino.integration.test.util.GravitinoITUtils;
@@ -398,7 +399,7 @@ public class HadoopUserImpersonationIT extends AbstractIT {
// create fileset with null fileset name
Assertions.assertThrows(
- IllegalArgumentException.class,
+ IllegalNameIdentifierException.class,
() ->
createFileset(
null,
diff --git
a/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/HiveCatalogOperations.java
b/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/HiveCatalogOperations.java
index 15357715e..2036a6b51 100644
---
a/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/HiveCatalogOperations.java
+++
b/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/HiveCatalogOperations.java
@@ -55,6 +55,7 @@ import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.Catalog;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.SchemaChange;
@@ -64,6 +65,7 @@ import org.apache.gravitino.connector.CatalogOperations;
import org.apache.gravitino.connector.HasPropertyMetadata;
import org.apache.gravitino.connector.ProxyPlugin;
import org.apache.gravitino.connector.SupportsSchemas;
+import org.apache.gravitino.exceptions.ConnectionFailedException;
import org.apache.gravitino.exceptions.NoSuchCatalogException;
import org.apache.gravitino.exceptions.NoSuchSchemaException;
import org.apache.gravitino.exceptions.NoSuchTableException;
@@ -85,6 +87,7 @@ import org.apache.gravitino.rel.indexes.Index;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hive.conf.HiveConf;
import org.apache.hadoop.hive.conf.HiveConf.ConfVars;
+import org.apache.hadoop.hive.metastore.IMetaStoreClient;
import org.apache.hadoop.hive.metastore.api.AlreadyExistsException;
import org.apache.hadoop.hive.metastore.api.Database;
import org.apache.hadoop.hive.metastore.api.FieldSchema;
@@ -1091,6 +1094,30 @@ public class HiveCatalogOperations implements
CatalogOperations, SupportsSchemas
}
}
+ /**
+ * Performs `getAllDatabases` operation in Hive Metastore to test the
connection.
+ *
+ * @param catalogIdent the name of the catalog.
+ * @param type the type of the catalog.
+ * @param provider the provider of the catalog.
+ * @param comment the comment of the catalog.
+ * @param properties the properties of the catalog.
+ */
+ @Override
+ public void testConnection(
+ NameIdentifier catalogIdent,
+ Catalog.Type type,
+ String provider,
+ String comment,
+ Map<String, String> properties) {
+ try {
+ clientPool.run(IMetaStoreClient::getAllDatabases);
+ } catch (Exception e) {
+ throw new ConnectionFailedException(
+ e, "Failed to run getAllDatabases in Hive Metastore: %s",
e.getMessage());
+ }
+ }
+
/**
* Checks if the given namespace is a valid namespace for the Hive schema.
*
diff --git
a/catalogs/catalog-hive/src/test/java/org/apache/gravitino/catalog/hive/TestHiveCatalogOperations.java
b/catalogs/catalog-hive/src/test/java/org/apache/gravitino/catalog/hive/TestHiveCatalogOperations.java
index 256492880..e78d81ad0 100644
---
a/catalogs/catalog-hive/src/test/java/org/apache/gravitino/catalog/hive/TestHiveCatalogOperations.java
+++
b/catalogs/catalog-hive/src/test/java/org/apache/gravitino/catalog/hive/TestHiveCatalogOperations.java
@@ -32,13 +32,20 @@ import static
org.apache.gravitino.catalog.hive.HiveCatalogPropertiesMeta.METAST
import static
org.apache.gravitino.catalog.hive.HiveCatalogPropertiesMeta.PRINCIPAL;
import static
org.apache.gravitino.catalog.hive.TestHiveCatalog.HIVE_PROPERTIES_METADATA;
import static org.apache.gravitino.connector.BaseCatalog.CATALOG_BYPASS_PREFIX;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import java.util.Map;
import org.apache.gravitino.Catalog;
+import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.connector.BaseCatalog;
import org.apache.gravitino.connector.PropertyEntry;
+import org.apache.gravitino.exceptions.ConnectionFailedException;
import org.apache.hadoop.hive.conf.HiveConf.ConfVars;
+import org.apache.thrift.TException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -126,4 +133,25 @@ class TestHiveCatalogOperations {
Assertions.assertEquals("v2", op.hiveConf.get("a.b"));
Assertions.assertEquals("v4", op.hiveConf.get("c.d"));
}
+
+ @Test
+ void testTestConnection() throws TException, InterruptedException {
+ HiveCatalogOperations op = new HiveCatalogOperations();
+ op.clientPool = mock(CachedClientPool.class);
+ when(op.clientPool.run(any())).thenThrow(new TException("mock connection
exception"));
+
+ ConnectionFailedException exception =
+ Assertions.assertThrows(
+ ConnectionFailedException.class,
+ () ->
+ op.testConnection(
+ NameIdentifier.of("metalake", "catalog"),
+ Catalog.Type.RELATIONAL,
+ "hive",
+ "comment",
+ ImmutableMap.of()));
+ Assertions.assertEquals(
+ "Failed to run getAllDatabases in Hive Metastore: mock connection
exception",
+ exception.getMessage());
+ }
}
diff --git
a/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/JdbcCatalogOperations.java
b/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/JdbcCatalogOperations.java
index 610a556f6..8961df99e 100644
---
a/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/JdbcCatalogOperations.java
+++
b/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/JdbcCatalogOperations.java
@@ -36,6 +36,7 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.sql.DataSource;
import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.Catalog;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.SchemaChange;
@@ -173,6 +174,25 @@ public class JdbcCatalogOperations implements
CatalogOperations, SupportsSchemas
.toArray(NameIdentifier[]::new);
}
+ /**
+ * Performs `show databases` operation to check if the JDBC connection is
valid.
+ *
+ * @param catalogIdent the name of the catalog.
+ * @param type the type of the catalog.
+ * @param provider the provider of the catalog.
+ * @param comment the comment of the catalog.
+ * @param properties the properties of the catalog.
+ */
+ @Override
+ public void testConnection(
+ NameIdentifier catalogIdent,
+ Catalog.Type type,
+ String provider,
+ String comment,
+ Map<String, String> properties) {
+ databaseOperation.listDatabases();
+ }
+
/**
* Creates a new schema with the provided identifier, comment and metadata.
*
diff --git
a/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/converter/JdbcExceptionConverter.java
b/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/converter/JdbcExceptionConverter.java
index be50433a6..7d8e5cc92 100644
---
a/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/converter/JdbcExceptionConverter.java
+++
b/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/converter/JdbcExceptionConverter.java
@@ -28,8 +28,8 @@ public abstract class JdbcExceptionConverter {
* Convert JDBC exception to GravitinoException.
*
* @param sqlException The sql exception to map
- * @return A best attempt at a corresponding connector exception or generic
with the SQLException
- * as the cause
+ * @return The best attempt at a corresponding connector exception or
generic with the
+ * SQLException as the cause
*/
@SuppressWarnings("FormatStringAnnotation")
public GravitinoRuntimeException toGravitinoException(final SQLException
sqlException) {
diff --git
a/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/TestJdbcCatalogOperations.java
b/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/TestJdbcCatalogOperations.java
new file mode 100644
index 000000000..aab5760f9
--- /dev/null
+++
b/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/TestJdbcCatalogOperations.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * http://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 org.apache.gravitino.catalog.jdbc;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.NameIdentifier;
+import
org.apache.gravitino.catalog.jdbc.converter.SqliteColumnDefaultValueConverter;
+import org.apache.gravitino.catalog.jdbc.converter.SqliteExceptionConverter;
+import org.apache.gravitino.catalog.jdbc.converter.SqliteTypeConverter;
+import org.apache.gravitino.catalog.jdbc.operation.SqliteDatabaseOperations;
+import org.apache.gravitino.catalog.jdbc.operation.SqliteTableOperations;
+import org.apache.gravitino.exceptions.GravitinoRuntimeException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestJdbcCatalogOperations {
+
+ @Test
+ public void testTestConnection() {
+ JdbcCatalogOperations catalogOperations =
+ new JdbcCatalogOperations(
+ new SqliteExceptionConverter(),
+ new SqliteTypeConverter(),
+ new SqliteDatabaseOperations("/illegal/path"),
+ new SqliteTableOperations(),
+ new SqliteColumnDefaultValueConverter());
+ Assertions.assertThrows(
+ GravitinoRuntimeException.class,
+ () ->
+ catalogOperations.testConnection(
+ NameIdentifier.of("metalake", "catalog"),
+ Catalog.Type.RELATIONAL,
+ "sqlite",
+ "comment",
+ ImmutableMap.of()));
+ }
+}
diff --git
a/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/operation/SqliteDatabaseOperations.java
b/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/operation/SqliteDatabaseOperations.java
index f65c585af..282f1f48b 100644
---
a/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/operation/SqliteDatabaseOperations.java
+++
b/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/operation/SqliteDatabaseOperations.java
@@ -32,6 +32,7 @@ import org.apache.commons.io.FileUtils;
import org.apache.gravitino.catalog.jdbc.JdbcSchema;
import org.apache.gravitino.catalog.jdbc.converter.SqliteExceptionConverter;
import org.apache.gravitino.catalog.jdbc.utils.JdbcConnectorUtils;
+import org.apache.gravitino.exceptions.GravitinoRuntimeException;
import org.apache.gravitino.exceptions.NoSuchSchemaException;
import org.apache.gravitino.exceptions.SchemaAlreadyExistsException;
import org.apache.gravitino.meta.AuditInfo;
@@ -70,11 +71,16 @@ public class SqliteDatabaseOperations extends
JdbcDatabaseOperations {
@Override
public List<String> listDatabases() {
- File file = new File(dbPath);
- Preconditions.checkArgument(file.exists(), "Database path %s does not
exist", dbPath);
- return Arrays.stream(Objects.requireNonNull(file.listFiles()))
- .map(File::getName)
- .collect(Collectors.toList());
+ try {
+ File file = new File(dbPath);
+ Preconditions.checkArgument(file.exists(), "Database path %s does not
exist", dbPath);
+ return Arrays.stream(Objects.requireNonNull(file.listFiles()))
+ .map(File::getName)
+ .collect(Collectors.toList());
+ } catch (Exception e) {
+ throw new GravitinoRuntimeException(
+ e, "Failed to list databases in %s: %s", dbPath, e.getMessage());
+ }
}
@Override
diff --git
a/catalogs/catalog-kafka/src/main/java/org/apache/gravitino/catalog/kafka/KafkaCatalogOperations.java
b/catalogs/catalog-kafka/src/main/java/org/apache/gravitino/catalog/kafka/KafkaCatalogOperations.java
index d78761262..d3901218d 100644
---
a/catalogs/catalog-kafka/src/main/java/org/apache/gravitino/catalog/kafka/KafkaCatalogOperations.java
+++
b/catalogs/catalog-kafka/src/main/java/org/apache/gravitino/catalog/kafka/KafkaCatalogOperations.java
@@ -18,6 +18,7 @@
*/
package org.apache.gravitino.catalog.kafka;
+import static org.apache.gravitino.StringIdentifier.DUMMY_ID;
import static org.apache.gravitino.StringIdentifier.ID_KEY;
import static org.apache.gravitino.StringIdentifier.newPropertiesWithId;
import static org.apache.gravitino.connector.BaseCatalog.CATALOG_BYPASS_PREFIX;
@@ -38,6 +39,7 @@ import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
+import org.apache.gravitino.Catalog;
import org.apache.gravitino.Entity;
import org.apache.gravitino.EntityStore;
import org.apache.gravitino.GravitinoEnv;
@@ -50,6 +52,7 @@ import org.apache.gravitino.connector.CatalogInfo;
import org.apache.gravitino.connector.CatalogOperations;
import org.apache.gravitino.connector.HasPropertyMetadata;
import org.apache.gravitino.connector.SupportsSchemas;
+import org.apache.gravitino.exceptions.ConnectionFailedException;
import org.apache.gravitino.exceptions.NoSuchCatalogException;
import org.apache.gravitino.exceptions.NoSuchEntityException;
import org.apache.gravitino.exceptions.NoSuchSchemaException;
@@ -181,6 +184,21 @@ public class KafkaCatalogOperations implements
CatalogOperations, SupportsSchema
}
}
+ @Override
+ public void testConnection(
+ NameIdentifier catalogIdent,
+ Catalog.Type type,
+ String provider,
+ String comment,
+ Map<String, String> properties) {
+ try {
+ adminClient.listTopics().names().get();
+ } catch (Exception e) {
+ throw new ConnectionFailedException(
+ e, "Failed to run listTopics in Kafka: %s", e.getMessage());
+ }
+ }
+
@Override
public Topic loadTopic(NameIdentifier ident) throws NoSuchTopicException {
NameIdentifier schemaIdent = NameIdentifier.of(ident.namespace().levels());
@@ -552,9 +570,10 @@ public class KafkaCatalogOperations implements
CatalogOperations, SupportsSchema
}
private void createDefaultSchemaIfNecessary() {
- // If the default schema already exists, do nothing
+ // If the default schema already exists or is testConnection operation, do
nothing
try {
- if (store.exists(defaultSchemaIdent, Entity.EntityType.SCHEMA)) {
+ if (DUMMY_ID.toString().equals(info.properties().get(ID_KEY))
+ || store.exists(defaultSchemaIdent, Entity.EntityType.SCHEMA)) {
return;
}
} catch (IOException e) {
diff --git
a/catalogs/catalog-kafka/src/test/java/org/apache/gravitino/catalog/kafka/TestKafkaCatalogOperations.java
b/catalogs/catalog-kafka/src/test/java/org/apache/gravitino/catalog/kafka/TestKafkaCatalogOperations.java
index 8826e2804..58cd55042 100644
---
a/catalogs/catalog-kafka/src/test/java/org/apache/gravitino/catalog/kafka/TestKafkaCatalogOperations.java
+++
b/catalogs/catalog-kafka/src/test/java/org/apache/gravitino/catalog/kafka/TestKafkaCatalogOperations.java
@@ -186,6 +186,7 @@ public class TestKafkaCatalogOperations extends
KafkaClusterEmbedded {
.withNamespace(Namespace.of(METALAKE_NAME))
.withType(MESSAGING)
.withProvider("kafka")
+ .withProperties(MOCK_CATALOG_PROPERTIES)
.withAuditInfo(
AuditInfo.builder()
.withCreator("testKafkaUser")
@@ -221,6 +222,7 @@ public class TestKafkaCatalogOperations extends
KafkaClusterEmbedded {
.withCreator("testKafkaUser")
.withCreateTime(Instant.now())
.build())
+ .withProperties(MOCK_CATALOG_PROPERTIES)
.build();
KafkaCatalogOperations ops = new KafkaCatalogOperations(store,
idGenerator);
Assertions.assertNull(ops.adminClientConfig);
@@ -256,6 +258,7 @@ public class TestKafkaCatalogOperations extends
KafkaClusterEmbedded {
.withCreator("testKafkaUser")
.withCreateTime(Instant.now())
.build())
+ .withProperties(MOCK_CATALOG_PROPERTIES)
.build();
KafkaCatalogOperations ops = new KafkaCatalogOperations(store,
idGenerator);
ops.initialize(
@@ -544,4 +547,16 @@ public class TestKafkaCatalogOperations extends
KafkaClusterEmbedded {
ident, TopicChange.setProperty(PARTITION_COUNT, "1")));
Assertions.assertEquals("Cannot reduce partition count from 3 to 1",
exception.getMessage());
}
+
+ @Test
+ public void testTestConnection() {
+ Assertions.assertDoesNotThrow(
+ () ->
+ kafkaCatalogOperations.testConnection(
+ NameIdentifier.of("metalake", "catalog"),
+ MESSAGING,
+ "kafka",
+ "comment",
+ ImmutableMap.of()));
+ }
}
diff --git
a/catalogs/catalog-kafka/src/test/java/org/apache/gravitino/catalog/kafka/integration/test/CatalogKafkaIT.java
b/catalogs/catalog-kafka/src/test/java/org/apache/gravitino/catalog/kafka/integration/test/CatalogKafkaIT.java
index 0f580170d..88e1df80c 100644
---
a/catalogs/catalog-kafka/src/test/java/org/apache/gravitino/catalog/kafka/integration/test/CatalogKafkaIT.java
+++
b/catalogs/catalog-kafka/src/test/java/org/apache/gravitino/catalog/kafka/integration/test/CatalogKafkaIT.java
@@ -139,11 +139,18 @@ public class CatalogKafkaIT extends AbstractIT {
@Test
public void testCatalog() throws ExecutionException, InterruptedException {
- // test create catalog
String catalogName = GravitinoITUtils.genRandomName("test_catalog");
String comment = "test catalog";
Map<String, String> properties =
ImmutableMap.of(BOOTSTRAP_SERVERS, kafkaBootstrapServers, "key1",
"value1");
+
+ // test before creation
+ Assertions.assertDoesNotThrow(
+ () ->
+ metalake.testConnection(
+ catalogName, Catalog.Type.MESSAGING, PROVIDER, comment,
properties));
+
+ // test create catalog
Catalog createdCatalog = createCatalog(catalogName, comment, properties);
Assertions.assertEquals(catalogName, createdCatalog.name());
Assertions.assertEquals(comment, createdCatalog.comment());
@@ -190,6 +197,23 @@ public class CatalogKafkaIT extends AbstractIT {
IllegalArgumentException.class, () ->
catalog1.asSchemas().listSchemas());
Assertions.assertTrue(exception.getMessage().contains("Invalid url in
bootstrap.servers: 2"));
+ // test before creation
+ ImmutableMap<String, String> illegalProps = ImmutableMap.of("abc", "2");
+ exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ metalake.testConnection(
+ GravitinoITUtils.genRandomName("test_catalog"),
+ Catalog.Type.MESSAGING,
+ PROVIDER,
+ "comment",
+ illegalProps));
+ Assertions.assertTrue(
+ exception
+ .getMessage()
+ .contains("Properties are required and must be set:
[bootstrap.servers]"));
+
exception =
Assertions.assertThrows(
IllegalArgumentException.class,
@@ -199,7 +223,7 @@ public class CatalogKafkaIT extends AbstractIT {
Catalog.Type.MESSAGING,
PROVIDER,
"comment",
- ImmutableMap.of("abc", "2")));
+ illegalProps));
Assertions.assertTrue(
exception
.getMessage()
diff --git
a/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalogOperations.java
b/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalogOperations.java
index 1946bcb81..bf520ba1a 100644
---
a/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalogOperations.java
+++
b/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalogOperations.java
@@ -33,6 +33,7 @@ import java.util.Optional;
import java.util.stream.Collectors;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.Catalog;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.SchemaChange;
@@ -42,6 +43,7 @@ import org.apache.gravitino.connector.CatalogInfo;
import org.apache.gravitino.connector.CatalogOperations;
import org.apache.gravitino.connector.HasPropertyMetadata;
import org.apache.gravitino.connector.SupportsSchemas;
+import org.apache.gravitino.exceptions.ConnectionFailedException;
import org.apache.gravitino.exceptions.NoSuchCatalogException;
import org.apache.gravitino.exceptions.NoSuchSchemaException;
import org.apache.gravitino.exceptions.NoSuchTableException;
@@ -552,6 +554,30 @@ public class IcebergCatalogOperations implements
CatalogOperations, SupportsSche
}
}
+ /**
+ * Performs `listNamespaces` operation on the Iceberg catalog to test the
connection.
+ *
+ * @param catalogIdent the name of the catalog.
+ * @param type the type of the catalog.
+ * @param provider the provider of the catalog.
+ * @param comment the comment of the catalog.
+ * @param properties the properties of the catalog.
+ */
+ @Override
+ public void testConnection(
+ NameIdentifier catalogIdent,
+ Catalog.Type type,
+ String provider,
+ String comment,
+ Map<String, String> properties) {
+ try {
+
icebergTableOps.listNamespace(IcebergTableOpsHelper.getIcebergNamespace());
+ } catch (Exception e) {
+ throw new ConnectionFailedException(
+ e, "Failed to run listNamespace on Iceberg catalog: %s",
e.getMessage());
+ }
+ }
+
// TODO. We should figure out a better way to get the current user from
servlet container.
private static String currentUser() {
return System.getProperty("user.name");
diff --git
a/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/TestIcebergCatalogOperations.java
b/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/TestIcebergCatalogOperations.java
new file mode 100644
index 000000000..06e95e4bd
--- /dev/null
+++
b/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/TestIcebergCatalogOperations.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * http://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 org.apache.gravitino.catalog.lakehouse.iceberg;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.exceptions.GravitinoRuntimeException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestIcebergCatalogOperations {
+ @Test
+ public void testTestConnection() {
+ IcebergCatalogOperations catalogOperations = new
IcebergCatalogOperations();
+ Exception exception =
+ Assertions.assertThrows(
+ GravitinoRuntimeException.class,
+ () ->
+ catalogOperations.testConnection(
+ NameIdentifier.of("metalake", "catalog"),
+ Catalog.Type.RELATIONAL,
+ "iceberg",
+ "comment",
+ ImmutableMap.of()));
+ Assertions.assertTrue(
+ exception.getMessage().contains("Failed to run listNamespace on
Iceberg catalog"));
+ }
+}
diff --git
a/catalogs/catalog-lakehouse-paimon/src/main/java/org/apache/gravitino/catalog/lakehouse/paimon/PaimonCatalogOperations.java
b/catalogs/catalog-lakehouse-paimon/src/main/java/org/apache/gravitino/catalog/lakehouse/paimon/PaimonCatalogOperations.java
index eca0e8b46..225c8e017 100644
---
a/catalogs/catalog-lakehouse-paimon/src/main/java/org/apache/gravitino/catalog/lakehouse/paimon/PaimonCatalogOperations.java
+++
b/catalogs/catalog-lakehouse-paimon/src/main/java/org/apache/gravitino/catalog/lakehouse/paimon/PaimonCatalogOperations.java
@@ -39,6 +39,7 @@ import org.apache.gravitino.connector.CatalogInfo;
import org.apache.gravitino.connector.CatalogOperations;
import org.apache.gravitino.connector.HasPropertyMetadata;
import org.apache.gravitino.connector.SupportsSchemas;
+import org.apache.gravitino.exceptions.ConnectionFailedException;
import org.apache.gravitino.exceptions.NoSuchCatalogException;
import org.apache.gravitino.exceptions.NoSuchSchemaException;
import org.apache.gravitino.exceptions.NoSuchTableException;
@@ -120,6 +121,30 @@ public class PaimonCatalogOperations implements
CatalogOperations, SupportsSchem
.toArray(NameIdentifier[]::new);
}
+ /**
+ * Performs `listDatabases` operation on the Paimon catalog to test the
catalog creation.
+ *
+ * @param catalogIdent the name of the catalog.
+ * @param type the type of the catalog.
+ * @param provider the provider of the catalog.
+ * @param comment the comment of the catalog.
+ * @param properties the properties of the catalog.
+ */
+ @Override
+ public void testConnection(
+ NameIdentifier catalogIdent,
+ org.apache.gravitino.Catalog.Type type,
+ String provider,
+ String comment,
+ Map<String, String> properties) {
+ try {
+ paimonCatalogOps.listDatabases();
+ } catch (Exception e) {
+ throw new ConnectionFailedException(
+ e, "Failed to run listDatabases on Paimon catalog: %s",
e.getMessage());
+ }
+ }
+
/**
* Creates a new schema with the provided identifier, comment, and metadata.
*
diff --git
a/catalogs/catalog-lakehouse-paimon/src/test/java/org/apache/gravitino/catalog/lakehouse/paimon/TestPaimonCatalog.java
b/catalogs/catalog-lakehouse-paimon/src/test/java/org/apache/gravitino/catalog/lakehouse/paimon/TestPaimonCatalog.java
index caede716b..e6439a8d7 100644
---
a/catalogs/catalog-lakehouse-paimon/src/test/java/org/apache/gravitino/catalog/lakehouse/paimon/TestPaimonCatalog.java
+++
b/catalogs/catalog-lakehouse-paimon/src/test/java/org/apache/gravitino/catalog/lakehouse/paimon/TestPaimonCatalog.java
@@ -22,12 +22,15 @@ import static
org.apache.gravitino.catalog.lakehouse.paimon.PaimonCatalog.CATALO
import static
org.apache.gravitino.catalog.lakehouse.paimon.PaimonCatalog.SCHEMA_PROPERTIES_META;
import static
org.apache.gravitino.catalog.lakehouse.paimon.PaimonCatalog.TABLE_PROPERTIES_META;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import java.io.File;
import java.io.IOException;
import java.time.Instant;
import java.util.Map;
import org.apache.commons.io.FileUtils;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.catalog.PropertiesMetadataHelpers;
import org.apache.gravitino.catalog.lakehouse.paimon.ops.PaimonCatalogOps;
@@ -111,6 +114,16 @@ public class TestPaimonCatalog {
Assertions.assertEquals(
paimonCatalogOperations.listSchemas(Namespace.empty()).length,
paimonCatalogOps.listDatabases().size());
+
+ // test testConnection
+ Assertions.assertDoesNotThrow(
+ () ->
+ paimonCatalogOperations.testConnection(
+ NameIdentifier.of("metalake", "catalog"),
+ Catalog.Type.RELATIONAL,
+ "paimon",
+ "comment",
+ ImmutableMap.of()));
}
@Test
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/ErrorHandlers.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/ErrorHandlers.java
index 77e581b7c..ae87cf6e1 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/ErrorHandlers.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/ErrorHandlers.java
@@ -27,6 +27,7 @@ import org.apache.gravitino.dto.responses.ErrorResponse;
import org.apache.gravitino.dto.responses.OAuth2ErrorResponse;
import org.apache.gravitino.exceptions.BadRequestException;
import org.apache.gravitino.exceptions.CatalogAlreadyExistsException;
+import org.apache.gravitino.exceptions.ConnectionFailedException;
import org.apache.gravitino.exceptions.FilesetAlreadyExistsException;
import org.apache.gravitino.exceptions.GroupAlreadyExistsException;
import org.apache.gravitino.exceptions.MetalakeAlreadyExistsException;
@@ -340,6 +341,9 @@ public class ErrorHandlers {
case ErrorConstants.ILLEGAL_ARGUMENTS_CODE:
throw new IllegalArgumentException(errorMessage);
+ case ErrorConstants.CONNECTION_FAILED_CODE:
+ throw new ConnectionFailedException(errorMessage);
+
case ErrorConstants.NOT_FOUND_CODE:
if
(errorResponse.getType().equals(NoSuchMetalakeException.class.getSimpleName()))
{
throw new NoSuchMetalakeException(errorMessage);
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
index b0311fd7b..1311930b8 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
@@ -117,6 +117,28 @@ public class GravitinoClient extends GravitinoClientBase
implements SupportsCata
return new ClientBuilder(uri);
}
+ /**
+ * Test whether a catalog can be created successfully with the specified
parameters, without
+ * actually creating it.
+ *
+ * @param catalogName the name of the catalog.
+ * @param type the type of the catalog.
+ * @param provider the provider of the catalog.
+ * @param comment the comment of the catalog.
+ * @param properties the properties of the catalog.
+ * @throws Exception if the test failed.
+ */
+ @Override
+ public void testConnection(
+ String catalogName,
+ Catalog.Type type,
+ String provider,
+ String comment,
+ Map<String, String> properties)
+ throws Exception {
+ getMetalake().testConnection(catalogName, type, provider, comment,
properties);
+ }
+
/** Builder class for constructing a GravitinoClient. */
public static class ClientBuilder extends
GravitinoClientBase.Builder<GravitinoClient> {
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java
index 1c3276f64..f8a95aa9b 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java
@@ -39,6 +39,7 @@ import org.apache.gravitino.dto.responses.CatalogListResponse;
import org.apache.gravitino.dto.responses.CatalogResponse;
import org.apache.gravitino.dto.responses.DropResponse;
import org.apache.gravitino.dto.responses.EntityListResponse;
+import org.apache.gravitino.dto.responses.ErrorResponse;
import org.apache.gravitino.exceptions.CatalogAlreadyExistsException;
import org.apache.gravitino.exceptions.NoSuchCatalogException;
import org.apache.gravitino.exceptions.NoSuchMetalakeException;
@@ -216,6 +217,48 @@ public class GravitinoMetalake extends MetalakeDTO
implements SupportsCatalogs {
return resp.dropped();
}
+ /**
+ * Test whether a catalog can be created successfully with the specified
parameters, without
+ * actually creating it.
+ *
+ * @param catalogName the name of the catalog.
+ * @param type the type of the catalog.
+ * @param provider the provider of the catalog.
+ * @param comment the comment of the catalog.
+ * @param properties the properties of the catalog.
+ * @throws Exception if the test failed.
+ */
+ @Override
+ public void testConnection(
+ String catalogName,
+ Catalog.Type type,
+ String provider,
+ String comment,
+ Map<String, String> properties)
+ throws Exception {
+ CatalogCreateRequest req =
+ new CatalogCreateRequest(catalogName, type, provider, comment,
properties);
+ req.validate();
+
+ // The response maybe a `BaseResponse` (test successfully) or an
`ErrorResponse` (test failed),
+ // we use the `ErrorResponse` here because it contains all fields of
`BaseResponse` (code field
+ // only)
+ ErrorResponse resp =
+ restClient.post(
+ String.format("api/metalakes/%s/catalogs/testConnection",
this.name()),
+ req,
+ ErrorResponse.class,
+ Collections.emptyMap(),
+ ErrorHandlers.catalogErrorHandler());
+
+ if (resp.getCode() == 0) {
+ return;
+ }
+
+ // Throw the corresponding exception
+ ErrorHandlers.catalogErrorHandler().accept(resp);
+ }
+
static class Builder extends MetalakeDTO.Builder<Builder> {
private RESTClient restClient;
diff --git
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoClient.java
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoClient.java
index 3aa0e1a4a..4c5cef167 100644
---
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoClient.java
+++
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoClient.java
@@ -24,19 +24,23 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
+import org.apache.gravitino.Catalog;
import org.apache.gravitino.MetalakeChange;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Version;
import org.apache.gravitino.dto.AuditDTO;
import org.apache.gravitino.dto.MetalakeDTO;
import org.apache.gravitino.dto.VersionDTO;
+import org.apache.gravitino.dto.requests.CatalogCreateRequest;
import org.apache.gravitino.dto.requests.MetalakeCreateRequest;
import org.apache.gravitino.dto.requests.MetalakeUpdatesRequest;
+import org.apache.gravitino.dto.responses.BaseResponse;
import org.apache.gravitino.dto.responses.DropResponse;
import org.apache.gravitino.dto.responses.ErrorResponse;
import org.apache.gravitino.dto.responses.MetalakeListResponse;
import org.apache.gravitino.dto.responses.MetalakeResponse;
import org.apache.gravitino.dto.responses.VersionResponse;
+import org.apache.gravitino.exceptions.ConnectionFailedException;
import org.apache.gravitino.exceptions.GravitinoRuntimeException;
import org.apache.gravitino.exceptions.IllegalNamespaceException;
import org.apache.gravitino.exceptions.MetalakeAlreadyExistsException;
@@ -338,4 +342,65 @@ public class TestGravitinoClient extends TestBase {
Assertions.assertEquals(currentVersion.gitCommit,
version.gitCommit());
});
}
+
+ @Test
+ public void testTestConnection() throws Exception {
+ MetalakeDTO mockMetalake =
+ MetalakeDTO.builder()
+ .withName("mock")
+ .withComment("comment")
+ .withAudit(
+
AuditDTO.builder().withCreator("creator").withCreateTime(Instant.now()).build())
+ .build();
+ MetalakeCreateRequest metalakeReq =
+ new MetalakeCreateRequest("mock", "comment", Collections.emptyMap());
+ MetalakeResponse metalakeResp = new MetalakeResponse(mockMetalake);
+ buildMockResource(Method.POST, "/api/metalakes", metalakeReq,
metalakeResp, HttpStatus.SC_OK);
+ NameIdentifier id = NameIdentifier.parse("mock");
+ GravitinoMetalake metaLake =
+ client.createMetalake(id.name(), "comment", Collections.emptyMap());
+
+ // test TestConnection success
+ CatalogCreateRequest req =
+ new CatalogCreateRequest(
+ "catalog", Catalog.Type.RELATIONAL, "hive", "comment",
Collections.emptyMap());
+ BaseResponse resp = new BaseResponse();
+ buildMockResource(
+ Method.POST, "/api/metalakes/mock/catalogs/testConnection", req, resp,
HttpStatus.SC_OK);
+ Assertions.assertDoesNotThrow(
+ () ->
+ metaLake.testConnection(
+ "catalog", Catalog.Type.RELATIONAL, "hive", "comment",
Collections.emptyMap()));
+
+ // test TestConnection failed
+ resp = ErrorResponse.illegalArguments("mock error");
+ buildMockResource(
+ Method.POST,
+ "/api/metalakes/mock/catalogs/testConnection",
+ req,
+ resp,
+ HttpStatus.SC_BAD_REQUEST);
+ Exception exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ metaLake.testConnection(
+ "catalog", Catalog.Type.RELATIONAL, "hive", "comment",
Collections.emptyMap()));
+ Assertions.assertTrue(exception.getMessage().contains("mock error"));
+
+ resp = ErrorResponse.connectionFailed("connection failed");
+ buildMockResource(
+ Method.POST,
+ "/api/metalakes/mock/catalogs/testConnection",
+ req,
+ resp,
+ HttpStatus.SC_BAD_GATEWAY);
+ exception =
+ Assertions.assertThrows(
+ ConnectionFailedException.class,
+ () ->
+ metaLake.testConnection(
+ "catalog", Catalog.Type.RELATIONAL, "hive", "comment",
Collections.emptyMap()));
+ Assertions.assertTrue(exception.getMessage().contains("connection
failed"));
+ }
}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/responses/ErrorConstants.java
b/common/src/main/java/org/apache/gravitino/dto/responses/ErrorConstants.java
index fcaa2cb90..8772a09d1 100644
---
a/common/src/main/java/org/apache/gravitino/dto/responses/ErrorConstants.java
+++
b/common/src/main/java/org/apache/gravitino/dto/responses/ErrorConstants.java
@@ -42,6 +42,9 @@ public class ErrorConstants {
/** Error codes for unsupported operation. */
public static final int UNSUPPORTED_OPERATION_CODE = 1006;
+ /** Error codes for connect to catalog failed. */
+ public static final int CONNECTION_FAILED_CODE = 1007;
+
/** Error codes for invalid state. */
public static final int UNKNOWN_ERROR_CODE = 1100;
diff --git
a/common/src/main/java/org/apache/gravitino/dto/responses/ErrorResponse.java
b/common/src/main/java/org/apache/gravitino/dto/responses/ErrorResponse.java
index f79a9973b..c4706e0ee 100644
--- a/common/src/main/java/org/apache/gravitino/dto/responses/ErrorResponse.java
+++ b/common/src/main/java/org/apache/gravitino/dto/responses/ErrorResponse.java
@@ -27,6 +27,7 @@ import java.util.List;
import javax.annotation.Nullable;
import lombok.EqualsAndHashCode;
import lombok.Getter;
+import org.apache.gravitino.exceptions.ConnectionFailedException;
import org.apache.gravitino.exceptions.RESTException;
/** Represents an error response. */
@@ -125,6 +126,31 @@ public class ErrorResponse extends BaseResponse {
getStackTrace(throwable));
}
+ /**
+ * Create a new connection failed error instance of {@link ErrorResponse}.
+ *
+ * @param message The message of the error.
+ * @return The new instance.
+ */
+ public static ErrorResponse connectionFailed(String message) {
+ return connectionFailed(message, null);
+ }
+
+ /**
+ * Create a new connection failed error instance of {@link ErrorResponse}.
+ *
+ * @param message The message of the error.
+ * @param throwable The throwable that caused the error.
+ * @return The new instance.
+ */
+ public static ErrorResponse connectionFailed(String message, Throwable
throwable) {
+ return new ErrorResponse(
+ ErrorConstants.CONNECTION_FAILED_CODE,
+ ConnectionFailedException.class.getSimpleName(),
+ message,
+ getStackTrace(throwable));
+ }
+
/**
* Create a new not found error instance of {@link ErrorResponse}.
*
diff --git a/core/src/main/java/org/apache/gravitino/StringIdentifier.java
b/core/src/main/java/org/apache/gravitino/StringIdentifier.java
index 9c7057d21..c60846f6d 100644
--- a/core/src/main/java/org/apache/gravitino/StringIdentifier.java
+++ b/core/src/main/java/org/apache/gravitino/StringIdentifier.java
@@ -37,6 +37,9 @@ public class StringIdentifier {
public static final String ID_KEY = "gravitino.identifier";
+ /** For test connection only */
+ public static final StringIdentifier DUMMY_ID = fromId(-1L);
+
@VisibleForTesting static final int CURRENT_FORMAT_VERSION = 1;
@VisibleForTesting static final String CURRENT_FORMAT =
"gravitino.v%d.uid%d";
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
b/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
index c2efa83e5..574248f8e 100644
--- a/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
+++ b/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
@@ -18,6 +18,7 @@
*/
package org.apache.gravitino.catalog;
+import static org.apache.gravitino.StringIdentifier.DUMMY_ID;
import static org.apache.gravitino.StringIdentifier.ID_KEY;
import static
org.apache.gravitino.catalog.PropertiesMetadataHelpers.validatePropertyForAlter;
import static
org.apache.gravitino.catalog.PropertiesMetadataHelpers.validatePropertyForCreate;
@@ -67,11 +68,13 @@ import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.StringIdentifier;
import org.apache.gravitino.connector.BaseCatalog;
+import org.apache.gravitino.connector.CatalogOperations;
import org.apache.gravitino.connector.HasPropertyMetadata;
import org.apache.gravitino.connector.PropertyEntry;
import org.apache.gravitino.connector.SupportsSchemas;
import org.apache.gravitino.connector.capability.Capability;
import org.apache.gravitino.exceptions.CatalogAlreadyExistsException;
+import org.apache.gravitino.exceptions.GravitinoRuntimeException;
import org.apache.gravitino.exceptions.NoSuchCatalogException;
import org.apache.gravitino.exceptions.NoSuchEntityException;
import org.apache.gravitino.exceptions.NoSuchMetalakeException;
@@ -149,6 +152,10 @@ public class CatalogManager implements CatalogDispatcher,
Closeable {
});
}
+ public <R> R doWithCatalogOps(ThrowableFunction<CatalogOperations, R> fn)
throws Exception {
+ return classLoader.withClassLoader(cl -> fn.apply(catalog.ops()));
+ }
+
public <R> R doWithPartitionOps(
NameIdentifier tableIdent, ThrowableFunction<SupportsPartitions, R>
fn) throws Exception {
return classLoader.withClassLoader(
@@ -333,10 +340,7 @@ public class CatalogManager implements CatalogDispatcher,
Closeable {
String comment,
Map<String, String> properties)
throws NoSuchMetalakeException, CatalogAlreadyExistsException {
- // load catalog-related configuration from catalog-specific configuration
file
- Map<String, String> newProperties =
Optional.ofNullable(properties).orElse(Maps.newHashMap());
- Map<String, String> catalogSpecificConfig =
loadCatalogSpecificConfig(newProperties, provider);
- Map<String, String> mergedConfig = mergeConf(newProperties,
catalogSpecificConfig);
+ Map<String, String> mergedConfig = buildCatalogConf(provider, properties);
long uid = idGenerator.nextId();
StringIdentifier stringId = StringIdentifier.fromId(uid);
@@ -395,24 +399,69 @@ public class CatalogManager implements CatalogDispatcher,
Closeable {
}
}
- private Pair<Map<String, String>, Map<String, String>>
getCatalogAlterProperty(
- CatalogChange... catalogChanges) {
- Map<String, String> upserts = Maps.newHashMap();
- Map<String, String> deletes = Maps.newHashMap();
+ /**
+ * Test whether a catalog can be created with the specified parameters,
without actually creating
+ * it.
+ *
+ * @param ident The identifier of the catalog to be tested.
+ * @param type the type of the catalog.
+ * @param provider the provider of the catalog.
+ * @param comment the comment of the catalog.
+ * @param properties the properties of the catalog.
+ */
+ @Override
+ public void testConnection(
+ NameIdentifier ident,
+ Catalog.Type type,
+ String provider,
+ String comment,
+ Map<String, String> properties) {
+ NameIdentifier metalakeIdent =
NameIdentifier.of(ident.namespace().levels());
+ try {
+ if (!store.exists(metalakeIdent, EntityType.METALAKE)) {
+ throw new NoSuchMetalakeException(METALAKE_DOES_NOT_EXIST_MSG,
metalakeIdent);
+ }
- Arrays.stream(catalogChanges)
- .forEach(
- catalogChange -> {
- if (catalogChange instanceof SetProperty) {
- SetProperty setProperty = (SetProperty) catalogChange;
- upserts.put(setProperty.getProperty(), setProperty.getValue());
- } else if (catalogChange instanceof RemoveProperty) {
- RemoveProperty removeProperty = (RemoveProperty) catalogChange;
- deletes.put(removeProperty.getProperty(),
removeProperty.getProperty());
- }
- });
+ if (store.exists(ident, EntityType.CATALOG)) {
+ throw new CatalogAlreadyExistsException("Catalog %s already exists",
ident);
+ }
- return Pair.of(upserts, deletes);
+ Map<String, String> mergedConfig = buildCatalogConf(provider,
properties);
+ Instant now = Instant.now();
+ String creator = PrincipalUtils.getCurrentPrincipal().getName();
+ CatalogEntity dummyEntity =
+ CatalogEntity.builder()
+ .withId(DUMMY_ID.id())
+ .withName(ident.name())
+ .withNamespace(ident.namespace())
+ .withType(type)
+ .withProvider(provider)
+ .withComment(comment)
+ .withProperties(StringIdentifier.newPropertiesWithId(DUMMY_ID,
mergedConfig))
+ .withAuditInfo(
+ AuditInfo.builder()
+ .withCreator(creator)
+ .withCreateTime(now)
+ .withLastModifier(creator)
+ .withLastModifiedTime(now)
+ .build())
+ .build();
+
+ CatalogWrapper wrapper = createCatalogWrapper(dummyEntity);
+ wrapper.doWithCatalogOps(
+ c -> {
+ c.testConnection(ident, type, provider, comment, mergedConfig);
+ return null;
+ });
+ } catch (GravitinoRuntimeException e) {
+ throw e;
+ } catch (Exception e) {
+ LOG.warn("Failed to test catalog creation {}", ident, e);
+ if (e instanceof RuntimeException) {
+ throw (RuntimeException) e;
+ }
+ throw new RuntimeException(e);
+ }
}
/**
@@ -550,6 +599,33 @@ public class CatalogManager implements CatalogDispatcher,
Closeable {
return catalogCache.get(ident, this::loadCatalogInternal);
}
+ private Map<String, String> buildCatalogConf(String provider, Map<String,
String> properties) {
+ Map<String, String> newProperties =
Optional.ofNullable(properties).orElse(Maps.newHashMap());
+ // load catalog-related configuration from catalog-specific configuration
file
+ Map<String, String> catalogSpecificConfig =
loadCatalogSpecificConfig(newProperties, provider);
+ return mergeConf(newProperties, catalogSpecificConfig);
+ }
+
+ private Pair<Map<String, String>, Map<String, String>>
getCatalogAlterProperty(
+ CatalogChange... catalogChanges) {
+ Map<String, String> upserts = Maps.newHashMap();
+ Map<String, String> deletes = Maps.newHashMap();
+
+ Arrays.stream(catalogChanges)
+ .forEach(
+ catalogChange -> {
+ if (catalogChange instanceof SetProperty) {
+ SetProperty setProperty = (SetProperty) catalogChange;
+ upserts.put(setProperty.getProperty(), setProperty.getValue());
+ } else if (catalogChange instanceof RemoveProperty) {
+ RemoveProperty removeProperty = (RemoveProperty) catalogChange;
+ deletes.put(removeProperty.getProperty(),
removeProperty.getProperty());
+ }
+ });
+
+ return Pair.of(upserts, deletes);
+ }
+
private void checkMetalakeExists(NameIdentifier ident) throws
NoSuchMetalakeException {
try {
if (!store.exists(ident, EntityType.METALAKE)) {
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/CatalogNormalizeDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/CatalogNormalizeDispatcher.java
index 725527843..32923acb8 100644
---
a/core/src/main/java/org/apache/gravitino/catalog/CatalogNormalizeDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/catalog/CatalogNormalizeDispatcher.java
@@ -104,6 +104,18 @@ public class CatalogNormalizeDispatcher implements
CatalogDispatcher {
return dispatcher.dropCatalog(ident);
}
+ @Override
+ public void testConnection(
+ NameIdentifier ident,
+ Catalog.Type type,
+ String provider,
+ String comment,
+ Map<String, String> properties)
+ throws Exception {
+ validateCatalogName(ident.name());
+ dispatcher.testConnection(ident, type, provider, comment, properties);
+ }
+
private void validateCatalogName(String name) throws
IllegalArgumentException {
if (RESERVED_WORDS.contains(name.toLowerCase())) {
throw new IllegalArgumentException("The catalog name '" + name + "' is
reserved.");
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/SupportsCatalogs.java
b/core/src/main/java/org/apache/gravitino/catalog/SupportsCatalogs.java
index ddec0581f..33f5d4094 100644
--- a/core/src/main/java/org/apache/gravitino/catalog/SupportsCatalogs.java
+++ b/core/src/main/java/org/apache/gravitino/catalog/SupportsCatalogs.java
@@ -121,4 +121,22 @@ public interface SupportsCatalogs {
* @return True if the catalog was dropped, false if the catalog does not
exist.
*/
boolean dropCatalog(NameIdentifier ident);
+
+ /**
+ * Test whether the catalog with specified parameters can be connected to
before creating it.
+ *
+ * @param ident The identifier of the catalog to be tested.
+ * @param type the type of the catalog.
+ * @param provider the provider of the catalog.
+ * @param comment the comment of the catalog.
+ * @param properties the properties of the catalog.
+ * @throws Exception If the connection test fails.
+ */
+ void testConnection(
+ NameIdentifier ident,
+ Catalog.Type type,
+ String provider,
+ String comment,
+ Map<String, String> properties)
+ throws Exception;
}
diff --git
a/core/src/main/java/org/apache/gravitino/connector/CatalogOperations.java
b/core/src/main/java/org/apache/gravitino/connector/CatalogOperations.java
index 14906d005..3d7f944d7 100644
--- a/core/src/main/java/org/apache/gravitino/connector/CatalogOperations.java
+++ b/core/src/main/java/org/apache/gravitino/connector/CatalogOperations.java
@@ -20,6 +20,8 @@ package org.apache.gravitino.connector;
import java.io.Closeable;
import java.util.Map;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.SupportsSchemas;
import org.apache.gravitino.annotation.Evolving;
import org.apache.gravitino.rel.TableCatalog;
@@ -46,4 +48,22 @@ public interface CatalogOperations extends Closeable {
void initialize(
Map<String, String> config, CatalogInfo info, HasPropertyMetadata
propertiesMetadata)
throws RuntimeException;
+
+ /**
+ * Test whether a catalog can be created with the specified parameters,
without actually creating
+ * it.
+ *
+ * @param catalogIdent the name of the catalog.
+ * @param type the type of the catalog.
+ * @param provider the provider of the catalog.
+ * @param comment the comment of the catalog.
+ * @param properties the properties of the catalog.
+ */
+ void testConnection(
+ NameIdentifier catalogIdent,
+ Catalog.Type type,
+ String provider,
+ String comment,
+ Map<String, String> properties)
+ throws Exception;
}
diff --git
a/core/src/main/java/org/apache/gravitino/listener/CatalogEventDispatcher.java
b/core/src/main/java/org/apache/gravitino/listener/CatalogEventDispatcher.java
index e4072d022..93cffe351 100644
---
a/core/src/main/java/org/apache/gravitino/listener/CatalogEventDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/listener/CatalogEventDispatcher.java
@@ -156,4 +156,16 @@ public class CatalogEventDispatcher implements
CatalogDispatcher {
throw e;
}
}
+
+ @Override
+ public void testConnection(
+ NameIdentifier ident,
+ Catalog.Type type,
+ String provider,
+ String comment,
+ Map<String, String> properties)
+ throws Exception {
+ // TODO: Support event dispatching for testConnection
+ dispatcher.testConnection(ident, type, provider, comment, properties);
+ }
}
diff --git
a/core/src/test/java/org/apache/gravitino/catalog/DummyCatalogOperations.java
b/core/src/test/java/org/apache/gravitino/catalog/DummyCatalogOperations.java
index 70f5e7f1a..7ad6ba787 100644
---
a/core/src/test/java/org/apache/gravitino/catalog/DummyCatalogOperations.java
+++
b/core/src/test/java/org/apache/gravitino/catalog/DummyCatalogOperations.java
@@ -20,6 +20,8 @@ package org.apache.gravitino.catalog;
import java.io.IOException;
import java.util.Map;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.connector.CatalogInfo;
import org.apache.gravitino.connector.CatalogOperations;
import org.apache.gravitino.connector.HasPropertyMetadata;
@@ -31,6 +33,15 @@ public class DummyCatalogOperations implements
CatalogOperations {
Map<String, String> config, CatalogInfo info, HasPropertyMetadata
propertiesMetadata)
throws RuntimeException {}
+ @Override
+ public void testConnection(
+ NameIdentifier catalogIdent,
+ Catalog.Type type,
+ String provider,
+ String comment,
+ Map<String, String> properties)
+ throws Exception {}
+
@Override
public void close() throws IOException {}
}
diff --git
a/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
b/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
index d1e9c623e..83d7308ca 100644
--- a/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
+++ b/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
@@ -234,6 +234,12 @@ public class TestCatalogManager {
props.put("key1", "value1");
props.put("key2", "value2");
+ // test before creation
+ Assertions.assertDoesNotThrow(
+ () ->
+ catalogManager.testConnection(
+ ident, Catalog.Type.RELATIONAL, provider, "comment", props));
+
Catalog testCatalog =
catalogManager.createCatalog(ident, Catalog.Type.RELATIONAL, provider,
"comment", props);
Assertions.assertEquals("test1", testCatalog.name());
@@ -243,8 +249,15 @@ public class TestCatalogManager {
Assertions.assertNotNull(catalogManager.catalogCache.getIfPresent(ident));
- // Test create under non-existed metalake
+ // test before creation
NameIdentifier ident2 = NameIdentifier.of("metalake1", "test1");
+ Assertions.assertThrows(
+ NoSuchMetalakeException.class,
+ () ->
+ catalogManager.testConnection(
+ ident2, Catalog.Type.RELATIONAL, provider, "comment", props));
+
+ // Test create under non-existed metalake
Throwable exception1 =
Assertions.assertThrows(
NoSuchMetalakeException.class,
@@ -254,6 +267,13 @@ public class TestCatalogManager {
Assertions.assertTrue(exception1.getMessage().contains("Metalake metalake1
does not exist"));
Assertions.assertNull(catalogManager.catalogCache.getIfPresent(ident2));
+ // test before creation
+ Assertions.assertThrows(
+ NoSuchMetalakeException.class,
+ () ->
+ catalogManager.testConnection(
+ ident2, Catalog.Type.RELATIONAL, provider, "comment", props));
+
// Test create with duplicated name
Throwable exception2 =
Assertions.assertThrows(
diff --git
a/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java
b/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java
index ba609edca..7588af17c 100644
---
a/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java
+++
b/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java
@@ -23,6 +23,7 @@ import java.io.IOException;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
+import org.apache.gravitino.Catalog;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.Schema;
@@ -31,6 +32,7 @@ import org.apache.gravitino.TestFileset;
import org.apache.gravitino.TestSchema;
import org.apache.gravitino.TestTable;
import org.apache.gravitino.TestTopic;
+import org.apache.gravitino.exceptions.ConnectionFailedException;
import org.apache.gravitino.exceptions.FilesetAlreadyExistsException;
import org.apache.gravitino.exceptions.NoSuchCatalogException;
import org.apache.gravitino.exceptions.NoSuchFilesetException;
@@ -71,6 +73,8 @@ public class TestCatalogOperations
public static final String FAIL_CREATE = "fail-create";
+ public static final String FAIL_TEST = "need-fail";
+
public TestCatalogOperations(Map<String, String> config) {
tables = Maps.newHashMap();
schemas = Maps.newHashMap();
@@ -520,4 +524,16 @@ public class TestCatalogOperations
return false;
}
}
+
+ @Override
+ public void testConnection(
+ NameIdentifier name,
+ Catalog.Type type,
+ String provider,
+ String comment,
+ Map<String, String> properties) {
+ if ("true".equals(properties.get(FAIL_TEST))) {
+ throw new ConnectionFailedException("Connection failed");
+ }
+ }
}
diff --git a/docs/open-api/catalogs.yaml b/docs/open-api/catalogs.yaml
index de89b253a..ab68c06ed 100644
--- a/docs/open-api/catalogs.yaml
+++ b/docs/open-api/catalogs.yaml
@@ -79,6 +79,65 @@ paths:
"5xx":
$ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+ /metalakes/{metalake}/catalogs/testConnection:
+ parameters:
+ - $ref: "./openapi.yaml#/components/parameters/metalake"
+ post:
+ tags:
+ - catalog
+ summary: Test catalog connection
+ operationId: testConnection
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CatalogCreateRequest"
+ examples:
+ CatalogCreate:
+ $ref: "#/components/examples/CatalogCreate"
+ responses:
+ "200":
+ description: Test connection completed
+ content:
+ application/vnd.gravitino.v1+json:
+ schema:
+ type: object
+ required:
+ - code
+ properties:
+ code:
+ type: integer
+ format: int32
+ description: Status code of the response
+ type:
+ type: string
+ description: Internal type definition of the exception
+ message:
+ type: string
+ description: The message of the exception
+ stack:
+ type: array
+ items:
+ type: string
+ description: The stack trace of the exception
+ examples:
+ TestConnectionSuccess:
+ value: {
+ "code": 0
+ }
+ TestConnectionFailed:
+ $ref: "#/components/examples/ConnectionFailedException"
+ CatalogAlreadyExists:
+ $ref: "#/components/examples/CatalogAlreadyExistsException"
+ MetalakeNotFound:
+ $ref:
"./metalakes.yaml#/components/examples/NoSuchMetalakeException"
+ "400":
+ $ref: "./openapi.yaml#/components/responses/BadRequestErrorResponse"
+ "5xx":
+ $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+
+
+
/metalakes/{metalake}/catalogs/{catalog}:
parameters:
- $ref: "./openapi.yaml#/components/parameters/metalake"
@@ -495,6 +554,17 @@ components:
]
}
+ ConnectionFailedException:
+ value: {
+ "code": 1007,
+ "type": "ConnectionFailedException",
+ "message": "Failed to run getAllDatabases in Hive Metastore: Failed to
connect to Hive Metastore",
+ "stack": [
+ "org.apache.gravitino.exceptions.ConnectionFailedException: Failed
to run getAllDatabases in Hive Metastore: Failed to connect to Hive Metastore",
+ "..."
+ ]
+ }
+
NoSuchCatalogException:
value: {
"code": 1003,
diff --git a/docs/open-api/openapi.yaml b/docs/open-api/openapi.yaml
index afc011446..9218a6b67 100644
--- a/docs/open-api/openapi.yaml
+++ b/docs/open-api/openapi.yaml
@@ -61,6 +61,9 @@ paths:
/metalakes/{metalake}/catalogs:
$ref: "./catalogs.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1catalogs"
+ /metalakes/{metalake}/catalogs/testConnection:
+ $ref:
"./catalogs.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1catalogs~1testConnection"
+
/metalakes/{metalake}/catalogs/{catalog}:
$ref:
"./catalogs.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1catalogs~1%7Bcatalog%7D"
diff --git
a/integration-test/src/test/java/org/apache/gravitino/integration/test/client/CatalogIT.java
b/integration-test/src/test/java/org/apache/gravitino/integration/test/client/CatalogIT.java
index 943f8e17e..60f7a7dd0 100644
---
a/integration-test/src/test/java/org/apache/gravitino/integration/test/client/CatalogIT.java
+++
b/integration-test/src/test/java/org/apache/gravitino/integration/test/client/CatalogIT.java
@@ -84,12 +84,19 @@ public class CatalogIT extends AbstractIT {
}
@Test
- public void testCreateCatalog() {
+ public void testTestConnection() {
String catalogName = GravitinoITUtils.genRandomName("catalog");
Assertions.assertFalse(metalake.catalogExists(catalogName));
Map<String, String> properties = Maps.newHashMap();
properties.put("metastore.uris", hmsUri);
+ // test before creation
+ Assertions.assertDoesNotThrow(
+ () ->
+ metalake.testConnection(
+ catalogName, Catalog.Type.RELATIONAL, "hive", "catalog
comment", properties));
+
+ // test creation
Catalog catalog =
metalake.createCatalog(
catalogName, Catalog.Type.RELATIONAL, "hive", "catalog comment",
properties);
@@ -140,7 +147,16 @@ public class CatalogIT extends AbstractIT {
// test cloud related properties
ImmutableMap<String, String> illegalProps = ImmutableMap.of("cloud.name",
"myCloud");
- IllegalArgumentException exception =
+ // test before creation
+ Exception exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ metalake.testConnection(
+ catalogName, Catalog.Type.FILESET, "hadoop", "catalog
comment", illegalProps));
+ Assertions.assertTrue(exception.getMessage().contains("Invalid value
[myCloud]"));
+
+ exception =
Assertions.assertThrows(
IllegalArgumentException.class,
() ->
diff --git
a/integration-test/src/test/java/org/apache/gravitino/integration/test/client/MetalakeIT.java
b/integration-test/src/test/java/org/apache/gravitino/integration/test/client/MetalakeIT.java
index c95f0e201..3d87e9d4c 100644
---
a/integration-test/src/test/java/org/apache/gravitino/integration/test/client/MetalakeIT.java
+++
b/integration-test/src/test/java/org/apache/gravitino/integration/test/client/MetalakeIT.java
@@ -30,6 +30,7 @@ import org.apache.gravitino.MetalakeChange;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.auth.AuthConstants;
import org.apache.gravitino.client.GravitinoMetalake;
+import org.apache.gravitino.exceptions.IllegalNameIdentifierException;
import org.apache.gravitino.exceptions.MetalakeAlreadyExistsException;
import org.apache.gravitino.exceptions.NoSuchMetalakeException;
import org.apache.gravitino.integration.test.util.AbstractIT;
@@ -98,7 +99,7 @@ public class MetalakeIT extends AbstractIT {
});
// metalake empty name - note it's NameIdentifier.of("") that fails not
the load
- assertThrows(IllegalArgumentException.class, () ->
client.loadMetalake(""));
+ assertThrows(IllegalNameIdentifierException.class, () ->
client.loadMetalake(""));
}
@Test
diff --git a/server/src/main/java/org/apache/gravitino/server/web/Utils.java
b/server/src/main/java/org/apache/gravitino/server/web/Utils.java
index d092bd0a9..e2405a6ad 100644
--- a/server/src/main/java/org/apache/gravitino/server/web/Utils.java
+++ b/server/src/main/java/org/apache/gravitino/server/web/Utils.java
@@ -57,6 +57,20 @@ public class Utils {
.build();
}
+ public static Response connectionFailed(String message) {
+ return Response.status(Response.Status.BAD_GATEWAY)
+ .entity(ErrorResponse.connectionFailed(message))
+ .type(MediaType.APPLICATION_JSON)
+ .build();
+ }
+
+ public static Response connectionFailed(String message, Throwable throwable)
{
+ return Response.status(Response.Status.BAD_GATEWAY)
+ .entity(ErrorResponse.connectionFailed(message, throwable))
+ .type(MediaType.APPLICATION_JSON)
+ .build();
+ }
+
public static Response internalError(String message) {
return internalError(message, null);
}
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/CatalogOperations.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/CatalogOperations.java
index 78a1c540c..ebb1813e2 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/rest/CatalogOperations.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/CatalogOperations.java
@@ -43,6 +43,7 @@ import org.apache.gravitino.catalog.CatalogDispatcher;
import org.apache.gravitino.dto.requests.CatalogCreateRequest;
import org.apache.gravitino.dto.requests.CatalogUpdateRequest;
import org.apache.gravitino.dto.requests.CatalogUpdatesRequest;
+import org.apache.gravitino.dto.responses.BaseResponse;
import org.apache.gravitino.dto.responses.CatalogListResponse;
import org.apache.gravitino.dto.responses.CatalogResponse;
import org.apache.gravitino.dto.responses.DropResponse;
@@ -148,6 +149,44 @@ public class CatalogOperations {
}
}
+ @POST
+ @Path("testConnection")
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "test-connection." + MetricNames.HTTP_PROCESS_DURATION,
absolute = true)
+ @ResponseMetered(name = "test-connection", absolute = true)
+ public Response testConnection(
+ @PathParam("metalake") String metalake, CatalogCreateRequest request) {
+ LOG.info("Received test connection request for catalog: {}.{}", metalake,
request.getName());
+ try {
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ request.validate();
+ NameIdentifier ident = NameIdentifierUtil.ofCatalog(metalake,
request.getName());
+ TreeLockUtils.doWithTreeLock(
+ NameIdentifierUtil.ofMetalake(metalake),
+ LockType.READ,
+ () -> {
+ catalogDispatcher.testConnection(
+ ident,
+ request.getType(),
+ request.getProvider(),
+ request.getComment(),
+ request.getProperties());
+ return null;
+ });
+ Response response = Utils.ok(new BaseResponse());
+ LOG.info(
+ "Successfully test connection for catalog: {}.{}", metalake,
request.getName());
+ return response;
+ });
+
+ } catch (Exception e) {
+ LOG.info("Failed to test connection for catalog: {}.{}", metalake,
request.getName());
+ return ExceptionHandlers.handleTestConnectionException(e);
+ }
+ }
+
@GET
@Path("{catalog}")
@Produces("application/vnd.gravitino.v1+json")
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
index cd57c570a..677c42de9 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
@@ -19,8 +19,12 @@
package org.apache.gravitino.server.web.rest;
import com.google.common.annotations.VisibleForTesting;
+import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
+import org.apache.gravitino.dto.responses.ErrorResponse;
+import org.apache.gravitino.exceptions.AlreadyExistsException;
import org.apache.gravitino.exceptions.CatalogAlreadyExistsException;
+import org.apache.gravitino.exceptions.ConnectionFailedException;
import org.apache.gravitino.exceptions.FilesetAlreadyExistsException;
import org.apache.gravitino.exceptions.GroupAlreadyExistsException;
import org.apache.gravitino.exceptions.MetalakeAlreadyExistsException;
@@ -103,6 +107,30 @@ public class ExceptionHandlers {
return GroupPermissionOperationExceptionHandler.INSTANCE.handle(op, roles,
parent, e);
}
+ public static Response handleTestConnectionException(Exception e) {
+ ErrorResponse response;
+ if (e instanceof IllegalArgumentException) {
+ response = ErrorResponse.illegalArguments(e.getMessage(), e);
+
+ } else if (e instanceof ConnectionFailedException) {
+ response = ErrorResponse.connectionFailed(e.getMessage(), e);
+
+ } else if (e instanceof NotFoundException) {
+ response = ErrorResponse.notFound(e.getClass().getSimpleName(),
e.getMessage(), e);
+
+ } else if (e instanceof AlreadyExistsException) {
+ response = ErrorResponse.alreadyExists(e.getClass().getSimpleName(),
e.getMessage(), e);
+
+ } else {
+ return Utils.internalError(e.getMessage(), e);
+ }
+
+ return Response.status(Response.Status.OK)
+ .entity(response)
+ .type(MediaType.APPLICATION_JSON)
+ .build();
+ }
+
private static class PartitionExceptionHandler extends BaseExceptionHandler {
private static final ExceptionHandler INSTANCE = new
PartitionExceptionHandler();
@@ -231,6 +259,9 @@ public class ExceptionHandlers {
if (e instanceof IllegalArgumentException) {
return Utils.illegalArguments(errorMsg, e);
+ } else if (e instanceof ConnectionFailedException) {
+ return Utils.connectionFailed(errorMsg, e);
+
} else if (e instanceof NotFoundException) {
return Utils.notFound(errorMsg, e);
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/TestUtils.java
b/server/src/test/java/org/apache/gravitino/server/web/TestUtils.java
index 950a2905e..adaaacaed 100644
--- a/server/src/test/java/org/apache/gravitino/server/web/TestUtils.java
+++ b/server/src/test/java/org/apache/gravitino/server/web/TestUtils.java
@@ -92,6 +92,16 @@ public class TestUtils {
assertEquals("Invalid argument", errorResponse.getMessage());
}
+ @Test
+ public void testConnectionFailed() {
+ Response response = Utils.connectionFailed("Connection failed");
+ assertNotNull(response);
+ assertEquals(Response.Status.BAD_GATEWAY.getStatusCode(),
response.getStatus());
+ assertEquals(MediaType.APPLICATION_JSON,
response.getMediaType().toString());
+ ErrorResponse errorResponse = (ErrorResponse) response.getEntity();
+ assertEquals("Connection failed", errorResponse.getMessage());
+ }
+
@Test
public void testInternalError() {
Response response = Utils.internalError("Internal error");
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalog.java
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalog.java
index 73297a4a5..bcd5107b4 100644
--- a/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalog.java
+++ b/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalog.java
@@ -21,6 +21,7 @@ package org.apache.gravitino.server.web.rest;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.util.Map;
+import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.connector.BaseCatalog;
import org.apache.gravitino.connector.CatalogInfo;
import org.apache.gravitino.connector.CatalogOperations;
@@ -44,6 +45,15 @@ public class TestCatalog extends BaseCatalog<TestCatalog> {
Map<String, String> config, CatalogInfo info, HasPropertyMetadata
propertiesMetadata)
throws RuntimeException {}
+ @Override
+ public void testConnection(
+ NameIdentifier catalogIdent,
+ Type type,
+ String provider,
+ String comment,
+ Map<String, String> properties)
+ throws Exception {}
+
@Override
public void close() throws IOException {}
};
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogOperations.java
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogOperations.java
index da011ba34..98ac3ed40 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogOperations.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogOperations.java
@@ -18,10 +18,12 @@
*/
package org.apache.gravitino.server.web.rest;
+import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR;
import static org.apache.gravitino.Configs.TREE_LOCK_CLEAN_INTERVAL;
import static org.apache.gravitino.Configs.TREE_LOCK_MAX_NODE_IN_MEMORY;
import static org.apache.gravitino.Configs.TREE_LOCK_MIN_NODE_IN_MEMORY;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -48,6 +50,7 @@ import org.apache.gravitino.dto.CatalogDTO;
import org.apache.gravitino.dto.requests.CatalogCreateRequest;
import org.apache.gravitino.dto.requests.CatalogUpdateRequest;
import org.apache.gravitino.dto.requests.CatalogUpdatesRequest;
+import org.apache.gravitino.dto.responses.BaseResponse;
import org.apache.gravitino.dto.responses.CatalogListResponse;
import org.apache.gravitino.dto.responses.CatalogResponse;
import org.apache.gravitino.dto.responses.DropResponse;
@@ -280,14 +283,53 @@ public class TestCatalogOperations extends JerseyTest {
.accept("application/vnd.gravitino.v1+json")
.post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
- Assertions.assertEquals(
- Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
resp3.getStatus());
+ Assertions.assertEquals(INTERNAL_SERVER_ERROR.getStatusCode(),
resp3.getStatus());
ErrorResponse errorResponse2 = resp3.readEntity(ErrorResponse.class);
Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE,
errorResponse2.getCode());
Assertions.assertEquals(RuntimeException.class.getSimpleName(),
errorResponse2.getType());
}
+ @Test
+ public void testConnection() {
+ CatalogCreateRequest req =
+ new CatalogCreateRequest(
+ "catalog1",
+ Catalog.Type.RELATIONAL,
+ "test",
+ "comment",
+ ImmutableMap.of("key", "value"));
+ doNothing().when(manager).testConnection(any(), any(), any(), any(),
any());
+ Response resp =
+ target("/metalakes/metalake1/catalogs/testConnection")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp.getMediaType());
+
+ BaseResponse testResponse = resp.readEntity(BaseResponse.class);
+ Assertions.assertEquals(0, testResponse.getCode());
+
+ // test throw RuntimeException
+ doThrow(new RuntimeException("connection failed"))
+ .when(manager)
+ .testConnection(any(), any(), any(), any(), any());
+ Response resp1 =
+ target("/metalakes/metalake1/catalogs/testConnection")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(INTERNAL_SERVER_ERROR.getStatusCode(),
resp1.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp1.getMediaType());
+
+ ErrorResponse errorResponse = resp1.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE,
errorResponse.getCode());
+ Assertions.assertEquals(RuntimeException.class.getSimpleName(),
errorResponse.getType());
+ }
+
@Test
public void testLoadCatalog() {
TestCatalog catalog = buildCatalog("metalake1", "catalog1");
@@ -347,8 +389,7 @@ public class TestCatalogOperations extends JerseyTest {
.accept("application/vnd.gravitino.v1+json")
.get();
- Assertions.assertEquals(
- Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
resp3.getStatus());
+ Assertions.assertEquals(INTERNAL_SERVER_ERROR.getStatusCode(),
resp3.getStatus());
ErrorResponse errorResponse2 = resp3.readEntity(ErrorResponse.class);
Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE,
errorResponse2.getCode());
@@ -418,8 +459,7 @@ public class TestCatalogOperations extends JerseyTest {
.accept("application/vnd.gravitino.v1+json")
.put(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
- Assertions.assertEquals(
- Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
resp4.getStatus());
+ Assertions.assertEquals(INTERNAL_SERVER_ERROR.getStatusCode(),
resp4.getStatus());
ErrorResponse errorResponse2 = resp4.readEntity(ErrorResponse.class);
Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE,
errorResponse2.getCode());
@@ -463,8 +503,7 @@ public class TestCatalogOperations extends JerseyTest {
.accept("application/vnd.gravitino.v1+json")
.delete();
- Assertions.assertEquals(
- Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
resp3.getStatus());
+ Assertions.assertEquals(INTERNAL_SERVER_ERROR.getStatusCode(),
resp3.getStatus());
ErrorResponse errorResponse = resp3.readEntity(ErrorResponse.class);
Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE,
errorResponse.getCode());