-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Azure: Support vended credentials refresh in ADLSFileIO. #11577
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 19 commits
a63232a
7cb7e72
e19f7e8
4b869c5
6b0f859
54112b9
c92479d
bc08205
5913fd1
7fb5ff3
016347d
a135ca0
b97ee0c
0a2f094
9fc4f23
a89980e
b398961
3f33808
0f491c8
28e7ba6
d419ecb
da31d42
ec3e679
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,23 +25,43 @@ | |
| import java.util.Collections; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
| import org.apache.iceberg.CatalogProperties; | ||
| import org.apache.iceberg.azure.adlsv2.VendedAdlsCredentialProvider; | ||
| 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.rest.RESTUtil; | ||
| import org.apache.iceberg.rest.auth.OAuth2Properties; | ||
| import org.apache.iceberg.util.PropertyUtil; | ||
| import org.apache.iceberg.util.SerializableMap; | ||
|
|
||
| public class AzureProperties implements Serializable { | ||
| public static final String ADLS_SAS_TOKEN_PREFIX = "adls.sas-token."; | ||
| public static final String ADLS_SAS_TOKEN_EXPIRES_AT_MS_PREFIX = "adls.sas-token-expires-at-ms."; | ||
| public static final String ADLS_CONNECTION_STRING_PREFIX = "adls.connection-string."; | ||
| public static final String ADLS_READ_BLOCK_SIZE = "adls.read.block-size-bytes"; | ||
| public static final String ADLS_WRITE_BLOCK_SIZE = "adls.write.block-size-bytes"; | ||
| public static final String ADLS_SHARED_KEY_ACCOUNT_NAME = "adls.auth.shared-key.account.name"; | ||
| public static final String ADLS_SHARED_KEY_ACCOUNT_KEY = "adls.auth.shared-key.account.key"; | ||
|
|
||
| /** | ||
| * When set, the {@link VendedAdlsCredentialProvider} will be used to fetch and refresh vended | ||
| * credentials from this endpoint. | ||
| */ | ||
| public static final String ADLS_REFRESH_CREDENTIALS_ENDPOINT = | ||
| "adls.refresh-credentials-endpoint"; | ||
|
|
||
| /** Controls whether vended credentials should be refreshed or not. Defaults to true. */ | ||
| public static final String ADLS_REFRESH_CREDENTIALS_ENABLED = "adls.refresh-credentials-enabled"; | ||
|
danielcweeks marked this conversation as resolved.
|
||
|
|
||
| private Map<String, String> adlsSasTokens = Collections.emptyMap(); | ||
| private Map<String, String> adlsConnectionStrings = Collections.emptyMap(); | ||
| private Map.Entry<String, String> namedKeyCreds; | ||
| private Integer adlsReadBlockSize; | ||
| private Long adlsWriteBlockSize; | ||
| private String adlsRefreshCredentialsEndpoint; | ||
| private boolean adlsRefreshCredentialsEnabled; | ||
| private Map<String, String> allProperties; | ||
|
|
||
| public AzureProperties() {} | ||
|
|
||
|
|
@@ -67,6 +87,13 @@ public AzureProperties(Map<String, String> properties) { | |
| if (properties.containsKey(ADLS_WRITE_BLOCK_SIZE)) { | ||
| this.adlsWriteBlockSize = Long.parseLong(properties.get(ADLS_WRITE_BLOCK_SIZE)); | ||
| } | ||
| this.adlsRefreshCredentialsEndpoint = | ||
| RESTUtil.resolveEndpoint( | ||
| properties.get(CatalogProperties.URI), | ||
| properties.get(ADLS_REFRESH_CREDENTIALS_ENDPOINT)); | ||
| this.adlsRefreshCredentialsEnabled = | ||
| PropertyUtil.propertyAsBoolean(properties, ADLS_REFRESH_CREDENTIALS_ENABLED, true); | ||
| this.allProperties = SerializableMap.copyOf(properties); | ||
| } | ||
|
|
||
| public Optional<Integer> adlsReadBlockSize() { | ||
|
|
@@ -77,6 +104,20 @@ public Optional<Long> adlsWriteBlockSize() { | |
| return Optional.ofNullable(adlsWriteBlockSize); | ||
| } | ||
|
|
||
| public Optional<VendedAdlsCredentialProvider> vendedAdlsCredentialProvider() { | ||
| if (adlsRefreshCredentialsEnabled && !Strings.isNullOrEmpty(adlsRefreshCredentialsEndpoint)) { | ||
| Map<String, String> credentialProviderProperties = Maps.newHashMap(allProperties); | ||
| credentialProviderProperties.put( | ||
| VendedAdlsCredentialProvider.URI, adlsRefreshCredentialsEndpoint); | ||
| Optional.ofNullable(allProperties.get(OAuth2Properties.TOKEN)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can remove this, since we're already copying over
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. removed redundant operation of setting token. |
||
| .ifPresent( | ||
| token -> credentialProviderProperties.putIfAbsent(OAuth2Properties.TOKEN, token)); | ||
| return Optional.of(new VendedAdlsCredentialProvider(credentialProviderProperties)); | ||
| } else { | ||
| return Optional.empty(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Applies configuration to the {@link DataLakeFileSystemClientBuilder} to provide the endpoint | ||
| * and credentials required to create an instance of the client. | ||
|
|
@@ -87,14 +128,16 @@ public Optional<Long> adlsWriteBlockSize() { | |
| * @param builder the builder instance | ||
| */ | ||
| public void applyClientConfiguration(String account, DataLakeFileSystemClientBuilder builder) { | ||
| String sasToken = adlsSasTokens.get(account); | ||
| if (sasToken != null && !sasToken.isEmpty()) { | ||
| builder.sasToken(sasToken); | ||
| } else if (namedKeyCreds != null) { | ||
| builder.credential( | ||
| new StorageSharedKeyCredential(namedKeyCreds.getKey(), namedKeyCreds.getValue())); | ||
| } else { | ||
| builder.credential(new DefaultAzureCredentialBuilder().build()); | ||
| if (!adlsRefreshCredentialsEnabled || Strings.isNullOrEmpty(adlsRefreshCredentialsEndpoint)) { | ||
| String sasToken = adlsSasTokens.get(account); | ||
| if (sasToken != null && !sasToken.isEmpty()) { | ||
| builder.sasToken(sasToken); | ||
| } else if (namedKeyCreds != null) { | ||
| builder.credential( | ||
| new StorageSharedKeyCredential(namedKeyCreds.getKey(), namedKeyCreds.getValue())); | ||
| } else { | ||
| builder.credential(new DefaultAzureCredentialBuilder().build()); | ||
| } | ||
| } | ||
|
|
||
| // apply connection string last so its parameters take precedence, e.g. SAS token | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,7 @@ | |
| import com.azure.storage.file.datalake.models.ListPathsOptions; | ||
| import java.util.Collections; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
| import java.util.concurrent.atomic.AtomicInteger; | ||
| import org.apache.iceberg.azure.AzureProperties; | ||
| import org.apache.iceberg.common.DynConstructors; | ||
|
|
@@ -55,6 +56,7 @@ public class ADLSFileIO implements DelegateFileIO { | |
| private AzureProperties azureProperties; | ||
| private MetricsContext metrics = MetricsContext.nullMetrics(); | ||
| private SerializableMap<String, String> properties; | ||
| private VendedAdlsCredentialProvider vendedAdlsCredentialProvider; | ||
|
|
||
| /** | ||
| * No-arg constructor to load the FileIO dynamically. | ||
|
|
@@ -111,6 +113,9 @@ DataLakeFileSystemClient client(ADLSLocation location) { | |
| new DataLakeFileSystemClientBuilder().httpClient(HTTP); | ||
|
|
||
| location.container().ifPresent(clientBuilder::fileSystemName); | ||
| Optional.ofNullable(vendedAdlsCredentialProvider) | ||
| .map(p -> new VendedAzureSasCredentialPolicy(location.host(), p)) | ||
| .ifPresent(clientBuilder::addPolicy); | ||
| azureProperties.applyClientConfiguration(location.host(), clientBuilder); | ||
|
|
||
| return clientBuilder.buildClient(); | ||
|
|
@@ -126,6 +131,8 @@ public void initialize(Map<String, String> props) { | |
| this.properties = SerializableMap.copyOf(props); | ||
| this.azureProperties = new AzureProperties(properties); | ||
| initMetrics(properties); | ||
| this.vendedAdlsCredentialProvider = | ||
| this.azureProperties.vendedAdlsCredentialProvider().orElse(null); | ||
|
ChaladiMohanVamsi marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| @SuppressWarnings("CatchBlockLogException") | ||
|
|
@@ -212,4 +219,12 @@ public void deletePrefix(String prefix) { | |
| } | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void close() { | ||
| if (vendedAdlsCredentialProvider != null) { | ||
| vendedAdlsCredentialProvider.close(); | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: newline after }
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. applied |
||
| DelegateFileIO.super.close(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be enclosed in finally block so that it is always run ? |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| /* | ||
| * 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.azure.adlsv2; | ||
|
|
||
| import com.azure.core.credential.AccessToken; | ||
| import com.azure.core.credential.SimpleTokenCache; | ||
| import java.io.IOException; | ||
| import java.io.Serializable; | ||
| import java.io.UncheckedIOException; | ||
| import java.time.Instant; | ||
| import java.time.ZoneOffset; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.stream.Collectors; | ||
| import org.apache.iceberg.azure.AzureProperties; | ||
| import org.apache.iceberg.io.CloseableGroup; | ||
| import org.apache.iceberg.relocated.com.google.common.base.Preconditions; | ||
| import org.apache.iceberg.relocated.com.google.common.collect.Maps; | ||
| import org.apache.iceberg.rest.ErrorHandlers; | ||
| import org.apache.iceberg.rest.HTTPClient; | ||
| import org.apache.iceberg.rest.RESTClient; | ||
| import org.apache.iceberg.rest.auth.AuthManager; | ||
| import org.apache.iceberg.rest.auth.AuthManagers; | ||
| import org.apache.iceberg.rest.auth.AuthSession; | ||
| import org.apache.iceberg.rest.credentials.Credential; | ||
| import org.apache.iceberg.rest.responses.LoadCredentialsResponse; | ||
| import org.apache.iceberg.util.SerializableMap; | ||
| import reactor.core.publisher.Mono; | ||
|
|
||
| public class VendedAdlsCredentialProvider implements Serializable, AutoCloseable { | ||
|
ChaladiMohanVamsi marked this conversation as resolved.
ChaladiMohanVamsi marked this conversation as resolved.
|
||
|
|
||
| public static final String URI = "credentials.uri"; | ||
|
|
||
| private final SerializableMap<String, String> properties; | ||
| private transient volatile Map<String, SimpleTokenCache> sasCredentialByAccount; | ||
| private transient volatile HTTPClient client; | ||
| private transient AuthManager authManager; | ||
| private transient AuthSession authSession; | ||
|
|
||
| public VendedAdlsCredentialProvider(Map<String, String> properties) { | ||
| Preconditions.checkArgument(null != properties, "Invalid properties: null"); | ||
| Preconditions.checkArgument(null != properties.get(URI), "Invalid URI: null"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you please apply the below diff to this class? This is fixing an issue that we're currently also fixing for S3 / GCP in #12612 / #12638. You will also have to update the tests in a similar way as in those PRs
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Applied the patch and associated test cases. |
||
| this.properties = SerializableMap.copyOf(properties); | ||
| } | ||
|
|
||
| String credentialForAccount(String storageAccount) { | ||
| return sasCredentialByAccount() | ||
| .computeIfAbsent( | ||
| storageAccount, | ||
| ignored -> | ||
| new SimpleTokenCache( | ||
| () -> Mono.fromSupplier(() -> sasTokenForAccount(storageAccount)))) | ||
| .getToken() | ||
| .map(AccessToken::getToken) | ||
| .block(); | ||
| } | ||
|
|
||
| private AccessToken sasTokenForAccount(String storageAccount) { | ||
| LoadCredentialsResponse response = fetchCredentials(); | ||
| List<Credential> adlsCredentials = | ||
| response.credentials().stream() | ||
| .filter(c -> c.prefix().contains(storageAccount)) | ||
| .collect(Collectors.toList()); | ||
| Preconditions.checkState( | ||
| !adlsCredentials.isEmpty(), | ||
| String.format("Invalid ADLS Credentials for storage-account %s: empty", storageAccount)); | ||
| Preconditions.checkState( | ||
| adlsCredentials.size() == 1, | ||
| "Invalid ADLS Credentials: only one ADLS credential should exist per storage-account"); | ||
|
|
||
| Credential adlsCredential = adlsCredentials.get(0); | ||
| checkCredential(adlsCredential, AzureProperties.ADLS_SAS_TOKEN_PREFIX + storageAccount); | ||
| checkCredential( | ||
| adlsCredential, AzureProperties.ADLS_SAS_TOKEN_EXPIRES_AT_MS_PREFIX + storageAccount); | ||
|
|
||
| String sasToken = | ||
| adlsCredential.config().get(AzureProperties.ADLS_SAS_TOKEN_PREFIX + storageAccount); | ||
| Instant tokenExpiresAt = | ||
| Instant.ofEpochMilli( | ||
| Long.parseLong( | ||
| adlsCredential | ||
| .config() | ||
| .get(AzureProperties.ADLS_SAS_TOKEN_EXPIRES_AT_MS_PREFIX + storageAccount))); | ||
|
|
||
| return new AccessToken(sasToken, tokenExpiresAt.atOffset(ZoneOffset.UTC)); | ||
| } | ||
|
|
||
| private Map<String, SimpleTokenCache> sasCredentialByAccount() { | ||
|
nastra marked this conversation as resolved.
|
||
| if (this.sasCredentialByAccount == null) { | ||
| synchronized (this) { | ||
| if (this.sasCredentialByAccount == null) { | ||
| this.sasCredentialByAccount = Maps.newHashMap(); | ||
| } | ||
| } | ||
| } | ||
| return this.sasCredentialByAccount; | ||
| } | ||
|
|
||
| private RESTClient httpClient() { | ||
|
ChaladiMohanVamsi marked this conversation as resolved.
|
||
| if (null == client) { | ||
| synchronized (this) { | ||
| if (null == client) { | ||
| authManager = AuthManagers.loadAuthManager("adls-credentials-refresh", properties); | ||
| HTTPClient httpClient = HTTPClient.builder(properties).uri(properties.get(URI)).build(); | ||
| authSession = authManager.catalogSession(httpClient, properties); | ||
| client = httpClient.withAuthSession(authSession); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return client; | ||
| } | ||
|
|
||
| private LoadCredentialsResponse fetchCredentials() { | ||
| return httpClient() | ||
|
nastra marked this conversation as resolved.
|
||
| .get( | ||
| properties.get(URI), | ||
| null, | ||
| LoadCredentialsResponse.class, | ||
| Map.of(), | ||
| ErrorHandlers.defaultErrorHandler()); | ||
| } | ||
|
|
||
| private void checkCredential(Credential credential, String property) { | ||
| Preconditions.checkState( | ||
| credential.config().containsKey(property), | ||
| "Invalid ADLS Credentials: %s not set", | ||
| property); | ||
| } | ||
|
|
||
| @Override | ||
| public void close() { | ||
| CloseableGroup closeableGroup = new CloseableGroup(); | ||
| closeableGroup.addCloseable(authSession); | ||
| closeableGroup.addCloseable(authManager); | ||
| closeableGroup.addCloseable(client); | ||
| closeableGroup.setSuppressCloseFailure(true); | ||
| try { | ||
| closeableGroup.close(); | ||
| } catch (IOException e) { | ||
| throw new UncheckedIOException("Failed to close the VendedAdlsCredentialProvider", e); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| /* | ||
| * 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.azure.adlsv2; | ||
|
|
||
| import com.azure.core.credential.AzureSasCredential; | ||
| import com.azure.core.http.HttpPipelineCallContext; | ||
| import com.azure.core.http.HttpPipelineNextPolicy; | ||
| import com.azure.core.http.HttpPipelineNextSyncPolicy; | ||
| import com.azure.core.http.HttpResponse; | ||
| import com.azure.core.http.policy.AzureSasCredentialPolicy; | ||
| import com.azure.core.http.policy.HttpPipelinePolicy; | ||
| import reactor.core.publisher.Mono; | ||
|
|
||
| public class VendedAzureSasCredentialPolicy implements HttpPipelinePolicy { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe we should be able to make this non-public
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. updated it to non-public. |
||
| private final String account; | ||
| private final VendedAdlsCredentialProvider vendedAdlsCredentialProvider; | ||
| private AzureSasCredential azureSasCredential; | ||
| private AzureSasCredentialPolicy azureSasCredentialPolicy; | ||
|
|
||
| public VendedAzureSasCredentialPolicy( | ||
| String account, VendedAdlsCredentialProvider vendedAdlsCredentialProvider) { | ||
| this.account = account; | ||
| this.vendedAdlsCredentialProvider = vendedAdlsCredentialProvider; | ||
| } | ||
|
|
||
| @Override | ||
| public Mono<HttpResponse> process( | ||
| HttpPipelineCallContext httpPipelineCallContext, | ||
| HttpPipelineNextPolicy httpPipelineNextPolicy) { | ||
| maybeUpdateCredential(); | ||
| return azureSasCredentialPolicy.process(httpPipelineCallContext, httpPipelineNextPolicy); | ||
| } | ||
|
|
||
| @Override | ||
| public HttpResponse processSync( | ||
| HttpPipelineCallContext context, HttpPipelineNextSyncPolicy next) { | ||
| maybeUpdateCredential(); | ||
| return azureSasCredentialPolicy.processSync(context, next); | ||
| } | ||
|
|
||
| private void maybeUpdateCredential() { | ||
| String sasToken = vendedAdlsCredentialProvider.credentialForAccount(account); | ||
| if (azureSasCredential == null) { | ||
| this.azureSasCredential = new AzureSasCredential(sasToken); | ||
| this.azureSasCredentialPolicy = new AzureSasCredentialPolicy(azureSasCredential, false); | ||
| } else { | ||
| azureSasCredential.update(sasToken); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.