This is an automated email from the ASF dual-hosted git repository.

diqiu50 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 172c9031d3 [#13012] fix(catalog-glue): Fail fast and give actionable 
errors on missing AWS credentials (#13013)
172c9031d3 is described below

commit 172c9031d31ca6dbe7c8b38f6db9ff015d0985c8
Author: Yuhui <[email protected]>
AuthorDate: Thu Sep 10 14:09:57 2026 +0800

    [#13012] fix(catalog-glue): Fail fast and give actionable errors on missing 
AWS credentials (#13013)
    
    ### What changes were proposed in this pull request?
    
    - Validate AWS credentials at catalog creation/update time instead of on
    first use.
    - When credentials fail at runtime, name the connector's own properties
    instead of
      surfacing the raw AWS SDK error.
    
    ### Why are the changes needed?
    
    A Glue catalog created without usable AWS credentials was stored
    successfully and
    then failed on every operation with a raw AWS SDK error that never
    mentioned the
    connector's own properties.
    
    Fix: #13012
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes: creating/updating a Glue catalog with no usable AWS credential
    source now
    fails immediately instead of succeeding and failing later.
    
    ### How was this patch tested?
    
    Added unit tests in `GlueClientProvider`/`GlueCatalogOperations`;
    existing tests pass.
    
    ---------
    
    Co-authored-by: Claude Sonnet 5 <[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   | 40 ++++++++++++
 6 files changed, 248 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 533e105411..4ab3ed9c9b 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
@@ -26,6 +26,7 @@ import 
org.apache.gravitino.exceptions.SchemaAlreadyExistsException;
 import org.apache.gravitino.exceptions.TableAlreadyExistsException;
 import org.apache.gravitino.utils.ExceptionMessages;
 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;
@@ -35,8 +36,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 7e88924137..23a32114a7 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
@@ -18,6 +18,8 @@
  */
 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 +31,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 +47,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 =

Reply via email to