All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vmware.photon.controller.model.adapters.awsadapter.util.AWSClientManager Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2015-2016 VMware, Inc. All Rights Reserved.
 *
 * Licensed 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 com.vmware.photon.controller.model.adapters.awsadapter.util;

import static com.vmware.photon.controller.model.adapterapi.EndpointConfigRequest.ARN_KEY;
import static com.vmware.photon.controller.model.adapterapi.EndpointConfigRequest.EXTERNAL_ID_KEY;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.CW_CLIENT_CACHE_INITIAL_SIZE;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.CW_CLIENT_CACHE_MAX_SIZE;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.EC2_CLIENT_CACHE_INITIAL_SIZE;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.EC2_CLIENT_CACHE_MAX_SIZE;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.INVALID_CLIENT_CACHE_INITIAL_SIZE;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.INVALID_CLIENT_CACHE_MAX_SIZE;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.LB_CLIENT_CACHE_INITIAL_SIZE;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.LB_CLIENT_CACHE_MAX_SIZE;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.S3_CLIENT_CACHE_INITIAL_SIZE;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.S3_CLIENT_CACHE_MAX_SIZE;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.S3_TM_CLIENT_CACHE_INITIAL_SIZE;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.S3_TM_CLIENT_CACHE_MAX_SIZE;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSUtils.TILDA;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSUtils.awsSessionCredentialsToAuthCredentialsState;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSUtils.getArnSessionCredentialsAsync;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSUtils.isArnCredentials;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

import com.amazonaws.handlers.AsyncHandler;
import com.amazonaws.services.cloudwatch.AmazonCloudWatchAsyncClient;
import com.amazonaws.services.cloudwatch.model.DescribeAlarmsRequest;
import com.amazonaws.services.cloudwatch.model.DescribeAlarmsResult;
import com.amazonaws.services.ec2.AmazonEC2AsyncClient;
import com.amazonaws.services.elasticloadbalancing.AmazonElasticLoadBalancingAsyncClient;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.transfer.TransferManager;

import com.vmware.photon.controller.model.UriPaths;
import com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.AwsClientType;
import com.vmware.photon.controller.model.adapters.awsadapter.AWSUtils;
import com.vmware.photon.controller.model.adapters.util.LRUCache;
import com.vmware.xenon.common.DeferredResult;
import com.vmware.xenon.common.OperationContext;
import com.vmware.xenon.common.StatelessService;
import com.vmware.xenon.common.Utils;
import com.vmware.xenon.services.common.AuthCredentialsService.AuthCredentialsServiceState;

/**
 * Holds the cache for managing the AWS Clients used to make calls to AWS from the photon model adapters.
 */
public class AWSClientManager {

    private static String SEPARATOR = "-";
    // Flag for determining the type of AWS client managed by this client manager.
    private AwsClientType awsClientType;
    private Map ec2ClientCache;
    private Map cloudWatchClientCache;
    private Map s3clientCache;
    private Map s3TransferManagerCache;
    private Map loadBalancingClientCache;
    private Map> arnCredentialsCache;
    private ExecutorService executorService;

    private LRUCache invalidEc2Clients;
    private LRUCache invalidCloudWatchClients;
    private LRUCache invalidLoadBalancingClients;
    private LRUCache invalidS3Clients;

    public static final String AWS_RETRY_AFTER_INTERVAL_MINUTES = UriPaths.PROPERTY_PREFIX
            + "AWSClientManager.retryInterval";
    private static final int DEFAULT_RETRY_AFTER_INTERVAL_MINUTES = 60;
    private static final int RETRY_AFTER_INTERVAL_MINUTES = Integer
            .getInteger(AWS_RETRY_AFTER_INTERVAL_MINUTES, DEFAULT_RETRY_AFTER_INTERVAL_MINUTES);

