/*
* Copyright 2010-2012 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.elasticbeanstalk.jobs;
import static com.amazonaws.eclipse.elasticbeanstalk.ElasticBeanstalkPlugin.trace;
import java.io.File;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.jdt.launching.IVMConnector;
import org.eclipse.jdt.launching.JavaRuntime;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.progress.IProgressConstants;
import org.eclipse.wst.server.core.IModule;
import org.eclipse.wst.server.core.IServer;
import org.eclipse.wst.server.ui.internal.LaunchClientJob;
import com.amazonaws.AmazonClientException;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.eclipse.core.AccountInfo;
import com.amazonaws.eclipse.core.AwsToolkitCore;
import com.amazonaws.eclipse.elasticbeanstalk.ConfigurationOptionConstants;
import com.amazonaws.eclipse.elasticbeanstalk.ElasticBeanstalkHttpLaunchable;
import com.amazonaws.eclipse.elasticbeanstalk.ElasticBeanstalkLaunchableAdapter;
import com.amazonaws.eclipse.elasticbeanstalk.ElasticBeanstalkPlugin;
import com.amazonaws.eclipse.elasticbeanstalk.ElasticBeanstalkPublishingUtils;
import com.amazonaws.eclipse.elasticbeanstalk.Environment;
import com.amazonaws.eclipse.elasticbeanstalk.EnvironmentBehavior;
import com.amazonaws.eclipse.elasticbeanstalk.git.AWSGitPushCommand;
import com.amazonaws.eclipse.elasticbeanstalk.util.ElasticBeanstalkClientExtensions;
import com.amazonaws.eclipse.elasticbeanstalk.util.PollForEvent;
import com.amazonaws.eclipse.elasticbeanstalk.util.PollForEvent.Event;
import com.amazonaws.eclipse.elasticbeanstalk.util.PollForEvent.Interval;
import com.amazonaws.services.ec2.model.DescribeInstancesRequest;
import com.amazonaws.services.ec2.model.DescribeInstancesResult;
import com.amazonaws.services.elasticbeanstalk.model.ConfigurationSettingsDescription;
import com.amazonaws.services.elasticbeanstalk.model.EnvironmentDescription;
import com.amazonaws.util.StringUtils;
public class UpdateEnvironmentJob extends Job {
private IPath exportedWar;
private IModule moduleToPublish;
private final Environment environment;
private Job launchClientJob;
private String versionLabel;
private String debugInstanceId;
private final IServer server;
public UpdateEnvironmentJob(Environment environment, IServer server) {
super("Updating AWS Elastic Beanstalk environment: " + environment.getEnvironmentName());
this.environment = environment;
this.server = server;
ImageDescriptor imageDescriptor = AwsToolkitCore.getDefault().getImageRegistry()
.getDescriptor(AwsToolkitCore.IMAGE_AWS_ICON);
setProperty(IProgressConstants.ICON_PROPERTY, imageDescriptor);
setUser(true);
}
public void setModuleToPublish(IModule moduleToPublish, IPath exportedWar) {
this.moduleToPublish = moduleToPublish;
this.exportedWar = exportedWar;
}
public IModule getModuleToPublish() {
return this.moduleToPublish;
}
public boolean needsToDeployNewVersion() {
return (exportedWar != null);
}
/**
* Ensure the launch client job has been canceled so that the internal browser isn't opened with
* the user's application
*/
private void forceCancelLaunchClientJob(EnvironmentBehavior behavior) {
long startTime = System.currentTimeMillis();
while (launchClientJob == null && (System.currentTimeMillis() - startTime) < 1000 * 60) {
try {
Thread.sleep(1000);
} catch (InterruptedException ie) {
}
cancelLaunchClientJob();
}
behavior.updateServerState(IServer.STATE_UNKNOWN);
behavior.updateModuleState(moduleToPublish, IServer.STATE_UNKNOWN, IServer.PUBLISH_STATE_UNKNOWN);
}
// Try to delay the scheduling of the LaunchClientJob
// since use a scheduling rule to lock the server, which
// locks up if we try to save files deployed to that server.
private void cancelLaunchClientJob() {
if (launchClientJob != null) {
return;
}
launchClientJob = findLaunchClientJob();
if (launchClientJob != null) {
launchClientJob.cancel();
}
}
private Job findLaunchClientJob() {
// Try to delay the scheduling of the LaunchClientJob
// since use a scheduling rule to lock the server, which
// locks up if we try to save files deployed to that server.
Job[] jobs = Job.getJobManager().find(null);
for (Job job : jobs) {
if (job instanceof LaunchClientJob) {
if (((LaunchClientJob) job).getServer().getId().equals(environment.getServer().getId())) {
trace("Identified LaunchClientJob: " + job);
return job;
}
}
}
trace("Unable to find LaunchClientJob!");
return null;
}
@Override
protected IStatus run(IProgressMonitor monitor) {
cancelLaunchClientJob();
monitor.beginTask("Publishing to AWS Elastic Beanstalk", IProgressMonitor.UNKNOWN);
EnvironmentBehavior behavior = (EnvironmentBehavior) environment.getServer().loadAdapter(
EnvironmentBehavior.class, null);
if (needsToDeployNewVersion()) {
try {
behavior.updateServerState(IServer.STATE_STARTING);
ElasticBeanstalkPublishingUtils utils = new ElasticBeanstalkPublishingUtils(environment);
boolean doesEnvironmentExist = environment.doesEnvironmentExistInBeanstalk();
if (environment.getIncrementalDeployment()) {
doIncrementalDeployment(utils, doesEnvironmentExist);
} else {
doFullDeployment(monitor, utils);
}
waitForEnvironmentToBecomeAvailable(monitor, utils);
behavior.updateServerState(IServer.STATE_STARTED);
if (moduleToPublish != null) {
behavior.updateModuleState(moduleToPublish, IServer.STATE_STARTED, IServer.PUBLISH_STATE_NONE);
}
if (server.getMode().equals(ILaunchManager.DEBUG_MODE)) {
connectDebugger(monitor);
}
} catch (CoreException e) {
forceCancelLaunchClientJob(behavior);
return e.getStatus();
}
}
updateLaunchableHost(monitor);
IsCnameAvailable.waitForCnameToBeAvailable(environment, monitor);
if (!monitor.isCanceled() && launchClientJob != null
&& ConfigurationOptionConstants.WEB_SERVER.equals(environment.getEnvironmentTier())) {
launchClientJob.schedule();
}
return Status.OK_STATUS;
}
/**
* Update the URL of the client launch job (in a roundabout manner) to point to the correct
* endpoint, depending on whether we intend to connect to the environment CNAME or a particular
* instance
*/
private void updateLaunchableHost(IProgressMonitor monitor) {
ElasticBeanstalkHttpLaunchable launchable = ElasticBeanstalkLaunchableAdapter.getLaunchable(server);
if (launchable != null) {
if (debugInstanceId != null && debugInstanceId.length() > 0) {
try {
launchable.setHost(getEc2InstanceHostname());
} catch (Exception e) {
ElasticBeanstalkPlugin.getDefault().logError("Failed to set hostname", e);
}
} else {
launchable.clearHost();
}
}
}
private void waitForEnvironmentToBecomeAvailable(IProgressMonitor monitor, ElasticBeanstalkPublishingUtils utils)
throws CoreException {
Runnable runnable = new Runnable() {
public void run() {
cancelLaunchClientJob();
}
};
utils.waitForEnvironmentToBecomeAvailable(moduleToPublish, new SubProgressMonitor(monitor, 20), runnable);
}
private void doIncrementalDeployment(ElasticBeanstalkPublishingUtils utils, boolean doesEnvironmentExist)
throws CoreException {
AccountInfo accountInfo = AwsToolkitCore.getDefault().getAccountManager()
.getAccountInfo(environment.getAccountId());
AWSGitPushCommand pushCommand = new AWSGitPushCommand(getPrivateGitRepoLocation(environment),
exportedWar.toFile(), environment, new BasicAWSCredentials(accountInfo.getAccessKey(),
accountInfo.getSecretKey()));
if (doesEnvironmentExist) {
/*
* If the environment already exists, then all we have to do is push through Git and
* it'll automatically create a new application version and kick off a deployment to the
* environment.
*/
pushCommand.execute();
} else {
/*
* If the environment doesn't exist yet, then we need to create the application and push
* a new version with Git, then grab the ID of that new version and call the Beanstalk
* CreateEnvironment API.
*/
utils.createNewApplication(environment.getApplicationName(), environment.getApplicationDescription());
pushCommand.skipEnvironmentDeployment(true);
pushCommand.execute();
try {
versionLabel = utils.getLatestApplicationVersion(environment.getApplicationName());
utils.createNewEnvironment(versionLabel);
} catch (AmazonClientException ace) {
throw new CoreException(new Status(IStatus.ERROR, ElasticBeanstalkPlugin.PLUGIN_ID,
"Unable to create new environment: " + ace.getMessage(), ace));
}
}
}
private void doFullDeployment(IProgressMonitor monitor, ElasticBeanstalkPublishingUtils utils) throws CoreException {
if (versionLabel == null) {
versionLabel = UUID.randomUUID().toString();
}
utils.publishApplicationToElasticBeanstalk(exportedWar, versionLabel, new SubProgressMonitor(monitor, 20));
}
public void setVersionLabel(String versionLabel) {
this.versionLabel = versionLabel;
}
public void setDebugInstanceId(String debugInstanceId) {
this.debugInstanceId = debugInstanceId;
}
private File getPrivateGitRepoLocation(Environment environment) {
String accountId = environment.getAccountId();
String environmentName = environment.getEnvironmentName();
IPath stateLocation = Platform.getStateLocation(ElasticBeanstalkPlugin.getDefault().getBundle());
File gitReposDir = new File(stateLocation.toFile(), "git");
return new File(gitReposDir, accountId + "-" + environmentName);
}
/**
* Opens up a remote debugger connection based on the specified launch, host, and port and
* optionally reports progress through a specified progress monitor.
*
* @param monitor
* An optional progress monitor if progress reporting is desired.
* @throws CoreException
* If any problems were encountered setting up the remote debugger connection to the
* specified host.
*/
private void connectDebugger(IProgressMonitor monitor) throws CoreException {
ILaunch launch = findLaunch();
if (launch == null) {
return;
}
try {
List<ConfigurationSettingsDescription> settings = environment.getCurrentSettings();
String debugPort = Environment.getDebugPort(settings);
if (!confirmSecurityGroupIngress(debugPort, settings)) {
return;
}
IVMConnector debuggerConnector = JavaRuntime.getDefaultVMConnector();
Map<String, String> arguments = new HashMap<String, String>();
arguments.put("timeout", "60000");
arguments.put("hostname", getEc2InstanceHostname());
arguments.put("port", debugPort);
debuggerConnector.connect(arguments, monitor, launch);
} catch (Exception e) {
ElasticBeanstalkPlugin.getDefault().logError("Unable to connect debugger: " + e.getMessage(), e);
}
}
/**
* Confirms that the security group of the environment allows ingress on the debug port given,
* prompting the user for permission to open it if not.
*/
private boolean confirmSecurityGroupIngress(String debugPort, List<ConfigurationSettingsDescription> settings) {
int debugPortInt = Integer.parseInt(debugPort);
String securityGroup = Environment.getSecurityGroup(settings);
if (environment.isIngressAllowed(debugPortInt, settings)) {
return true;
}
// Prompt the user for security group ingress -- this is an edge case to
// cover races only. In almost all cases, the user should have been
// prompted for this information much earlier.
final DebugPortDialog dialog = new DebugPortDialog(securityGroup, debugPort);
Display.getDefault().syncExec(new Runnable() {
public void run() {
dialog.openDialog();
}
});
if (dialog.result == 0) {
environment.openSecurityGroupPort(debugPortInt, securityGroup);
return true;
} else {
return false;
}
}
/**
* Simple dialog to confirm the opening of a port on a security group.
*/
private static class DebugPortDialog {
private int result;
private final String debugPort;
private final String securityGroup;
public DebugPortDialog(String securityGroup, String debugPort) {
super();
this.securityGroup = securityGroup;
this.debugPort = debugPort;
}
private void openDialog() {
MessageDialog dialog = new MessageDialog(Display.getDefault().getActiveShell(),
"Authorize security group ingress?", AwsToolkitCore.getDefault().getImageRegistry()
.get(AwsToolkitCore.IMAGE_AWS_ICON),
"To connect the remote debugger, you will need to allow TCP ingress on port " + debugPort
+ " for your EC2 security group " + securityGroup + ". Continue?", MessageDialog.WARNING,
new String[] { "Continue", "Abort" }, 0);
result = dialog.open();
}
}
/**
* Returns the public dns name of the instance in the environment to connect the remote debugger
* to.
*/
private String getEc2InstanceHostname() {
String instanceId = debugInstanceId;
// For some launches, we won't know the EC2 instance ID until this point.
if (instanceId == null || instanceId.length() == 0) {
instanceId = environment.getEC2InstanceIds().iterator().next();
}
DescribeInstancesResult describeInstances = environment.getEc2Client().describeInstances(
new DescribeInstancesRequest().withInstanceIds(instanceId));
if (describeInstances.getReservations().isEmpty()
|| describeInstances.getReservations().get(0).getInstances().isEmpty()) {
return null;
}
return describeInstances.getReservations().get(0).getInstances().get(0).getPublicDnsName();
}
/**
* Returns the debug launch object corresponding to this update operation, or null if no such
* launch exists.
*/
private ILaunch findLaunch() throws CoreException {
ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager();
for (ILaunch launch : manager.getLaunches()) {
// TODO: figure out a more correct way of doing this
if (launch.getLaunchMode().equals(ILaunchManager.DEBUG_MODE)
&& launch.getLaunchConfiguration() != null
&& launch.getLaunchConfiguration().getAttribute("launchable-adapter-id", "")
.equals("com.amazonaws.eclipse.wtp.elasticbeanstalk.launchableAdapter")
&& launch.getLaunchConfiguration().getAttribute("module-artifact", "")
.contains(moduleToPublish.getName())) {
return launch;
}
}
return null;
}
private static class IsCnameAvailable implements Event {
private static final int MAX_NUMBER_OF_INTERVALS_TO_POLL = 10;
private static final Interval DEFAULT_POLLING_INTERVAL = new Interval(10, TimeUnit.SECONDS);
private final String cname;
public IsCnameAvailable(String cname) {
this.cname = cname;
}
public boolean hasEventOccurred() {
try {
InetAddress address = InetAddress.getByName(cname);
return !(address == null || StringUtils.isNullOrEmpty(address.getHostAddress()));
} catch (UnknownHostException e) {
return false;
}
}
public static void waitForCnameToBeAvailable(Environment environment, IProgressMonitor monitor) {
ElasticBeanstalkClientExtensions clientExt = new ElasticBeanstalkClientExtensions(environment.getClient());
EnvironmentDescription environmentDesc = clientExt.getEnvironmentDescription(environment
.getEnvironmentName());
IProgressMonitor cnameMonitor = startCnameMonitor(monitor);
new PollForEvent(getPollInterval(), MAX_NUMBER_OF_INTERVALS_TO_POLL).poll(new IsCnameAvailable(
environmentDesc.getCNAME()));
cnameMonitor.done();
}
private static IProgressMonitor startCnameMonitor(IProgressMonitor monitor) {
final String taskName = "Waiting for environment's domain name to become available";
final IProgressMonitor cnameMonitor = new SubProgressMonitor(monitor, 1);
cnameMonitor.beginTask(taskName, 1);
cnameMonitor.setTaskName(taskName);
return cnameMonitor;
}
/**
* Try and use networkaddress.cache.negative.ttl if it's set, otherwise return default
* interval
*/
private static Interval getPollInterval() {
try {
final int dnsNegativeTtl = Integer.valueOf(System.getProperty("networkaddress.cache.negative.ttl"));
if (dnsNegativeTtl > 0) {
return new Interval(dnsNegativeTtl, TimeUnit.SECONDS);
} else {
return DEFAULT_POLLING_INTERVAL;
}
} catch (Exception e) {
return DEFAULT_POLLING_INTERVAL;
}
}
}
}