Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
frosforever committed Oct 19, 2015
0 parents commit 8214de1
Show file tree
Hide file tree
Showing 13 changed files with 340 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
Sbt plugin to upload app version to elastic beanstalk.
This is very much a work in progress

# Set up

The following need to be set:

```
ebSettings
ebS3BucketName := "bucket-to-store-app-version"
ebAppBundleSource := (stage in Docker).value // Source to bundle up
//Defaults to eu-west-1
ebRegion := "region"
ebDescription := "version description"
//Defaults to application name
ebAppName := "My First Elastic Beanstalk Application"
//Default to application version
ebVersion := "version"
```

# TODO
- Make auto plugin
- Use version for `sbt-git` if active

# Acknowledgements

Project inspired by https://github.com/sqs/sbt-elasticbeanstalk and https://github.com/sbt/sbt-s3
16 changes: 16 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

name := "sbt-eb"

description := "Plugin for elastic beanstalk uploads"

version := "0.1"

organization := "com.frosforever"

sbtPlugin := true

libraryDependencies ++= Seq(
"com.amazonaws" % "aws-java-sdk-s3" % "1.10.5.1",
"com.amazonaws" % "aws-java-sdk-elasticbeanstalk" % "1.10.5.1"
)

1 change: 1 addition & 0 deletions project/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=0.13.8
82 changes: 82 additions & 0 deletions src/main/scala/Aws.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import java.io.File

import com.amazonaws._
import com.amazonaws.auth._
import com.amazonaws.event.{ProgressEvent, ProgressEventType, SyncProgressListener}
import com.amazonaws.regions.Regions
import com.amazonaws.services.elasticbeanstalk.AWSElasticBeanstalkClient
import com.amazonaws.services.elasticbeanstalk.model.{CreateApplicationVersionRequest, DeleteApplicationVersionRequest, S3Location}
import com.amazonaws.services.s3._
import com.amazonaws.services.s3.model.PutObjectRequest

object ApplicationDeployer {
def getEBClient(regionName: String) = {
val credentials = new DefaultAWSCredentialsProviderChain
val client = new AWSElasticBeanstalkClient(credentials, new ClientConfiguration().withProtocol(Protocol.HTTPS))
//Using the Fluent `withRegion` throws ClassCastException: com.amazonaws.services.elasticbeanstalk.AWSElasticBeanstalkClient cannot be cast to scala.runtime.Nothing$
client.configureRegion(Regions.fromName(regionName))
client
}

def deleteEbVersion(client: AWSElasticBeanstalkClient, applicationName: String, versionLabel: String) = {
val request = new DeleteApplicationVersionRequest(applicationName, versionLabel).withDeleteSourceBundle(true)
client.deleteApplicationVersion(request)
}

def createEbVersion(client: AWSElasticBeanstalkClient, applicationName: String, versionLabel: String, description: String, bundleS3Location: S3Location) = {
//TODO: versionLabel between 1 and 100 chars without '/'. Should we modify it or just let it fail?
val request = new CreateApplicationVersionRequest(applicationName, versionLabel).
withSourceBundle(bundleS3Location).
withDescription(description.take(199)) //Description max 200 chars
client.createApplicationVersion(request)
}
}


object BundleUploader {
private def getS3Client = {
val credentials = new DefaultAWSCredentialsProviderChain
new AmazonS3Client(credentials, new ClientConfiguration().withProtocol(Protocol.HTTPS))
}

def uploadBundle(bucket: String, bundle: File): S3Location = {
uploadBundle(bucket, bundle, bundle.getName)
}

def uploadBundle(bucket: String, bundle: File, key: String): S3Location = {
val request = new PutObjectRequest(bucket, key, bundle)
request.setGeneralProgressListener(UploadProgressListener(bundle.length()))
getS3Client.putObject(request)
new S3Location(bucket, key)
}

private def progressBar(percent:Int) = {
val b="=================================================="
val s=" "
val p=percent/2
val z:StringBuilder=new StringBuilder(80)
z.append("\r[")
z.append(b.substring(0,p))
if (p<50) {z.append(">"); z.append(s.substring(p))}
z.append("] ")
if (p<5) z.append(" ")
if (p<50) z.append(" ")
z.append(percent)
z.append("% ")
z.mkString
}

private case class UploadProgressListener(fileSize: Long) extends SyncProgressListener {
var uploadedBytes = 0L

override def progressChanged(progressEvent: ProgressEvent): Unit = {
if (progressEvent.getEventType == ProgressEventType.REQUEST_BYTE_TRANSFER_EVENT ||
progressEvent.getEventType == ProgressEventType.RESPONSE_BYTE_TRANSFER_EVENT) {
uploadedBytes = uploadedBytes + progressEvent.getBytesTransferred
}
print(progressBar(if (fileSize > 0) ((uploadedBytes * 100) / fileSize).toInt else 100))
if (progressEvent.getEventType == ProgressEventType.TRANSFER_COMPLETED_EVENT)
println()
}
}
}
90 changes: 90 additions & 0 deletions src/main/scala/Eb.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import java.io.File

