/*
* Copyright 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.git;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.apache.commons.io.IOUtils;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.PushCommand;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryBuilder;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.util.FileUtils;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.eclipse.core.regions.Region;
import com.amazonaws.eclipse.core.regions.RegionUtils;
import com.amazonaws.eclipse.elasticbeanstalk.ElasticBeanstalkPlugin;
import com.amazonaws.eclipse.elasticbeanstalk.Environment;
public class AWSGitPushCommand {
private final File repoLocation;
private final File archiveFile;
private final Environment environment;
private boolean skipEnvironmentDeployment;
private final String accessKey;
private final String secretKey;
public AWSGitPushCommand(File repoLocation, File archiveFile, Environment environment, AWSCredentials credentials) {
this.repoLocation = repoLocation;
this.archiveFile = archiveFile;
this.environment = environment;
this.accessKey = credentials.getAWSAccessKeyId();
this.secretKey = credentials.getAWSSecretKey();
}
private static final Logger log = Logger.getLogger(AWSGitPushCommand.class.getCanonicalName());
public void execute() throws CoreException {
Iterable<PushResult> pushResults = null;
try {
Repository repository = initializeRepository();
// Add the new files
clearOutRepository(repository);
extractZipFile(archiveFile, repoLocation);
commitChanges(repository, "Incremental Deployment: " + new Date().toString());
// Push to AWS
String remoteUrl = getRemoteUrl();
if (log.isLoggable(Level.FINE)) log.fine("Pushing to: " + remoteUrl);
PushCommand pushCommand = new Git(repository).push().setRemote(remoteUrl).setForce(true).add("master");
// TODO: we could use a ProgressMonitor here for reporting status back to the UI
pushResults = pushCommand.call();
} catch (Throwable t) {
throwCoreException(null, t);
}
for (PushResult pushResult : pushResults) {
String messages = pushResult.getMessages();
if (messages != null && messages.trim().length() > 0) {
throwCoreException(messages, null);
}
}
}
/**
* Use this method to configure this request to only create a new
* application version, and not automatically deploy it to the specified
* environment.
*
* @param skipEnvironmentDeployment
* True if this Git Push should only create a new application
* version, and not automatically deploy it to the specified
* environment.
*/
public void skipEnvironmentDeployment(boolean skipEnvironmentDeployment) {
this.skipEnvironmentDeployment = skipEnvironmentDeployment;
}
private void throwCoreException(String customMessage, Throwable t) throws CoreException {
if (customMessage != null) customMessage = ": " + customMessage;
else customMessage = "";
String errorMessage = "Unable to update environment with an incremental deployment" + customMessage
+ "\nIf you continue having problems with incremental deployments, try turning off incremental deployments in the server configuration editor.";
throw new CoreException(new Status(IStatus.ERROR, ElasticBeanstalkPlugin.PLUGIN_ID, errorMessage, t));
}
private String getRemoteUrl() throws Exception {
AWSElasticBeanstalkGitPushRequest request = new AWSElasticBeanstalkGitPushRequest();
request.setHost(getGitPushHost());
Region region = RegionUtils.getRegionByEndpoint(environment.getRegionEndpoint());
request.setRegion(region.getId());
request.setApplication(environment.getApplicationName());
if (!skipEnvironmentDeployment) {
request.setEnvironment(environment.getEnvironmentName());
}
AWSGitPushAuth auth = new AWSGitPushAuth(request);
URI uri = auth.deriveRemote(accessKey, secretKey);
return uri.toString();
}
private String getGitPushHost() {
// TODO: it would be better to get this from the endpoints file at some point
String regionEndpoint = environment.getRegionEndpoint();
if (regionEndpoint.startsWith("https://")) {
regionEndpoint = "git." + regionEndpoint.substring("https://".length());
} else if (regionEndpoint.startsWith("http://")) {
regionEndpoint = "git." + regionEndpoint.substring("http://".length());
} else {
regionEndpoint = "git." + regionEndpoint;
}
if (regionEndpoint.endsWith("/")) {
regionEndpoint = regionEndpoint.substring(0, regionEndpoint.length() - 1);
}
return regionEndpoint;
}
private void clearOutRepository(Repository repository) throws IOException {
// Delete all the old files before copying the new files
final File gitMetadataDirectory = repository.getIndexFile().getParentFile();
FileFilter fileFilter = new FileFilter() {
public boolean accept(final File file) {
if (file.equals(gitMetadataDirectory)) return false;
if (!file.getParentFile().equals(repoLocation)) return false;
return true;
}
};
for (File f : repoLocation.listFiles(fileFilter)) {
FileUtils.delete(f, FileUtils.RECURSIVE);
}
}
private Repository initializeRepository() throws IOException {
if (repoLocation == null) {
throw new RuntimeException("No repository location specified");
}
if ((!repoLocation.exists() && !repoLocation.mkdirs()) ||
!repoLocation.isDirectory()) {
throw new RuntimeException("Unable to initialize Git repository from location: " + repoLocation);
}
Repository repository = new RepositoryBuilder().setWorkTree(repoLocation).build();
if (!repository.getObjectDatabase().exists()) {
repository.create();
}
return repository;
}
// TODO: We could use a better util for this
private void extractZipFile(File zipFile, File destination) throws IOException {
int BUFFER = 2048;
FileInputStream fis = new FileInputStream(zipFile);
ZipInputStream zis = new ZipInputStream(new BufferedInputStream(fis));
BufferedOutputStream dest = null;
try {
ZipEntry entry;
while((entry = zis.getNextEntry()) != null) {
int count;
byte data[] = new byte[BUFFER];
// write the files to the disk
if (entry.isDirectory()) continue;
File entryFile = new File(destination, entry.getName());
if (!entryFile.getParentFile().exists()) {
entryFile.getParentFile().mkdirs();
}
FileOutputStream fos = new FileOutputStream(entryFile);
dest = new BufferedOutputStream(fos, BUFFER);
while ((count = zis.read(data, 0, BUFFER)) != -1) {
dest.write(data, 0, count);
}
dest.flush();
}
} finally {
IOUtils.closeQuietly(fis);
IOUtils.closeQuietly(zis);
IOUtils.closeQuietly(dest);
}
}
private void commitChanges(Repository repository, String message) throws GitAPIException, IOException {
Git git = new Git(repository);
// Add once (without the update flag) to add new and modified files
git.add().addFilepattern(".").call();
// Then again with the update flag to add deleted and modified files
git.add().addFilepattern(".").setUpdate(true).call();
git.commit().setMessage(message).call();
}
}