/* * 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); } } }