    AWSClientManager(AwsClientType awsClientType) {
        this.arnCredentialsCache = Collections.synchronizedMap(new HashMap<>());
        this.awsClientType = awsClientType;
        switch (awsClientType) {
        case EC2:
            this.ec2ClientCache = Collections
                    .synchronizedMap(new LRUCache<>(EC2_CLIENT_CACHE_INITIAL_SIZE,
                            EC2_CLIENT_CACHE_MAX_SIZE));
            this.invalidEc2Clients = new LRUCache<>(INVALID_CLIENT_CACHE_INITIAL_SIZE,
                    INVALID_CLIENT_CACHE_MAX_SIZE);
            return;
        case CLOUD_WATCH:
            this.cloudWatchClientCache = Collections
                    .synchronizedMap(new LRUCache<>(CW_CLIENT_CACHE_INITIAL_SIZE,
                            CW_CLIENT_CACHE_MAX_SIZE));
            this.invalidCloudWatchClients = new LRUCache<>(INVALID_CLIENT_CACHE_INITIAL_SIZE,
                    INVALID_CLIENT_CACHE_MAX_SIZE);
            return;
        case S3:
            this.s3clientCache = Collections
                    .synchronizedMap(new LRUCache<>(S3_CLIENT_CACHE_INITIAL_SIZE,
                            S3_CLIENT_CACHE_MAX_SIZE));
            this.invalidS3Clients = new LRUCache<>(INVALID_CLIENT_CACHE_INITIAL_SIZE,
                    INVALID_CLIENT_CACHE_MAX_SIZE);
            return;
        case S3_TRANSFER_MANAGER:
            this.s3TransferManagerCache = Collections
                    .synchronizedMap(new LRUCache<>(S3_TM_CLIENT_CACHE_INITIAL_SIZE,
                            S3_TM_CLIENT_CACHE_MAX_SIZE));
            return;
        case LOAD_BALANCING:
            this.loadBalancingClientCache = Collections
                    .synchronizedMap(new LRUCache<>(LB_CLIENT_CACHE_INITIAL_SIZE,
                            LB_CLIENT_CACHE_MAX_SIZE));
            this.invalidLoadBalancingClients = new LRUCache<>(INVALID_CLIENT_CACHE_INITIAL_SIZE,
                    INVALID_CLIENT_CACHE_MAX_SIZE);
            return;
        default:
            String msg = "The specified AWS client type " + awsClientType
                    + " is not supported by this client manager.";
            throw new UnsupportedOperationException(msg);
        }
    }

    public AWSClientManager(AwsClientType awsClientType, ExecutorService executorService) {
        this(awsClientType);
        this.executorService = executorService;
    }

    /**
     * Accesses the client cache to get the EC2 client for the given auth credentials and regionId.
     * If a client is not found to exist, creates a new one and adds an entry in the cache for it.
     * This process is done asynchronously, and the EC2 client will be passed via the
     * successConsumer.
     *
     * Allows for ARN-based credentials (as well as traditional key-based credentials), where a set
     * of credentials with the ARN key set will communicate with AWS to trade for a set of session
     * credentials that can allow the instantiation of an Amazon client.
     *
     * @param credentials The auth credentials to be used for the client creation
     * @param regionId The region of the AWS client
     * @param service The stateless service making the request and for which the executor pool needs
     *                to be allocated.
     * @return A {@link DeferredResult} of the AWS client.
     */
    public synchronized DeferredResult getOrCreateEC2ClientAsync(
            AuthCredentialsServiceState credentials, String regionId, StatelessService service) {
        if (this.awsClientType != AwsClientType.EC2) {
            return DeferredResult.failed(new UnsupportedOperationException(
                    "This client manager supports only AWS " + this.awsClientType + " clients."));
        }

        OperationContext operationContext = OperationContext.getOperationContext();
        return getArnCredentialsFromCache(credentials, service)
                .thenApply(refreshedCredentials -> {
                    OperationContext.restoreOperationContext(operationContext);
                    return getOrCreateEC2Client(refreshedCredentials, regionId, service,
                            t -> { throw new CompletionException(t); });
                });
    }

