Skip to content

Instantly share code, notes, and snippets.

@4lex1v
Created January 19, 2016 16:42
Show Gist options
  • Save 4lex1v/18d9f4b0294d70678d83 to your computer and use it in GitHub Desktop.
Save 4lex1v/18d9f4b0294d70678d83 to your computer and use it in GitHub Desktop.
package io.alterstack.build
import sbt._
import Keys._
import com.typesafe.config.{Config, ConfigFactory}
import com.decodified.scalassh.{
Command => SshCommand,
CommandResult => SshCommandResult,
_
}
import BuildUtilz.ConfigUtilz._
import sbt.Configuration
object Deployment {
/** DEPLOYMENT RESULT */
sealed trait DeployResult {
def fold[X](nd: String => X, d: => X): X = this match {
case NotDeployed(r) => nd(r)
case Deployed => d
}
}
case class NotDeployed(reason: String) extends DeployResult
case object Deployed extends DeployResult
/**
* DEPLOYMENT SCRIPT
* @param before - should be executed before the project would be deployed to the server
* @param after - should be executed after the project would be deployed to the server
*/
case class DeployScripts(before: List[SshCommand], after: List[SshCommand])
object DeployScripts {
def fromConfig(config: Config): DeployScripts = {
def extract(name: String): List[SshCommand] = {
val commands = config getOptStrList name
commands.getOrElse(Nil).map(SshCommand(_))
}
DeployScripts(extract("before"), extract("after"))
}
def runScript(scripts: List[SshCommand])
(config: HostConfig)
(implicit log: Logger)
: Validated[List[Validated[SshCommandResult]]] = {
SSH(config.hostName, config) { client =>
scripts map { script =>
log.info(s"Executing script: ${ script.command }")
client.exec(script) match {
case right @ Right(result) =>
log.success(s"Result: ${
val msg = result.stdOutAsString()
if (msg.isEmpty) "no output"
else msg.split("\n").take(10).mkString("\n") + "\n\t......"
}"); right
case left @ Left(ex) => log.error(s"${script.command} resulted to an $ex"); left
}
}
}
}
}
/**
* DEPLOYMENT CONFIGURATION for current project
*/
case class SshConfig(user: String, host: String, keyfile: File, path: String)
object SshConfig {
def fromConfig(config: Config): SshConfig = {
SshConfig("root", config.getString("host"), file(config.getString("keyfile")), config.getString("path"))
}
implicit def toHostConfig(conf: SshConfig): HostConfig = {
HostConfig(PublicKeyLogin(conf.user, conf.keyfile.getAbsolutePath), conf.host)
}
implicit def toResolver(conf: SshConfig): Resolver = {
val pattern = Patterns("dist/[artifact]/[revision]/[artifact].[ext]")
Resolver.ssh(conf.host, conf.host, conf.path)(pattern).as("root", conf.keyfile)
}
}
case class DeployConfig(scripts: DeployScripts, sshConfig: SshConfig)
object DeploymentKeys {
val Deploy = new Configuration("deploy") describedAs "Configuration for project deployment"
/** Build deploy configuration */
val deployConfigFolder = settingKey[File]("Folder where different deployment configurations are stored")
val deployProjects = settingKey[List[(ProjectRef, File)]]("List of projects, which can be deployed, and associated configuration file")
/** Project deploy configuration */
val deployable = settingKey[Boolean]("Can this project be deployed (has deploy config)")
val deployConfigFile = settingKey[Option[File]]("File with deploy configuration for the current project")
val deployConfig = settingKey[Option[DeployConfig]]("Deploy configuration for the current project")
val deploy = taskKey[DeployResult]("Deploy current project to the production server")
}
object DeploymentModule {
import DeploymentKeys._
import Distribution.DistributionKeys
private val rootDir = baseDirectory in ThisBuild
lazy val buildDeployConfig = Seq(
deployConfigFolder in ThisBuild := rootDir(_ / "configs" / "deploy").value,
deployProjects in ThisBuild := resolveProjectsForDeploy.value,
publishTo := (deployConfig in Deploy).value.map(_.sshConfig)
)
lazy val deploymentSettings = inConfig(Deploy) {
Seq(
deployable := deployProjects.value.contains(thisProjectRef.value),
deployConfigFile := currentProjectConfigurationFile.value,
deployConfig := currentProjectDeployConfiguration.value,
deploy := deployProject.value
)
}
private lazy val currentProjectConfigurationFile = Def.setting {
val currentProject = thisProjectRef.value
val configs = (deployProjects in ThisBuild).value
configs find { _._1 == currentProject } map { _._2 }
}
private lazy val currentProjectDeployConfiguration = Def.setting {
(deployConfigFile in Deploy).value map { configFile =>
val configuration = ConfigFactory.parseFile(configFile).resolve()
val scripts = DeployScripts fromConfig configuration.getConfig("deploy")
val sshConfig = SshConfig fromConfig configuration
new DeployConfig(scripts, sshConfig)
}
}
private lazy val resolveProjectsForDeploy = Def.setting {
val folder = (deployConfigFolder in ThisBuild).value
val allProjects = loadedBuild.value.allProjectRefs.map(_._1)
val configs = for {
file <- folder.listFiles
if file.name.endsWith(".conf")
name = file.name.takeWhile(_ != '.')
project = allProjects find { _.project == name } getOrElse {
sys error s"Project $name doesn't exists"
}
} yield project -> file
configs.toList
}
private lazy val deployProject = Def.task {
implicit val log: Logger = streams.value.log
val project = thisProject.value
def deploy(config: DeployConfig): DeployResult = {
import DeployScripts.runScript
import config._
def error(msg: String): DeployResult = NotDeployed(msg)
log.info(s"Start deploying project: ${ project.id }")
val beforeScriptExecutionResult = {
log.info("Executing Before deployment scripts")
runScript(scripts.before)(sshConfig)
}
val deployResult: DeployResult = {
beforeScriptExecutionResult match {
case Left(ex) => error(ex)
case Right(_) =>
Project.runTask(publish in LocalProject(name.value), state.value) match {
case Some((_, Value(_))) => Deployed
case Some((_, Inc(incomplete))) =>
(incomplete.directCause, incomplete.message) match {
case (Some(ex), _) => error(ex.getMessage)
case (_, Some(msg)) => error(msg)
case failed => error(s"Failed to deploy with $failed")
}
case failed => error(s"Failed to deploy with $failed")
}
}
}
deployResult.fold[DeployResult](NotDeployed, {
log.info("Executing After deployment scripts")
runScript(scripts.after)(sshConfig) match {
case Left(er) => error(er)
case Right(result) =>
result foreach { commandResult =>
if (commandResult.isLeft) {
log.error(s"Failed ssh command: $commandResult")
}
}
Deployed
}
})
}
val ifNoConfig: DeployResult = NotDeployed("No destination specified, please provide configuration in deployment.conf file")
(deployConfig in Deploy).value.fold(ifNoConfig)(deploy)
}
def distribution(artifName: String) = addArtifact(Artifact(s"$artifName-dist", "zip", "zip"), DistributionKeys.pack)
def deploySettings(artifName: String): Seq[Setting[_]] = distribution(artifName) ++ buildDeployConfig ++ deploymentSettings
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment