/*
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. 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.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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.
*/
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.profile.ProfileCredentialsProvider;
import com.amazonaws.regions.Region;
import com.amazonaws.regions.RegionUtils;
import com.amazonaws.services.identitymanagement.AmazonIdentityManagement;
import com.amazonaws.services.identitymanagement.AmazonIdentityManagementClient;
import com.amazonaws.services.identitymanagement.model.CreateRoleRequest;
import com.amazonaws.services.identitymanagement.model.EntityAlreadyExistsException;
import com.amazonaws.services.identitymanagement.model.GetRoleRequest;
import com.amazonaws.services.identitymanagement.model.MalformedPolicyDocumentException;
import com.amazonaws.services.identitymanagement.model.PutRolePolicyRequest;
import com.amazonaws.services.kinesisfirehose.AmazonKinesisFirehoseClient;
import com.amazonaws.services.kinesisfirehose.model.DeliveryStreamDescription;
import com.amazonaws.services.kinesisfirehose.model.DescribeDeliveryStreamRequest;
import com.amazonaws.services.kinesisfirehose.model.DescribeDeliveryStreamResult;
import com.amazonaws.services.kinesisfirehose.model.ListDeliveryStreamsRequest;
import com.amazonaws.services.kinesisfirehose.model.ListDeliveryStreamsResult;
import com.amazonaws.services.kinesisfirehose.model.PutRecordBatchRequest;
import com.amazonaws.services.kinesisfirehose.model.PutRecordBatchResult;
import com.amazonaws.services.kinesisfirehose.model.PutRecordRequest;
import com.amazonaws.services.kinesisfirehose.model.Record;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.util.IOUtils;
import com.amazonaws.util.StringUtils;
/**
* Abstract class that contains all the common methods used in samples for
* Amazon S3 and Amazon Redshift destination.
*/
public abstract class AbstractAmazonKinesisFirehoseDelivery {
// S3 properties
protected static AmazonS3Client s3Client;
protected static boolean createS3Bucket;
protected static String s3BucketARN;
protected static String s3BucketName;
protected static String s3ObjectPrefix;
protected static String s3RegionName;
// DeliveryStream properties
protected static AmazonKinesisFirehoseClient firehoseClient;
protected static String accountId;
protected static String deliveryStreamName;
protected static String firehoseRegion;
protected static boolean enableUpdateDestination;
// S3Destination Properties
protected static String iamRoleName;
protected static String s3DestinationAWSKMSKeyId;
protected static Integer s3DestinationSizeInMBs;
protected static Integer s3DestinationIntervalInSeconds;
// Properties Reader
protected static Properties properties;
// IAM Permissions Policy Document with KMS Resources. If KMS key ARN is
// specified, role will be created with KMS resources permission
private static final String IAM_ROLE_PERMISSIONS_POLICY_WITH_KMS_RESOURCES_DOCUMENT =
"permissionsPolicyDocument.json";
// IAM Permissions Policy Document without KMS Resources.
private static final String IAM_ROLE_PERMISSIONS_POLICY_WITHOUT_KMS_RESOURCES_DOCUMENT =
"permissionsPolicyWithoutKMSResourcesDocument.json";
// IAM Trust Policy Document
private static final String IAM_ROLE_TRUST_POLICY_DOCUMENT = "trustPolicyDocument.json";
// IAM Role Policy Name
private static final String FIREHOSE_ROLE_POLICY_NAME = "Firehose_Delivery_Permissions_Policy";
// Put Data Source File
private static final String PUT_RECORD_STREAM_SOURCE = "putRecordInput.txt";
private static final String BATCH_PUT_STREAM_SOURCE = "batchPutInput.txt";
private static final int BATCH_PUT_MAX_SIZE = 500;
// Default wait interval for data to be delivered in specified destination.
protected static final int DEFAULT_WAIT_INTERVAL_FOR_DATA_DELIVERY_SECS = 300;
// S3 Bucket ARN
private static final String S3_ARN_PREFIX = "arn:aws:s3:::";
// IAM Role
protected static String iamRegion;
protected static AmazonIdentityManagement iamClient;
// Logger
private static final Log LOG = LogFactory.getLog(AbstractAmazonKinesisFirehoseDelivery.class);
/**
* Method to initialize the clients using the specified AWSCredentials.
*
* @param Exception
*/
protected static void initClients() throws Exception {
/*
* The ProfileCredentialsProvider will return your [default] credential
* profile by reading from the credentials file located at
* (~/.aws/credentials).
*/
AWSCredentials credentials = null;
try {
credentials = new ProfileCredentialsProvider().getCredentials();
} catch (Exception e) {
throw new AmazonClientException("Cannot load the credentials from the credential profiles file. "
+ "Please make sure that your credentials file is at the correct "
+ "location (~/.aws/credentials), and is in valid format.", e);
}
// S3 client
s3Client = new AmazonS3Client(credentials);
Region s3Region = RegionUtils.getRegion(s3RegionName);
s3Client.setRegion(s3Region);
// Firehose client
firehoseClient = new AmazonKinesisFirehoseClient(credentials);
firehoseClient.setRegion(RegionUtils.getRegion(firehoseRegion));
// IAM client
iamClient = new AmazonIdentityManagementClient(credentials);
iamClient.setRegion(RegionUtils.getRegion(iamRegion));
}
/**
* Method to create the S3 bucket in specified region.
*
* @throws Exception
*/
protected static void createS3Bucket() throws Exception {
if (StringUtils.isNullOrEmpty(s3BucketName.trim())) {
throw new IllegalArgumentException("Bucket name is empty. Please enter a bucket name "
+ "in firehosetos3sample.properties file");
}
// Create S3 bucket if specified in the properties
if (createS3Bucket) {
s3Client.createBucket(s3BucketName);
LOG.info("Created bucket " + s3BucketName + " in S3 to deliver Firehose records");
}
}
/**
* Method to print all the delivery streams in the customer account.
*/
protected static void printDeliveryStreams() {
// list all of my DeliveryStreams
List<String> deliveryStreamNames = listDeliveryStreams();
LOG.info("Printing my list of DeliveryStreams : ");
if (deliveryStreamNames.isEmpty()) {
LOG.info("There are no DeliveryStreams for account: " + accountId);
} else {
LOG.info("List of my DeliveryStreams: ");
}
for (int i = 0; i < deliveryStreamNames.size(); i++) {
LOG.info(deliveryStreamNames.get(i));
}
}
/**
* Method to list all the delivery streams in the customer account.
*
* @return the collection of delivery streams
*/
protected static List<String> listDeliveryStreams() {
ListDeliveryStreamsRequest listDeliveryStreamsRequest = new ListDeliveryStreamsRequest();
ListDeliveryStreamsResult listDeliveryStreamsResult =
firehoseClient.listDeliveryStreams(listDeliveryStreamsRequest);
List<String> deliveryStreamNames = listDeliveryStreamsResult.getDeliveryStreamNames();
while (listDeliveryStreamsResult.isHasMoreDeliveryStreams()) {
if (deliveryStreamNames.size() > 0) {
listDeliveryStreamsRequest.setExclusiveStartDeliveryStreamName(deliveryStreamNames.get(deliveryStreamNames.size() - 1));
}
listDeliveryStreamsResult = firehoseClient.listDeliveryStreams(listDeliveryStreamsRequest);
deliveryStreamNames.addAll(listDeliveryStreamsResult.getDeliveryStreamNames());
}
return deliveryStreamNames;
}
/**
* Method to put records in the specified delivery stream by reading
* contents from sample input file using PutRecord API.
*
* @throws IOException
*/
protected static void putRecordIntoDeliveryStream() throws IOException {
try (InputStream inputStream =
Thread.currentThread().getContextClassLoader().getResourceAsStream(PUT_RECORD_STREAM_SOURCE)) {
if (inputStream == null) {
throw new FileNotFoundException("Could not find file " + PUT_RECORD_STREAM_SOURCE);
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
String line = null;
while ((line = reader.readLine()) != null) {
PutRecordRequest putRecordRequest = new PutRecordRequest();
putRecordRequest.setDeliveryStreamName(deliveryStreamName);
String data = line + "\n";
Record record = createRecord(data);
putRecordRequest.setRecord(record);
// Put record into the DeliveryStream
firehoseClient.putRecord(putRecordRequest);
}
}
}
}
/**
* Method to put records in the specified delivery stream by reading
* contents from sample input file using PutRecordBatch API.
*
* @throws IOException
*/
protected static void putRecordBatchIntoDeliveryStream() throws IOException {
try (InputStream inputStream =
Thread.currentThread().getContextClassLoader().getResourceAsStream(BATCH_PUT_STREAM_SOURCE)) {
if (inputStream == null) {
throw new FileNotFoundException("Could not find file " + BATCH_PUT_STREAM_SOURCE);
}
List<Record> recordList = new ArrayList<Record>();
int batchSize = 0;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
String line = null;
while ((line = reader.readLine()) != null) {
String data = line + "\n";
Record record = createRecord(data);
recordList.add(record);
batchSize++;
if (batchSize == BATCH_PUT_MAX_SIZE) {
putRecordBatch(recordList);
recordList.clear();
batchSize = 0;
}
}
if (batchSize > 0) {
putRecordBatch(recordList);
}
}
}
}
/**
* Method to create the IAM role.
*
* @param s3Prefix the s3Prefix to be specified in role policy (only when KMS key ARN is specified)
* @return the role ARN
* @throws InterruptedException
*/
protected static String createIamRole(String s3Prefix) throws InterruptedException {
try {
//set trust policy for the role
iamClient.createRole(new CreateRoleRequest()
.withRoleName(iamRoleName)
.withAssumeRolePolicyDocument(getTrustPolicy()));
} catch (EntityAlreadyExistsException e) {
LOG.info("IAM role with name " + iamRoleName + " already exists");
} catch (MalformedPolicyDocumentException policyDocumentException){
LOG.error(String.format("Please check the trust policy document for malformation: %s",
IAM_ROLE_TRUST_POLICY_DOCUMENT));
throw policyDocumentException;
}
// Update the role policy with permissions so that principal can access the resources
// with necessary conditions
putRolePolicy(s3Prefix);
String roleARN = iamClient.getRole(new GetRoleRequest().withRoleName(iamRoleName)).getRole().getArn();
// Sleep for 5 seconds because IAM role creation takes some time to propagate
Thread.sleep(5000);
return roleARN;
}
/**
* Method to wait until the delivery stream becomes active.
*
* @param deliveryStreamName the delivery stream
* @throws Exception
*/
protected static void waitForDeliveryStreamToBecomeAvailable(String deliveryStreamName) throws Exception {
LOG.info("Waiting for " + deliveryStreamName + " to become ACTIVE...");
long startTime = System.currentTimeMillis();
long endTime = startTime + (10 * 60 * 1000);
while (System.currentTimeMillis() < endTime) {
try {
Thread.sleep(1000 * 20);
} catch (InterruptedException e) {
// Ignore interruption (doesn't impact deliveryStream creation)
}
DeliveryStreamDescription deliveryStreamDescription = describeDeliveryStream(deliveryStreamName);
String deliveryStreamStatus = deliveryStreamDescription.getDeliveryStreamStatus();
LOG.info(" - current state: " + deliveryStreamStatus);
if (deliveryStreamStatus.equals("ACTIVE")) {
return;
}
}
throw new AmazonServiceException("DeliveryStream " + deliveryStreamName + " never went active");
}
/**
* Method to describe the delivery stream.
*
* @param deliveryStreamName the delivery stream
* @return the delivery description
*/
protected static DeliveryStreamDescription describeDeliveryStream(String deliveryStreamName) {
DescribeDeliveryStreamRequest describeDeliveryStreamRequest = new DescribeDeliveryStreamRequest();
describeDeliveryStreamRequest.withDeliveryStreamName(deliveryStreamName);
DescribeDeliveryStreamResult describeDeliveryStreamResponse =
firehoseClient.describeDeliveryStream(describeDeliveryStreamRequest);
return describeDeliveryStreamResponse.getDeliveryStreamDescription();
}
/**
* Method to wait for the specified buffering interval seconds so that data
* will be delivered to corresponding destination.
*
* @param waitTimeSecs the buffering interval seconds to wait upon
* @throws InterruptedException
*/
protected static void waitForDataDelivery(int waitTimeSecs) throws InterruptedException {
LOG.info("Since the Buffering Hints IntervalInSeconds parameter is specified as: " + waitTimeSecs
+ " seconds. Waiting for " + waitTimeSecs + " seconds for the data to be written to S3 bucket");
TimeUnit.SECONDS.sleep(waitTimeSecs);
LOG.info("Data delivery to S3 bucket " + s3BucketName + " is complete");
}
/**
* Method to return the bucket ARN.
*
* @param bucketName the bucket name to be formulated as ARN
* @return the bucket ARN
* @throws IllegalArgumentException
*/
protected static String getBucketARN(String bucketName) throws IllegalArgumentException {
return new StringBuilder().append(S3_ARN_PREFIX).append(bucketName).toString();
}
/**
* Method to perform PutRecordBatch operation with the given record list.
*
* @param recordList the collection of records
* @return the output of PutRecordBatch
*/
private static PutRecordBatchResult putRecordBatch(List<Record> recordList) {
PutRecordBatchRequest putRecordBatchRequest = new PutRecordBatchRequest();
putRecordBatchRequest.setDeliveryStreamName(deliveryStreamName);
putRecordBatchRequest.setRecords(recordList);
// Put Record Batch records. Max No.Of Records we can put in a
// single put record batch request is 500
return firehoseClient.putRecordBatch(putRecordBatchRequest);
}
/**
* Method to put the role policy with permissions document. Permission document would change
* based on KMS Key ARN specified in properties file. If KMS Key ARN is specified, permissions
* document will contain KMS resource.
*
* @param s3Prefix the s3Prefix which will be included in KMS Condition (only if KMS Key is provided)
*/
protected static void putRolePolicy(String s3Prefix) {
try {
// set permissions policy for the role
String permissionsPolicyDocument =
containsKMSKeyARN() ? getPermissionsPolicyWithKMSResources(s3Prefix)
: getPermissionsPolicyWithoutKMSResources();
iamClient.putRolePolicy(new PutRolePolicyRequest()
.withRoleName(iamRoleName)
.withPolicyName(FIREHOSE_ROLE_POLICY_NAME)
.withPolicyDocument(permissionsPolicyDocument));
} catch (MalformedPolicyDocumentException policyDocumentException){
LOG.error(String.format("Please check the permissions policy document for malformation: %s",
containsKMSKeyARN() ? IAM_ROLE_PERMISSIONS_POLICY_WITH_KMS_RESOURCES_DOCUMENT
: IAM_ROLE_PERMISSIONS_POLICY_WITHOUT_KMS_RESOURCES_DOCUMENT));
throw policyDocumentException;
}
}
/**
* Method to return the trust policy document to create a role.
*
* @return the trust policy document
*/
private static String getTrustPolicy() {
return readResource(IAM_ROLE_TRUST_POLICY_DOCUMENT)
.replace("{{CUSTOMER_ACCOUNT_ID}}", accountId);
}
/**
* Method to return the permissions policy document with KMS resource.
*
* @param s3Prefix the s3Prefix to be specified in KMS Condition
* @return the permissions policy document
*/
private static String getPermissionsPolicyWithKMSResources(String s3Prefix) {
return readResource(IAM_ROLE_PERMISSIONS_POLICY_WITH_KMS_RESOURCES_DOCUMENT)
.replace("{{S3_BUCKET_NAME}}", s3BucketName)
.replace("{{KMS_KEY_ARN}}", s3DestinationAWSKMSKeyId)
.replace("{{S3_REGION}}", s3RegionName)
.replace("{{S3_PREFIX}}", s3Prefix);
}
/**
* Method to return the permissions policy document without KMS resource.
*
* @return the permissions policy document
*/
private static String getPermissionsPolicyWithoutKMSResources() {
return readResource(IAM_ROLE_PERMISSIONS_POLICY_WITHOUT_KMS_RESOURCES_DOCUMENT)
.replace("{{S3_BUCKET_NAME}}", s3BucketName);
}
/**
* Method to read the resource for the given filename.
*
* @param name the file name
* @return the input stream as string
*/
private static String readResource(String name) {
try {
return IOUtils.toString(AmazonKinesisFirehoseToRedshiftSample.class.getResourceAsStream(name));
} catch (IOException e) {
throw new RuntimeException("Failed to read document resource: " + name, e);
}
}
/**
* Returns true if the KMS Key ARN is specified in properties file.
*
* @return true, if KMS Key is specified
*/
private static boolean containsKMSKeyARN() {
return !StringUtils.isNullOrEmpty(s3DestinationAWSKMSKeyId);
}
/**
* Method to create the record object for given data.
*
* @param data the content data
* @return the Record object
*/
private static Record createRecord(String data) {
return new Record().withData(ByteBuffer.wrap(data.getBytes()));
}
}