    /**
     * Accesses the client cache to get the EC2 client for the given auth credentials and regionId.
     * If a client is not found to exist, creates a new one and adds an entry in the cache for it.
     *
     * Note: ARN-based credentials will not be accepted unless they have already been exchanged to
     * AWS for session credentials. If unset, this method will throw a
     * {@link UnsupportedOperationException} exception in this circumstance. To enable ARN-based
     * credentials, migrate to {@link #getOrCreateEC2ClientAsync(AuthCredentialsServiceState,
     * String, StatelessService)}.
     *
     * @param credentials The auth credentials to be used for the client creation
     * @param regionId The region of the AWS client
     * @param service The stateless service making the request and for which the executor pool needs to be allocated.
     * @return The AWSClient
     */
    public AmazonEC2AsyncClient getOrCreateEC2Client(AuthCredentialsServiceState credentials,
            String regionId, StatelessService service, Consumer failConsumer) {
        if (this.awsClientType != AwsClientType.EC2) {
            throw new UnsupportedOperationException(
                    "This client manager supports only AWS " + this.awsClientType + " clients.");
        }

        if (isArnCredentials(credentials) && !isSetCredentials(credentials)) {
            throw new UnsupportedOperationException(
                    "For ARN-based credentials, exchange for session-based access key/secret key first before retrieving the client.");
        }

        AmazonEC2AsyncClient amazonEC2Client = null;
        String cacheKey = createCredentialRegionCacheKey(credentials, regionId);
        try {
            amazonEC2Client = this.ec2ClientCache.computeIfAbsent(cacheKey, key -> AWSUtils
                    .getAsyncClient(credentials, regionId, getExecutor()));
        } catch (Throwable e) {
            service.logSevere(e);
            failConsumer.accept(e);
        }
        return amazonEC2Client;
    }

    /**
     * Returns true if a set of credentials has set the private key and privateKeyId fields.
     *
     * @param credentials A set of credentials to test.
     * @return True if credentials object has set the privateKey and privateKeyId fields.
     */
    private boolean isSetCredentials(AuthCredentialsServiceState credentials) {
        return credentials.privateKey != null && !credentials.privateKey.isEmpty() &&
                credentials.privateKeyId != null && !credentials.privateKeyId.isEmpty();

    }

    /**
     * Checks if an EC2 client has been marked as invalid.
     * @param credentials
     *         The auth credentials to be used for the client creation
     * @param regionId
     *         The region of the AWS client
     * @return true if the EC2 client is marked as invalid, false otherwise.
     */
    public boolean isEc2ClientInvalid(AuthCredentialsServiceState credentials,
            String regionId) {
        String cacheKey = createCredentialRegionCacheKey(credentials, regionId);
        synchronized (this.ec2ClientCache) {
            return isInvalidClient(this.invalidEc2Clients, cacheKey);
        }
    }

    /**
     * Marks an EC2 client as invalid.
     * @param service
     *         The stateless service for which the operation is being performed.
     * @param credentials
     *         The auth credentials to be used for the client creation
     * @param regionId
     *         The region of the AWS client
     */
    public void markEc2ClientInvalid(StatelessService service,
            AuthCredentialsServiceState credentials, String regionId) {
        String cacheKey = createCredentialRegionCacheKey(credentials, regionId);
        service.logWarning("Marking EC2 client cache entry invalid for key: " + cacheKey);
        synchronized (this.ec2ClientCache) {
            this.invalidEc2Clients.put(cacheKey, Utils.getNowMicrosUtc());
            this.ec2ClientCache.remove(cacheKey);
        }
    }

    /**
     * Get or create a CloudWatch Client instance that will be used to get stats from AWS.
     *
     * Allows for ARN-based credentials (as well as traditional key-based credentials), where a set
     * of credentials with the ARN key set will communicate with AWS to trade for a set of session
     * credentials that can allow the instantiation of an Amazon client.
     *
     * @param credentials The auth credentials to be used for the client creation
     * @param regionId The region of the AWS client
     * @param service The stateless service making the request and for which the executor pool needs
     *                to be allocated.
     * @return The AWSClient
     */
    public synchronized DeferredResult getOrCreateCloudWatchClientAsync(
            AuthCredentialsServiceState credentials, String regionId, StatelessService service,
            boolean isMock) {
        if (this.awsClientType != AwsClientType.CLOUD_WATCH) {
            return DeferredResult.failed(new UnsupportedOperationException(
                    "This client manager supports only AWS " + this.awsClientType + " clients."));
        }

        OperationContext operationContext = OperationContext.getOperationContext();
        return getArnCredentialsFromCache(credentials, service)
                .thenApply(refreshedCredentials -> {
                    OperationContext.restoreOperationContext(operationContext);
                    return getOrCreateCloudWatchClient(refreshedCredentials, regionId, service,
                                isMock, t -> { throw new CompletionException(t); });
                });
    }

