All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.heroku.sdk.deploy.App Maven / Gradle / Ivy

There is a newer version: 3.0.7
Show newest version
package com.heroku.sdk.deploy;

import org.apache.commons.compress.archivers.ArchiveException;
import org.apache.commons.io.FileUtils;
import sun.misc.BASE64Encoder;

import java.io.*;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.*;

public class App {

  private static Map> jdkUrlsByStack = new HashMap>();

  static {
    Map cedarJdkUrlStrings = new HashMap();
    cedarJdkUrlStrings.put("1.6", "https://lang-jvm.s3.amazonaws.com/jdk/cedar/openjdk1.6-latest.tar.gz");
    cedarJdkUrlStrings.put("1.7", "https://lang-jvm.s3.amazonaws.com/jdk/cedar/openjdk1.7-latest.tar.gz");
    cedarJdkUrlStrings.put("1.8", "https://lang-jvm.s3.amazonaws.com/jdk/cedar/openjdk1.8-latest.tar.gz");

    Map cedar14JdkUrlStrings = new HashMap();
    cedar14JdkUrlStrings.put("1.6", "https://lang-jvm.s3.amazonaws.com/jdk/cedar-14/openjdk1.6-latest.tar.gz");
    cedar14JdkUrlStrings.put("1.7", "https://lang-jvm.s3.amazonaws.com/jdk/cedar-14/openjdk1.7-latest.tar.gz");
    cedar14JdkUrlStrings.put("1.8", "https://lang-jvm.s3.amazonaws.com/jdk/cedar-14/openjdk1.8-latest.tar.gz");

    jdkUrlsByStack.put("cedar", cedarJdkUrlStrings);
    jdkUrlsByStack.put("cedar-14", cedar14JdkUrlStrings);
  }

  private String buildPackDesc;

  private String name;

  private File rootDir;

  private File targetDir;

  private String encodedApiKey = null;

  public void logInfo(String message) { /* nothing by default */ }

  public void logDebug(String message) { /* nothing by default */ }

  public void logWarn(String message) { /* nothing by default */ }

  public App(String name) throws IOException {
    this("heroku-deploy", name, new File(System.getProperty("user.dir")), createTempDir());
  }

  public App(String buildPackDesc, String name, File rootDir, File targetDir) {
    this.buildPackDesc = buildPackDesc;
    this.name = name;
    this.rootDir = rootDir;
    this.targetDir = targetDir;

    try {
      FileUtils.forceDelete(getHerokuDir());
    } catch (IOException e) {
      // do nothing
    }

    getHerokuDir().mkdir();
    getAppDir().mkdir();
  }

  protected void deploy(List includedFiles, Map configVars, String jdkVersion, URL jdkUrl, String stack, Map processTypes) throws Exception {
    prepare(includedFiles);

    Map existingConfigVars = getConfigVars();
    logDebug("Heroku existing config variables: " + existingConfigVars.keySet());

    Map newConfigVars = new HashMap();
    newConfigVars.putAll(addConfigVar("PATH", ".jdk/bin:/usr/local/bin:/usr/bin:/bin", existingConfigVars, true));
    for (String key : configVars.keySet()) {
      newConfigVars.putAll(addConfigVar(key, configVars.get(key), existingConfigVars));
    }
    setConfigVars(newConfigVars);

    vendorJdk(jdkVersion, jdkUrl, stack);

    deploySlug(stack, processTypes);
  }

  public void deploy(List includedFiles, Map configVars, String jdkVersion, String stack, Map processTypes) throws Exception {
    deploy(includedFiles, configVars, jdkVersion, null, stack, processTypes);
  }

  public void deploy(List includedFiles, Map configVars, URL jdkUrl, String stack, Map processTypes) throws Exception {
    deploy(includedFiles, configVars, jdkUrl.toString(), jdkUrl, stack, processTypes);
  }

  protected void prepare(List includedFiles) throws Exception {
    logInfo("---> Packaging application...");
    logInfo("     - app: " + name);

    try {
      for (File file : includedFiles) {
        logInfo("     - including: ./" + relativize(file));
        copy(file, new File(getAppDir(), relativize(file)));
      }
      addProfileScript();
    } catch (IOException ioe) {
      throw new Exception("There was an error packaging the application for deployment.", ioe);
    }
  }

  protected void copy(File file, File copyTarget) throws IOException {
    if (SystemSettings.hasNio()) {
      if (file.isDirectory()) {
        Files.walkFileTree(file.toPath(), new CopyFileVisitor(copyTarget.toPath()));
      } else {
        Files.createDirectories(copyTarget.getParentFile().toPath());
        Files.copy(file.toPath(), copyTarget.toPath(), StandardCopyOption.COPY_ATTRIBUTES);
      }
    } else {
      if (file.isDirectory()) {
        FileUtils.copyDirectory(file, new File(getAppDir(), relativize(file)));
      } else {
        FileUtils.copyFile(file, new File(getAppDir(), relativize(file)));
      }
    }
  }

