/*
* 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.ui;
import static com.amazonaws.eclipse.core.ui.wizards.WizardWidgetFactory.newGroup;
import static com.amazonaws.eclipse.lambda.LambdaAnalytics.trackRegionComboChangeSelection;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.FileInputStream;
import java.util.List;
import java.util.Map.Entry;
import org.apache.commons.io.IOUtils;
import org.eclipse.core.databinding.AggregateValidationStatus;
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.observable.ChangeEvent;
import org.eclipse.core.databinding.observable.IChangeListener;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.databinding.observable.value.WritableValue;
import org.eclipse.core.databinding.validation.IValidator;
import org.eclipse.core.databinding.validation.ValidationStatus;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Group;
import com.amazonaws.eclipse.core.AwsToolkitCore;
import com.amazonaws.eclipse.core.regions.Region;
import com.amazonaws.eclipse.core.regions.ServiceAbbreviations;
import com.amazonaws.eclipse.core.ui.CancelableThread;
import com.amazonaws.eclipse.core.ui.RegionComposite;
import com.amazonaws.eclipse.core.ui.SelectOrCreateBucketComposite;
import com.amazonaws.eclipse.core.util.S3BucketUtil;
import com.amazonaws.eclipse.databinding.ChainValidator;
import com.amazonaws.eclipse.lambda.LambdaPlugin;
import com.amazonaws.eclipse.lambda.model.SelectOrInputStackDataModel;
import com.amazonaws.eclipse.lambda.project.wizard.model.DeployServerlessProjectDataModel;
import com.amazonaws.eclipse.lambda.project.wizard.util.FunctionProjectUtil;
import com.amazonaws.eclipse.lambda.serverless.Serverless;
import com.amazonaws.eclipse.lambda.serverless.model.transform.ServerlessFunction;
import com.amazonaws.eclipse.lambda.serverless.model.transform.ServerlessModel;
import com.amazonaws.eclipse.lambda.ui.SelectOrInputStackComposite;
import com.amazonaws.services.cloudformation.AmazonCloudFormation;
import com.amazonaws.services.cloudformation.model.TemplateParameter;
import com.amazonaws.services.cloudformation.model.ValidateTemplateRequest;
public class DeployServerlessProjectPage extends WizardPage {
private static final String VALIDATING = "validating";
private static final String INVALID = "invalid";
private static final String VALID = "valid";
private final DeployServerlessProjectDataModel dataModel;
private final DataBindingContext bindingContext;
private final AggregateValidationStatus aggregateValidationStatus;
private RegionComposite regionComposite;
private SelectOrCreateBucketComposite bucketComposite;
private SelectOrInputStackComposite stackComposite;
private IObservableValue templateValidated = new WritableValue();
private ValidateTemplateThread validateTemplateThread;
private Exception templateValidationException;
public DeployServerlessProjectPage(DeployServerlessProjectDataModel dataModel) {
super("ServerlessDeployWizardPage");
setTitle("Deploy Serverless stack to AWS CloudFormation.");
setDescription("Deploy your Serverless template to AWS CloudFormation as a stack.");
this.dataModel = dataModel;
this.bindingContext = new DataBindingContext();
this.aggregateValidationStatus = new AggregateValidationStatus(
bindingContext, AggregateValidationStatus.MAX_SEVERITY);
}
public void createControl(Composite parent) {
Composite container = new Composite(parent, SWT.NONE);
container.setLayout(new GridLayout(1, false));
createRegionSection(container);
createS3BucketSection(container);
createStackSection(container);
createValidationBinding();
aggregateValidationStatus.addChangeListener(new IChangeListener() {
public void handleChange(ChangeEvent arg0) {
populateHandlerValidationStatus();
}
});
dataModel.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
// We skip this event for inputing a new Stack name. We only listener to changes of Bucket, Existing Stack
if (!evt.getPropertyName().equals(SelectOrInputStackDataModel.P_NEW_RESOURCE_NAME)) {
onDataModelPropertiesChange();
}
}
});
onRegionSelectionChange();
setControl(container);
}
private void populateHandlerValidationStatus() {
if (aggregateValidationStatus == null) {
return;
}
Object value = aggregateValidationStatus.getValue();
if (! (value instanceof IStatus)) return;
IStatus handlerInfoStatus = (IStatus) value;
boolean isHandlerInfoValid = (handlerInfoStatus.getSeverity() == IStatus.OK);
if (isHandlerInfoValid) {
setErrorMessage(null);
super.setPageComplete(true);
} else {
setErrorMessage(handlerInfoStatus.getMessage());
super.setPageComplete(false);
}
}
private void createRegionSection(Composite parent) {
Group regionGroup = newGroup(parent, "Select AWS Region for the AWS CloudFormation stack");
regionGroup.setLayout(new GridLayout(1, false));
regionComposite = RegionComposite.builder()
.parent(regionGroup)
.bindingContext(bindingContext)
.serviceName(ServiceAbbreviations.CLOUD_FORMATION)
.dataModel(dataModel.getRegionDataModel())
.labelValue("Select AWS Region:")
.addListener(new ISelectionChangedListener() {
@Override
public void selectionChanged(SelectionChangedEvent event) {
trackRegionComboChangeSelection();
onRegionSelectionChange();
}
})
.build();
}
private void onRegionSelectionChange() {
Region currentSelectedRegion = regionComposite.getCurrentSelectedRegion();
if (bucketComposite != null) {
String defaultBucket = dataModel.getMetadata().getLastDeploymentBucket(currentSelectedRegion.getId());
bucketComposite.refreshBucketsInRegion(currentSelectedRegion, defaultBucket);
}
if (stackComposite != null) {
String defaultStackName = dataModel.getMetadata().getLastDeploymentStack();
stackComposite.refreshInRegion(currentSelectedRegion, defaultStackName);
}
}
private void createS3BucketSection(Composite parent) {
Group group = newGroup(parent, "Select or Create S3 Bucket for Your Function Code");
group.setLayout(new GridLayout(1, false));
bucketComposite = new SelectOrCreateBucketComposite(
group, bindingContext, dataModel.getBucketDataModel());
}
private void createStackSection(Composite parent) {
Group group = newGroup(parent, "Select or Create CloudFormation Stack");
group.setLayout(new GridLayout(1, false));
stackComposite = new SelectOrInputStackComposite(
group, bindingContext, dataModel.getStackDataModel());
}
private void createValidationBinding() {
templateValidated.setValue(null);
bindingContext.addValidationStatusProvider(new ChainValidator<String>(templateValidated, new IValidator() {
@Override
public IStatus validate(Object value) {
if ( value == null ) {
return ValidationStatus.error("No template selected");
}
if ( ((String) value).equals(VALID) ) {
return ValidationStatus.ok();
} else if ( ((String) value).equals(VALIDATING) ) {
return ValidationStatus.warning("Validating template...");
} else if ( ((String) value).equals(INVALID) ) {
if ( templateValidationException != null ) {
return ValidationStatus.error("Invalid template: " + templateValidationException.getMessage());
} else {
return ValidationStatus.error("No template selected");
}
}
return ValidationStatus.ok();
}
}));
}
private void onDataModelPropertiesChange() {
CancelableThread.cancelThread(validateTemplateThread);
validateTemplateThread = new ValidateTemplateThread();
validateTemplateThread.start();
}
// The IObservable value could only be updated in the UI thread.
private void updateTemplateValidatedStatus(final CancelableThread motherThread, final String newStatus) {
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
synchronized (motherThread) {
if (!motherThread.isCanceled()) {
templateValidated.setValue(newStatus);
}
}
}
});
}
/**
* Cancelable thread to validate a template and update the validation
* status.
*/
private final class ValidateTemplateThread extends CancelableThread {
@Override
public void run() {
try {
updateTemplateValidatedStatus(this, VALIDATING);
// Update serverless template upon provided bucket name
String lambdaFunctionJarFileKeyName = dataModel.getStackDataModel().getStackName() + "-" + System.currentTimeMillis() + ".zip";
File serverlessTemplateFile = FunctionProjectUtil.getServerlessTemplateFile(dataModel.getProject());
ServerlessModel model = Serverless.load(serverlessTemplateFile);
model = Serverless.cookServerlessModel(model, dataModel.getMetadata().getPackagePrefix(),
S3BucketUtil.createS3Path(dataModel.getBucketDataModel().getBucketName(), lambdaFunctionJarFileKeyName));
validateHandlersExist(model);
String generatedServerlessFilePath = File.createTempFile(
"serverless-template", ".json").getAbsolutePath();
File serverlessGeneratedTemplateFile = Serverless.write(model, generatedServerlessFilePath);
serverlessGeneratedTemplateFile.deleteOnExit();
dataModel.setLambdaFunctionJarFileKeyName(lambdaFunctionJarFileKeyName);
dataModel.setUpdatedServerlessTemplate(serverlessGeneratedTemplateFile);
// Validate the updated serverless template
AmazonCloudFormation cloudFormation = AwsToolkitCore.getClientFactory().getCloudFormationClientByRegion(
dataModel.getRegionDataModel().getRegion().getId());
List<TemplateParameter> templateParameters = cloudFormation.validateTemplate(new ValidateTemplateRequest()
.withTemplateBody(IOUtils.toString(new FileInputStream(serverlessGeneratedTemplateFile))))
.getParameters();
dataModel.getParametersDataModel().setTemplateParameters(templateParameters);
updateTemplateValidatedStatus(this, VALID);
} catch (Exception e) {
templateValidationException = e;
updateTemplateValidatedStatus(this, INVALID);
LambdaPlugin.getDefault().logError(e.getMessage(), e);
}
}
// Validate the Lambda handlers defined in the serverless template exist in the project.
private void validateHandlersExist(ServerlessModel model) {
for (Entry<String, ServerlessFunction> handler : model.getServerlessFunctions().entrySet()) {
if (!dataModel.getHandlerClasses().contains(handler.getValue().getHandler())) {
throw new IllegalArgumentException(String.format(
"The configured handler class %s for Lambda handler %s doesn't exist!",
handler.getValue().getHandler(), handler.getKey()));
}
}
}
}
}