    /**
     * Get or create a CloudWatch Client instance that will be used to get stats from AWS.
     *
     * Note: ARN-based credentials will not be accepted unless they have already been exchanged to
     * AWS for session credentials. If unset, this method will throw a
     * {@link UnsupportedOperationException} exception in this circumstance. To enable ARN-based
     * credentials, migrate to {@link #getOrCreateCloudWatchClientAsync(AuthCredentialsServiceState,
     * String, StatelessService, boolean)}.
     *
     * @param credentials The auth credentials to be used for the client creation
     * @param regionId The region of the AWS client
     * @param service The stateless service for which the operation is being performed.
     * @param isMock Indicates if this a mock request
     * @return
     */
    public AmazonCloudWatchAsyncClient getOrCreateCloudWatchClient(
            AuthCredentialsServiceState credentials, String regionId, StatelessService service,
            boolean isMock, Consumer failConsumer) {
        if (this.awsClientType != AwsClientType.CLOUD_WATCH) {
            throw new UnsupportedOperationException(
                    "This client manager supports only AWS " + this.awsClientType + " clients.");
        }

        if (isArnCredentials(credentials) && !isSetCredentials(credentials)) {
            throw new UnsupportedOperationException(
                    "For ARN-based credentials, exchange for session-based access key/secret key first before retrieving the client.");
        }

        String cacheKey = createCredentialRegionCacheKey(credentials, regionId);
        if (isCloudWatchClientInvalid(cacheKey)) {
            failConsumer.accept(
                    new IllegalStateException("Invalid cloud watch client for key: " + cacheKey));
            return null;
        }
        AmazonCloudWatchAsyncClient amazonCloudWatchClient = null;
        try {
            amazonCloudWatchClient = this.cloudWatchClientCache.computeIfAbsent(cacheKey, key -> {
                AmazonCloudWatchAsyncClient client = AWSUtils.getStatsAsyncClient
                        (credentials, regionId, getExecutor(), isMock);
                client.describeAlarmsAsync(
                        new AsyncHandler() {
                            @Override
                            public void onError(Exception exception) {
                                markCloudWatchClientInvalid(service, cacheKey);
                            }

                            @Override
                            public void onSuccess(DescribeAlarmsRequest request,
                                    DescribeAlarmsResult result) {
                                //noop
                            }
                        });
                return client;
            });
        } catch (Throwable e) {
            service.logSevere(e);
            failConsumer.accept(e);
        }
        return amazonCloudWatchClient;
    }

    private boolean isCloudWatchClientInvalid(String cacheKey) {
        synchronized (this.cloudWatchClientCache) {
            return isInvalidClient(this.invalidCloudWatchClients, cacheKey);
        }
    }

    public void markCloudWatchClientInvalid(StatelessService service,
            String cacheKey) {
        service.logWarning("Marking cloudwatch client cache entry invalid for key: " + cacheKey);
        synchronized (this.cloudWatchClientCache) {
            this.invalidCloudWatchClients.put(cacheKey, Utils.getNowMicrosUtc());
            this.cloudWatchClientCache.remove(cacheKey);
        }
    }

    /**
     * Get or create an S3 Transfer Manager client.
     *
     * Allows for ARN-based credentials (as well as traditional key-based credentials), where a set
     * of credentials with the ARN key set will communicate with AWS to trade for a set of session
     * credentials that can allow the instantiation of an Amazon client.
     *
     * @param credentials The auth credentials to be used for the client creation
     * @param regionId The region of the AWS client
     * @param service The stateless service making the request and for which the executor pool needs
     *                to be allocated.
     * @return The AWSClient
     */
    public synchronized DeferredResult getOrCreateS3TransferManagerAsync(
            AuthCredentialsServiceState credentials, String regionId, StatelessService service) {
        if (this.awsClientType != AwsClientType.S3_TRANSFER_MANAGER) {
            return DeferredResult.failed(new UnsupportedOperationException(
                    "This client manager supports only AWS " + this.awsClientType + " clients."));
        }

        OperationContext operationContext = OperationContext.getOperationContext();
        return getArnCredentialsFromCache(credentials, service)
                .thenApply(refreshedCredentials -> {
                    OperationContext.restoreOperationContext(operationContext);
                    return getOrCreateS3TransferManager(refreshedCredentials, regionId, service,
                            t -> { throw new CompletionException(t); });
                });
    }

