This is an automated email from the ASF dual-hosted git repository.
jerryshao pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/branch-1.3 by this push:
new 3b4d8c945f [Cherry-pick to branch-1.3] [#13012] fix(catalog-glue):
Fail fast and give actionable errors on missing AWS credentials (#13013)
(#13071)
3b4d8c945f is described below
commit 3b4d8c945f835ea1985f955288bdfaea1048e713
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Thu Sep 10 18:10:32 2026 +0800
[Cherry-pick to branch-1.3] [#13012] fix(catalog-glue): Fail fast and give
actionable errors on missing AWS credentials (#13013) (#13071)
**Cherry-pick Information:**
- Original commit: 172c9031d31ca6dbe7c8b38f6db9ff015d0985c8
- Target branch: `branch-1.3`
---------
Co-authored-by: Yuhui <[email protected]>
Co-authored-by: Claude Sonnet 5 <[email protected]>
Co-authored-by: diqiu50 <[email protected]>
---
.../catalog/glue/GlueCatalogOperations.java | 76 +++++++++++++++++-----
.../gravitino/catalog/glue/GlueClientProvider.java | 47 +++++++++++--
.../catalog/glue/GlueExceptionConverter.java | 35 ++++++++++
.../glue/TestGlueCatalogSchemaOperations.java | 25 +++++++
.../catalog/glue/TestGlueClientProvider.java | 55 ++++++++++++++--
.../catalog/glue/TestGlueExceptionConverter.java | 39 +++++++++++
6 files changed, 247 insertions(+), 30 deletions(-)
diff --git
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalogOperations.java
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalogOperations.java
index 99f205e203..8b36af4129 100644
---
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalogOperations.java
+++
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalogOperations.java
@@ -34,6 +34,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
+import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
@@ -69,6 +70,7 @@ import org.apache.gravitino.utils.PrincipalUtils;
import org.apache.iceberg.catalog.TableIdentifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.core.exception.SdkException;
import software.amazon.awssdk.services.glue.GlueClient;
import software.amazon.awssdk.services.glue.model.CreateDatabaseRequest;
@@ -180,19 +182,20 @@ public class GlueCatalogOperations implements
CatalogOperations, SupportsSchemas
public NameIdentifier[] listSchemas(Namespace namespace) throws
NoSuchCatalogException {
List<NameIdentifier> result = new ArrayList<>();
String nextToken = null;
+ String context = "listing schemas under " + namespace;
try {
do {
GetDatabasesRequest.Builder req = GetDatabasesRequest.builder();
applyCatalogId(catalogId, req::catalogId);
if (nextToken != null) req.nextToken(nextToken);
- GetDatabasesResponse resp = glueClient.getDatabases(req.build());
+ GetDatabasesResponse resp = callGlue(() ->
glueClient.getDatabases(req.build()), context);
resp.databaseList().stream()
.map(db -> NameIdentifier.of(namespace, db.name()))
.forEach(result::add);
nextToken = resp.nextToken();
} while (nextToken != null);
} catch (GlueException e) {
- throw GlueExceptionConverter.toSchemaException(e, "listing schemas under
" + namespace);
+ throw GlueExceptionConverter.toSchemaException(e, context);
}
return result.toArray(new NameIdentifier[0]);
}
@@ -218,7 +221,7 @@ public class GlueCatalogOperations implements
CatalogOperations, SupportsSchemas
applyCatalogId(catalogId, req::catalogId);
try {
- glueClient.createDatabase(req.build());
+ callGlue(() -> glueClient.createDatabase(req.build()), "schema " +
ident.name());
} catch (GlueException e) {
throw GlueExceptionConverter.toSchemaException(e, "schema " +
ident.name());
}
@@ -243,7 +246,9 @@ public class GlueCatalogOperations implements
CatalogOperations, SupportsSchemas
applyCatalogId(catalogId, req::catalogId);
try {
GlueSchema schema =
-
GlueSchema.fromGlueDatabase(glueClient.getDatabase(req.build()).database());
+ GlueSchema.fromGlueDatabase(
+ callGlue(() -> glueClient.getDatabase(req.build()), "schema " +
ident.name())
+ .database());
LOG.info("Loaded Glue schema (database) {}", ident.name());
return schema;
} catch (GlueException e) {
@@ -289,7 +294,7 @@ public class GlueCatalogOperations implements
CatalogOperations, SupportsSchemas
applyCatalogId(catalogId, req::catalogId);
try {
- glueClient.updateDatabase(req.build());
+ callGlue(() -> glueClient.updateDatabase(req.build()), "schema " +
ident.name());
} catch (GlueException e) {
throw GlueExceptionConverter.toSchemaException(e, "schema " +
ident.name());
}
@@ -311,7 +316,13 @@ public class GlueCatalogOperations implements
CatalogOperations, SupportsSchemas
GetTablesRequest.builder().databaseName(ident.name()).maxResults(1);
applyCatalogId(catalogId, tabReq::catalogId);
try {
- if (!glueClient.getTables(tabReq.build()).tableList().isEmpty()) {
+ boolean hasTables =
+ !callGlue(
+ () -> glueClient.getTables(tabReq.build()),
+ "checking tables in schema " + ident.name())
+ .tableList()
+ .isEmpty();
+ if (hasTables) {
throw new NonEmptySchemaException(
"Schema %s is not empty. Use cascade=true to drop it with its
tables.", ident.name());
}
@@ -324,7 +335,7 @@ public class GlueCatalogOperations implements
CatalogOperations, SupportsSchemas
DeleteDatabaseRequest.Builder req =
DeleteDatabaseRequest.builder().name(ident.name());
applyCatalogId(catalogId, req::catalogId);
try {
- glueClient.deleteDatabase(req.build());
+ callGlue(() -> glueClient.deleteDatabase(req.build()), "schema " +
ident.name());
LOG.info("Dropped Glue schema (database) {}", ident.name());
return true;
} catch (EntityNotFoundException e) {
@@ -339,12 +350,13 @@ public class GlueCatalogOperations implements
CatalogOperations, SupportsSchemas
String dbName = schemaName(namespace);
List<NameIdentifier> result = new ArrayList<>();
String nextToken = null;
+ String context = "listing tables in schema " + dbName;
try {
do {
GetTablesRequest.Builder req =
GetTablesRequest.builder().databaseName(dbName);
applyCatalogId(catalogId, req::catalogId);
if (nextToken != null) req.nextToken(nextToken);
- GetTablesResponse resp = glueClient.getTables(req.build());
+ GetTablesResponse resp = callGlue(() ->
glueClient.getTables(req.build()), context);
resp.tableList().stream()
.filter(t -> !isView(t))
.filter(this::matchesFormatFilter)
@@ -355,7 +367,7 @@ public class GlueCatalogOperations implements
CatalogOperations, SupportsSchemas
} catch (EntityNotFoundException e) {
throw new NoSuchSchemaException(e, "Schema %s does not exist", dbName);
} catch (GlueException e) {
- throw GlueExceptionConverter.toSchemaException(e, "listing tables in
schema " + dbName);
+ throw GlueExceptionConverter.toSchemaException(e, context);
}
return result.toArray(new NameIdentifier[0]);
}
@@ -367,7 +379,7 @@ public class GlueCatalogOperations implements
CatalogOperations, SupportsSchemas
applyCatalogId(catalogId, req::catalogId);
try {
software.amazon.awssdk.services.glue.model.Table rawGlueTable =
- glueClient.getTable(req.build()).table();
+ callGlue(() -> glueClient.getTable(req.build()), "table " +
ident.name()).table();
rejectIfView(rawGlueTable, ident, dbName);
GlueTable table = GlueTable.fromGlueTable(rawGlueTable, typeConverter);
@@ -506,7 +518,8 @@ public class GlueCatalogOperations implements
CatalogOperations, SupportsSchemas
applyCatalogId(catalogId, rawReq::catalogId);
Table rawGlueTable;
try {
- rawGlueTable = glueClient.getTable(rawReq.build()).table();
+ rawGlueTable =
+ callGlue(() -> glueClient.getTable(rawReq.build()), "table " +
ident.name()).table();
} catch (GlueException e) {
throw GlueExceptionConverter.toTableException(e, "table " +
ident.name());
}
@@ -652,7 +665,10 @@ public class GlueCatalogOperations implements
CatalogOperations, SupportsSchemas
GetTableRequest.builder().databaseName(dbName).name(ident.name());
applyCatalogId(catalogId, getReq::catalogId);
try {
- rejectIfView(glueClient.getTable(getReq.build()).table(), ident, dbName);
+ rejectIfView(
+ callGlue(() -> glueClient.getTable(getReq.build()), "table " +
ident.name()).table(),
+ ident,
+ dbName);
} catch (EntityNotFoundException e) {
return false;
} catch (GlueException e) {
@@ -663,7 +679,7 @@ public class GlueCatalogOperations implements
CatalogOperations, SupportsSchemas
DeleteTableRequest.builder().databaseName(dbName).name(ident.name());
applyCatalogId(catalogId, req::catalogId);
try {
- glueClient.deleteTable(req.build());
+ callGlue(() -> glueClient.deleteTable(req.build()), "table " +
ident.name());
LOG.info("Dropped Glue table {}.{}", dbName, ident.name());
return true;
} catch (EntityNotFoundException e) {
@@ -717,7 +733,7 @@ public class GlueCatalogOperations implements
CatalogOperations, SupportsSchemas
String dbName, NameIdentifier ident, CreateTableRequest.Builder req) {
applyCatalogId(catalogId, req::catalogId);
try {
- glueClient.createTable(req.build());
+ callGlue(() -> glueClient.createTable(req.build()), "table " +
ident.name());
} catch (EntityNotFoundException e) {
throw new NoSuchSchemaException(e, "Schema %s does not exist", dbName);
} catch (GlueException e) {
@@ -728,7 +744,7 @@ public class GlueCatalogOperations implements
CatalogOperations, SupportsSchemas
private void executeUpdateTable(NameIdentifier ident,
UpdateTableRequest.Builder req) {
applyCatalogId(catalogId, req::catalogId);
try {
- glueClient.updateTable(req.build());
+ callGlue(() -> glueClient.updateTable(req.build()), "table " +
ident.name());
} catch (GlueException e) {
throw GlueExceptionConverter.toTableException(e, "table " +
ident.name());
}
@@ -893,7 +909,9 @@ public class GlueCatalogOperations implements
CatalogOperations, SupportsSchemas
GetDatabaseRequest.Builder req = GetDatabaseRequest.builder().name(dbName);
applyCatalogId(catalogId, req::catalogId);
try {
- return glueClient.getDatabase(req.build()).database().locationUri();
+ return callGlue(() -> glueClient.getDatabase(req.build()), "schema " +
dbName)
+ .database()
+ .locationUri();
} catch (GlueException e) {
throw GlueExceptionConverter.toSchemaException(e, "schema " + dbName);
}
@@ -957,6 +975,32 @@ public class GlueCatalogOperations implements
CatalogOperations, SupportsSchemas
if (catalogId != null) setter.accept(catalogId);
}
+ /**
+ * Translates a raw AWS SDK credential-chain failure into a message naming
this connector's own
+ * {@code aws-access-key-id} / {@code aws-secret-access-key} properties, so
operators are not left
+ * to guess from the SDK's generic provider-chain error. Non-credential
{@link
+ * SdkClientException}s (e.g. network failures) are rethrown unchanged.
+ */
+ private static RuntimeException
translateCredentialFailure(SdkClientException e, String context) {
+ return GlueExceptionConverter.isCredentialFailure(e)
+ ? GlueExceptionConverter.toCredentialException(e, context)
+ : e;
+ }
+
+ /**
+ * Invokes a Glue SDK call, translating a credential-chain {@link
SdkClientException} into an
+ * actionable error naming this connector's own credential properties.
{@link GlueException} is
+ * left untouched so each call site's own catch block still applies its
usual (e.g.
+ * not-found/already-exists) semantics.
+ */
+ private static <T> T callGlue(Supplier<T> call, String context) {
+ try {
+ return call.get();
+ } catch (SdkClientException e) {
+ throw translateCredentialFailure(e, context);
+ }
+ }
+
/**
* Finds the first column matching {@code name} in {@code cols}, replaces it
with the result of
* {@code updater}, and returns {@code true} if a replacement was made.
diff --git
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueClientProvider.java
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueClientProvider.java
index 3c792449e4..0cc5bb9e90 100644
---
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueClientProvider.java
+++
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueClientProvider.java
@@ -18,13 +18,16 @@
*/
package org.apache.gravitino.catalog.glue;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import java.net.URI;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.glue.GlueClient;
import software.amazon.awssdk.services.glue.GlueClientBuilder;
@@ -52,7 +55,8 @@ public final class GlueClientProvider {
* @param config Catalog configuration properties.
* @return A configured and ready-to-use {@link GlueClient}.
* @throws IllegalArgumentException if {@code aws-region} is missing or
blank, if only one of the
- * credential keys is provided, or if {@code aws-glue-endpoint} is not a
valid URI.
+ * credential keys is provided, if {@code aws-glue-endpoint} is not a
valid URI, or if no
+ * usable AWS credential source can be resolved.
*/
public static GlueClient buildClient(Map<String, String> config) {
String region = config.get(GlueConstants.AWS_REGION);
@@ -76,12 +80,12 @@ public final class GlueClientProvider {
String secretKey = config.get(GlueConstants.AWS_SECRET_ACCESS_KEY);
boolean hasStaticCredentials = hasAwsStaticCredentials(accessKey,
secretKey);
- if (hasStaticCredentials) {
- builder.credentialsProvider(
-
StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey,
secretKey)));
- } else {
-
builder.credentialsProvider(DefaultCredentialsProvider.builder().build());
- }
+ AwsCredentialsProvider credentialsProvider =
+ hasStaticCredentials
+ ?
StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey,
secretKey))
+ : DefaultCredentialsProvider.builder().build();
+ validateCredentials(credentialsProvider);
+ builder.credentialsProvider(credentialsProvider);
// Optional custom endpoint override for VPC endpoints or LocalStack
testing.
String endpoint = config.get(GlueConstants.AWS_GLUE_ENDPOINT);
@@ -92,6 +96,35 @@ public final class GlueClientProvider {
return builder.build();
}
+ /**
+ * Eagerly resolves {@code credentialsProvider} to confirm a usable
credential source exists,
+ * instead of leaving resolution to the first real Glue API call. Without
this check, a catalog
+ * created with no static credentials and no usable default-chain source
(env vars, instance
+ * profile, etc.) is stored successfully and then fails on every operation
with a raw AWS SDK
+ * error that never mentions this connector's own credential properties.
+ *
+ * @throws IllegalArgumentException if no credentials can be resolved
+ */
+ @VisibleForTesting
+ static void validateCredentials(AwsCredentialsProvider credentialsProvider) {
+ try {
+ credentialsProvider.resolveCredentials();
+ } catch (SdkClientException e) {
+ if (!GlueExceptionConverter.isCredentialFailure(e)) {
+ throw new IllegalArgumentException(
+ "Failed to resolve AWS credentials for the Glue catalog: " +
e.getMessage(), e);
+ }
+ throw new IllegalArgumentException(
+ String.format(
+ "No usable AWS credentials found for the Glue catalog. Set both
'%s' and '%s' "
+ + "catalog properties for static authentication, or ensure
the default AWS "
+ + "credential chain (environment variables, instance
profile, web identity "
+ + "token, etc.) can resolve credentials.",
+ GlueConstants.AWS_ACCESS_KEY_ID,
GlueConstants.AWS_SECRET_ACCESS_KEY),
+ e);
+ }
+ }
+
static boolean hasAwsStaticCredentials(String accessKey, String secretKey) {
boolean hasAccessKey = StringUtils.isNotBlank(accessKey);
boolean hasSecretKey = StringUtils.isNotBlank(secretKey);
diff --git
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueExceptionConverter.java
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueExceptionConverter.java
index c0ca31966c..544726bf5b 100644
---
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueExceptionConverter.java
+++
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueExceptionConverter.java
@@ -24,6 +24,7 @@ import org.apache.gravitino.exceptions.NoSuchTableException;
import org.apache.gravitino.exceptions.SchemaAlreadyExistsException;
import org.apache.gravitino.exceptions.TableAlreadyExistsException;
import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
+import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.services.glue.model.AlreadyExistsException;
import software.amazon.awssdk.services.glue.model.EntityNotFoundException;
import software.amazon.awssdk.services.glue.model.GlueException;
@@ -32,8 +33,42 @@ import
software.amazon.awssdk.services.glue.model.InvalidInputException;
/** Converts AWS Glue SDK exceptions to Gravitino exceptions. */
final class GlueExceptionConverter {
+ private static final String NO_CREDENTIALS_MARKER =
+ "Unable to load credentials from any of the providers";
+
private GlueExceptionConverter() {}
+ /**
+ * Whether {@code e} is the AWS SDK's default-credential-chain-exhausted
error, which otherwise
+ * surfaces as a raw {@link SdkClientException} listing SDK-internal
credential sources instead of
+ * this connector's own {@code aws-access-key-id} / {@code
aws-secret-access-key} properties.
+ *
+ * @param e the client exception raised while calling AWS Glue
+ * @return true if {@code e} is a credential-resolution failure
+ */
+ static boolean isCredentialFailure(SdkClientException e) {
+ return e.getMessage() != null &&
e.getMessage().contains(NO_CREDENTIALS_MARKER);
+ }
+
+ /**
+ * Converts a credential-resolution {@link SdkClientException} into a
message that names this
+ * connector's own credential properties, so operators are not left guessing
which environment
+ * variable or IAM role the raw SDK message intended.
+ *
+ * @param e the credential-resolution failure
+ * @param context description of the operation context for error messages
+ * @return a Gravitino runtime exception with an actionable message
+ */
+ static RuntimeException toCredentialException(SdkClientException e, String
context) {
+ return new RuntimeException(
+ String.format(
+ "Failed to authenticate with AWS Glue for %s. No usable AWS
credentials were "
+ + "found. Set both '%s' and '%s' catalog properties, or ensure
the default AWS "
+ + "credential chain can resolve credentials.",
+ context, GlueConstants.AWS_ACCESS_KEY_ID,
GlueConstants.AWS_SECRET_ACCESS_KEY),
+ e);
+ }
+
/**
* Converts a {@link GlueException} to the appropriate Gravitino schema
exception.
*
diff --git
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogSchemaOperations.java
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogSchemaOperations.java
index 438a07edc5..7c03aaeddb 100644
---
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogSchemaOperations.java
+++
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogSchemaOperations.java
@@ -125,6 +125,31 @@ class TestGlueCatalogSchemaOperations {
assertEquals(0, result.length);
}
+ @Test
+ void testListSchemasMapsCredentialFailureToActionableMessage() {
+ Namespace ns = Namespace.of("metalake", "catalog");
+ SdkClientException cause =
+ SdkClientException.create("Unable to load credentials from any of the
providers");
+
when(mockClient.getDatabases(any(GetDatabasesRequest.class))).thenThrow(cause);
+
+ RuntimeException ex = assertThrows(RuntimeException.class, () ->
ops.listSchemas(ns));
+
+ assertEquals(cause, ex.getCause());
+ assertTrue(ex.getMessage().contains("aws-access-key-id"));
+ assertTrue(ex.getMessage().contains("aws-secret-access-key"));
+ }
+
+ @Test
+ void testListSchemasRethrowsNonCredentialSdkClientException() {
+ Namespace ns = Namespace.of("metalake", "catalog");
+ SdkClientException cause = SdkClientException.create("connection refused");
+
when(mockClient.getDatabases(any(GetDatabasesRequest.class))).thenThrow(cause);
+
+ SdkClientException ex = assertThrows(SdkClientException.class, () ->
ops.listSchemas(ns));
+
+ assertEquals(cause, ex);
+ }
+
// -------------------------------------------------------------------------
// createSchema
// -------------------------------------------------------------------------
diff --git
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueClientProvider.java
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueClientProvider.java
index f69a1c0739..e3b1a2cb1c 100644
---
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueClientProvider.java
+++
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueClientProvider.java
@@ -22,13 +22,21 @@ import static
org.apache.gravitino.catalog.glue.GlueConstants.AWS_ACCESS_KEY_ID;
import static
org.apache.gravitino.catalog.glue.GlueConstants.AWS_GLUE_ENDPOINT;
import static org.apache.gravitino.catalog.glue.GlueConstants.AWS_REGION;
import static
org.apache.gravitino.catalog.glue.GlueConstants.AWS_SECRET_ACCESS_KEY;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.services.glue.GlueClient;
class TestGlueClientProvider {
@@ -45,15 +53,48 @@ class TestGlueClientProvider {
}
}
+ // Note: buildClient()'s default-credential-chain branch is not exercised
end-to-end here with
+ // no static credentials — whether it resolves depends on the machine's real
AWS environment
+ // (e.g. a developer's ~/.aws/credentials), which would make the test flaky.
The fail-fast
+ // validation logic itself (validateCredentials) is covered
deterministically below with a
+ // fake provider instead.
+
@Test
- void testBuildClientWithDefaultCredentialChain() {
- // Without explicit credentials the default chain is used.
- Map<String, String> config = new HashMap<>();
- config.put(AWS_REGION, "eu-west-1");
+ void testValidateCredentialsWithResolvableCredentialsSucceeds() {
+ AwsCredentialsProvider provider =
+ StaticCredentialsProvider.create(AwsBasicCredentials.create("ak",
"sk"));
- try (GlueClient client = GlueClientProvider.buildClient(config)) {
- assertNotNull(client);
- }
+ GlueClientProvider.validateCredentials(provider);
+ }
+
+ @Test
+ void testValidateCredentialsWithUnresolvableCredentialsThrows() {
+ AwsCredentialsProvider provider = mock(AwsCredentialsProvider.class);
+ doThrow(SdkClientException.create("Unable to load credentials from any of
the providers"))
+ .when(provider)
+ .resolveCredentials();
+
+ IllegalArgumentException ex =
+ assertThrows(
+ IllegalArgumentException.class, () ->
GlueClientProvider.validateCredentials(provider));
+ assertTrue(ex.getMessage().contains(AWS_ACCESS_KEY_ID));
+ assertTrue(ex.getMessage().contains(AWS_SECRET_ACCESS_KEY));
+ }
+
+ @Test
+ void
testValidateCredentialsWithNonCredentialFailureDoesNotClaimNoCredentials() {
+ // A network/IMDS error while resolving credentials is not the same as "no
credentials
+ // configured" — the message must not assert that no usable credentials
exist.
+ AwsCredentialsProvider provider = mock(AwsCredentialsProvider.class);
+ SdkClientException cause = SdkClientException.create("connection refused");
+ doThrow(cause).when(provider).resolveCredentials();
+
+ IllegalArgumentException ex =
+ assertThrows(
+ IllegalArgumentException.class, () ->
GlueClientProvider.validateCredentials(provider));
+ assertEquals(cause, ex.getCause());
+ assertTrue(ex.getMessage().contains("connection refused"));
+ assertFalse(ex.getMessage().contains("No usable AWS credentials"));
}
@Test
diff --git
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueExceptionConverter.java
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueExceptionConverter.java
index 14ad7d557d..33ff92a56e 100644
---
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueExceptionConverter.java
+++
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueExceptionConverter.java
@@ -19,6 +19,7 @@
package org.apache.gravitino.catalog.glue;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -29,6 +30,7 @@ import
org.apache.gravitino.exceptions.SchemaAlreadyExistsException;
import org.apache.gravitino.exceptions.TableAlreadyExistsException;
import org.junit.jupiter.api.Test;
import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
+import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.services.glue.model.AccessDeniedException;
import software.amazon.awssdk.services.glue.model.AlreadyExistsException;
import software.amazon.awssdk.services.glue.model.EntityNotFoundException;
@@ -44,6 +46,43 @@ public class TestGlueExceptionConverter {
+ "arn:aws:glue:us-east-2:123456789012:database/drop_me3 "
+ "because no identity-based policy allows the glue:CreateDatabase
action";
+ @Test
+ public void testIsCredentialFailureMatchesChainExhaustedMessage() {
+ SdkClientException e =
+ SdkClientException.create(
+ "Unable to load credentials from any of the providers in the chain
"
+ + "AwsCredentialsProviderChain(...)");
+
+ assertTrue(GlueExceptionConverter.isCredentialFailure(e));
+ }
+
+ @Test
+ public void testIsCredentialFailureRejectsUnrelatedMessage() {
+ SdkClientException e = SdkClientException.create("connection refused");
+
+ assertFalse(GlueExceptionConverter.isCredentialFailure(e));
+ }
+
+ @Test
+ public void testIsCredentialFailureRejectsNullMessage() {
+ SdkClientException e = SdkClientException.builder().message(null).build();
+
+ assertFalse(GlueExceptionConverter.isCredentialFailure(e));
+ }
+
+ @Test
+ public void testToCredentialExceptionIncludesContextAndPropertyNames() {
+ SdkClientException cause =
+ SdkClientException.create("Unable to load credentials from any of the
providers");
+
+ RuntimeException ex = GlueExceptionConverter.toCredentialException(cause,
"table mydb.mytbl");
+
+ assertEquals(cause, ex.getCause());
+ assertTrue(ex.getMessage().contains("table mydb.mytbl"));
+ assertTrue(ex.getMessage().contains(GlueConstants.AWS_ACCESS_KEY_ID));
+ assertTrue(ex.getMessage().contains(GlueConstants.AWS_SECRET_ACCESS_KEY));
+ }
+
@Test
public void testSchemaAccessDeniedKeepsAwsMessage() {
AccessDeniedException e =