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

yuqi1129 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 2559337520 [#12990] fix(catalog-glue): Include the AWS error detail in 
Glue failure messages (#12991)
2559337520 is described below

commit 2559337520be561a3489c76ff2fac7699626cf81
Author: Yuhui <[email protected]>
AuthorDate: Wed Sep 9 14:18:55 2026 +0800

    [#12990] fix(catalog-glue): Include the AWS error detail in Glue failure 
messages (#12991)
    
    ### What changes were proposed in this pull request?
    
    Append the AWS error code and message to the exception
    `GlueExceptionConverter` returns from its fallback branch, leaving the
    recognised branches and all call sites unchanged.
    
    ### Why are the changes needed?
    
    The fallback branch reported every failure as `Glue error: <object>` and
    discarded the AWS message, so an operator hitting an
    `AccessDeniedException` could not tell which permission was missing or
    on which resource.
    
    Fix: #12990
    
    ### Does this PR introduce _any_ user-facing change?
    
    Glue error messages now carry the AWS error code and message, which name
    the IAM principal and resource.
    
    ### How was this patch tested?
    
    Unit tests for the enriched message, the fallbacks when AWS supplies no
    detail, and the unchanged branches.
    
    ---------
    
    Co-authored-by: Claude Opus 5 <[email protected]>
---
 .../catalog/glue/GlueExceptionConverter.java       |  32 +++-
 .../catalog/glue/TestGlueExceptionConverter.java   | 181 +++++++++++++++++++++
 2 files changed, 211 insertions(+), 2 deletions(-)

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 7857875e0c..c0ca31966c 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
@@ -18,10 +18,12 @@
  */
 package org.apache.gravitino.catalog.glue;
 
+import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.exceptions.NoSuchSchemaException;
 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.services.glue.model.AlreadyExistsException;
 import software.amazon.awssdk.services.glue.model.EntityNotFoundException;
 import software.amazon.awssdk.services.glue.model.GlueException;
@@ -49,7 +51,7 @@ final class GlueExceptionConverter {
     if (e instanceof InvalidInputException) {
       return new IllegalArgumentException(context + ": " + e.getMessage(), e);
     }
-    return new RuntimeException("Glue error: " + context, e);
+    return new RuntimeException("Glue error: " + context + ": " + 
awsErrorDetail(e), e);
   }
 
   /**
@@ -69,6 +71,32 @@ final class GlueExceptionConverter {
     if (e instanceof InvalidInputException) {
       return new IllegalArgumentException(context + ": " + e.getMessage(), e);
     }
-    return new RuntimeException("Glue error: " + context, e);
+    return new RuntimeException("Glue error: " + context + ": " + 
awsErrorDetail(e), e);
+  }
+
+  /**
+   * Renders the AWS-side detail of a Glue exception. AWS names the failing 
action and the resource
+   * there, which is what the caller needs to act on; the error code is 
prefixed so the failure can
+   * be classified at a glance.
+   *
+   * @param e the Glue exception to describe
+   * @return the AWS error code and message, or a best-effort description when 
they are unavailable
+   */
+  private static String awsErrorDetail(GlueException e) {
+    AwsErrorDetails details = e.awsErrorDetails();
+    if (details != null) {
+      String code = details.errorCode();
+      String message = details.errorMessage();
+      if (StringUtils.isNotBlank(code) && StringUtils.isNotBlank(message)) {
+        return "[" + code + "] " + message;
+      }
+      if (StringUtils.isNotBlank(message)) {
+        return message;
+      }
+      if (StringUtils.isNotBlank(code)) {
+        return "[" + code + "]";
+      }
+    }
+    return StringUtils.isNotBlank(e.getMessage()) ? e.getMessage() : 
e.getClass().getSimpleName();
   }
 }
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
new file mode 100644
index 0000000000..14ad7d557d
--- /dev/null
+++ 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueExceptionConverter.java
@@ -0,0 +1,181 @@
+/*
+ * 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.glue;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.gravitino.exceptions.NoSuchSchemaException;
+import org.apache.gravitino.exceptions.NoSuchTableException;
+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.services.glue.model.AccessDeniedException;
+import software.amazon.awssdk.services.glue.model.AlreadyExistsException;
+import software.amazon.awssdk.services.glue.model.EntityNotFoundException;
+import software.amazon.awssdk.services.glue.model.GlueException;
+import software.amazon.awssdk.services.glue.model.InvalidInputException;
+
+/** Tests for {@link GlueExceptionConverter}. */
+public class TestGlueExceptionConverter {
+
+  private static final String IAM_MESSAGE =
+      "User: arn:aws:iam::123456789012:user/gravitino is not authorized to 
perform: "
+          + "glue:CreateDatabase on resource: "
+          + "arn:aws:glue:us-east-2:123456789012:database/drop_me3 "
+          + "because no identity-based policy allows the glue:CreateDatabase 
action";
+
+  @Test
+  public void testSchemaAccessDeniedKeepsAwsMessage() {
+    AccessDeniedException e =
+        AccessDeniedException.builder()
+            .message(IAM_MESSAGE)
+            .awsErrorDetails(
+                AwsErrorDetails.builder()
+                    .errorCode("AccessDeniedException")
+                    .errorMessage(IAM_MESSAGE)
+                    .build())
+            .build();
+
+    RuntimeException converted = GlueExceptionConverter.toSchemaException(e, 
"schema drop_me");
+
+    assertEquals(RuntimeException.class, converted.getClass());
+    assertSame(e, converted.getCause());
+    String message = converted.getMessage();
+    assertTrue(message.contains("schema drop_me"), message);
+    assertTrue(message.contains("[AccessDeniedException] "), message);
+    assertTrue(message.contains("glue:CreateDatabase"), message);
+    assertTrue(message.contains("database/drop_me3"), message);
+  }
+
+  @Test
+  public void testTableAccessDeniedKeepsAwsMessage() {
+    AccessDeniedException e =
+        AccessDeniedException.builder()
+            .message(IAM_MESSAGE)
+            .awsErrorDetails(
+                AwsErrorDetails.builder()
+                    .errorCode("AccessDeniedException")
+                    .errorMessage(IAM_MESSAGE)
+                    .build())
+            .build();
+
+    RuntimeException converted = GlueExceptionConverter.toTableException(e, 
"table ctas_test");
+
+    assertEquals(RuntimeException.class, converted.getClass());
+    assertSame(e, converted.getCause());
+    String message = converted.getMessage();
+    assertTrue(message.contains("table ctas_test"), message);
+    assertTrue(message.contains("AccessDeniedException"), message);
+    assertTrue(message.contains("glue:CreateDatabase"), message);
+  }
+
+  @Test
+  public void testErrorMessageAloneIsSurfaced() {
+    GlueException e =
+        (GlueException)
+            GlueException.builder()
+                .awsErrorDetails(
+                    AwsErrorDetails.builder().errorMessage("throttled by 
Glue").build())
+                .build();
+
+    RuntimeException converted = GlueExceptionConverter.toSchemaException(e, 
"schema db6a");
+
+    assertTrue(converted.getMessage().contains("schema db6a"), 
converted.getMessage());
+    assertTrue(converted.getMessage().contains("throttled by Glue"), 
converted.getMessage());
+  }
+
+  @Test
+  public void testErrorCodeAloneIsSurfaced() {
+    GlueException e =
+        (GlueException)
+            GlueException.builder()
+                .awsErrorDetails(
+                    
AwsErrorDetails.builder().errorCode("InternalServiceException").build())
+                .build();
+
+    RuntimeException converted = GlueExceptionConverter.toSchemaException(e, 
"schema db6a");
+
+    assertTrue(
+        converted.getMessage().contains("[InternalServiceException]"), 
converted.getMessage());
+  }
+
+  @Test
+  public void testFallsBackToExceptionMessageWithoutAwsErrorDetails() {
+    GlueException e = (GlueException) 
GlueException.builder().message("connection reset").build();
+
+    RuntimeException converted = GlueExceptionConverter.toSchemaException(e, 
"schema db6a");
+
+    assertTrue(converted.getMessage().contains("schema db6a"), 
converted.getMessage());
+    assertTrue(converted.getMessage().contains("connection reset"), 
converted.getMessage());
+  }
+
+  @Test
+  public void testFallsBackWhenAwsErrorDetailsAreBlank() {
+    GlueException e =
+        (GlueException)
+            GlueException.builder()
+                .message("connection reset")
+                
.awsErrorDetails(AwsErrorDetails.builder().errorCode("").errorMessage("").build())
+                .build();
+
+    RuntimeException converted = GlueExceptionConverter.toSchemaException(e, 
"schema db6a");
+
+    assertTrue(converted.getMessage().contains("connection reset"), 
converted.getMessage());
+  }
+
+  @Test
+  public void testFallsBackToExceptionTypeWithoutAnyMessage() {
+    GlueException e = (GlueException) GlueException.builder().build();
+
+    RuntimeException converted = GlueExceptionConverter.toSchemaException(e, 
"schema db6a");
+
+    assertTrue(converted.getMessage().contains("GlueException"), 
converted.getMessage());
+  }
+
+  @Test
+  public void testRecognisedExceptionsKeepTheirMapping() {
+    EntityNotFoundException notFound = 
EntityNotFoundException.builder().message("gone").build();
+    AlreadyExistsException exists = 
AlreadyExistsException.builder().message("dup").build();
+    InvalidInputException invalid = 
InvalidInputException.builder().message("bad name").build();
+
+    assertInstanceOf(
+        NoSuchSchemaException.class,
+        GlueExceptionConverter.toSchemaException(notFound, "schema db6a"));
+    assertInstanceOf(
+        SchemaAlreadyExistsException.class,
+        GlueExceptionConverter.toSchemaException(exists, "schema db6a"));
+    assertInstanceOf(
+        IllegalArgumentException.class,
+        GlueExceptionConverter.toSchemaException(invalid, "schema db6a"));
+
+    assertInstanceOf(
+        NoSuchTableException.class,
+        GlueExceptionConverter.toTableException(notFound, "table ctas_test"));
+    assertInstanceOf(
+        TableAlreadyExistsException.class,
+        GlueExceptionConverter.toTableException(exists, "table ctas_test"));
+    assertInstanceOf(
+        IllegalArgumentException.class,
+        GlueExceptionConverter.toTableException(invalid, "table ctas_test"));
+  }
+}

Reply via email to