    /**
     * Get or create an S3 Transfer Manager client.
     *
     * Note: ARN-based credentials will not be accepted unless they have already been exchanged to
     * AWS for session credentials. If unset, this method will throw a
     * {@link UnsupportedOperationException} exception in this circumstance. To enable ARN-based
     * credentials, migrate to {@link #getOrCreateS3TransferManagerAsync}.
     *
     * @param credentials The auth credentials to be used for the client creation
     * @param regionId The region of the AWS client
     * @param service The stateless service making the request and for which the executor pool needs
     *                to be allocated.
     * @param failConsumer A callback to handle failure responses.
     * @return The AWSClient
     */
    public synchronized TransferManager getOrCreateS3TransferManager(
            AuthCredentialsServiceState credentials,
            String regionId, StatelessService service, Consumer failConsumer) {
        if (this.awsClientType != AwsClientType.S3_TRANSFER_MANAGER) {
            throw new UnsupportedOperationException(
                    "This client manager supports only AWS " + this.awsClientType + " clients.");
        }

        if (isArnCredentials(credentials) && !isSetCredentials(credentials)) {
            throw new UnsupportedOperationException(
                    "For ARN-based credentials, exchange for session-based access key/secret key first before retrieving the client.");
        }

        String cacheKey = createCredentialRegionCacheKey(credentials, regionId);
        try {
            return this.s3TransferManagerCache.computeIfAbsent(cacheKey, key -> AWSUtils
                    .getS3TransferManager(credentials, regionId, getExecutor()));

        } catch (Throwable t) {
            service.logSevere(t);
            failConsumer.accept(t);
            return null;
        }
    }

    /**
     * Get or create a ElasticLoadBalancing Client instance that will be used to create/delete
     * load balancers from AWS.
     *
     * Allows for ARN-based credentials (as well as traditional key-based credentials), where a set
     * of credentials with the ARN key set will communicate with AWS to trade for a set of session
     * credentials that can allow the instantiation of an Amazon client.
     *
     * @param credentials The auth credentials to be used for the client creation
     * @param regionId The region of the AWS client
     * @param service The stateless service making the request and for which the executor pool needs
     *                to be allocated.
     * @return The AWSClient
     */
    public synchronized DeferredResult getOrCreateLoadBalancingClientAsync(
            AuthCredentialsServiceState credentials, String regionId, StatelessService service,
            boolean isMock) {
        if (this.awsClientType != AwsClientType.LOAD_BALANCING) {
            return DeferredResult.failed(new UnsupportedOperationException(
                    "This client manager supports only AWS " + this.awsClientType + " clients."));
        }

        OperationContext operationContext = OperationContext.getOperationContext();
        return getArnCredentialsFromCache(credentials, service)
                .thenApply(refreshedCredentials -> {
                    OperationContext.restoreOperationContext(operationContext);
                    return getOrCreateLoadBalancingClient(refreshedCredentials, regionId, service,
                            isMock, t -> { throw new CompletionException(t); });
                });
    }

