sdaberdaku opened a new issue, #17860:
URL: https://github.com/apache/iceberg/issues/17860

   ### Apache Iceberg version
   
   1.11.0 (latest release)
   
   ### Query engine
   
   Spark
   
   ### Please describe the bug 🐞
   
   We run a single Spark Connect cluster (Spark 4.1.2) serving several tenants. 
Each tenant has its own Glue catalog in its own AWS account, reached with 
`AssumeRoleAwsClientFactory`:
   
   ```
   spark.sql.catalog.tenant                           
org.apache.iceberg.spark.SparkCatalog
   spark.sql.catalog.tenant.type                      glue
   spark.sql.catalog.tenant.io-impl                   
org.apache.iceberg.aws.s3.S3FileIO
   spark.sql.catalog.tenant.client.factory            
org.apache.iceberg.aws.AssumeRoleAwsClientFactory
   spark.sql.catalog.tenant.client.assume-role.arn    
arn:aws:iam::111122223333:role/lakehouse-access
   spark.sql.catalog.tenant.client.assume-role.region us-east-2
   spark.sql.catalog.tenant.glue.id                   111122223333
   ```
   
   The cluster itself runs on EKS as an IRSA role in a different account, which 
deliberately has no access to the tenant buckets:
   
   ```
   spark.hadoop.fs.s3.impl                       
org.apache.hadoop.fs.s3a.S3AFileSystem
   spark.hadoop.fs.s3a.aws.credentials.provider  
com.amazonaws.auth.WebIdentityTokenCredentialsProvider
   ```
   
   Nightly maintenance calls `expire_snapshots` and then `remove_orphan_files` 
on each table. The first one works, the second one gets a 403, seconds later, 
on the same table:
   
   ```sql
   -- ok
   CALL tenant.system.expire_snapshots(table => 'db.my_table', older_than => 
TIMESTAMP '2026-08-21 00:00:00');
   
   -- 403
   CALL tenant.system.remove_orphan_files(table => 'db.my_table', older_than => 
TIMESTAMP '2026-08-25 00:00:00');
   ```
   
   ```
   java.io.UncheckedIOException: java.nio.file.AccessDeniedException:
     s3://my-lakehouse-bucket/iceberg_tables/my_table: listObjects() on 
s3://my-lakehouse-bucket/iceberg_tables/my_table:
     software.amazon.awssdk.services.s3.model.AccessDeniedException:
     User: arn:aws:sts::444455556666:assumed-role/spark-cluster-irsa/... is not 
authorized to perform:
     s3:ListBucket on resource: "arn:aws:s3:::my-lakehouse-bucket" (Service: 
S3, Status Code: 403)
   
       at 
org.apache.iceberg.util.FileSystemWalker.listDirRecursivelyWithHadoop(FileSystemWalker.java:150)
       at 
org.apache.iceberg.spark.actions.DeleteOrphanFilesSparkAction.listedFileDS(DeleteOrphanFilesSparkAction.java:440)
       at 
org.apache.iceberg.spark.actions.DeleteOrphanFilesSparkAction.doExecute(DeleteOrphanFilesSparkAction.java:257)
       at 
org.apache.iceberg.spark.procedures.RemoveOrphanFilesProcedure.call(RemoveOrphanFilesProcedure.java:153)
   ```
   
   The principal in that error is the cluster's own IRSA role, which appears 
nowhere in the catalog config. So the listing is not using the catalog's 
credentials, while the rest of the procedure is. The cause is that the action 
takes its Hadoop config from the session and drops the catalog it came from:
   
   ```java
   // DeleteOrphanFilesSparkAction.java:139
   this.hadoopConf = new 
SerializableConfiguration(spark.sessionState().newHadoopConf());
   ```
   
   `newHadoopConf()` only sees `spark.hadoop.*`. Everything else in the 
procedure goes through `table.io()`, which the catalog built with the 
assume-role factory, which is why `expire_snapshots` is fine.
   
   I found #11541, which describes this exact coupling, and PR #12254 which 
closed it by adding the `FileIO` listing path. But that path is opt-in 
(`prefix_listing`, `usePrefixListing` defaults to `false` at 
`DeleteOrphanFilesSparkAction.java:133`), so the Hadoop path still behaves this 
way and it is what you get unless you know to ask for the other one.
   
   `prefix_listing => true` does fix the credentials for us, but we can't use 
it: it lists serially on the driver and then does `parallelize(matchingFiles, 
1)` (#16932, #17387). On Spark Connect there's an extra ceiling, since that 
single partition puts the whole listing under `spark.rpc.message.maxSize`, 
which is a server startup setting we can't raise from a client session. So 
we're stuck with the Hadoop path, and the Hadoop path has the wrong credentials.
   
   What took me longest to work out is that there is no per-catalog knob for 
this at all. `SparkUtil.hadoopConfCatalogOverrides(SparkSession, catalogName)` 
already exists and applies `spark.sql.catalog.<name>.hadoop.*` on top of the 
session config, which is exactly what's needed here, but it only has two 
callers, both in `SparkCatalog` (`SparkCatalog.java:142` and `:717`). No action 
or procedure uses it. So setting `spark.sql.catalog.tenant.hadoop.fs.s3a.*` is 
accepted, looks right, and is then ignored by the walk. I assumed I had a typo 
for a while.
   
   Proposal: build the action's Hadoop config with 
`SparkUtil.hadoopConfCatalogOverrides(spark, catalogName)` instead of 
`spark.sessionState().newHadoopConf()`, with a `catalogName()` setter on the 
action alongside the existing ones and `RemoveOrphanFilesProcedure` passing 
`tableCatalog().name()` (`BaseProcedure` already holds it). Nothing changes for 
anyone who doesn't set `spark.sql.catalog.<name>.hadoop.*`.
   
   I'm happy to open a PR for this, plus the v4.0 and v3.5 backports, if that 
sounds like the right direction. If the preference is to leave the Hadoop path 
alone because the FileIO one is where things are heading, then at minimum the 
docs should say that the listing uses the session's Hadoop config and not the 
catalog's, because nothing in the config surface hints at it.
   
   For anyone else hitting the same 403, the workaround is to give S3A the same 
role, scoped to the buckets:
   
   ```
   spark.hadoop.fs.s3a.bucket.my-lakehouse-bucket.aws.credentials.provider 
org.apache.hadoop.fs.s3a.auth.AssumedRoleCredentialProvider
   spark.hadoop.fs.s3a.bucket.my-lakehouse-bucket.assumed.role.arn         
arn:aws:iam::111122223333:role/lakehouse-access
   spark.hadoop.fs.s3a.assumed.role.credentials.provider                   
com.amazonaws.auth.WebIdentityTokenCredentialsProvider
   ```
   
   Per-bucket options key on the bucket name from the URI 
(`fs.s3a.bucket.<bucket>.`), independent of the scheme, so this covers `s3://` 
locations too. The role's trust policy has to allow the cluster principal to 
assume it, which it already does if the catalog is working.
   
   ### Willingness to contribute
   
   - [x] I can contribute a fix for this bug independently
   - [ ] I would be willing to contribute a fix for this bug with guidance from 
the Iceberg community
   - [ ] I cannot contribute a fix for this bug at this time
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to