/*
* Copyright 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.
*/
package com.amazonaws.eclipse.lambda.serverless.wizard;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.swt.widgets.Display;
import com.amazonaws.eclipse.core.AwsToolkitCore;
import com.amazonaws.eclipse.core.plugin.AbstractAwsJobWizard;
import com.amazonaws.eclipse.core.regions.Region;
import com.amazonaws.eclipse.core.regions.RegionUtils;
import com.amazonaws.eclipse.core.regions.ServiceAbbreviations;
import com.amazonaws.eclipse.explorer.cloudformation.OpenStackEditorAction;
import com.amazonaws.eclipse.lambda.LambdaAnalytics;
import com.amazonaws.eclipse.lambda.LambdaPlugin;
import com.amazonaws.eclipse.lambda.project.metadata.ProjectMetadataManager;
import com.amazonaws.eclipse.lambda.project.metadata.ServerlessProjectMetadata;
import com.amazonaws.eclipse.lambda.project.wizard.model.DeployServerlessProjectDataModel;
import com.amazonaws.eclipse.lambda.serverless.ui.DeployServerlessProjectPage;
import com.amazonaws.eclipse.lambda.serverless.ui.DeployServerlessProjectPageTwo;
import com.amazonaws.eclipse.lambda.upload.wizard.util.FunctionJarExportHelper;
import com.amazonaws.services.cloudformation.AmazonCloudFormation;
import com.amazonaws.services.cloudformation.model.AmazonCloudFormationException;
import com.amazonaws.services.cloudformation.model.Capability;
import com.amazonaws.services.cloudformation.model.ChangeSetStatus;
import com.amazonaws.services.cloudformation.model.ChangeSetType;
import com.amazonaws.services.cloudformation.model.CreateChangeSetRequest;
import com.amazonaws.services.cloudformation.model.DeleteChangeSetRequest;
import com.amazonaws.services.cloudformation.model.DeleteStackRequest;
import com.amazonaws.services.cloudformation.model.DescribeChangeSetRequest;
import com.amazonaws.services.cloudformation.model.DescribeChangeSetResult;
import com.amazonaws.services.cloudformation.model.DescribeStacksRequest;
import com.amazonaws.services.cloudformation.model.ExecuteChangeSetRequest;
import com.amazonaws.services.cloudformation.model.ListStacksRequest;
import com.amazonaws.services.cloudformation.model.ListStacksResult;
import com.amazonaws.services.cloudformation.model.Parameter;
import com.amazonaws.services.cloudformation.model.Stack;
import com.amazonaws.services.cloudformation.model.StackStatus;
import com.amazonaws.services.cloudformation.model.StackSummary;
import com.amazonaws.services.cloudformation.model.Tag;
import com.amazonaws.services.cloudformation.model.TemplateParameter;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.services.s3.transfer.TransferManager;
import com.amazonaws.services.s3.transfer.TransferManagerBuilder;
import com.amazonaws.services.s3.transfer.Upload;
import com.amazonaws.util.StringUtils;
public class DeployServerlessProjectWizard extends AbstractAwsJobWizard {
// AWS CloudFormation stack statuses for updating the ChangeSet.
private static final Set<String> STATUSES_FOR_UPDATE = new HashSet<>(Arrays.asList(
StackStatus.CREATE_COMPLETE.toString(),
StackStatus.UPDATE_COMPLETE.toString(),
StackStatus.UPDATE_ROLLBACK_COMPLETE.toString()));
// AWS CloudFormation stack statuses for creating a new ChangeSet.
private static final Set<String> STATUSES_FOR_CREATE = new HashSet<>(Arrays.asList(
StackStatus.REVIEW_IN_PROGRESS.toString(),
StackStatus.DELETE_COMPLETE.toString()));
// AWS CloudFormation stack statuses for waiting and deleting the stack first, then creating a new ChangeSet.
private static final Set<String> STATUSES_FOR_DELETE = new HashSet<>(Arrays.asList(
StackStatus.ROLLBACK_IN_PROGRESS.toString(),
StackStatus.ROLLBACK_COMPLETE.toString(),
StackStatus.DELETE_IN_PROGRESS.toString()));
private DeployServerlessProjectDataModel dataModel;
public DeployServerlessProjectWizard(IProject project, Set<String> handlerClasses) {
super("Deploy Serverless application to AWS");
this.dataModel = new DeployServerlessProjectDataModel(project, handlerClasses);
initDataModel();
}
@Override
public void addPages() {
addPage(new DeployServerlessProjectPage(dataModel));
addPage(new DeployServerlessProjectPageTwo(dataModel));
}
@Override
public IStatus doFinish(IProgressMonitor monitor) {
monitor.beginTask("Deploying Serverless template to AWS CloudFormation.", 100);
try {
if (deployServerlessTemplate(monitor, 100)) {
new OpenStackEditorAction(
dataModel.getStackDataModel().getStackName(), dataModel.getRegionDataModel().getRegion(), true).run();
} else {
return Status.CANCEL_STATUS;
}
} catch (Exception e) {
LambdaPlugin.getDefault().reportException(
"Failed to deploy serverless project to AWS CloudFormation.", e);
LambdaAnalytics.trackDeployServerlessProjectFailed();
return new Status(Status.ERROR, LambdaPlugin.PLUGIN_ID,
"Failed to deploy serverless project to AWS CloudFormation.", e);
}
monitor.done();
LambdaAnalytics.trackDeployServerlessProjectSucceeded();
return Status.OK_STATUS;
}
@Override
protected String getJobTitle() {
return "Deploying Serverless template to AWS CloudFormation.";
}
@Override
public boolean performCancel() {
LambdaAnalytics.trackServerlessProjectCreationCanceled();
return true;
}
private boolean deployServerlessTemplate(IProgressMonitor monitor, int totalUnitOfWork) throws IOException, InterruptedException {
String stackName = dataModel.getStackDataModel().getStackName();
monitor.subTask("Exporting Lambda functions...");
File jarFile = FunctionJarExportHelper.exportProjectToJarFile(dataModel.getProject(), true);
monitor.worked((int)(totalUnitOfWork * 0.1));
if (monitor.isCanceled()) {
return false;
}
Region region = dataModel.getRegionDataModel().getRegion();
String bucketName = dataModel.getBucketDataModel().getBucketName();
AmazonS3 s3 = AwsToolkitCore.getClientFactory().getS3ClientByRegion(region.getId());
TransferManager tm = TransferManagerBuilder.standard()
.withS3Client(s3)
.build();
monitor.subTask("Uploading Lambda function to S3...");
LambdaAnalytics.trackExportedJarSize(jarFile.length());
long startTime = System.currentTimeMillis();
Upload upload = tm.upload(new PutObjectRequest(bucketName, dataModel.getLambdaFunctionJarFileKeyName(), jarFile));
while (!upload.isDone() && !monitor.isCanceled()) {
Thread.sleep(500L); // Sleep for half a second
}
if (upload.isDone()) {
long uploadTime = System.currentTimeMillis() - startTime;
LambdaAnalytics.trackUploadS3BucketTime(uploadTime);
LambdaAnalytics.trackUploadS3BucketSpeed((double) jarFile.length()
/ (double) uploadTime);
monitor.worked((int) (totalUnitOfWork * 0.4));
} else if (monitor.isCanceled()) {
upload.abort(); // Abort the uploading and return
return false;
}
monitor.subTask("Uploading Generated Serverless template to S3...");
String generatedServerlessTemplateKeyName = stackName + "-" + System.currentTimeMillis() + ".template";
s3.putObject(bucketName, generatedServerlessTemplateKeyName, dataModel.getUpdatedServerlessTemplate());
monitor.worked((int)(totalUnitOfWork * 0.1));
if (monitor.isCanceled()) {
return false;
}
AmazonCloudFormation cloudFormation = AwsToolkitCore.getClientFactory().getCloudFormationClientByRegion(region.getId());
monitor.subTask("Creating ChangeSet...");
String changeSetName = stackName + "-changeset-" + System.currentTimeMillis();
ChangeSetType changeSetType = ChangeSetType.CREATE;
StackSummary stackSummary = getCloudFormationStackSummary(cloudFormation, stackName);
Stack stack = stackSummary == null ? null : getCloudFormationStackById(cloudFormation, stackSummary.getStackId());
if (stack == null || STATUSES_FOR_CREATE.contains(stack.getStackStatus())) {
changeSetType = ChangeSetType.CREATE;
} else if (STATUSES_FOR_DELETE.contains(stack.getStackStatus())) {
String stackId = stack.getStackId();
if (stack.getStackStatus().equals(StackStatus.ROLLBACK_IN_PROGRESS.toString())) {
stack = waitStackForRollbackComplete(cloudFormation, stackId);
}
if (stack != null && stack.getStackStatus().equals(StackStatus.ROLLBACK_COMPLETE.toString())) {
cloudFormation.deleteStack(new DeleteStackRequest().withStackName(stackName));
waitStackForDeleteComplete(cloudFormation, stackId);
}
if (stack != null && stack.getStackStatus().equals(StackStatus.DELETE_IN_PROGRESS.toString())) {
waitStackForDeleteComplete(cloudFormation, stackId);
}
changeSetType = ChangeSetType.CREATE;
} else if (STATUSES_FOR_UPDATE.contains(stack.getStackStatus())) {
changeSetType = ChangeSetType.UPDATE;
} else {
String errorMessage = String.format("The stack's current state of %s is invalid for updating", stack.getStackStatus());
LambdaPlugin.getDefault().logError(errorMessage, null);
throw new RuntimeException(errorMessage);
}
cloudFormation.createChangeSet(new CreateChangeSetRequest()
.withTemplateURL(s3.getUrl(bucketName, generatedServerlessTemplateKeyName).toString())
.withChangeSetName(changeSetName).withStackName(stackName)
.withChangeSetType(changeSetType)
.withCapabilities(Capability.CAPABILITY_IAM)
.withParameters(dataModel.getParametersDataModel().getParameters())
.withTags(new Tag().withKey("ApiGateway").withValue("true")));
waitChangeSetCreateComplete(cloudFormation, stackName, changeSetName);
if (monitor.isCanceled()) {
cloudFormation.deleteChangeSet(new DeleteChangeSetRequest().withChangeSetName(changeSetName).withStackName(stackName));
return false;
} else {
monitor.worked((int)(totalUnitOfWork * 0.2));
}
monitor.subTask("Executing ChangeSet...");
cloudFormation.executeChangeSet(new ExecuteChangeSetRequest()
.withChangeSetName(changeSetName).withStackName(stackName));
monitor.worked((int)(totalUnitOfWork * 0.2));
return true;
}
/**
* We use {@link AmazonCloudFormation#listStacks(ListStacksRequest)} instead of {@link AmazonCloudFormation#describeStacks(DescribeStacksRequest)}}
* because describeStacks API doesn't return deleted Stacks.
*/
private StackSummary getCloudFormationStackSummary(AmazonCloudFormation client, String stackName) {
String nextToken = null;
do {
ListStacksResult result = client.listStacks(new ListStacksRequest().withNextToken(nextToken));
nextToken = result.getNextToken();
for (StackSummary summary : result.getStackSummaries()) {
if (summary.getStackName().equals(stackName)) {
return summary;
}
}
} while (nextToken != null);
return null;
}
/**
* Get stack by ID. Note: the parameter must be stack id other than stack name to return the deleted stacks.
*/
private Stack getCloudFormationStackById(AmazonCloudFormation client, String stackId) {
try {
List<Stack> stacks = client.describeStacks(new DescribeStacksRequest().withStackName(stackId)).getStacks();
return stacks.isEmpty() ? null : stacks.get(0);
} catch (AmazonCloudFormationException e) {
// AmazonCloudFormation throws exception if the specified stack doesn't exist.
return null;
}
}
private Stack waitStackForNoLongerInProgress(AmazonCloudFormation client, String stackId) {
try {
Stack currentStack;
do {
Thread.sleep(3000L); // check every 3 seconds
currentStack = getCloudFormationStackById(client, stackId);
} while (currentStack != null && currentStack.getStackStatus().endsWith("IN_PROGRESS"));
return currentStack;
} catch (Exception e) {
throw new RuntimeException("Failed for waiting stack to valid status: " + e.getMessage(), e);
}
}
private Stack waitStackForRollbackComplete(AmazonCloudFormation client, String stackId) {
Stack stack = waitStackForNoLongerInProgress(client, stackId);
// If failed to rollback the stack, throw Runtime Exception.
if (stack != null && !stack.getStackStatus().equals(StackStatus.ROLLBACK_COMPLETE.toString())) {
String errorMessage = String.format("Failed to rollback the stack: ", stack.getStackStatusReason());
LambdaPlugin.getDefault().logError(errorMessage, null);
throw new RuntimeException(errorMessage);
}
return stack;
}
private Stack waitStackForDeleteComplete(AmazonCloudFormation client, String stackId) {
Stack stack = waitStackForNoLongerInProgress(client, stackId);
// If failed to rollback the stack, throw Runtime Exception.
if (stack != null && !stack.getStackStatus().equals(StackStatus.DELETE_COMPLETE.toString())) {
String errorMessage = String.format("Failed to delete the stack: ", stack.getStackStatusReason());
LambdaPlugin.getDefault().logError(errorMessage, null);
throw new RuntimeException(errorMessage);
}
return stack;
}
private void waitChangeSetCreateComplete(AmazonCloudFormation client, String stackName, String changeSetName) {
try {
DescribeChangeSetResult result;
do {
Thread.sleep(1000L);
result = client.describeChangeSet(new DescribeChangeSetRequest()
.withChangeSetName(changeSetName)
.withStackName(stackName));
} while (result.getStatus().equals(ChangeSetStatus.CREATE_IN_PROGRESS.toString())
|| result.getStatus().equals(ChangeSetStatus.CREATE_PENDING.toString()));
if (result.getStatus().equals(ChangeSetStatus.FAILED.toString())) {
String errorMessage = "Failed to create CloudFormation change set: " + result.getStatusReason();
LambdaPlugin.getDefault().logError(errorMessage, null);
throw new RuntimeException(errorMessage, null);
}
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
}
}
@Override
protected void initDataModel() {
ServerlessProjectMetadata metadata = null;
try {
metadata = ProjectMetadataManager.loadServerlessProjectMetadata(dataModel.getProject());
} catch (IOException e) {
LambdaPlugin.getDefault().logError(e.getMessage(), e);
}
dataModel.setMetadata(metadata);
if (metadata != null) {
dataModel.getRegionDataModel().setRegion(RegionUtils.getRegion(metadata.getLastDeploymentRegionId()));
dataModel.getBucketDataModel().setBucketName(metadata.getLastDeploymentBucket());
dataModel.getStackDataModel().setStackName(metadata.getLastDeploymentStack());
}
if (metadata == null || metadata.getLastDeploymentRegionId() == null) {
dataModel.getRegionDataModel().setRegion(
RegionUtils.isServiceSupportedInCurrentRegion(ServiceAbbreviations.CLOUD_FORMATION)
? RegionUtils.getCurrentRegion()
: RegionUtils.getRegion(LambdaPlugin.DEFAULT_REGION));
}
if (StringUtils.isNullOrEmpty(dataModel.getMetadata().getPackagePrefix())) {
dataModel.getMetadata().setPackagePrefix(getPackagePrefix(dataModel.getHandlerClasses()));
}
}
//A hacky way to get the package prefix when it is not cached in the metadata.
private static String getPackagePrefix(Set<String> handlerClasses) {
if (handlerClasses.isEmpty()) {
return null;
}
String sampleClass = handlerClasses.iterator().next();
int index = sampleClass.lastIndexOf(".function.");
return index == -1 ? null : sampleClass.substring(0, index);
}
@Override
protected void beforeExecution() {
saveMetadata();
List<Parameter> params = dataModel.getParametersDataModel().getParameters();
for ( TemplateParameter parameter : dataModel.getParametersDataModel().getTemplateParameters() ) {
String value = (String) dataModel.getParametersDataModel().getParameterValues().get(parameter.getParameterKey());
params.add(new Parameter().withParameterKey(parameter.getParameterKey()).withParameterValue(value == null ? "" : value));
}
}
private void saveMetadata() {
ServerlessProjectMetadata metadata = dataModel.getMetadata();
metadata.setLastDeploymentRegionId(dataModel.getRegionDataModel().getRegion().getId());
metadata.setLastDeploymentBucket(dataModel.getBucketDataModel().getBucketName());
metadata.setLastDeploymentStack(dataModel.getStackDataModel().getStackName());
try {
ProjectMetadataManager.saveServerlessProjectMetadata(dataModel.getProject(), metadata);
} catch (IOException e) {
LambdaPlugin.getDefault().logError(e.getMessage(), e);
}
}
}