    /**
     * Get or create a ElasticLoadBalancing Client instance that will be used to create/delete
     * load balancers from AWS.
     *
     * Note: ARN-based credentials will not be accepted unless they have already been exchanged to
     * AWS for session credentials. If unset, this method will throw a
     * {@link UnsupportedOperationException} exception in this circumstance. To enable ARN-based
     * credentials, migrate to {@link #getOrCreateLoadBalancingClientAsync}.
     *
     * @param credentials The auth credentials to be used for the client creation
     * @param regionId The region of the AWS client
     * @param service The stateless service for which the operation is being performed.
     * @return
     */
    public AmazonElasticLoadBalancingAsyncClient getOrCreateLoadBalancingClient(
            AuthCredentialsServiceState credentials,
            String regionId, StatelessService service, boolean isMock,
            Consumer failConsumer) {
        if (this.awsClientType != AwsClientType.LOAD_BALANCING) {
            throw new UnsupportedOperationException(
                    "This client manager supports only AWS " + this.awsClientType + " clients.");
        }

        if (isArnCredentials(credentials) && !isSetCredentials(credentials)) {
            throw new UnsupportedOperationException(
                    "For ARN-based credentials, exchange for session-based access key/secret key first before retrieving the client.");
        }

        String cacheKey = createCredentialRegionCacheKey(credentials, regionId);
        if (isLoadBalancingClientInvalid(cacheKey)) {
            failConsumer.accept(new IllegalStateException(
                    "Invalid load balancing client for key: " + cacheKey));
            return null;
        }
        try {
            return this.loadBalancingClientCache.computeIfAbsent(cacheKey, key -> AWSUtils
                    .getLoadBalancingAsyncClient(credentials, regionId, getExecutor()));
        } catch (Throwable e) {
            service.logSevere(e);
            failConsumer.accept(e);
        }
        return null;
    }

    private boolean isLoadBalancingClientInvalid(String cacheKey) {
        synchronized (this.loadBalancingClientCache) {
            return isInvalidClient(this.invalidLoadBalancingClients, cacheKey);
        }
    }

    /**
     * Marks an ElasticLoadBalancing client as invalid.
     * @param service
     *         The stateless service for which the operation is being performed.
     * @param credentials
     *         The auth credentials to be used for the client creation
     * @param regionId
     *         The region of the AWS client
     */
    public void markLoadBalancingClientInvalid(StatelessService service,
            AuthCredentialsServiceState credentials, String regionId) {
        String cacheKey = createCredentialRegionCacheKey(credentials, regionId);
        service.logWarning(
                "Marking load balancing client cache entry invalid for key: " + cacheKey);
        synchronized (this.loadBalancingClientCache) {
            this.invalidLoadBalancingClients.put(cacheKey, Utils.getNowMicrosUtc());
            this.loadBalancingClientCache.remove(cacheKey);
        }
    }

    /**
     * Get or create an S3 Client asynchronously.
     *
     * Allows for ARN-based credentials (as well as traditional key-based credentials), where a set
     * of credentials with the ARN key set will communicate with AWS to trade for a set of session
     * credentials that can allow the instantiation of an Amazon client.
     *
     * @param credentials The auth credentials to be used for the client creation
     * @param regionId The region of the AWS client
     * @param service The stateless service making the request and for which the executor pool needs
     *                to be allocated.
     * @return The AWSClient
     */
    public synchronized DeferredResult getOrCreateS3ClientAsync(
            AuthCredentialsServiceState credentials, String regionId, StatelessService service) {
        if (this.awsClientType != AwsClientType.S3) {
            return DeferredResult.failed(new UnsupportedOperationException(
                    "This client manager supports only AWS " + this.awsClientType + " clients."));
        }

        OperationContext operationContext = OperationContext.getOperationContext();
        return getArnCredentialsFromCache(credentials, service)
                .thenApply(refreshedCredentials -> {
                    OperationContext.restoreOperationContext(operationContext);
                    return getOrCreateS3Client(refreshedCredentials, regionId, service,
                            t -> { throw new CompletionException(t); });
                });
    }

    /**
     * Get or create an S3 Client.
     *
     * Note: ARN-based credentials will not be accepted unless they have already been exchanged to
     * AWS for session credentials. If unset, this method will throw a
     * {@link UnsupportedOperationException} exception in this circumstance. To enable ARN-based
     * credentials, migrate to {@link #getOrCreateS3ClientAsync}.
     *
     * @param credentials The auth credentials to be used for the client creation
     * @param regionId The region of the AWS client
     * @param service The stateless service making the request and for which the executor pool needs
     *                to be allocated.
     * @param failConsumer A callback to handle failure responses.
     * @return The AWSClient
     */
    public AmazonS3Client getOrCreateS3Client(AuthCredentialsServiceState credentials,
            String regionId, StatelessService service, Consumer failConsumer) {
        if (this.awsClientType != AwsClientType.S3) {
            throw new UnsupportedOperationException(
                    "This client manager supports only AWS " + this.awsClientType + " clients.");
        }

        if (isArnCredentials(credentials) && !isSetCredentials(credentials)) {
            throw new UnsupportedOperationException(
                    "For ARN-based credentials, exchange for session-based access key/secret key first before retrieving the client.");
        }

        String cacheKey = createCredentialRegionCacheKey(credentials, regionId);

        try {
            return this.s3clientCache.computeIfAbsent(cacheKey, key -> AWSUtils.getS3Client
                    (credentials, regionId));
        } catch (Exception e) {
            markS3ClientInvalid(service, credentials, regionId);
            service.logSevere(e);
            failConsumer.accept(e);
        }
        return null;
    }

