mirror of https://github.com/apache/nifi.git
NIFI-10152 Storage client caching in Azure ADLS processors
This closes #6158. Signed-off-by: Peter Turcsanyi <turcsanyi@apache.org>
This commit is contained in:
parent
d82ce18f88
commit
320aed024f
|
@ -195,6 +195,11 @@ The following binary components are provided under the Apache Software License v
|
||||||
Reactive Streams Netty Driver
|
Reactive Streams Netty Driver
|
||||||
Copyright 2020, Project Reactor
|
Copyright 2020, Project Reactor
|
||||||
|
|
||||||
|
(ASLv2) Caffeine (com.github.ben-manes.caffeine:caffeine:jar:2.9.2 - https://github.com/ben-manes/caffeine)
|
||||||
|
The following NOTICE information applies:
|
||||||
|
Caffeine (caching library)
|
||||||
|
Copyright Ben Manes
|
||||||
|
|
||||||
************************
|
************************
|
||||||
Common Development and Distribution License 1.0
|
Common Development and Distribution License 1.0
|
||||||
************************
|
************************
|
||||||
|
|
|
@ -135,6 +135,12 @@
|
||||||
<groupId>commons-io</groupId>
|
<groupId>commons-io</groupId>
|
||||||
<artifactId>commons-io</artifactId>
|
<artifactId>commons-io</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||||
|
<artifactId>caffeine</artifactId>
|
||||||
|
<version>2.9.2</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.nifi</groupId>
|
<groupId>org.apache.nifi</groupId>
|
||||||
<artifactId>nifi-mock</artifactId>
|
<artifactId>nifi-mock</artifactId>
|
||||||
|
@ -159,7 +165,6 @@
|
||||||
<version>1.18.0-SNAPSHOT</version>
|
<version>1.18.0-SNAPSHOT</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.nifi</groupId>
|
<groupId>org.apache.nifi</groupId>
|
||||||
<artifactId>nifi-schema-registry-service-api</artifactId>
|
<artifactId>nifi-schema-registry-service-api</artifactId>
|
||||||
|
|
|
@ -16,18 +16,10 @@
|
||||||
*/
|
*/
|
||||||
package org.apache.nifi.processors.azure;
|
package org.apache.nifi.processors.azure;
|
||||||
|
|
||||||
import com.azure.core.credential.AccessToken;
|
|
||||||
import com.azure.core.credential.TokenCredential;
|
|
||||||
import com.azure.core.http.HttpClient;
|
|
||||||
import com.azure.core.http.netty.NettyAsyncHttpClientBuilder;
|
|
||||||
import com.azure.identity.ClientSecretCredential;
|
|
||||||
import com.azure.identity.ClientSecretCredentialBuilder;
|
|
||||||
import com.azure.identity.ManagedIdentityCredential;
|
|
||||||
import com.azure.identity.ManagedIdentityCredentialBuilder;
|
|
||||||
import com.azure.storage.common.StorageSharedKeyCredential;
|
|
||||||
import com.azure.storage.file.datalake.DataLakeServiceClient;
|
import com.azure.storage.file.datalake.DataLakeServiceClient;
|
||||||
import com.azure.storage.file.datalake.DataLakeServiceClientBuilder;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.nifi.annotation.lifecycle.OnScheduled;
|
||||||
|
import org.apache.nifi.annotation.lifecycle.OnStopped;
|
||||||
import org.apache.nifi.components.PropertyDescriptor;
|
import org.apache.nifi.components.PropertyDescriptor;
|
||||||
import org.apache.nifi.components.ValidationContext;
|
import org.apache.nifi.components.ValidationContext;
|
||||||
import org.apache.nifi.components.ValidationResult;
|
import org.apache.nifi.components.ValidationResult;
|
||||||
|
@ -40,9 +32,10 @@ import org.apache.nifi.processor.ProcessContext;
|
||||||
import org.apache.nifi.processor.Relationship;
|
import org.apache.nifi.processor.Relationship;
|
||||||
import org.apache.nifi.processor.exception.ProcessException;
|
import org.apache.nifi.processor.exception.ProcessException;
|
||||||
import org.apache.nifi.processor.util.StandardValidators;
|
import org.apache.nifi.processor.util.StandardValidators;
|
||||||
|
import org.apache.nifi.processors.azure.storage.utils.AzureStorageUtils;
|
||||||
|
import org.apache.nifi.processors.azure.storage.utils.DataLakeServiceClientFactory;
|
||||||
import org.apache.nifi.services.azure.storage.ADLSCredentialsDetails;
|
import org.apache.nifi.services.azure.storage.ADLSCredentialsDetails;
|
||||||
import org.apache.nifi.services.azure.storage.ADLSCredentialsService;
|
import org.apache.nifi.services.azure.storage.ADLSCredentialsService;
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -51,7 +44,6 @@ import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.apache.nifi.processors.azure.storage.utils.ADLSAttributes.ATTR_NAME_FILENAME;
|
import static org.apache.nifi.processors.azure.storage.utils.ADLSAttributes.ATTR_NAME_FILENAME;
|
||||||
import static org.apache.nifi.processors.azure.storage.utils.AzureStorageUtils.getProxyOptions;
|
|
||||||
|
|
||||||
public abstract class AbstractAzureDataLakeStorageProcessor extends AbstractProcessor {
|
public abstract class AbstractAzureDataLakeStorageProcessor extends AbstractProcessor {
|
||||||
|
|
||||||
|
@ -65,7 +57,7 @@ public abstract class AbstractAzureDataLakeStorageProcessor extends AbstractProc
|
||||||
|
|
||||||
public static final PropertyDescriptor FILESYSTEM = new PropertyDescriptor.Builder()
|
public static final PropertyDescriptor FILESYSTEM = new PropertyDescriptor.Builder()
|
||||||
.name("filesystem-name").displayName("Filesystem Name")
|
.name("filesystem-name").displayName("Filesystem Name")
|
||||||
.description("Name of the Azure Storage File System. It is assumed to be already existing.")
|
.description("Name of the Azure Storage File System (also called Container). It is assumed to be already existing.")
|
||||||
.addValidator(StandardValidators.NON_BLANK_VALIDATOR)
|
.addValidator(StandardValidators.NON_BLANK_VALIDATOR)
|
||||||
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
|
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
|
||||||
.required(true)
|
.required(true)
|
||||||
|
@ -103,65 +95,31 @@ public abstract class AbstractAzureDataLakeStorageProcessor extends AbstractProc
|
||||||
|
|
||||||
public static final String TEMP_FILE_DIRECTORY = "_nifitempdirectory";
|
public static final String TEMP_FILE_DIRECTORY = "_nifitempdirectory";
|
||||||
|
|
||||||
|
private DataLakeServiceClientFactory clientFactory;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<Relationship> getRelationships() {
|
public Set<Relationship> getRelationships() {
|
||||||
return RELATIONSHIPS;
|
return RELATIONSHIPS;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DataLakeServiceClient getStorageClient(PropertyContext context, FlowFile flowFile) {
|
@OnScheduled
|
||||||
|
public void onScheduled(final ProcessContext context) {
|
||||||
|
clientFactory = new DataLakeServiceClientFactory(getLogger(), AzureStorageUtils.getProxyOptions(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnStopped
|
||||||
|
public void onStopped() {
|
||||||
|
clientFactory = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DataLakeServiceClient getStorageClient(PropertyContext context, FlowFile flowFile) {
|
||||||
final Map<String, String> attributes = flowFile != null ? flowFile.getAttributes() : Collections.emptyMap();
|
final Map<String, String> attributes = flowFile != null ? flowFile.getAttributes() : Collections.emptyMap();
|
||||||
|
|
||||||
final ADLSCredentialsService credentialsService = context.getProperty(ADLS_CREDENTIALS_SERVICE).asControllerService(ADLSCredentialsService.class);
|
final ADLSCredentialsService credentialsService = context.getProperty(ADLS_CREDENTIALS_SERVICE).asControllerService(ADLSCredentialsService.class);
|
||||||
|
|
||||||
final ADLSCredentialsDetails credentialsDetails = credentialsService.getCredentialsDetails(attributes);
|
final ADLSCredentialsDetails credentialsDetails = credentialsService.getCredentialsDetails(attributes);
|
||||||
|
|
||||||
final String accountName = credentialsDetails.getAccountName();
|
final DataLakeServiceClient storageClient = clientFactory.getStorageClient(credentialsDetails);
|
||||||
final String accountKey = credentialsDetails.getAccountKey();
|
|
||||||
final String sasToken = credentialsDetails.getSasToken();
|
|
||||||
final AccessToken accessToken = credentialsDetails.getAccessToken();
|
|
||||||
final String endpointSuffix = credentialsDetails.getEndpointSuffix();
|
|
||||||
final boolean useManagedIdentity = credentialsDetails.getUseManagedIdentity();
|
|
||||||
final String managedIdentityClientId = credentialsDetails.getManagedIdentityClientId();
|
|
||||||
final String servicePrincipalTenantId = credentialsDetails.getServicePrincipalTenantId();
|
|
||||||
final String servicePrincipalClientId = credentialsDetails.getServicePrincipalClientId();
|
|
||||||
final String servicePrincipalClientSecret = credentialsDetails.getServicePrincipalClientSecret();
|
|
||||||
|
|
||||||
final String endpoint = String.format("https://%s.%s", accountName, endpointSuffix);
|
|
||||||
|
|
||||||
final DataLakeServiceClientBuilder dataLakeServiceClientBuilder = new DataLakeServiceClientBuilder();
|
|
||||||
dataLakeServiceClientBuilder.endpoint(endpoint);
|
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(accountKey)) {
|
|
||||||
final StorageSharedKeyCredential credential = new StorageSharedKeyCredential(accountName, accountKey);
|
|
||||||
dataLakeServiceClientBuilder.credential(credential);
|
|
||||||
} else if (StringUtils.isNotBlank(sasToken)) {
|
|
||||||
dataLakeServiceClientBuilder.sasToken(sasToken);
|
|
||||||
} else if (accessToken != null) {
|
|
||||||
final TokenCredential credential = tokenRequestContext -> Mono.just(accessToken);
|
|
||||||
dataLakeServiceClientBuilder.credential(credential);
|
|
||||||
} else if (useManagedIdentity) {
|
|
||||||
final ManagedIdentityCredential misCredential = new ManagedIdentityCredentialBuilder()
|
|
||||||
.clientId(managedIdentityClientId)
|
|
||||||
.build();
|
|
||||||
dataLakeServiceClientBuilder.credential(misCredential);
|
|
||||||
} else if (StringUtils.isNoneBlank(servicePrincipalTenantId, servicePrincipalClientId, servicePrincipalClientSecret)) {
|
|
||||||
final ClientSecretCredential credential = new ClientSecretCredentialBuilder()
|
|
||||||
.tenantId(servicePrincipalTenantId)
|
|
||||||
.clientId(servicePrincipalClientId)
|
|
||||||
.clientSecret(servicePrincipalClientSecret)
|
|
||||||
.build();
|
|
||||||
dataLakeServiceClientBuilder.credential(credential);
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("No valid credentials were provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
final NettyAsyncHttpClientBuilder nettyClientBuilder = new NettyAsyncHttpClientBuilder();
|
|
||||||
nettyClientBuilder.proxy(getProxyOptions(context));
|
|
||||||
|
|
||||||
final HttpClient nettyClient = nettyClientBuilder.build();
|
|
||||||
dataLakeServiceClientBuilder.httpClient(nettyClient);
|
|
||||||
|
|
||||||
final DataLakeServiceClient storageClient = dataLakeServiceClientBuilder.buildClient();
|
|
||||||
|
|
||||||
return storageClient;
|
return storageClient;
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,10 @@ import org.apache.nifi.processor.ProcessContext;
|
||||||
import org.apache.nifi.processor.util.StandardValidators;
|
import org.apache.nifi.processor.util.StandardValidators;
|
||||||
import org.apache.nifi.processors.azure.storage.utils.ADLSFileInfo;
|
import org.apache.nifi.processors.azure.storage.utils.ADLSFileInfo;
|
||||||
import org.apache.nifi.processors.azure.storage.utils.AzureStorageUtils;
|
import org.apache.nifi.processors.azure.storage.utils.AzureStorageUtils;
|
||||||
|
import org.apache.nifi.processors.azure.storage.utils.DataLakeServiceClientFactory;
|
||||||
import org.apache.nifi.serialization.record.RecordSchema;
|
import org.apache.nifi.serialization.record.RecordSchema;
|
||||||
|
import org.apache.nifi.services.azure.storage.ADLSCredentialsDetails;
|
||||||
|
import org.apache.nifi.services.azure.storage.ADLSCredentialsService;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
@ -66,7 +69,6 @@ import static org.apache.nifi.processors.azure.AbstractAzureDataLakeStorageProce
|
||||||
import static org.apache.nifi.processors.azure.AbstractAzureDataLakeStorageProcessor.TEMP_FILE_DIRECTORY;
|
import static org.apache.nifi.processors.azure.AbstractAzureDataLakeStorageProcessor.TEMP_FILE_DIRECTORY;
|
||||||
import static org.apache.nifi.processors.azure.AbstractAzureDataLakeStorageProcessor.evaluateDirectoryProperty;
|
import static org.apache.nifi.processors.azure.AbstractAzureDataLakeStorageProcessor.evaluateDirectoryProperty;
|
||||||
import static org.apache.nifi.processors.azure.AbstractAzureDataLakeStorageProcessor.evaluateFileSystemProperty;
|
import static org.apache.nifi.processors.azure.AbstractAzureDataLakeStorageProcessor.evaluateFileSystemProperty;
|
||||||
import static org.apache.nifi.processors.azure.AbstractAzureDataLakeStorageProcessor.getStorageClient;
|
|
||||||
import static org.apache.nifi.processors.azure.storage.utils.ADLSAttributes.ATTR_DESCRIPTION_DIRECTORY;
|
import static org.apache.nifi.processors.azure.storage.utils.ADLSAttributes.ATTR_DESCRIPTION_DIRECTORY;
|
||||||
import static org.apache.nifi.processors.azure.storage.utils.ADLSAttributes.ATTR_DESCRIPTION_ETAG;
|
import static org.apache.nifi.processors.azure.storage.utils.ADLSAttributes.ATTR_DESCRIPTION_ETAG;
|
||||||
import static org.apache.nifi.processors.azure.storage.utils.ADLSAttributes.ATTR_DESCRIPTION_FILENAME;
|
import static org.apache.nifi.processors.azure.storage.utils.ADLSAttributes.ATTR_DESCRIPTION_FILENAME;
|
||||||
|
@ -170,6 +172,8 @@ public class ListAzureDataLakeStorage extends AbstractListAzureProcessor<ADLSFil
|
||||||
private volatile Pattern filePattern;
|
private volatile Pattern filePattern;
|
||||||
private volatile Pattern pathPattern;
|
private volatile Pattern pathPattern;
|
||||||
|
|
||||||
|
private DataLakeServiceClientFactory clientFactory;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
|
protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
|
||||||
return PROPERTIES;
|
return PROPERTIES;
|
||||||
|
@ -179,12 +183,14 @@ public class ListAzureDataLakeStorage extends AbstractListAzureProcessor<ADLSFil
|
||||||
public void onScheduled(final ProcessContext context) {
|
public void onScheduled(final ProcessContext context) {
|
||||||
filePattern = getPattern(context, FILE_FILTER);
|
filePattern = getPattern(context, FILE_FILTER);
|
||||||
pathPattern = getPattern(context, PATH_FILTER);
|
pathPattern = getPattern(context, PATH_FILTER);
|
||||||
|
clientFactory = new DataLakeServiceClientFactory(getLogger(), AzureStorageUtils.getProxyOptions(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnStopped
|
@OnStopped
|
||||||
public void onStopped() {
|
public void onStopped() {
|
||||||
filePattern = null;
|
filePattern = null;
|
||||||
pathPattern = null;
|
pathPattern = null;
|
||||||
|
clientFactory = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -264,7 +270,11 @@ public class ListAzureDataLakeStorage extends AbstractListAzureProcessor<ADLSFil
|
||||||
final Pattern filePattern = listingMode == ListingMode.EXECUTION ? this.filePattern : getPattern(context, FILE_FILTER);
|
final Pattern filePattern = listingMode == ListingMode.EXECUTION ? this.filePattern : getPattern(context, FILE_FILTER);
|
||||||
final Pattern pathPattern = listingMode == ListingMode.EXECUTION ? this.pathPattern : getPattern(context, PATH_FILTER);
|
final Pattern pathPattern = listingMode == ListingMode.EXECUTION ? this.pathPattern : getPattern(context, PATH_FILTER);
|
||||||
|
|
||||||
final DataLakeServiceClient storageClient = getStorageClient(context, null);
|
final ADLSCredentialsService credentialsService = context.getProperty(ADLS_CREDENTIALS_SERVICE).asControllerService(ADLSCredentialsService.class);
|
||||||
|
|
||||||
|
final ADLSCredentialsDetails credentialsDetails = credentialsService.getCredentialsDetails(Collections.emptyMap());
|
||||||
|
|
||||||
|
final DataLakeServiceClient storageClient = clientFactory.getStorageClient(credentialsDetails);
|
||||||
final DataLakeFileSystemClient fileSystemClient = storageClient.getFileSystemClient(fileSystem);
|
final DataLakeFileSystemClient fileSystemClient = storageClient.getFileSystemClient(fileSystem);
|
||||||
|
|
||||||
final ListPathsOptions options = new ListPathsOptions();
|
final ListPathsOptions options = new ListPathsOptions();
|
||||||
|
|
|
@ -321,7 +321,7 @@ public final class AzureStorageUtils {
|
||||||
*
|
*
|
||||||
* Creates the {@link ProxyOptions proxy options} that {@link HttpClient} will use.
|
* Creates the {@link ProxyOptions proxy options} that {@link HttpClient} will use.
|
||||||
*
|
*
|
||||||
* @param propertyContext is sed to supply Proxy configurations
|
* @param propertyContext to supply Proxy configurations
|
||||||
* @return {@link ProxyOptions proxy options}, null if Proxy is not set
|
* @return {@link ProxyOptions proxy options}, null if Proxy is not set
|
||||||
*/
|
*/
|
||||||
public static ProxyOptions getProxyOptions(final PropertyContext propertyContext) {
|
public static ProxyOptions getProxyOptions(final PropertyContext propertyContext) {
|
||||||
|
@ -342,7 +342,7 @@ public final class AzureStorageUtils {
|
||||||
return proxyOptions;
|
return proxyOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ProxyOptions.Type getProxyType(ProxyConfiguration proxyConfiguration) {
|
private static ProxyOptions.Type getProxyType(ProxyConfiguration proxyConfiguration) {
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
/*
|
||||||
|
* 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.nifi.processors.azure.storage.utils;
|
||||||
|
|
||||||
|
import com.azure.core.credential.AccessToken;
|
||||||
|
import com.azure.core.credential.TokenCredential;
|
||||||
|
import com.azure.core.http.HttpClient;
|
||||||
|
import com.azure.core.http.ProxyOptions;
|
||||||
|
import com.azure.core.http.netty.NettyAsyncHttpClientBuilder;
|
||||||
|
import com.azure.identity.ClientSecretCredential;
|
||||||
|
import com.azure.identity.ClientSecretCredentialBuilder;
|
||||||
|
import com.azure.identity.ManagedIdentityCredential;
|
||||||
|
import com.azure.identity.ManagedIdentityCredentialBuilder;
|
||||||
|
import com.azure.storage.common.StorageSharedKeyCredential;
|
||||||
|
import com.azure.storage.file.datalake.DataLakeServiceClient;
|
||||||
|
import com.azure.storage.file.datalake.DataLakeServiceClientBuilder;
|
||||||
|
import com.github.benmanes.caffeine.cache.Cache;
|
||||||
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.nifi.logging.ComponentLog;
|
||||||
|
import org.apache.nifi.services.azure.storage.ADLSCredentialsDetails;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
public class DataLakeServiceClientFactory {
|
||||||
|
|
||||||
|
private static final long STORAGE_CLIENT_CACHE_SIZE = 10;
|
||||||
|
|
||||||
|
private final ComponentLog logger;
|
||||||
|
private final ProxyOptions proxyOptions;
|
||||||
|
|
||||||
|
private final Cache<ADLSCredentialsDetails, DataLakeServiceClient> clientCache;
|
||||||
|
|
||||||
|
public DataLakeServiceClientFactory(ComponentLog logger, ProxyOptions proxyOptions) {
|
||||||
|
this.logger = logger;
|
||||||
|
this.proxyOptions = proxyOptions;
|
||||||
|
this.clientCache = createCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Cache<ADLSCredentialsDetails, DataLakeServiceClient> createCache() {
|
||||||
|
// Beware! By default, Caffeine does not perform cleanup and evict values
|
||||||
|
// "automatically" or instantly after a value expires. Because of that it
|
||||||
|
// can happen that there are more elements in the cache than the maximum size.
|
||||||
|
// See: https://github.com/ben-manes/caffeine/wiki/Cleanup
|
||||||
|
return Caffeine.newBuilder()
|
||||||
|
.maximumSize(STORAGE_CLIENT_CACHE_SIZE)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a {@link DataLakeServiceClient}
|
||||||
|
*
|
||||||
|
* @param credentialsDetails used for caching because it can contain properties that are results of an expression
|
||||||
|
* @return DataLakeServiceClient
|
||||||
|
*/
|
||||||
|
public DataLakeServiceClient getStorageClient(ADLSCredentialsDetails credentialsDetails) {
|
||||||
|
return clientCache.get(credentialsDetails, __ -> {
|
||||||
|
logger.debug("DataLakeServiceClient is not found in the cache with the given credentials. Creating it.");
|
||||||
|
return createStorageClient(credentialsDetails, proxyOptions);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DataLakeServiceClient createStorageClient(ADLSCredentialsDetails credentialsDetails, ProxyOptions proxyOptions) {
|
||||||
|
final String accountName = credentialsDetails.getAccountName();
|
||||||
|
final String accountKey = credentialsDetails.getAccountKey();
|
||||||
|
final String sasToken = credentialsDetails.getSasToken();
|
||||||
|
final AccessToken accessToken = credentialsDetails.getAccessToken();
|
||||||
|
final String endpointSuffix = credentialsDetails.getEndpointSuffix();
|
||||||
|
final boolean useManagedIdentity = credentialsDetails.getUseManagedIdentity();
|
||||||
|
final String managedIdentityClientId = credentialsDetails.getManagedIdentityClientId();
|
||||||
|
final String servicePrincipalTenantId = credentialsDetails.getServicePrincipalTenantId();
|
||||||
|
final String servicePrincipalClientId = credentialsDetails.getServicePrincipalClientId();
|
||||||
|
final String servicePrincipalClientSecret = credentialsDetails.getServicePrincipalClientSecret();
|
||||||
|
|
||||||
|
final String endpoint = String.format("https://%s.%s", accountName, endpointSuffix);
|
||||||
|
|
||||||
|
final DataLakeServiceClientBuilder dataLakeServiceClientBuilder = new DataLakeServiceClientBuilder();
|
||||||
|
dataLakeServiceClientBuilder.endpoint(endpoint);
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(accountKey)) {
|
||||||
|
final StorageSharedKeyCredential credential = new StorageSharedKeyCredential(accountName, accountKey);
|
||||||
|
dataLakeServiceClientBuilder.credential(credential);
|
||||||
|
} else if (StringUtils.isNotBlank(sasToken)) {
|
||||||
|
dataLakeServiceClientBuilder.sasToken(sasToken);
|
||||||
|
} else if (accessToken != null) {
|
||||||
|
final TokenCredential credential = tokenRequestContext -> Mono.just(accessToken);
|
||||||
|
dataLakeServiceClientBuilder.credential(credential);
|
||||||
|
} else if (useManagedIdentity) {
|
||||||
|
final ManagedIdentityCredential misCredential = new ManagedIdentityCredentialBuilder()
|
||||||
|
.clientId(managedIdentityClientId)
|
||||||
|
.build();
|
||||||
|
dataLakeServiceClientBuilder.credential(misCredential);
|
||||||
|
} else if (StringUtils.isNoneBlank(servicePrincipalTenantId, servicePrincipalClientId, servicePrincipalClientSecret)) {
|
||||||
|
final ClientSecretCredential credential = new ClientSecretCredentialBuilder()
|
||||||
|
.tenantId(servicePrincipalTenantId)
|
||||||
|
.clientId(servicePrincipalClientId)
|
||||||
|
.clientSecret(servicePrincipalClientSecret)
|
||||||
|
.build();
|
||||||
|
dataLakeServiceClientBuilder.credential(credential);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("No valid credentials were provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
final NettyAsyncHttpClientBuilder nettyClientBuilder = new NettyAsyncHttpClientBuilder();
|
||||||
|
nettyClientBuilder.proxy(proxyOptions);
|
||||||
|
|
||||||
|
final HttpClient nettyClient = nettyClientBuilder.build();
|
||||||
|
dataLakeServiceClientBuilder.httpClient(nettyClient);
|
||||||
|
|
||||||
|
return dataLakeServiceClientBuilder.buildClient();
|
||||||
|
}
|
||||||
|
}
|
|
@ -98,7 +98,7 @@ public class ITListAzureDataLakeStorage extends AbstractAzureDataLakeStorageIT {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testListRootRecursive() throws Exception {
|
public void testListRootRecursive() {
|
||||||
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "");
|
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "");
|
||||||
|
|
||||||
runProcessor();
|
runProcessor();
|
||||||
|
@ -131,7 +131,7 @@ public class ITListAzureDataLakeStorage extends AbstractAzureDataLakeStorageIT {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testListRootNonRecursive() throws Exception {
|
public void testListRootNonRecursive() {
|
||||||
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "");
|
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "");
|
||||||
runner.setProperty(ListAzureDataLakeStorage.RECURSE_SUBDIRECTORIES, "false");
|
runner.setProperty(ListAzureDataLakeStorage.RECURSE_SUBDIRECTORIES, "false");
|
||||||
|
|
||||||
|
@ -152,7 +152,7 @@ public class ITListAzureDataLakeStorage extends AbstractAzureDataLakeStorageIT {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testListSubdirectoryRecursive() throws Exception {
|
public void testListSubdirectoryRecursive() {
|
||||||
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "dir1");
|
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "dir1");
|
||||||
|
|
||||||
runProcessor();
|
runProcessor();
|
||||||
|
@ -173,7 +173,7 @@ public class ITListAzureDataLakeStorage extends AbstractAzureDataLakeStorageIT {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testListSubdirectoryNonRecursive() throws Exception {
|
public void testListSubdirectoryNonRecursive() {
|
||||||
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "dir1");
|
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "dir1");
|
||||||
runner.setProperty(ListAzureDataLakeStorage.RECURSE_SUBDIRECTORIES, "false");
|
runner.setProperty(ListAzureDataLakeStorage.RECURSE_SUBDIRECTORIES, "false");
|
||||||
|
|
||||||
|
@ -194,7 +194,7 @@ public class ITListAzureDataLakeStorage extends AbstractAzureDataLakeStorageIT {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testListWithFileFilter() throws Exception {
|
public void testListWithFileFilter() {
|
||||||
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "");
|
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "");
|
||||||
runner.setProperty(ListAzureDataLakeStorage.FILE_FILTER, ".*file1.*$");
|
runner.setProperty(ListAzureDataLakeStorage.FILE_FILTER, ".*file1.*$");
|
||||||
|
|
||||||
|
@ -218,7 +218,7 @@ public class ITListAzureDataLakeStorage extends AbstractAzureDataLakeStorageIT {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testListWithFileFilterWithEL() throws Exception {
|
public void testListWithFileFilterWithEL() {
|
||||||
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "");
|
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "");
|
||||||
runner.setProperty(ListAzureDataLakeStorage.FILE_FILTER, ".*file${suffix}$");
|
runner.setProperty(ListAzureDataLakeStorage.FILE_FILTER, ".*file${suffix}$");
|
||||||
runner.setVariable("suffix", "1.*");
|
runner.setVariable("suffix", "1.*");
|
||||||
|
@ -244,7 +244,7 @@ public class ITListAzureDataLakeStorage extends AbstractAzureDataLakeStorageIT {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testListRootWithPathFilter() throws Exception {
|
public void testListRootWithPathFilter() {
|
||||||
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "");
|
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "");
|
||||||
runner.setProperty(ListAzureDataLakeStorage.PATH_FILTER, "^dir1.*$");
|
runner.setProperty(ListAzureDataLakeStorage.PATH_FILTER, "^dir1.*$");
|
||||||
|
|
||||||
|
@ -267,7 +267,7 @@ public class ITListAzureDataLakeStorage extends AbstractAzureDataLakeStorageIT {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testListRootWithPathFilterWithEL() throws Exception {
|
public void testListRootWithPathFilterWithEL() {
|
||||||
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "");
|
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "");
|
||||||
runner.setProperty(ListAzureDataLakeStorage.PATH_FILTER, "${prefix}${suffix}");
|
runner.setProperty(ListAzureDataLakeStorage.PATH_FILTER, "${prefix}${suffix}");
|
||||||
runner.setVariable("prefix", "^dir");
|
runner.setVariable("prefix", "^dir");
|
||||||
|
@ -294,7 +294,7 @@ public class ITListAzureDataLakeStorage extends AbstractAzureDataLakeStorageIT {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testListSubdirectoryWithPathFilter() throws Exception {
|
public void testListSubdirectoryWithPathFilter() {
|
||||||
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "dir1");
|
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "dir1");
|
||||||
runner.setProperty(ListAzureDataLakeStorage.PATH_FILTER, "dir1.*");
|
runner.setProperty(ListAzureDataLakeStorage.PATH_FILTER, "dir1.*");
|
||||||
|
|
||||||
|
@ -315,7 +315,7 @@ public class ITListAzureDataLakeStorage extends AbstractAzureDataLakeStorageIT {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testListRootWithFileAndPathFilter() throws Exception {
|
public void testListRootWithFileAndPathFilter() {
|
||||||
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "");
|
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "");
|
||||||
runner.setProperty(ListAzureDataLakeStorage.FILE_FILTER, ".*11");
|
runner.setProperty(ListAzureDataLakeStorage.FILE_FILTER, ".*11");
|
||||||
runner.setProperty(ListAzureDataLakeStorage.PATH_FILTER, "dir1.*");
|
runner.setProperty(ListAzureDataLakeStorage.PATH_FILTER, "dir1.*");
|
||||||
|
@ -339,7 +339,7 @@ public class ITListAzureDataLakeStorage extends AbstractAzureDataLakeStorageIT {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testListEmptyDirectory() throws Exception {
|
public void testListEmptyDirectory() {
|
||||||
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "dir3");
|
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "dir3");
|
||||||
|
|
||||||
runProcessor();
|
runProcessor();
|
||||||
|
@ -401,7 +401,7 @@ public class ITListAzureDataLakeStorage extends AbstractAzureDataLakeStorageIT {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testListWithMinAge() throws Exception {
|
public void testListWithMinAge() {
|
||||||
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "");
|
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "");
|
||||||
runner.setProperty(ListAzureDataLakeStorage.MIN_AGE, "1 hour");
|
runner.setProperty(ListAzureDataLakeStorage.MIN_AGE, "1 hour");
|
||||||
|
|
||||||
|
@ -422,7 +422,7 @@ public class ITListAzureDataLakeStorage extends AbstractAzureDataLakeStorageIT {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testListWithMaxAge() throws Exception {
|
public void testListWithMaxAge() {
|
||||||
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "");
|
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "");
|
||||||
runner.setProperty(ListAzureDataLakeStorage.MAX_AGE, "1 hour");
|
runner.setProperty(ListAzureDataLakeStorage.MAX_AGE, "1 hour");
|
||||||
|
|
||||||
|
@ -447,7 +447,7 @@ public class ITListAzureDataLakeStorage extends AbstractAzureDataLakeStorageIT {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testListWithMinSize() throws Exception {
|
public void testListWithMinSize() {
|
||||||
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "");
|
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "");
|
||||||
runner.setProperty(ListAzureDataLakeStorage.MIN_SIZE, "5 B");
|
runner.setProperty(ListAzureDataLakeStorage.MIN_SIZE, "5 B");
|
||||||
|
|
||||||
|
@ -471,7 +471,7 @@ public class ITListAzureDataLakeStorage extends AbstractAzureDataLakeStorageIT {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testListWithMaxSize() throws Exception {
|
public void testListWithMaxSize() {
|
||||||
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "");
|
runner.setProperty(AbstractAzureDataLakeStorageProcessor.DIRECTORY, "");
|
||||||
runner.setProperty(ListAzureDataLakeStorage.MAX_SIZE, "5 B");
|
runner.setProperty(ListAzureDataLakeStorage.MAX_SIZE, "5 B");
|
||||||
|
|
||||||
|
@ -496,7 +496,7 @@ public class ITListAzureDataLakeStorage extends AbstractAzureDataLakeStorageIT {
|
||||||
runner.run();
|
runner.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertSuccess(String... testFilePaths) throws Exception {
|
private void assertSuccess(String... testFilePaths) {
|
||||||
runner.assertTransferCount(ListAzureDataLakeStorage.REL_SUCCESS, testFilePaths.length);
|
runner.assertTransferCount(ListAzureDataLakeStorage.REL_SUCCESS, testFilePaths.length);
|
||||||
|
|
||||||
Map<String, TestFile> expectedFiles = new HashMap<>(testFiles);
|
Map<String, TestFile> expectedFiles = new HashMap<>(testFiles);
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
* 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.nifi.processors.azure.storage.utils;
|
||||||
|
|
||||||
|
import com.azure.storage.file.datalake.DataLakeServiceClient;
|
||||||
|
import org.apache.nifi.logging.ComponentLog;
|
||||||
|
import org.apache.nifi.services.azure.storage.ADLSCredentialsDetails;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class DataLakeServiceClientFactoryTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ComponentLog logger;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testThatServiceClientIsCachedByCredentials() {
|
||||||
|
final DataLakeServiceClientFactory clientFactory = new DataLakeServiceClientFactory(logger, null);
|
||||||
|
|
||||||
|
final ADLSCredentialsDetails credentials = createCredentialDetails("account");
|
||||||
|
|
||||||
|
final DataLakeServiceClient clientOne = clientFactory.getStorageClient(credentials);
|
||||||
|
final DataLakeServiceClient clientTwo = clientFactory.getStorageClient(credentials);
|
||||||
|
|
||||||
|
assertSame(clientOne, clientTwo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testThatDifferentServiceClientIsReturnedForDifferentCredentials() {
|
||||||
|
final DataLakeServiceClientFactory clientFactory = new DataLakeServiceClientFactory(logger, null);
|
||||||
|
|
||||||
|
final ADLSCredentialsDetails credentialsOne = createCredentialDetails("accountOne");
|
||||||
|
final ADLSCredentialsDetails credentialsTwo = createCredentialDetails("accountTwo");
|
||||||
|
|
||||||
|
final DataLakeServiceClient clientOne = clientFactory.getStorageClient(credentialsOne);
|
||||||
|
final DataLakeServiceClient clientTwo = clientFactory.getStorageClient(credentialsTwo);
|
||||||
|
|
||||||
|
assertNotSame(clientOne, clientTwo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testThatCachedClientIsReturnedAfterDifferentClientIsCreated() {
|
||||||
|
final DataLakeServiceClientFactory clientFactory = new DataLakeServiceClientFactory(logger, null);
|
||||||
|
|
||||||
|
final ADLSCredentialsDetails credentialsOne = createCredentialDetails("accountOne");
|
||||||
|
final ADLSCredentialsDetails credentialsTwo = createCredentialDetails("accountTwo");
|
||||||
|
final ADLSCredentialsDetails credentialsThree = createCredentialDetails("accountOne");
|
||||||
|
|
||||||
|
final DataLakeServiceClient clientOne = clientFactory.getStorageClient(credentialsOne);
|
||||||
|
final DataLakeServiceClient clientTwo = clientFactory.getStorageClient(credentialsTwo);
|
||||||
|
final DataLakeServiceClient clientThree = clientFactory.getStorageClient(credentialsThree);
|
||||||
|
|
||||||
|
assertNotSame(clientOne, clientTwo);
|
||||||
|
assertSame(clientOne, clientThree);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ADLSCredentialsDetails createCredentialDetails(String accountName) {
|
||||||
|
return ADLSCredentialsDetails.Builder.newBuilder()
|
||||||
|
.setAccountName(accountName)
|
||||||
|
.setAccountKey("accountKey")
|
||||||
|
.setEndpointSuffix("dfs.core.windows.net")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,8 @@ package org.apache.nifi.services.azure.storage;
|
||||||
|
|
||||||
import com.azure.core.credential.AccessToken;
|
import com.azure.core.credential.AccessToken;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public class ADLSCredentialsDetails {
|
public class ADLSCredentialsDetails {
|
||||||
private final String accountName;
|
private final String accountName;
|
||||||
|
|
||||||
|
@ -98,6 +100,45 @@ public class ADLSCredentialsDetails {
|
||||||
return servicePrincipalClientSecret;
|
return servicePrincipalClientSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (o == null || getClass() != o.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ADLSCredentialsDetails that = (ADLSCredentialsDetails) o;
|
||||||
|
return useManagedIdentity == that.useManagedIdentity
|
||||||
|
&& Objects.equals(accountName, that.accountName)
|
||||||
|
&& Objects.equals(accountKey, that.accountKey)
|
||||||
|
&& Objects.equals(sasToken, that.sasToken)
|
||||||
|
&& Objects.equals(endpointSuffix, that.endpointSuffix)
|
||||||
|
&& Objects.equals(accessToken, that.accessToken)
|
||||||
|
&& Objects.equals(managedIdentityClientId, that.managedIdentityClientId)
|
||||||
|
&& Objects.equals(servicePrincipalTenantId, that.servicePrincipalTenantId)
|
||||||
|
&& Objects.equals(servicePrincipalClientId, that.servicePrincipalClientId)
|
||||||
|
&& Objects.equals(servicePrincipalClientSecret, that.servicePrincipalClientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(
|
||||||
|
accountName,
|
||||||
|
accountKey,
|
||||||
|
sasToken,
|
||||||
|
endpointSuffix,
|
||||||
|
accessToken,
|
||||||
|
useManagedIdentity,
|
||||||
|
managedIdentityClientId,
|
||||||
|
servicePrincipalTenantId,
|
||||||
|
servicePrincipalClientId,
|
||||||
|
servicePrincipalClientSecret
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
private String accountName;
|
private String accountName;
|
||||||
private String accountKey;
|
private String accountKey;
|
||||||
|
|
Loading…
Reference in New Issue