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

morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new cbb31202e89 [feature](catalog) Support Alibaba Cloud OSS Tables REST 
catalog (#66567)
cbb31202e89 is described below

commit cbb31202e8958000f9f82c4c1980a08b29f26337
Author: Chenjunwei <[email protected]>
AuthorDate: Wed Aug 12 15:36:46 2026 +0800

    [feature](catalog) Support Alibaba Cloud OSS Tables REST catalog (#66567)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: None
    
    Problem Summary:
    
    Alibaba Cloud OSS Tables exposes an Iceberg REST Catalog that uses the
    `osstables` SigV4 service name and an S3-compatible data plane. The
    plugin-driven Iceberg connector only recognized Glue and S3 Tables as
    managed signed REST catalogs. Consequently, an OSS Tables catalog did
    not reuse the selected OSS credentials for REST signing, and its
    required signing region and SigV4 settings were not fully validated.
    
    This change:
    
    - recognizes `osstables` as a managed SigV4 signing name;
    - reuses the selected OSS/S3-compatible AK, SK, and STS token for both
    the REST control plane and S3FileIO data plane;
    - requires a signing region and `sigv4-enabled=true`;
    - adds coverage for the official OSS Tables endpoint shape, ACS
    warehouse ARN, OSS endpoint, and session token mapping.
    
    The current connector's connection check already initializes the real
    catalog through the full property-building path, so no separate
    connectivity-only implementation is needed.
---
 .../connector/iceberg/IcebergCatalogFactory.java   | 31 +++++++---
 .../doris/connector/iceberg/IcebergConnector.java  |  3 +-
 .../connector/iceberg/IcebergScanPlanProvider.java |  3 +-
 .../iceberg/IcebergWritePlanProvider.java          |  3 +-
 .../iceberg/FakeS3CompatibleStorageProperties.java | 25 ++++++++
 .../iceberg/IcebergCatalogFactoryTest.java         | 60 ++++++++++++++++++--
 .../iceberg/IcebergScanPlanProviderTest.java       | 28 +++++++++
 .../iceberg/IcebergWritePlanProviderTest.java      | 26 +++++++++
 .../rest/IcebergRestMetaStoreProperties.java       | 17 +++++-
 .../rest/IcebergRestMetaStorePropertiesTest.java   | 16 +++++-
 .../doris/connector/DefaultConnectorContext.java   | 14 +++++
 .../apache/doris/datasource/CatalogFactory.java    |  3 +-
 .../CatalogFactoryPluginRoutingTest.java           | 66 ++++++++++++++++++++++
 13 files changed, 276 insertions(+), 19 deletions(-)

diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergCatalogFactory.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergCatalogFactory.java
index 232623da99b..9cc82b71b0f 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergCatalogFactory.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergCatalogFactory.java
@@ -38,6 +38,7 @@ import org.apache.iceberg.aws.s3.S3FileIOProperties;
 import org.apache.iceberg.rest.auth.OAuth2Properties;
 
 import java.io.File;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
@@ -108,8 +109,6 @@ public final class IcebergCatalogFactory {
     private static final String REST_SIGV4_ENABLED_KEY = "rest.sigv4-enabled";
     private static final String REST_SIGNING_REGION_KEY = 
"rest.signing-region";
     private static final String SECURITY_TYPE_OAUTH2 = "oauth2";
-    private static final String SIGNING_NAME_GLUE = "glue";
-    private static final String SIGNING_NAME_S3TABLES = "s3tables";
 
     // GLUE.
     private static final String GLUE_CREDENTIALS_PROVIDER_KEY = 
"client.credentials-provider";
@@ -213,6 +212,25 @@ public final class IcebergCatalogFactory {
         return Optional.ofNullable(target != null ? target : fallback);
     }
 
+    /**
+     * Selects the storage bindings Iceberg should consume together. All 
non-S3-compatible bindings are
+     * preserved, while the S3-compatible family is reduced to the same single 
binding selected for S3FileIO:
+     * a cloud-specific provider such as OSS/COS/OBS wins over the generic S3 
fallback. Raw-property routing can
+     * legitimately bind both (for legacy parity), but merging both maps would 
let a later generic S3 binding
+     * overwrite the explicit provider's endpoint, credentials, and path-style 
setting.
+     */
+    public static List<StorageProperties> selectEffectiveStorages(
+            List<? extends StorageProperties> storages) {
+        S3CompatibleFileSystemProperties chosenS3 = 
chooseS3Compatible(storages).orElse(null);
+        List<StorageProperties> selected = new ArrayList<>();
+        for (StorageProperties storage : storages) {
+            if (!(storage instanceof S3CompatibleFileSystemProperties) || 
storage == chosenS3) {
+                selected.add(storage);
+            }
+        }
+        return selected;
+    }
+
     /**
      * Emits the iceberg {@code S3FileIO} catalog properties from the chosen 
fe-filesystem S3-compatible
      * storage, mirroring legacy {@code 
AbstractIcebergProperties.toS3FileIOProperties} (D-061): the
@@ -461,8 +479,8 @@ public final class IcebergCatalogFactory {
     /**
      * Mirrors legacy {@code IcebergRestProperties}: core ({@code uri} always, 
default empty), optional
      * ({@code prefix} / vended-credentials header / the two 
effectively-always timeouts), oauth2, and the glue
-     * sigv4 signing block (with credentials sourced from the chosen S3 store 
for glue/s3tables, else from the
-     * {@code iceberg.rest.*} aliases). PURE.
+     * sigv4 signing block (with credentials sourced from the chosen S3 store 
for managed signing names, else
+     * from the {@code iceberg.rest.*} aliases). PURE.
      *
      * <p>Every {@code iceberg.rest.*} value is read off the BOUND {@code 
rest} holder, which declares the alias
      * set once. {@code props} is still needed for the credential-provider 
mode, whose alias set spans the
@@ -515,9 +533,8 @@ public final class IcebergCatalogFactory {
         opts.put(REST_SIGNING_NAME_KEY, signingName);
         opts.put(REST_SIGV4_ENABLED_KEY, rest.getSigV4Enabled());
         opts.put(REST_SIGNING_REGION_KEY, rest.getSigningRegion());
-        if (SIGNING_NAME_GLUE.equals(signingName)
-                || SIGNING_NAME_S3TABLES.equals(signingName)) {
-            // glue/s3tables: credentials come from the chosen S3 store, 
switching on its credential type
+        if (rest.usesS3CredentialsForRestSigning()) {
+            // glue/s3tables/osstables: credentials come from the chosen 
S3-compatible store, switching on its type
             // (legacy getCredentialType precedence: EXPLICIT before 
ASSUME_ROLE before PROVIDER_CHAIN).
             if (chosenS3.isPresent()) {
                 S3CompatibleFileSystemProperties s3 = chosenS3.get();
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnector.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnector.java
index 47df2d3f9c6..c32f8bb6d8c 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnector.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnector.java
@@ -1219,7 +1219,8 @@ public class IcebergConnector implements Connector {
      */
     private Map<String, String> buildStorageHadoopConfig() {
         Map<String, String> merged = new HashMap<>();
-        for (StorageProperties sp : storage().getStorageProperties()) {
+        for (StorageProperties sp : 
IcebergCatalogFactory.selectEffectiveStorages(
+                storage().getStorageProperties())) {
             sp.toHadoopProperties().ifPresent(h -> 
merged.putAll(h.toHadoopConfigurationMap()));
         }
         return merged;
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergScanPlanProvider.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergScanPlanProvider.java
index 09f8fa44615..4f0f8417c66 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergScanPlanProvider.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergScanPlanProvider.java
@@ -1656,7 +1656,8 @@ public class IcebergScanPlanProvider implements 
ConnectorScanPlanProvider {
         // overlay, just the vended one below.
         if (context != null) {
             Map<String, String> backendStorageProps = new HashMap<>();
-            for (StorageProperties sp : storage().getStorageProperties()) {
+            for (StorageProperties sp : 
IcebergCatalogFactory.selectEffectiveStorages(
+                    storage().getStorageProperties())) {
                 sp.toBackendProperties().ifPresent(b -> 
backendStorageProps.putAll(b.toMap()));
             }
             backendStorageProps.forEach((k, v) -> 
props.put(ScanNodePropertyKeys.LOCATION_PREFIX + k, v));
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergWritePlanProvider.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergWritePlanProvider.java
index 16b6ea50c19..c6b9a85656a 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergWritePlanProvider.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergWritePlanProvider.java
@@ -951,7 +951,8 @@ public class IcebergWritePlanProvider implements 
ConnectorWritePlanProvider {
             // getBackendStorageProperties() second parse). The BE S3 sink 
(s3_util.cpp
             // convert_properties_to_s3_conf) reads ONLY AWS_*, so the 
fs.s3a.* hadoop form (correct for the FE
             // iceberg-catalog Configuration) would leave the BE writer with 
no creds.
-            for (StorageProperties sp : storage().getStorageProperties()) {
+            for (StorageProperties sp : 
IcebergCatalogFactory.selectEffectiveStorages(
+                    storage().getStorageProperties())) {
                 sp.toBackendProperties().ifPresent(b -> 
merged.putAll(b.toMap()));
             }
             // REST per-table vended overlay (colliding key takes the vended 
value — legacy/scan precedence): a
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/FakeS3CompatibleStorageProperties.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/FakeS3CompatibleStorageProperties.java
index eab46928e51..4195232ba21 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/FakeS3CompatibleStorageProperties.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/FakeS3CompatibleStorageProperties.java
@@ -18,11 +18,15 @@
 package org.apache.doris.connector.iceberg;
 
 import org.apache.doris.filesystem.FileSystemType;
+import org.apache.doris.filesystem.properties.BackendStorageKind;
+import org.apache.doris.filesystem.properties.BackendStorageProperties;
 import org.apache.doris.filesystem.properties.S3CompatibleFileSystemProperties;
 import org.apache.doris.filesystem.properties.StorageKind;
 
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 
 /**
@@ -43,6 +47,7 @@ final class FakeS3CompatibleStorageProperties implements 
S3CompatibleFileSystemP
     private String roleArn = "";
     private String externalId = "";
     private String usePathStyle = "";
+    private Map<String, String> backendProperties = Collections.emptyMap();
 
     FakeS3CompatibleStorageProperties(String providerName) {
         this.providerName = providerName;
@@ -88,6 +93,11 @@ final class FakeS3CompatibleStorageProperties implements 
S3CompatibleFileSystemP
         return this;
     }
 
+    FakeS3CompatibleStorageProperties backendProperties(Map<String, String> v) 
{
+        this.backendProperties = Collections.unmodifiableMap(new HashMap<>(v));
+        return this;
+    }
+
     @Override
     public String providerName() {
         return providerName;
@@ -183,4 +193,19 @@ final class FakeS3CompatibleStorageProperties implements 
S3CompatibleFileSystemP
         // Mirrors the real S3 provider (this fake's type() is 
FileSystemType.S3); no test asserts on it.
         return Set.of("s3", "s3a", "s3n");
     }
+
+    @Override
+    public Optional<BackendStorageProperties> toBackendProperties() {
+        return Optional.of(new BackendStorageProperties() {
+            @Override
+            public BackendStorageKind backendKind() {
+                return BackendStorageKind.S3_COMPATIBLE;
+            }
+
+            @Override
+            public Map<String, String> toMap() {
+                return backendProperties;
+            }
+        });
+    }
 }
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergCatalogFactoryTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergCatalogFactoryTest.java
index b86600b248d..e6bf7c0ca7d 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergCatalogFactoryTest.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergCatalogFactoryTest.java
@@ -325,6 +325,17 @@ public class IcebergCatalogFactoryTest {
         Assertions.assertEquals("S3", chosen.get().providerName());
     }
 
+    @Test
+    public void selectEffectiveStoragesDropsGenericS3WhenOssIsPresent() {
+        FakeS3CompatibleStorageProperties genericS3 = new 
FakeS3CompatibleStorageProperties("S3");
+        FakeS3CompatibleStorageProperties oss = new 
FakeS3CompatibleStorageProperties("OSS");
+
+        List<StorageProperties> selected = 
IcebergCatalogFactory.selectEffectiveStorages(
+                Arrays.asList(genericS3, oss));
+
+        Assertions.assertEquals(Collections.singletonList(oss), selected);
+    }
+
     @Test
     public void chooseS3CompatibleEmptyWhenNoS3Storage() {
         // WHY: a credential-less / HDFS-only catalog has no S3-compatible 
storage, so no S3FileIO props
@@ -436,7 +447,7 @@ public class IcebergCatalogFactoryTest {
     @Test
     public void 
appendRestSigningBlockEmitsSigningKeysAndS3ExplicitCredentials() {
         // WHY: when signing-name is set, legacy emits 
rest.signing-name/sigv4-enabled/signing-region; for
-        // glue/s3tables the credentials come from the chosen S3 store, 
EXPLICIT (static AK/SK) -> rest.* creds
+        // managed signing names get credentials from the chosen S3 store, 
EXPLICIT (static AK/SK) -> rest.* creds
         // (AwsProperties.REST_*). MUTATION: wrong signing keys, or sourcing 
creds from the wrong place -> red.
         Map<String, String> opts = new HashMap<>();
         appendRest(opts,
@@ -452,10 +463,49 @@ public class IcebergCatalogFactoryTest {
         Assertions.assertEquals("TK", opts.get("rest.session-token"));
     }
 
+    @Test
+    public void buildRestCatalogForOssTablesUsesSharedS3Credentials() {
+        Map<String, String> opts = 
IcebergCatalogFactory.buildCatalogProperties(
+                IcebergCatalogProperties.of(props("type", "iceberg",
+                        "iceberg.catalog.type", "rest",
+                        "iceberg.rest.uri", 
"https://cn-hangzhou.oss-tables.aliyuncs.com/iceberg";,
+                        "warehouse", 
"acs:osstables:cn-hangzhou:1234567890:bucket/my-table-bucket",
+                        "iceberg.rest.signing-name", "osstables",
+                        "iceberg.rest.signing-region", "cn-hangzhou",
+                        "iceberg.rest.sigv4-enabled", "true",
+                        "iceberg.rest.view-enabled", "false",
+                        "io-impl", "org.apache.iceberg.aws.s3.S3FileIO")),
+                Optional.of(new FakeS3CompatibleStorageProperties("OSS")
+                        .endpoint("https://oss-cn-hangzhou.aliyuncs.com";)
+                        .region("cn-hangzhou")
+                        .accessKey("OSS_AK")
+                        .secretKey("OSS_SK")
+                        .sessionToken("OSS_TOKEN")
+                        .usePathStyle("false")));
+
+        
Assertions.assertEquals("https://cn-hangzhou.oss-tables.aliyuncs.com/iceberg";, 
opts.get("uri"));
+        
Assertions.assertEquals("acs:osstables:cn-hangzhou:1234567890:bucket/my-table-bucket",
+                opts.get("warehouse"));
+        Assertions.assertEquals("org.apache.iceberg.aws.s3.S3FileIO", 
opts.get("io-impl"));
+        Assertions.assertEquals("osstables", opts.get("rest.signing-name"));
+        Assertions.assertEquals("cn-hangzhou", 
opts.get("rest.signing-region"));
+        Assertions.assertEquals("true", opts.get("rest.sigv4-enabled"));
+        Assertions.assertEquals("OSS_AK", opts.get("rest.access-key-id"));
+        Assertions.assertEquals("OSS_SK", opts.get("rest.secret-access-key"));
+        Assertions.assertEquals("OSS_TOKEN", opts.get("rest.session-token"));
+        Assertions.assertEquals("https://oss-cn-hangzhou.aliyuncs.com";, 
opts.get("s3.endpoint"));
+        Assertions.assertEquals("cn-hangzhou", opts.get("client.region"));
+        Assertions.assertEquals("OSS_AK", opts.get("s3.access-key-id"));
+        Assertions.assertEquals("OSS_SK", opts.get("s3.secret-access-key"));
+        Assertions.assertEquals("OSS_TOKEN", opts.get("s3.session-token"));
+        Assertions.assertEquals("false", opts.get("s3.path-style-access"));
+        Assertions.assertNull(opts.get("type"));
+    }
+
     @Test
     public void appendRestSigningGlueAssumeRoleWhenNoStaticCreds() {
         // WHY: legacy getCredentialType precedence is EXPLICIT then 
ASSUME_ROLE; with no static AK/SK but a role
-        // ARN the glue/s3tables signing path emits the assume-role block 
(client.factory + client.assume-role.*).
+        // ARN the managed signing path emits the assume-role block 
(client.factory + client.assume-role.*).
         // MUTATION: emitting rest.access-key-id from a blank AK, or skipping 
assume-role -> red.
         Map<String, String> opts = new HashMap<>();
         appendRest(opts,
@@ -470,7 +520,7 @@ public class IcebergCatalogFactoryTest {
 
     @Test
     public void appendRestSigningOtherNameUsesIcebergRestCredentials() {
-        // WHY: a signing-name NOT in {glue,s3tables} uses the iceberg.rest.* 
explicit creds (not the S3 store).
+        // WHY: a non-managed signing name uses the iceberg.rest.* explicit 
creds (not the S3 store).
         // MUTATION: reading the S3 store here -> red.
         Map<String, String> opts = new HashMap<>();
         appendRest(opts,
@@ -484,7 +534,7 @@ public class IcebergCatalogFactoryTest {
 
     @Test
     public void appendRestSigningGlueProviderChainPinsNonDefaultProvider() {
-        // F14: glue/s3tables signing with NO static creds and NO role -> 
PROVIDER_CHAIN. A non-DEFAULT
+        // F14: managed signing with NO static creds and NO role -> 
PROVIDER_CHAIN. A non-DEFAULT
         // s3.credentials_provider_type must pin client.credentials-provider 
to that provider class (was silently
         // dropped). MUTATION: dropping the else branch -> the key is absent 
-> red.
         Map<String, String> opts = new HashMap<>();
@@ -505,7 +555,7 @@ public class IcebergCatalogFactoryTest {
 
     @Test
     public void 
appendRestSigningOtherNameProviderChainPinsNonDefaultProvider() {
-        // F14: a non-glue/s3tables signing-name with NO explicit 
iceberg.rest.* creds falls to PROVIDER_CHAIN;
+        // F14: a non-managed signing name with NO explicit iceberg.rest.* 
creds falls to PROVIDER_CHAIN;
         // iceberg.rest.credentials_provider_type pins the provider class. 
MUTATION: dropping the else -> absent.
         Map<String, String> opts = new HashMap<>();
         appendRest(opts,
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergScanPlanProviderTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergScanPlanProviderTest.java
index 26fa913062a..9d0041bc602 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergScanPlanProviderTest.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergScanPlanProviderTest.java
@@ -42,6 +42,7 @@ import org.apache.doris.thrift.TIcebergFileDesc;
 import org.apache.doris.thrift.TTableFormatFileDesc;
 import org.apache.doris.thrift.schema.external.TFieldPtr;
 
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import org.apache.iceberg.DataFile;
 import org.apache.iceberg.DataFiles;
@@ -2903,6 +2904,33 @@ public class IcebergScanPlanProviderTest {
         Assertions.assertEquals("ep", props.get("location.AWS_ENDPOINT"));
     }
 
+    @Test
+    public void getScanNodePropertiesPrefersOssOverGenericS3Fallback() {
+        FakeIcebergTable table = fakeTable("t1");
+        RecordingConnectorContext context = new RecordingConnectorContext();
+        context.storageProperties = Arrays.asList(
+                new 
FakeS3CompatibleStorageProperties("OSS").backendProperties(ImmutableMap.of(
+                        "AWS_ENDPOINT", "https://oss-cn-beijing.aliyuncs.com";,
+                        "AWS_REGION", "cn-beijing",
+                        "use_path_style", "false")),
+                new 
FakeS3CompatibleStorageProperties("S3").backendProperties(ImmutableMap.of(
+                        "AWS_ENDPOINT", "https://s3.cn-beijing.amazonaws.com";,
+                        "AWS_REGION", "cn-beijing",
+                        "use_path_style", "true",
+                        "AWS_CREDENTIALS_PROVIDER_TYPE", "DEFAULT")));
+        IcebergScanPlanProvider provider =
+                new 
IcebergScanPlanProvider(IcebergCatalogProperties.of(Collections.emptyMap()), 
opsReturning(table), context);
+
+        Map<String, String> props = provider.getScanNodeProperties(
+                null, new IcebergTableHandle("db1", "t1"), 
Collections.emptyList(), Optional.empty());
+
+        Assertions.assertEquals("https://oss-cn-beijing.aliyuncs.com";,
+                props.get("location.AWS_ENDPOINT"));
+        Assertions.assertEquals("false", props.get("location.use_path_style"));
+        
Assertions.assertNull(props.get("location.AWS_CREDENTIALS_PROVIDER_TYPE"),
+                "generic S3-only properties must not leak into an explicitly 
matched OSS scan");
+    }
+
     @Test
     public void getScanNodePropertiesOverlaysVendedCredsOverStatic() {
         FakeIcebergTable table = fakeTable("t1");
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWritePlanProviderTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWritePlanProviderTest.java
index 4ab51cbe4e7..bcd7f07b129 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWritePlanProviderTest.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWritePlanProviderTest.java
@@ -758,6 +758,32 @@ public class IcebergWritePlanProviderTest {
                 "the sink must not ship the fs.s3a.* hadoop form (BE cannot 
read it)");
     }
 
+    @Test
+    public void planWritePrefersOssBackendConfigOverGenericS3Fallback() {
+        Table table = partitionedSortedTable(freshCatalog());
+        RecordingConnectorContext ctx = new RecordingConnectorContext();
+        ctx.backendFileType = TFileType.FILE_S3;
+        ctx.storageProperties = Arrays.asList(
+                new 
FakeS3CompatibleStorageProperties("OSS").backendProperties(ImmutableMap.of(
+                        "AWS_ENDPOINT", "https://oss-cn-beijing.aliyuncs.com";,
+                        "AWS_REGION", "cn-beijing",
+                        "use_path_style", "false")),
+                new 
FakeS3CompatibleStorageProperties("S3").backendProperties(ImmutableMap.of(
+                        "AWS_ENDPOINT", "https://s3.cn-beijing.amazonaws.com";,
+                        "AWS_REGION", "cn-beijing",
+                        "use_path_style", "true",
+                        "AWS_CREDENTIALS_PROVIDER_TYPE", "DEFAULT")));
+
+        TIcebergTableSink sink = planSink(table, ctx,
+                new WriteHandle(new IcebergTableHandle("db1", "t1")));
+
+        Assertions.assertEquals("https://oss-cn-beijing.aliyuncs.com";,
+                sink.getHadoopConfig().get("AWS_ENDPOINT"));
+        Assertions.assertEquals("false", 
sink.getHadoopConfig().get("use_path_style"));
+        
Assertions.assertNull(sink.getHadoopConfig().get("AWS_CREDENTIALS_PROVIDER_TYPE"),
+                "generic S3-only properties must not leak into an explicitly 
matched OSS write");
+    }
+
     // ───────────────────────────── broker backend (ofs:// / gfs:// -> 
FILE_BROKER) ─────────────────────────────
     //
     // WHY: SchemaTypeMapper maps ofs/gfs to FILE_BROKER; the sink must then 
carry the catalog's broker
diff --git 
a/fe/fe-connector/fe-connector-metastore-iceberg/src/main/java/org/apache/doris/connector/metastore/iceberg/rest/IcebergRestMetaStoreProperties.java
 
b/fe/fe-connector/fe-connector-metastore-iceberg/src/main/java/org/apache/doris/connector/metastore/iceberg/rest/IcebergRestMetaStoreProperties.java
index 538d52406c2..90e079640ec 100644
--- 
a/fe/fe-connector/fe-connector-metastore-iceberg/src/main/java/org/apache/doris/connector/metastore/iceberg/rest/IcebergRestMetaStoreProperties.java
+++ 
b/fe/fe-connector/fe-connector-metastore-iceberg/src/main/java/org/apache/doris/connector/metastore/iceberg/rest/IcebergRestMetaStoreProperties.java
@@ -41,6 +41,8 @@ import java.util.Map;
  *   <li>OAuth2 credential/token mutually exclusive (ParamRules)</li>
  *   <li>signing-name=glue requires signing-region + sigv4-enabled 
(ParamRules)</li>
  *   <li>signing-name=s3tables requires signing-region + sigv4-enabled 
(ParamRules)</li>
+ *   <li>signing-name=osstables requires signing-region + sigv4-enabled 
(ParamRules)</li>
+ *   <li>managed signing names require sigv4-enabled=true (ParamRules)</li>
  *   <li>access-key-id + secret-access-key set together (ParamRules)</li>
  * </ol>
  * No uri/warehouse requirement. The {@code Security}/{@code 
AwsCredentialsProviderMode} enum checks are
@@ -121,7 +123,7 @@ public final class IcebergRestMetaStoreProperties extends 
AbstractMetaStorePrope
     private String signingRegion = "";
 
     @ConnectorProperty(names = {"iceberg.rest.sigv4-enabled"}, required = 
false,
-            description = "True for Glue/S3Tables Rest Catalog.")
+            description = "True for Glue/S3Tables/OSS Tables Rest Catalog.")
     private String sigV4Enabled = "";
 
     @ConnectorProperty(names = {"iceberg.rest.access-key-id"}, required = 
false,
@@ -242,6 +244,13 @@ public final class IcebergRestMetaStoreProperties extends 
AbstractMetaStorePrope
         return sigV4Enabled;
     }
 
+    /** Whether REST signing reuses the selected S3-compatible storage 
credentials. */
+    public boolean usesS3CredentialsForRestSigning() {
+        return "glue".equals(signingName)
+                || "s3tables".equals(signingName)
+                || "osstables".equals(signingName);
+    }
+
     public String getAccessKeyId() {
         return accessKeyId;
     }
@@ -307,11 +316,15 @@ public final class IcebergRestMetaStoreProperties extends 
AbstractMetaStorePrope
                 throw new IllegalArgumentException("OAuth2 requires either 
credential or token");
             }
         }
-        // When signing-name is glue or s3tables: require signing-region and 
sigv4-enabled (registered).
+        // SigV4-backed REST catalogs require a signing region and SigV4 to be 
enabled (registered).
         rules.requireIf(signingName, "glue", new String[] {signingRegion, 
sigV4Enabled},
                 "Rest Catalog requires signing-region and sigv4-enabled set to 
true when signing-name is glue");
         rules.requireIf(signingName, "s3tables", new String[] {signingRegion, 
sigV4Enabled},
                 "Rest Catalog requires signing-region and sigv4-enabled set to 
true when signing-name is s3tables");
+        rules.requireIf(signingName, "osstables", new String[] {signingRegion, 
sigV4Enabled},
+                "Rest Catalog requires signing-region and sigv4-enabled set to 
true when signing-name is osstables");
+        rules.check(() -> usesS3CredentialsForRestSigning() && 
!"true".equalsIgnoreCase(sigV4Enabled),
+                "Rest Catalog requires sigv4-enabled set to true when 
signing-name is " + signingName);
         // AWS assume-role properties are not supported for the Iceberg REST 
catalog (eager).
         rejectUnsupportedAwsAssumeRoleProperty(ICEBERG_REST_ROLE_ARN);
         rejectUnsupportedAwsAssumeRoleProperty(ICEBERG_REST_EXTERNAL_ID);
diff --git 
a/fe/fe-connector/fe-connector-metastore-iceberg/src/test/java/org/apache/doris/connector/metastore/iceberg/rest/IcebergRestMetaStorePropertiesTest.java
 
b/fe/fe-connector/fe-connector-metastore-iceberg/src/test/java/org/apache/doris/connector/metastore/iceberg/rest/IcebergRestMetaStorePropertiesTest.java
index 8e29f5f67cc..a2280f37d9b 100644
--- 
a/fe/fe-connector/fe-connector-metastore-iceberg/src/test/java/org/apache/doris/connector/metastore/iceberg/rest/IcebergRestMetaStorePropertiesTest.java
+++ 
b/fe/fe-connector/fe-connector-metastore-iceberg/src/test/java/org/apache/doris/connector/metastore/iceberg/rest/IcebergRestMetaStorePropertiesTest.java
@@ -107,13 +107,16 @@ public class IcebergRestMetaStorePropertiesTest {
     }
 
     @Test
-    public void rule8And9SigningNameRequiresRegionAndSigV4() {
+    public void managedSigningNamesRequireRegionAndSigV4() {
         Assertions.assertEquals(
                 "Rest Catalog requires signing-region and sigv4-enabled set to 
true when signing-name is glue",
                 validateError(raw("iceberg.rest.signing-name", "glue")));
         Assertions.assertEquals(
                 "Rest Catalog requires signing-region and sigv4-enabled set to 
true when signing-name is s3tables",
                 validateError(raw("iceberg.rest.signing-name", "s3tables")));
+        Assertions.assertEquals(
+                "Rest Catalog requires signing-region and sigv4-enabled set to 
true when signing-name is osstables",
+                validateError(raw("iceberg.rest.signing-name", "osstables")));
         // satisfied when both region + sigv4-enabled present.
         IcebergRestMetaStoreProperties.of(raw("iceberg.rest.signing-name", 
"glue",
                 "iceberg.rest.signing-region", "us-east-1", 
"iceberg.rest.sigv4-enabled", "true")).validate();
@@ -122,6 +125,17 @@ public class IcebergRestMetaStorePropertiesTest {
         IcebergRestMetaStoreProperties.of(raw("iceberg.rest.signing-name", 
"Glue")).validate();
     }
 
+    @Test
+    public void managedSigningNamesRejectDisabledSigV4() {
+        for (String signingName : new String[] {"glue", "s3tables", 
"osstables"}) {
+            Assertions.assertEquals(
+                    "Rest Catalog requires sigv4-enabled set to true when 
signing-name is " + signingName,
+                    validateError(raw("iceberg.rest.signing-name", signingName,
+                            "iceberg.rest.signing-region", "us-east-1",
+                            "iceberg.rest.sigv4-enabled", "false")));
+        }
+    }
+
     @Test
     public void rule10AccessKeyAndSecretMustBeSetTogether() {
         Assertions.assertEquals("iceberg.rest.access-key-id and 
iceberg.rest.secret-access-key must be set together",
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/connector/DefaultConnectorContext.java
 
b/fe/fe-core/src/main/java/org/apache/doris/connector/DefaultConnectorContext.java
index d5a70b233e9..b0728d2d389 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/connector/DefaultConnectorContext.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/connector/DefaultConnectorContext.java
@@ -127,6 +127,20 @@ public class DefaultConnectorContext implements 
ConnectorContext, ConnectorStora
         this(catalogName, catalogId, () -> NOOP_AUTH);
     }
 
+    /**
+     * Creates the lightweight pre-initialization context used by the catalog 
factory for CREATE validation and
+     * edit-log replay. It exposes a snapshot of the raw storage properties so 
connector construction and
+     * connectivity checks can bind credentials, but it does not provide 
runtime authentication,
+     * connector-derived storage defaults, backend adapters, or a filesystem.
+     */
+    public static DefaultConnectorContext forCatalogCreationValidation(String 
catalogName, long catalogId,
+            Map<String, String> rawStorageProperties) {
+        Map<String, String> rawSnapshot = Collections.unmodifiableMap(
+                new HashMap<>(Objects.requireNonNull(rawStorageProperties, 
"rawStorageProperties")));
+        return new DefaultConnectorContext(catalogName, catalogId, () -> 
NOOP_AUTH,
+                Collections::emptyMap, () -> new HashMap<>(rawSnapshot));
+    }
+
     public DefaultConnectorContext(String catalogName, long catalogId,
             Supplier<ExecutionAuthenticator> authSupplier) {
         this(catalogName, catalogId, authSupplier, Collections::emptyMap);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogFactory.java 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogFactory.java
index a781fcc9eaa..cf410288032 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogFactory.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogFactory.java
@@ -114,7 +114,8 @@ public class CatalogFactory {
         Connector spiConnector;
         try {
             spiConnector = ConnectorFactory.createStandaloneCatalogConnector(
-                    catalogType, props, new DefaultConnectorContext(name, 
catalogId));
+                    catalogType, props,
+                    DefaultConnectorContext.forCatalogCreationValidation(name, 
catalogId, props));
         } catch (RuntimeException | Error e) {
             if (!isReplay) {
                 // Creating a catalog interactively must still fail loud: the 
user is waiting for the error.
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/CatalogFactoryPluginRoutingTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/CatalogFactoryPluginRoutingTest.java
index 7fd68249309..2707e406acb 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/CatalogFactoryPluginRoutingTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/CatalogFactoryPluginRoutingTest.java
@@ -26,8 +26,12 @@ import org.apache.doris.connector.spi.ConnectorContext;
 import org.apache.doris.connector.spi.ConnectorMetadata;
 import org.apache.doris.connector.spi.ConnectorProvider;
 import org.apache.doris.connector.spi.ConnectorSession;
+import org.apache.doris.connector.spi.ConnectorTestResult;
 import org.apache.doris.datasource.log.CatalogLog;
 import org.apache.doris.datasource.plugin.PluginDrivenExternalCatalog;
+import org.apache.doris.filesystem.properties.S3CompatibleFileSystemProperties;
+import org.apache.doris.fs.FileSystemFactory;
+import org.apache.doris.fs.FileSystemPluginManager;
 import org.apache.doris.nereids.trees.plans.commands.CreateCatalogCommand;
 
 import com.google.common.collect.Maps;
@@ -64,6 +68,7 @@ public class CatalogFactoryPluginRoutingTest {
     @AfterEach
     void tearDown() {
         ConnectorFactory.initPluginManager(new ConnectorPluginManager());
+        FileSystemFactory.initPluginManager(null);
     }
 
     @Test
@@ -156,6 +161,67 @@ public class CatalogFactoryPluginRoutingTest {
         }
     }
 
+    @Test
+    void createConnectivityTestSeesRawStorageProperties() throws Exception {
+        FileSystemPluginManager fileSystemManager = new 
FileSystemPluginManager();
+        fileSystemManager.loadBuiltins();
+        FileSystemFactory.initPluginManager(fileSystemManager);
+
+        AtomicInteger connectivityTests = new AtomicInteger();
+        ConnectorPluginManager connectorManager = new ConnectorPluginManager();
+        connectorManager.registerProvider(new ConnectorProvider() {
+            @Override
+            public String getType() {
+                return THIRD_PARTY_TYPE;
+            }
+
+            @Override
+            public boolean isStandaloneCatalogType() {
+                return true;
+            }
+
+            @Override
+            public Connector create(Map<String, String> properties, 
ConnectorContext context) {
+                return new Connector() {
+                    @Override
+                    public ConnectorMetadata getMetadata(ConnectorSession 
session) {
+                        return null;
+                    }
+
+                    @Override
+                    public boolean defaultTestConnection() {
+                        return true;
+                    }
+
+                    @Override
+                    public ConnectorTestResult testConnection(ConnectorSession 
session) {
+                        connectivityTests.incrementAndGet();
+                        boolean found = 
context.getStorageContext().getStorageProperties().stream()
+                                
.filter(S3CompatibleFileSystemProperties.class::isInstance)
+                                
.map(S3CompatibleFileSystemProperties.class::cast)
+                                .anyMatch(storage -> 
"OSS".equals(storage.providerName())
+                                        && 
"create-ak".equals(storage.getAccessKey())
+                                        && 
"create-sk".equals(storage.getSecretKey()));
+                        return found ? ConnectorTestResult.success()
+                                : ConnectorTestResult.failure("CREATE context 
did not expose raw storage props");
+                    }
+                };
+            }
+        });
+        ConnectorFactory.initPluginManager(connectorManager);
+
+        Map<String, String> properties = props(THIRD_PARTY_TYPE);
+        properties.put("oss.endpoint", "https://oss-cn-beijing.aliyuncs.com";);
+        properties.put("oss.region", "cn-beijing");
+        properties.put("oss.access_key", "create-ak");
+        properties.put("oss.secret_key", "create-sk");
+        CatalogIf<?> catalog = CatalogFactory.createFromCommand(
+                1L, command("storage_ctx_ctl", properties));
+
+        Assertions.assertInstanceOf(PluginDrivenExternalCatalog.class, 
catalog);
+        Assertions.assertEquals(1, connectivityTests.get());
+    }
+
     /** Registers a single fake provider and returns a counter of how often it 
was asked to build a connector. */
     private static AtomicInteger registerProvider(String type, boolean 
standalone) {
         AtomicInteger consulted = new AtomicInteger();


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

Reply via email to