    /**
     * Marks an S3 client as invalid.
     * @param service
     *         The stateless service for which the operation is being performed.
     * @param credentials
     *         The auth credentials to be used for the client creation
     * @param regionId
     *         The region of the AWS client
     */
    public void markS3ClientInvalid(StatelessService service,
            AuthCredentialsServiceState credentials, String regionId) {
        String cacheKey = createCredentialRegionCacheKey(credentials, regionId);
        service.logWarning("Marking S3 client cache entry invalid for key: " + cacheKey);
        synchronized (this.s3clientCache) {
            this.invalidS3Clients.put(cacheKey, Utils.getNowMicrosUtc());
            this.s3clientCache.remove(cacheKey);
        }
    }

    /**
     * Checks if an S3 client has been marked as invalid.
     * @param credentials
     *         The auth credentials to be used for the client creation
     * @param regionId
     *         The region of the AWS client
     * @return true if the S3 client is marked as invalid, false otherwise.
     */
    public boolean isS3ClientInvalid(AuthCredentialsServiceState credentials,
            String regionId) {
        String cacheKey = createCredentialRegionCacheKey(credentials, regionId);
        synchronized (this.s3clientCache) {
            return isInvalidClient(this.invalidS3Clients, cacheKey);
        }
    }

    /**
     * Checks if a client (via cache key) has been marked as invalid within the last
     * {@link #RETRY_AFTER_INTERVAL_MINUTES} minutes. If a client has been marked before, but
     * {@link #RETRY_AFTER_INTERVAL_MINUTES} minutes has passed, the client is removed from the
     * cache and it is no longer considered invalid.
     * @param cache
     *         The cache to check.
     * @param cacheKey
     *         The commonly used key to identify a client in the cache.
     * @return true if the client is marked as invalid, false otherwise.
     */
    private boolean isInvalidClient(LRUCache cache, String cacheKey) {
        Long entryTimestamp = cache.get(cacheKey);
        if (entryTimestamp != null) {
            if ((entryTimestamp + TimeUnit.MINUTES.toMicros(RETRY_AFTER_INTERVAL_MINUTES)) < Utils
                    .getNowMicrosUtc()) {
                cache.remove(cacheKey);
            } else {
                return true;
            }
        }
        return false;
    }

    /**
     * Generates a common cache key formed via credentials and specific region ID.
     * @param credentials
     *         The auth credentials to be used for the client creation
     * @param regionId
     *         The region of the AWS client
     * @return A common, consistent cache key for use in the AWS Client Manager.
     */
    public static String createCredentialRegionCacheKey(AuthCredentialsServiceState credentials,
            String regionId) {
        return Utils.computeHash(credentials.privateKeyId + SEPARATOR + credentials.privateKey)
                + TILDA +
                regionId;
    }