import com.amazonaws.services.elasticbeanstalk.AWSElasticBeanstalkClient
import com.amazonaws.services.elasticbeanstalk.model.{ApplicationVersionDescription, S3Location}
import sbt.{SettingKey, TaskKey}

object EbKeys {
//TODO: Investigate adding an `isSnapshot` key and overwriting existing snapshots if it is to allow repeated uploads. Would require a delete app version task

//User set
val ebS3BucketName = SettingKey[String]("ebS3BucketName", "S3 bucket which should contain uploaded zip files")

//User set. If possible default to `stage in Docker`
//TODO: Support multiple files. That way you get the source as well as the .ebextensions. Or have that as an optional other setting
val ebAppBundleSource = TaskKey[File]("eb-app-bundle-source", "Source to be zipped to a deployable app bundle")

//User set
val ebRegion = SettingKey[String]("ebRegion", "Elastic Beanstalk region (e.g., us-west-1)")

val ebAppBundle = TaskKey[File]("eb-app-bundle", "The application file ('source bundle' in AWS terms) to deploy.")

val ebCreateVersion = TaskKey[ApplicationVersionDescription]("eb-create-version", "Creates a new application version in the configured environment.")

val ebUploadSourceBundle = TaskKey[S3Location]("eb-upload-source-bundle", "Uploads the WAR source bundle to S3")

val ebClient = TaskKey[AWSElasticBeanstalkClient]("eb-client")

val ebStageTarget = SettingKey[File]("ebStageTarget", "location of eb staging target")

val ebVersion = TaskKey[String]("ebVersion", "version label and appBundle name to be used ")

val ebAppName = SettingKey[String]("ebAppName", "Name of the application on elastic beanstalk")

val ebDescription = TaskKey[String]("eb-description", "description of created version")
}

//TODO: AutoPlugin that if native packager is enabled sets the bundleSource. And if git-sbt is enabled, sets the version and description
trait EbSettings { this: EbTasks =>
import EbKeys._
import sbt.Keys.{name, target, version}
import sbt._

lazy val ebSettings = Seq[Setting[_]](
ebCreateVersion <<= ebCreateVersionTask,
ebUploadSourceBundle <<= ebUploadSourceBundleTask,
ebAppBundle <<= ebAppBundleTask,
ebClient <<= ebRegion map { (region) => ApplicationDeployer.getEBClient(region) },
ebVersion := version.value,
ebStageTarget := (target.value / "eb"),
ebRegion := "eu-west-1",
ebAppName := name.value
)
}

trait EbTasks {
import sbt.IO
import sbt.Keys.streams

val ebAppBundleTask = (EbKeys.ebStageTarget, EbKeys.ebAppBundleSource, EbKeys.ebVersion, streams) map {
(staging, docTar, versionLabel, s) =>
val zipFile = new File(staging, s"$versionLabel.zip")
s.log.info(s"Cleaning elastic-beanstalk staging directory $staging")

IO.delete(staging)

s.log.info(s"Writing elastic-beanstalk app bundle to $zipFile")

def entries(f: File):List[File] = f :: (if (f.isDirectory) IO.listFiles(f).toList.flatMap(entries) else Nil)
IO.zip(entries(docTar).collect{case d if d.getAbsolutePath != docTar.getAbsolutePath => (d, d.getAbsolutePath.substring(docTar.getAbsolutePath.length +1))}, zipFile)
zipFile
}

val ebUploadSourceBundleTask = (EbKeys.ebAppBundle, EbKeys.ebS3BucketName, EbKeys.ebAppName, streams) map {
(appBundle, s3BucketName, appName, s) => {
require(appBundle.getName.endsWith("zip"), "App bundle must be a zip archive")

s.log.info("Uploading " + appBundle.getName + " (" + (appBundle.length/1024/1024) + " MB) " +
"to Amazon S3 bucket '" + s3BucketName + "'")
BundleUploader.uploadBundle(s3BucketName, appBundle, s"$appName/${appBundle.getName}")
}
}

val ebCreateVersionTask = (EbKeys.ebClient, EbKeys.ebUploadSourceBundle, EbKeys.ebAppName, EbKeys.ebVersion, EbKeys.ebDescription, streams) map {
(ebClient, ebSourceBundle, appName, versionLabel, description, s) =>

s.log.info(s"Creating application: $appName version: $versionLabel description: $description from s3: $ebSourceBundle")
ApplicationDeployer.createEbVersion(ebClient, appName, versionLabel, description, ebSourceBundle).
getApplicationVersion
}
}
5 changes: 5 additions & 0 deletions src/main/scala/Plugin.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

