2929import org .apache .iceberg .aws .lakeformation .LakeFormationAwsClientFactory ;
3030import org .apache .iceberg .aws .s3 .S3FileIO ;
3131import org .apache .iceberg .aws .s3 .signer .S3V4RestSignerClient ;
32+ import org .apache .iceberg .common .DynClasses ;
3233import org .apache .iceberg .common .DynMethods ;
3334import org .apache .iceberg .exceptions .ValidationException ;
3435import org .apache .iceberg .relocated .com .google .common .base .Preconditions ;
4546import software .amazon .awssdk .auth .credentials .AwsSessionCredentials ;
4647import software .amazon .awssdk .auth .credentials .DefaultCredentialsProvider ;
4748import software .amazon .awssdk .auth .credentials .StaticCredentialsProvider ;
49+ import software .amazon .awssdk .awscore .client .builder .AwsClientBuilder ;
4850import software .amazon .awssdk .awscore .client .builder .AwsSyncClientBuilder ;
4951import software .amazon .awssdk .core .client .builder .SdkClientBuilder ;
5052import software .amazon .awssdk .core .client .config .SdkAdvancedClientOption ;
@@ -351,6 +353,38 @@ public class AwsProperties implements Serializable {
351353 */
352354 public static final String CLIENT_ASSUME_ROLE_SESSION_NAME = "client.assume-role.session-name" ;
353355
356+ /**
357+ * Configure the AWS credentials provider used to create AWS clients. A fully qualified concrete
358+ * class with package that implements the {@link AwsCredentialsProvider} interface is required.
359+ *
360+ * <p>Additionally, the implementation class must also have a create() or create(Map) method
361+ * implemented, which returns an instance of the class that provides aws credentials provider.
362+ *
363+ * <p>Example:
364+ * client.credentials-provider=software.amazon.awssdk.auth.credentials.SystemPropertyCredentialsProvider
365+ *
366+ * <p>When set, the default client factory {@link
367+ * org.apache.iceberg.aws.AwsClientFactories.DefaultAwsClientFactory} and also other client
368+ * factory classes will use this provider to get AWS credentials provided instead of reading the
369+ * default credential chain to get AWS access credentials.
370+ */
371+ public static final String CLIENT_CREDENTIALS_PROVIDER = "client.credentials-provider" ;
372+
373+ /**
374+ * Used by the client.credentials-provider configured value that will be used by {@link
375+ * org.apache.iceberg.aws.AwsClientFactories.DefaultAwsClientFactory} and also other client
376+ * factory classes to pass provider-specific properties. Each property consists of a key name and
377+ * an associated value.
378+ */
379+ private static final String CLIENT_CREDENTIAL_PROVIDER_PREFIX = "client.credentials-provider." ;
380+
381+ /**
382+ * Used by {@link org.apache.iceberg.aws.AwsClientFactories.DefaultAwsClientFactory} and also
383+ * other client factory classes. If set, all AWS clients except STS client will use the given
384+ * region instead of the default region chain.
385+ */
386+ public static final String CLIENT_REGION = "client.region" ;
387+
354388 /**
355389 * The type of {@link software.amazon.awssdk.http.SdkHttpClient} implementation used by {@link
356390 * AwsClientFactory} If set, all AWS clients will use this specified HTTP client. If not set,
@@ -677,6 +711,9 @@ public class AwsProperties implements Serializable {
677711 private int clientAssumeRoleTimeoutSec ;
678712 private String clientAssumeRoleRegion ;
679713 private String clientAssumeRoleSessionName ;
714+ private String clientRegion ;
715+ private String clientCredentialsProvider ;
716+ private final Map <String , String > clientCredentialsProviderProperties ;
680717
681718 private String s3FileIoSseType ;
682719 private String s3FileIoSseKey ;
@@ -733,6 +770,9 @@ public AwsProperties() {
733770 this .clientAssumeRoleExternalId = null ;
734771 this .clientAssumeRoleRegion = null ;
735772 this .clientAssumeRoleSessionName = null ;
773+ this .clientRegion = null ;
774+ this .clientCredentialsProvider = null ;
775+ this .clientCredentialsProviderProperties = null ;
736776
737777 this .s3FileIoSseType = S3FILEIO_SSE_TYPE_NONE ;
738778 this .s3FileIoSseKey = null ;
@@ -794,6 +834,10 @@ public AwsProperties(Map<String, String> properties) {
794834 this .clientAssumeRoleExternalId = properties .get (CLIENT_ASSUME_ROLE_EXTERNAL_ID );
795835 this .clientAssumeRoleRegion = properties .get (CLIENT_ASSUME_ROLE_REGION );
796836 this .clientAssumeRoleSessionName = properties .get (CLIENT_ASSUME_ROLE_SESSION_NAME );
837+ this .clientRegion = properties .get (CLIENT_REGION );
838+ this .clientCredentialsProvider = properties .get (CLIENT_CREDENTIALS_PROVIDER );
839+ this .clientCredentialsProviderProperties =
840+ PropertyUtil .propertiesWithPrefix (properties , CLIENT_CREDENTIAL_PROVIDER_PREFIX );
797841
798842 this .s3FileIoSseType = properties .getOrDefault (S3FILEIO_SSE_TYPE , S3FILEIO_SSE_TYPE_NONE );
799843 this .s3FileIoSseKey = properties .get (S3FILEIO_SSE_KEY );
@@ -1116,6 +1160,14 @@ public Map<String, String> httpClientProperties() {
11161160 return httpClientProperties ;
11171161 }
11181162
1163+ public String clientRegion () {
1164+ return clientRegion ;
1165+ }
1166+
1167+ public void setClientRegion (String clientRegion ) {
1168+ this .clientRegion = clientRegion ;
1169+ }
1170+
11191171 /**
11201172 * Configure the credentials for an S3 client.
11211173 *
@@ -1132,6 +1184,36 @@ public <T extends S3ClientBuilder> void applyS3CredentialConfigurations(T builde
11321184 : credentialsProvider (s3AccessKeyId , s3SecretAccessKey , s3SessionToken ));
11331185 }
11341186
1187+ /**
1188+ * Configure a client AWS region.
1189+ *
1190+ * <p>Sample usage:
1191+ *
1192+ * <pre>
1193+ * S3Client.builder().applyMutation(awsProperties::applyClientRegionConfiguration)
1194+ * </pre>
1195+ */
1196+ public <T extends AwsClientBuilder > void applyClientRegionConfiguration (T builder ) {
1197+ if (clientRegion != null ) {
1198+ builder .region (Region .of (clientRegion ));
1199+ }
1200+ }
1201+
1202+ /**
1203+ * Configure the credential provider for AWS clients.
1204+ *
1205+ * <p>Sample usage:
1206+ *
1207+ * <pre>
1208+ * DynamoDbClient.builder().applyMutation(awsProperties::applyClientCredentialConfigurations)
1209+ * </pre>
1210+ */
1211+ public <T extends AwsClientBuilder > void applyClientCredentialConfigurations (T builder ) {
1212+ if (!Strings .isNullOrEmpty (this .clientCredentialsProvider )) {
1213+ builder .credentialsProvider (credentialsProvider (this .clientCredentialsProvider ));
1214+ }
1215+ }
1216+
11351217 /**
11361218 * Configure services settings for an S3 client. The settings include: s3DualStack,
11371219 * s3UseArnRegion, s3PathStyleAccess, and s3Acceleration
@@ -1188,14 +1270,12 @@ public <T extends AwsSyncClientBuilder> void applyHttpClientConfigurations(T bui
11881270 switch (httpClientType ) {
11891271 case HTTP_CLIENT_TYPE_URLCONNECTION :
11901272 UrlConnectionHttpClientConfigurations urlConnectionHttpClientConfigurations =
1191- (UrlConnectionHttpClientConfigurations )
1192- loadHttpClientConfigurations (UrlConnectionHttpClientConfigurations .class .getName ());
1273+ loadHttpClientConfigurations (UrlConnectionHttpClientConfigurations .class .getName ());
11931274 urlConnectionHttpClientConfigurations .configureHttpClientBuilder (builder );
11941275 break ;
11951276 case HTTP_CLIENT_TYPE_APACHE :
11961277 ApacheHttpClientConfigurations apacheHttpClientConfigurations =
1197- (ApacheHttpClientConfigurations )
1198- loadHttpClientConfigurations (ApacheHttpClientConfigurations .class .getName ());
1278+ loadHttpClientConfigurations (ApacheHttpClientConfigurations .class .getName ());
11991279 apacheHttpClientConfigurations .configureHttpClientBuilder (builder );
12001280 break ;
12011281 default :
@@ -1291,8 +1371,52 @@ private AwsCredentialsProvider credentialsProvider(
12911371 return StaticCredentialsProvider .create (
12921372 AwsSessionCredentials .create (accessKeyId , secretAccessKey , sessionToken ));
12931373 }
1294- } else {
1295- return DefaultCredentialsProvider .create ();
1374+ }
1375+
1376+ if (!Strings .isNullOrEmpty (this .clientCredentialsProvider )) {
1377+ return credentialsProvider (this .clientCredentialsProvider );
1378+ }
1379+
1380+ return DefaultCredentialsProvider .create ();
1381+ }
1382+
1383+ private AwsCredentialsProvider credentialsProvider (String credentialsProviderClass ) {
1384+ Class <?> providerClass ;
1385+ try {
1386+ providerClass = DynClasses .builder ().impl (credentialsProviderClass ).buildChecked ();
1387+ } catch (ClassNotFoundException e ) {
1388+ throw new IllegalArgumentException (
1389+ String .format (
1390+ "Cannot load class %s, it does not exist in the classpath" , credentialsProviderClass ),
1391+ e );
1392+ }
1393+
1394+ Preconditions .checkArgument (
1395+ AwsCredentialsProvider .class .isAssignableFrom (providerClass ),
1396+ String .format (
1397+ "Cannot initialize %s, it does not implement %s." ,
1398+ credentialsProviderClass , AwsCredentialsProvider .class .getName ()));
1399+
1400+ AwsCredentialsProvider provider ;
1401+ try {
1402+ try {
1403+ provider =
1404+ DynMethods .builder ("create" )
1405+ .hiddenImpl (providerClass , Map .class )
1406+ .buildStaticChecked ()
1407+ .invoke (clientCredentialsProviderProperties );
1408+ } catch (NoSuchMethodException e ) {
1409+ provider =
1410+ DynMethods .builder ("create" ).hiddenImpl (providerClass ).buildStaticChecked ().invoke ();
1411+ }
1412+
1413+ return provider ;
1414+ } catch (NoSuchMethodException e ) {
1415+ throw new IllegalArgumentException (
1416+ String .format (
1417+ "Cannot create an instance of %s, it does not contain a static 'create' or 'create(Map<String, String>)' method" ,
1418+ credentialsProviderClass ),
1419+ e );
12961420 }
12971421 }
12981422
@@ -1308,15 +1432,15 @@ private <T extends SdkClientBuilder> void configureEndpoint(T builder, String en
13081432 * software.amazon.awssdk.http.apache.ApacheHttpClient}, since including both will cause error
13091433 * described in <a href="https://github.com/apache/iceberg/issues/6715">issue#6715</a>
13101434 */
1311- private Object loadHttpClientConfigurations (String impl ) {
1435+ private < T > T loadHttpClientConfigurations (String impl ) {
13121436 Object httpClientConfigurations ;
13131437 try {
13141438 httpClientConfigurations =
13151439 DynMethods .builder ("create" )
13161440 .hiddenImpl (impl , Map .class )
13171441 .buildStaticChecked ()
13181442 .invoke (httpClientProperties );
1319- return httpClientConfigurations ;
1443+ return ( T ) httpClientConfigurations ;
13201444 } catch (NoSuchMethodException e ) {
13211445 throw new IllegalArgumentException (
13221446 String .format ("Cannot create %s to generate and configure the http client builder" , impl ),
0 commit comments