    /**
     * Common client manager method to retrieve (or update) credentials from the
     * {@link #arnCredentialsCache} if ARN-based, or just return the current credentials set if not.
     * If expired, the credentials will be regenerated. Otherwise, they will continue to be
     * retrieved as normal.
     *
     * @param credentials The auth credentials to be used for the client creation
     * @param service A service to issue requests
     */
    public synchronized DeferredResult getArnCredentialsFromCache(
            AuthCredentialsServiceState credentials, StatelessService service) {
        if (!isArnCredentials(credentials)) {
            return DeferredResult.completed(credentials);
        }

        String arn = credentials.customProperties.get(ARN_KEY);
        String arnCacheKey = Utils.computeHash(arn);

        // If the ARN is in the arnCredentialsCache, then retrieve the credentials and check
        // if they are expired. If not expired, continue to use them.
        if (this.arnCredentialsCache.containsKey(arnCacheKey)) {

            // If there is already a cache entry, but it has not been completed, just return the
            // cache reference.
            if (!this.arnCredentialsCache.get(arnCacheKey).isDone()) {
                return this.arnCredentialsCache.get(arnCacheKey);
            }

            // Check if the credentials are expired. If not, return. If so, refresh.
            AuthCredentialsServiceState arnCredentials = this.arnCredentialsCache.get(arnCacheKey)
                    .getNow(new AuthCredentialsServiceState());

            if (!AWSUtils.isExpiredCredentials(arnCredentials)) {
                return this.arnCredentialsCache.get(arnCacheKey);
            }

            service.logInfo("Refreshing session credentials for arn: '%s'", arn);
        }

        this.arnCredentialsCache.put(arnCacheKey, new DeferredResult<>());

        // If unavailable in the cache, or expired, generate a new set of session credentials.
        OperationContext operationContext = OperationContext.getOperationContext();
        getArnSessionCredentialsAsync(arn, credentials.customProperties.get(EXTERNAL_ID_KEY),
                getExecutor()).whenComplete((awsSessionCredentials, t) -> {
                    OperationContext.restoreOperationContext(operationContext);
                    if (t != null) {
                        this.arnCredentialsCache.get(arnCacheKey).fail(t);
                        return;
                    }

                    service.logInfo("Generated session credentials for arn: '%s'", arn);

                    AuthCredentialsServiceState arnCredentials =
                            awsSessionCredentialsToAuthCredentialsState(awsSessionCredentials);

                    // Update the cache with the new credentials.
                    this.arnCredentialsCache.get(arnCacheKey).complete(arnCredentials);
                });
        return this.arnCredentialsCache.get(arnCacheKey);
    }

    /**
     * Helper method to clean up this client manager's arnCredentialsCache.
     */
    public void cleanUpArnCache() {
        this.arnCredentialsCache.clear();
    }

    /**
     * Clears out the client cache and all the resources associated with each of the AWS clients.
     */
    public void cleanUp() {
        cleanUpArnCache();
        switch (this.awsClientType) {
        case CLOUD_WATCH:
            cleanupCache(this.cloudWatchClientCache, c -> c.shutdown());
            break;

        case EC2:
            this.ec2ClientCache.values().forEach(c -> c.shutdown());
            this.ec2ClientCache.clear();
            cleanupCache(this.ec2ClientCache, c -> c.shutdown());
            break;

        case S3:
            this.s3clientCache.values().forEach(c -> c.shutdown());
            this.s3clientCache.clear();
            cleanupCache(this.s3clientCache, c -> c.shutdown());
            break;

        case S3_TRANSFER_MANAGER:
            cleanupCache(this.s3TransferManagerCache, c -> c.shutdownNow());
            break;

        case LOAD_BALANCING:
            cleanupCache(this.loadBalancingClientCache, c -> c.shutdown());
            break;

        default:
            throw new UnsupportedOperationException(
                    "AWS client type not supported by this client manager");
        }
    }

    private  void cleanupCache(Map cache, Consumer consumer) {
        synchronized (cache) {
            cache.values().forEach(c -> consumer.accept(c));
            cache.clear();
        }
    }

    /**
     * Returns the executor pool associated with the service host. In case one does not exist already,
     * creates a new one and saves that in a cache.
     */
    public ExecutorService getExecutor() {
        return this.executorService;
    }

    /**
     * Returns the count of the clients that are cached in the client cache for the specified client type.
     */
    public int getCacheCount() {
        int size = 0;
        switch (this.awsClientType) {
        case EC2:
            size = this.ec2ClientCache.size();
            break;
        case CLOUD_WATCH:
            size = this.cloudWatchClientCache.size();
            break;
        case S3:
            size = this.s3clientCache.size();
            break;
        case S3_TRANSFER_MANAGER:
            size = this.s3TransferManagerCache.size();
            break;
        case LOAD_BALANCING:
            size = this.loadBalancingClientCache.size();
            break;
        default:
        }
        return size;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy