blob: 849445f86f3a634372ceb773c3d224a05828c73a [file] [log] [blame]
/*
* 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.iceberg.aws;
import java.io.Serializable;
import java.net.URI;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.iceberg.aws.dynamodb.DynamoDbCatalog;
import org.apache.iceberg.aws.lakeformation.LakeFormationAwsClientFactory;
import org.apache.iceberg.common.DynClasses;
import org.apache.iceberg.common.DynMethods;
import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
import org.apache.iceberg.relocated.com.google.common.base.Strings;
import org.apache.iceberg.relocated.com.google.common.collect.Maps;
import org.apache.iceberg.relocated.com.google.common.collect.Sets;
import org.apache.iceberg.util.PropertyUtil;
import org.apache.iceberg.util.SerializableMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.awscore.client.builder.AwsClientBuilder;
import software.amazon.awssdk.core.client.builder.SdkClientBuilder;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain;
import software.amazon.awssdk.services.dynamodb.DynamoDbClientBuilder;
import software.amazon.awssdk.services.glue.GlueClientBuilder;
public class AwsProperties implements Serializable {
private static final Logger LOG = LoggerFactory.getLogger(AwsProperties.class);
/**
* The ID of the Glue Data Catalog where the tables reside. If none is provided, Glue
* automatically uses the caller's AWS account ID by default.
*
* <p>For more details, see
* https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-catalog-databases.html
*/
public static final String GLUE_CATALOG_ID = "glue.id";
/**
* The account ID used in a Glue resource ARN, e.g.
* arn:aws:glue:us-east-1:1000000000000:table/db1/table1
*/
public static final String GLUE_ACCOUNT_ID = "glue.account-id";
/**
* If Glue should skip archiving an old table version when creating a new version in a commit. By
* default Glue archives all old table versions after an UpdateTable call, but Glue has a default
* max number of archived table versions (can be increased). So for streaming use case with lots
* of commits, it is recommended to set this value to true.
*/
public static final String GLUE_CATALOG_SKIP_ARCHIVE = "glue.skip-archive";
public static final boolean GLUE_CATALOG_SKIP_ARCHIVE_DEFAULT = true;
/**
* If Glue should skip name validations It is recommended to stick to Glue best practice in
* https://docs.aws.amazon.com/athena/latest/ug/glue-best-practices.html to make sure operations
* are Hive compatible. This is only added for users that have existing conventions using
* non-standard characters. When database name and table name validation are skipped, there is no
* guarantee that downstream systems would all support the names.
*/
public static final String GLUE_CATALOG_SKIP_NAME_VALIDATION = "glue.skip-name-validation";
public static final boolean GLUE_CATALOG_SKIP_NAME_VALIDATION_DEFAULT = false;
/**
* If set, GlueCatalog will use Lake Formation for access control. For more credential vending
* details, see: https://docs.aws.amazon.com/lake-formation/latest/dg/api-overview.html. If
* enabled, the {@link AwsClientFactory} implementation must be {@link
* LakeFormationAwsClientFactory} or any class that extends it.
*/
public static final String GLUE_LAKEFORMATION_ENABLED = "glue.lakeformation-enabled";
public static final boolean GLUE_LAKEFORMATION_ENABLED_DEFAULT = false;
/**
* Configure an alternative endpoint of the Glue service for GlueCatalog to access.
*
* <p>This could be used to use GlueCatalog with any glue-compatible metastore service that has a
* different endpoint
*/
public static final String GLUE_CATALOG_ENDPOINT = "glue.endpoint";
/** Configure an alternative endpoint of the DynamoDB service to access. */
public static final String DYNAMODB_ENDPOINT = "dynamodb.endpoint";
/** DynamoDB table name for {@link DynamoDbCatalog} */
public static final String DYNAMODB_TABLE_NAME = "dynamodb.table-name";
public static final String DYNAMODB_TABLE_NAME_DEFAULT = "iceberg";
/**
* The implementation class of {@link AwsClientFactory} to customize AWS client configurations. If
* set, all AWS clients will be initialized by the specified factory. If not set, {@link
* AwsClientFactories#defaultFactory()} is used as default factory.
*/
public static final String CLIENT_FACTORY = "client.factory";
/**
* Used by {@link AssumeRoleAwsClientFactory}. If set, all AWS clients will assume a role of the
* given ARN, instead of using the default credential chain.
*/
public static final String CLIENT_ASSUME_ROLE_ARN = "client.assume-role.arn";
/**
* Used by {@link AssumeRoleAwsClientFactory} to pass a list of sessions. Each session tag
* consists of a key name and an associated value.
*/
public static final String CLIENT_ASSUME_ROLE_TAGS_PREFIX = "client.assume-role.tags.";
/**
* Used by {@link AssumeRoleAwsClientFactory}. The timeout of the assume role session in seconds,
* default to 1 hour. At the end of the timeout, a new set of role session credentials will be
* fetched through a STS client.
*/
public static final String CLIENT_ASSUME_ROLE_TIMEOUT_SEC = "client.assume-role.timeout-sec";
public static final int CLIENT_ASSUME_ROLE_TIMEOUT_SEC_DEFAULT = 3600;
/**
* Used by {@link AssumeRoleAwsClientFactory}. Optional external ID used to assume an IAM role.
*
* <p>For more details, see
* https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html
*/
public static final String CLIENT_ASSUME_ROLE_EXTERNAL_ID = "client.assume-role.external-id";
/**
* Used by {@link AssumeRoleAwsClientFactory}. If set, all AWS clients except STS client will use
* the given region instead of the default region chain.
*
* <p>The value must be one of {@link software.amazon.awssdk.regions.Region}, such as 'us-east-1'.
* For more details, see https://docs.aws.amazon.com/general/latest/gr/rande.html
*/
public static final String CLIENT_ASSUME_ROLE_REGION = "client.assume-role.region";
/**
* Used by {@link AssumeRoleAwsClientFactory}. Optional session name used to assume an IAM role.
*
* <p>For more details, see
* https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html#ck_rolesessionname
*/
public static final String CLIENT_ASSUME_ROLE_SESSION_NAME = "client.assume-role.session-name";
/**
* Used by {@link LakeFormationAwsClientFactory}. The table name used as part of lake formation
* credentials request.
*/
public static final String LAKE_FORMATION_TABLE_NAME = "lakeformation.table-name";
/**
* Used by {@link LakeFormationAwsClientFactory}. The database name used as part of lake formation
* credentials request.
*/
public static final String LAKE_FORMATION_DB_NAME = "lakeformation.db-name";
/** Region to be used by the SigV4 protocol for signing requests. */
public static final String REST_SIGNER_REGION = "rest.signing-region";
/** The service name to be used by the SigV4 protocol for signing requests. */
public static final String REST_SIGNING_NAME = "rest.signing-name";
/** The default service name (API Gateway and lambda) used during SigV4 signing. */
public static final String REST_SIGNING_NAME_DEFAULT = "execute-api";
/**
* Configure the static access key ID used for SigV4 signing.
*
* <p>When set, the default client factory will use the basic or session credentials provided
* instead of reading the default credential chain to create S3 access credentials. If {@link
* #REST_SESSION_TOKEN} is set, session credential is used, otherwise basic credential is used.
*/
public static final String REST_ACCESS_KEY_ID = "rest.access-key-id";
/**
* Configure the static secret access key used for SigV4 signing.
*
* <p>When set, the default client factory will use the basic or session credentials provided
* instead of reading the default credential chain to create S3 access credentials. If {@link
* #REST_SESSION_TOKEN} is set, session credential is used, otherwise basic credential is used.
*/
public static final String REST_SECRET_ACCESS_KEY = "rest.secret-access-key";
/**
* Configure the static session token used for SigV4.
*
* <p>When set, the default client factory will use the session credentials provided instead of
* reading the default credential chain to create access credentials.
*/
public static final String REST_SESSION_TOKEN = "rest.session-token";
private static final String HTTP_CLIENT_PREFIX = "http-client.";
private final Map<String, String> httpClientProperties;
private final Set<software.amazon.awssdk.services.sts.model.Tag> stsClientAssumeRoleTags;
private String clientAssumeRoleArn;
private String clientAssumeRoleExternalId;
private int clientAssumeRoleTimeoutSec;
private String clientAssumeRoleRegion;
private String clientAssumeRoleSessionName;
private String clientRegion;
private String clientCredentialsProvider;
private final Map<String, String> clientCredentialsProviderProperties;
private String glueEndpoint;
private String glueCatalogId;
private boolean glueCatalogSkipArchive;
private boolean glueCatalogSkipNameValidation;
private boolean glueLakeFormationEnabled;
private String dynamoDbTableName;
private String dynamoDbEndpoint;
private final Map<String, String> allProperties;
private String restSigningRegion;
private String restSigningName;
private String restAccessKeyId;
private String restSecretAccessKey;
private String restSessionToken;
public AwsProperties() {
this.httpClientProperties = Collections.emptyMap();
this.stsClientAssumeRoleTags = Sets.newHashSet();
this.clientAssumeRoleArn = null;
this.clientAssumeRoleTimeoutSec = CLIENT_ASSUME_ROLE_TIMEOUT_SEC_DEFAULT;
this.clientAssumeRoleExternalId = null;
this.clientAssumeRoleRegion = null;
this.clientAssumeRoleSessionName = null;
this.clientRegion = null;
this.clientCredentialsProvider = null;
this.clientCredentialsProviderProperties = null;
this.glueCatalogId = null;
this.glueEndpoint = null;
this.glueCatalogSkipArchive = GLUE_CATALOG_SKIP_ARCHIVE_DEFAULT;
this.glueCatalogSkipNameValidation = GLUE_CATALOG_SKIP_NAME_VALIDATION_DEFAULT;
this.glueLakeFormationEnabled = GLUE_LAKEFORMATION_ENABLED_DEFAULT;
this.dynamoDbEndpoint = null;
this.dynamoDbTableName = DYNAMODB_TABLE_NAME_DEFAULT;
this.allProperties = Maps.newHashMap();
this.restSigningName = REST_SIGNING_NAME_DEFAULT;
}
@SuppressWarnings("MethodLength")
public AwsProperties(Map<String, String> properties) {
this.httpClientProperties =
PropertyUtil.filterProperties(properties, key -> key.startsWith(HTTP_CLIENT_PREFIX));
this.stsClientAssumeRoleTags = toStsTags(properties, CLIENT_ASSUME_ROLE_TAGS_PREFIX);
this.clientAssumeRoleArn = properties.get(CLIENT_ASSUME_ROLE_ARN);
this.clientAssumeRoleTimeoutSec =
PropertyUtil.propertyAsInt(
properties, CLIENT_ASSUME_ROLE_TIMEOUT_SEC, CLIENT_ASSUME_ROLE_TIMEOUT_SEC_DEFAULT);
this.clientAssumeRoleExternalId = properties.get(CLIENT_ASSUME_ROLE_EXTERNAL_ID);
this.clientAssumeRoleRegion = properties.get(CLIENT_ASSUME_ROLE_REGION);
this.clientAssumeRoleSessionName = properties.get(CLIENT_ASSUME_ROLE_SESSION_NAME);
this.clientRegion = properties.get(AwsClientProperties.CLIENT_REGION);
this.clientCredentialsProvider =
properties.get(AwsClientProperties.CLIENT_CREDENTIALS_PROVIDER);
this.clientCredentialsProviderProperties =
PropertyUtil.propertiesWithPrefix(
properties, AwsClientProperties.CLIENT_CREDENTIAL_PROVIDER_PREFIX);
this.glueEndpoint = properties.get(GLUE_CATALOG_ENDPOINT);
this.glueCatalogId = properties.get(GLUE_CATALOG_ID);
this.glueCatalogSkipArchive =
PropertyUtil.propertyAsBoolean(
properties, GLUE_CATALOG_SKIP_ARCHIVE, GLUE_CATALOG_SKIP_ARCHIVE_DEFAULT);
this.glueCatalogSkipNameValidation =
PropertyUtil.propertyAsBoolean(
properties,
GLUE_CATALOG_SKIP_NAME_VALIDATION,
GLUE_CATALOG_SKIP_NAME_VALIDATION_DEFAULT);
this.glueLakeFormationEnabled =
PropertyUtil.propertyAsBoolean(
properties, GLUE_LAKEFORMATION_ENABLED, GLUE_LAKEFORMATION_ENABLED_DEFAULT);
this.dynamoDbEndpoint = properties.get(DYNAMODB_ENDPOINT);
this.dynamoDbTableName =
PropertyUtil.propertyAsString(properties, DYNAMODB_TABLE_NAME, DYNAMODB_TABLE_NAME_DEFAULT);
this.allProperties = SerializableMap.copyOf(properties);
this.restSigningRegion = properties.get(REST_SIGNER_REGION);
this.restSigningName = properties.getOrDefault(REST_SIGNING_NAME, REST_SIGNING_NAME_DEFAULT);
this.restAccessKeyId = properties.get(REST_ACCESS_KEY_ID);
this.restSecretAccessKey = properties.get(REST_SECRET_ACCESS_KEY);
this.restSessionToken = properties.get(REST_SESSION_TOKEN);
}
public Set<software.amazon.awssdk.services.sts.model.Tag> stsClientAssumeRoleTags() {
return stsClientAssumeRoleTags;
}
public String clientAssumeRoleArn() {
return clientAssumeRoleArn;
}
public int clientAssumeRoleTimeoutSec() {
return clientAssumeRoleTimeoutSec;
}
public String clientAssumeRoleExternalId() {
return clientAssumeRoleExternalId;
}
public String clientAssumeRoleRegion() {
return clientAssumeRoleRegion;
}
public String clientAssumeRoleSessionName() {
return clientAssumeRoleSessionName;
}
public String glueCatalogId() {
return glueCatalogId;
}
public void setGlueCatalogId(String id) {
this.glueCatalogId = id;
}
public boolean glueCatalogSkipArchive() {
return glueCatalogSkipArchive;
}
public void setGlueCatalogSkipArchive(boolean skipArchive) {
this.glueCatalogSkipArchive = skipArchive;
}
public boolean glueCatalogSkipNameValidation() {
return glueCatalogSkipNameValidation;
}
public void setGlueCatalogSkipNameValidation(boolean glueCatalogSkipNameValidation) {
this.glueCatalogSkipNameValidation = glueCatalogSkipNameValidation;
}
public boolean glueLakeFormationEnabled() {
return glueLakeFormationEnabled;
}
public void setGlueLakeFormationEnabled(boolean glueLakeFormationEnabled) {
this.glueLakeFormationEnabled = glueLakeFormationEnabled;
}
public String dynamoDbTableName() {
return dynamoDbTableName;
}
public void setDynamoDbTableName(String name) {
this.dynamoDbTableName = name;
}
/** @deprecated will be removed in 1.5.0, use {@link HttpClientProperties} instead */
@Deprecated
public Map<String, String> httpClientProperties() {
return httpClientProperties;
}
/**
* @deprecated will be removed in 1.5.0, use {@link AwsClientProperties#clientRegion()} instead
*/
@Deprecated
public String clientRegion() {
return clientRegion;
}
/**
* @deprecated will be removed in 1.5.0, use {@link AwsClientProperties#setClientRegion(String)}
* instead
*/
@Deprecated
public void setClientRegion(String clientRegion) {
this.clientRegion = clientRegion;
}
/**
* @deprecated will be removed in 1.5.0, use {@link
* AwsClientProperties#applyClientCredentialConfigurations(AwsClientBuilder)} instead
*/
@Deprecated
public <T extends AwsClientBuilder> void applyClientCredentialConfigurations(T builder) {
if (!Strings.isNullOrEmpty(this.clientCredentialsProvider)) {
builder.credentialsProvider(credentialsProvider(this.clientCredentialsProvider));
}
}
/**
* Override the endpoint for a glue client.
*
* <p>Sample usage:
*
* <pre>
* GlueClient.builder().applyMutation(awsProperties::applyS3EndpointConfigurations)
* </pre>
*/
public <T extends GlueClientBuilder> void applyGlueEndpointConfigurations(T builder) {
configureEndpoint(builder, glueEndpoint);
}
/**
* Override the endpoint for a dynamoDb client.
*
* <p>Sample usage:
*
* <pre>
* DynamoDbClient.builder().applyMutation(awsProperties::applyDynamoDbEndpointConfigurations)
* </pre>
*/
public <T extends DynamoDbClientBuilder> void applyDynamoDbEndpointConfigurations(T builder) {
configureEndpoint(builder, dynamoDbEndpoint);
}
public Region restSigningRegion() {
if (restSigningRegion == null) {
this.restSigningRegion = DefaultAwsRegionProviderChain.builder().build().getRegion().id();
}
return Region.of(restSigningRegion);
}
public String restSigningName() {
return restSigningName;
}
public AwsCredentialsProvider restCredentialsProvider() {
return credentialsProvider(
this.restAccessKeyId, this.restSecretAccessKey, this.restSessionToken);
}
private Set<software.amazon.awssdk.services.sts.model.Tag> toStsTags(
Map<String, String> properties, String prefix) {
return PropertyUtil.propertiesWithPrefix(properties, prefix).entrySet().stream()
.map(
e ->
software.amazon.awssdk.services.sts.model.Tag.builder()
.key(e.getKey())
.value(e.getValue())
.build())
.collect(Collectors.toSet());
}
private AwsCredentialsProvider credentialsProvider(
String accessKeyId, String secretAccessKey, String sessionToken) {
if (accessKeyId != null) {
if (sessionToken == null) {
return StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKeyId, secretAccessKey));
} else {
return StaticCredentialsProvider.create(
AwsSessionCredentials.create(accessKeyId, secretAccessKey, sessionToken));
}
}
if (!Strings.isNullOrEmpty(this.clientCredentialsProvider)) {
return credentialsProvider(this.clientCredentialsProvider);
}
// Create a new credential provider for each client
return DefaultCredentialsProvider.builder().build();
}
private AwsCredentialsProvider credentialsProvider(String credentialsProviderClass) {
Class<?> providerClass;
try {
providerClass = DynClasses.builder().impl(credentialsProviderClass).buildChecked();
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException(
String.format(
"Cannot load class %s, it does not exist in the classpath", credentialsProviderClass),
e);
}
Preconditions.checkArgument(
AwsCredentialsProvider.class.isAssignableFrom(providerClass),
String.format(
"Cannot initialize %s, it does not implement %s.",
credentialsProviderClass, AwsCredentialsProvider.class.getName()));
AwsCredentialsProvider provider;
try {
try {
provider =
DynMethods.builder("create")
.hiddenImpl(providerClass, Map.class)
.buildStaticChecked()
.invoke(clientCredentialsProviderProperties);
} catch (NoSuchMethodException e) {
provider =
DynMethods.builder("create").hiddenImpl(providerClass).buildStaticChecked().invoke();
}
return provider;
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException(
String.format(
"Cannot create an instance of %s, it does not contain a static 'create' or 'create(Map<String, String>)' method",
credentialsProviderClass),
e);
}
}
private <T extends SdkClientBuilder> void configureEndpoint(T builder, String endpoint) {
if (endpoint != null) {
builder.endpointOverride(URI.create(endpoint));
}
}
}