  public Map getConfigVars() throws Exception {
    String urlStr = Slug.BASE_URL + "/apps/" + URLEncoder.encode(name, "UTF-8") + "/config-vars";

    Map headers = new HashMap();
    headers.put("Authorization", getEncodedApiKey());
    headers.put("Accept", "application/vnd.heroku+json; version=3");

    Map m = Curl.get(urlStr, headers);
    Map configVars = new HashMap();
    for (Object key : m.keySet()) {
      Object value = m.get(key);
      if ((key instanceof String) && (value instanceof String)) {
        configVars.put(key.toString(), value.toString());
      } else {
        throw new Exception("Unexpected return type: " + m);
      }
    }
    return configVars;
  }

  protected void setConfigVars(Map configVars) throws IOException, Curl.CurlException {
    if (!configVars.isEmpty()) {
      String urlStr = Slug.BASE_URL + "/apps/" + URLEncoder.encode(name, "UTF-8") + "/config_vars";

      String data = "{";
      boolean first = true;
      for (String key : configVars.keySet()) {
        String value = configVars.get(key);
        if (!first) data += ", ";
        first = false;
        data += "\"" + key + "\"" + ":" + "\"" + sanitizeJson(value) + "\"";
      }
      data +=  "}";

      Map headers = new HashMap();
      headers.put("Authorization", getEncodedApiKey());
      headers.put("Accept", "application/json");

      Curl.put(urlStr, data, headers);
    }
  }

  protected Slug deploySlug(String stack, Map processTypes) throws IOException, Curl.CurlException, ArchiveException, InterruptedException {
    Map allProcessTypes = getProcfile();
    allProcessTypes.putAll(processTypes);
    if (allProcessTypes.isEmpty()) logWarn("No processTypes specified!");

    Slug slug = new Slug(buildPackDesc, name, stack, getEncodedApiKey(), allProcessTypes);
    logDebug("Heroku Slug request: " + slug.getSlugRequest());

    logInfo("---> Creating slug...");

    Map slugResponse = slug.create();
    logDebug("Heroku Slug response: " + slugResponse);
    logDebug("Heroku Blob URL: " + slug.getBlobUrl());
    logDebug("Heroku Slug Id: " + slug.getSlugId());

    File slugFile = Tar.create("slug", "./app", getHerokuDir());
    logInfo("     - file: ./" + relativize(slugFile));
    logInfo("     - size: " + (slugFile.length() / (1024 * 1024)) + "MB");

    logInfo("---> Uploading slug...");
    slug.upload(slugFile);
    logInfo("     - stack: " + slug.getStackName());
    logInfo("     - process types: " + ((Map) slugResponse.get("process_types")).keySet());

    logInfo("---> Releasing...");
    Map releaseResponse = slug.release();
    logDebug("Heroku Release response: " + releaseResponse);
    logInfo("     - version: " + releaseResponse.get("version"));

    return slug;
  }

  protected String getJdkVersion() {
    String defaultJdkVersion = "1.8";
    File sysPropsFile = new File(rootDir, "system.properties");
    if (sysPropsFile.exists()) {
      Properties props = new Properties();
      try {
        props.load(new FileInputStream(sysPropsFile));
        return props.getProperty("java.runtime.version", defaultJdkVersion);
      } catch (IOException e) {
        logDebug(e.getMessage());
      }
    }
    return defaultJdkVersion;
  }

  protected Map getProcfile() {
    Map procTypes = new HashMap();

    File procfile = new File(rootDir, "Procfile");
    if (procfile.exists()) {
      try {
        BufferedReader reader = new BufferedReader(new FileReader(procfile));
        String line = reader.readLine();
        while (line != null) {
          Integer colon = line.indexOf(":");
          String key = line.substring(0, colon);
          String value = line.substring(colon + 1);
          procTypes.put(key.trim(), value.trim());

          line = reader.readLine();
        }
      } catch (Exception e) {
        logDebug(e.getMessage());
      }
    }

    return procTypes;
  }