import sbt.Plugin

//TODO: Make AutoPlugin
object EB extends Plugin with EbSettings with EbTasks
58 changes: 58 additions & 0 deletions test-app/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import com.typesafe.sbt.packager.docker._
import EbKeys._
import EB._


name := "test-app"
version := "2.1"
scalaVersion := "2.11.7"

scalacOptions := Seq("-unchecked", "-deprecation", "-encoding", "utf8")

libraryDependencies ++= {
val akkaStreamV = "1.0"
Seq(
"com.typesafe.akka" %% "akka-http-experimental" % akkaStreamV
)
}

enablePlugins(JavaAppPackaging)

sourceDirectory in Docker := baseDirectory.value / "docker"

dockerBaseImage := "java:8"

maintainer in Docker := "frosforever"

dockerExposedPorts := Seq(9000)

//Manually building dockerFile to have better control and to ensure chmod is called before running
dockerCommands := Seq(
Cmd("FROM", dockerBaseImage.value),
Cmd("MAINTAINER", (maintainer in Docker).value),
Cmd("EXPOSE", dockerExposedPorts.value.mkString(" ")),
Cmd("ADD", {
val files = (defaultLinuxInstallLocation in Docker).value.split(java.io.File.separator)(1)
s"$files /$files"
}),
Cmd("WORKDIR", s"${(defaultLinuxInstallLocation in Docker).value}"),
ExecCmd("RUN", "chown", "-R", (daemonUser in Docker).value, "."),
ExecCmd("RUN", "chmod", "+x",
s"${(defaultLinuxInstallLocation in Docker).value}/bin/${executableScriptName.value}"),
Cmd("USER", (daemonUser in Docker).value),
ExecCmd("ENTRYPOINT", s"bin/${name.value}"),
ExecCmd("CMD")
)

ebSettings

ebS3BucketName := "us-west-1-test-bucket"

ebAppBundleSource := (stage in Docker).value

ebRegion := "us-west-1"

ebDescription := "hello"

ebAppName := "My First Elastic Beanstalk Application"

11 changes: 11 additions & 0 deletions test-app/docker/.ebextensions/01nginx_proxy.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
files:
"/etc/nginx/conf.d/proxy.conf" :
mode: "000755"
owner: root
group: root
content: |
client_max_body_size 80m;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
send_timeout 600;
8 changes: 8 additions & 0 deletions test-app/docker/Dockerrun.aws.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"AWSEBDockerrunVersion": "1",
"Ports": [
{
"ContainerPort": "9000"
}
]
}
1 change: 1 addition & 0 deletions test-app/project/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=0.13.8
9 changes: 9 additions & 0 deletions test-app/project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//addSbtPlugin("io.spray" % "sbt-revolver" % "0.7.2")

//addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.12.0")

addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.0.1")

lazy val root = Project("plugins", file(".")).dependsOn(plugin)

lazy val plugin = file("../").getCanonicalFile.toURI
25 changes: 25 additions & 0 deletions test-app/src/main/scala/App.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.server.Directives._
import akka.stream.{ActorMaterializer, Materializer}
import scala.concurrent.ExecutionContextExecutor

object HelloWorld extends App with Service {
override implicit val system = ActorSystem()
override implicit val executor = system.dispatcher
override implicit val materializer = ActorMaterializer()

Http().bindAndHandle(routes, "0.0.0.0", 9000)
}

trait Service {
implicit val system: ActorSystem
implicit def executor: ExecutionContextExecutor
implicit val materializer: Materializer

val routes = get {
complete {
"hello world from version 2.1!"
}
}
}

0 comments on commit 8214de1

Please sign in to comment.