  private void vendorJdk(String jdkVersion, URL jdkUrl, String stackName) throws IOException, InterruptedException, ArchiveException {
    URL realJdkUrl = jdkUrl;
    if (realJdkUrl == null) {
      String realJdkVersion = jdkVersion == null ? getJdkVersion() : jdkVersion;
      if (jdkUrlsByStack.containsKey(stackName)) {
        Map jdkUrlStrings = jdkUrlsByStack.get(stackName);
        if (jdkUrlStrings.containsKey(realJdkVersion)) {
          realJdkUrl = new URL(jdkUrlStrings.get(realJdkVersion));
        } else {
          throw new IllegalArgumentException("Invalid JDK version: " + realJdkVersion);
        }
      } else {
        throw new IllegalArgumentException("Unsupported Stack: " + stackName);
      }
      logInfo("     - installing: OpenJDK " + realJdkVersion);

      Files.write(
          Paths.get(new File(getAppDir(), "system.properties").getPath()),
          ("java.runtime.version=" + realJdkVersion).getBytes(StandardCharsets.UTF_8)
      );
    } else {
      logInfo("     - installing: Custom JDK");
    }

    File jdkHome = new File(getAppDir(), ".jdk");
    jdkHome.mkdir();

    File jdkTgz = new File(getHerokuDir(), "jdk-pkg.tar.gz");
    FileUtils.copyURLToFile(realJdkUrl, jdkTgz);

    Tar.extract(jdkTgz, jdkHome);
  }

  private void addProfileScript() throws IOException {
    File profiledDir = new File(getAppDir(), ".profile.d");
    profiledDir.mkdir();

    Files.write(
        Paths.get(new File(profiledDir, "jvmcommon.sh").getPath()),
        ("" +
            "limit=$(ulimit -u)\n" +
            "case $limit in\n" +
            "256)   # 1X Dyno\n" +
            "  heap=384\n" +
            ";;\n" +
            "512)   # 2X Dyno\n" +
            "  heap=768\n" +
            ";;\n" +
            "32768) # PX Dyno\n" +
            "  heap=6144\n" +
            ";;\n" +
            "*)\n" +
            "  heap=384\n" +
            ";;\n" +
            "esac\n" +
            "export JAVA_TOOL_OPTIONS=\"-Xmx${heap}m $JAVA_TOOL_OPTIONS -Djava.rmi.server.useCodebaseOnly=true\"\n" +
        "").getBytes(StandardCharsets.UTF_8)
    );
  }

  protected String relativize(File path) {
    return rootDir.toURI().relativize(path.toURI()).getPath();
  }

  protected String getEncodedApiKey() throws IOException {
    if (encodedApiKey == null) {
      String apiKey = System.getenv("HEROKU_API_KEY");
      if (null == apiKey || apiKey.equals("")) {

        ExecutorService executor = Executors.newSingleThreadExecutor();
        FutureTask future =
            new FutureTask(new Callable() {
              public String call() throws IOException {
                String herokuCmd = SystemSettings.isWindows() ? "heroku.bat" : "heroku";
                ProcessBuilder pb = new ProcessBuilder().command(herokuCmd, "auth:token");
                Process p = pb.start();

                BufferedReader bri = new BufferedReader(new InputStreamReader(p.getInputStream()));
                String line;
                String output = "";
                while ((line = bri.readLine()) != null) {
                  output += line;
                }
                return output;
              }});

        executor.execute(future);

        try {
          apiKey = future.get(10, TimeUnit.SECONDS);
        } catch (Exception e) {
          throw new RuntimeException("Could not get API key! Please login with `heroku auth:login` or set the HEROKU_API_KEY environment variable.");
        }
      }
      encodedApiKey = new BASE64Encoder().encode((":" + apiKey).getBytes());
    }
    return encodedApiKey;
  }

  private Map addConfigVar(String key, String value, Map existingConfigVars) {
    return addConfigVar(key, value, existingConfigVars, false);
  }

  private Map addConfigVar(String key, String value, Map existingConfigVars, Boolean force) {
    Map m = new HashMap();
    if (!existingConfigVars.containsKey(key) || (!value.equals(existingConfigVars.get(key)) && force)) {
      m.put(key, value);
    }
    return m;
  }

  protected File getAppDir() {
    return new File(getHerokuDir(), "app");
  }

  protected File getHerokuDir() {
    return new File(targetDir, "heroku");
  }

  protected File getRootDir() {
    return rootDir;
  }

  protected String sanitizeJson(String json) {
    return json.replace("\\", "\\\\").replace("\"", "\\\"");
  }

  private static File createTempDir() throws IOException {
    return Files.createTempDirectory("heroku-deploy").toFile();
  }

  public static class CopyFileVisitor extends SimpleFileVisitor {
    private final Path targetPath;
    private Path sourcePath = null;

    public CopyFileVisitor(Path targetPath) {
      this.targetPath = targetPath;
    }

    @Override
    public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) throws IOException {
      if (dir.equals(targetPath)) {
        return FileVisitResult.SKIP_SUBTREE;
      } else if (sourcePath == null) {
        sourcePath = dir;
      }
      Files.createDirectories(targetPath.resolve(sourcePath.relativize(dir)));
      return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
      Files.copy(file, targetPath.resolve(sourcePath.relativize(file)), StandardCopyOption.COPY_ATTRIBUTES);
      return FileVisitResult.CONTINUE;
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy