Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wip/docker refactoring #486

Merged
merged 2 commits into from
Feb 13, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 126 additions & 55 deletions src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import sbt.Keys.{
import packager.Keys._
import linux.LinuxPlugin.autoImport.{ daemonUser, defaultLinuxInstallLocation }
import universal.UniversalPlugin.autoImport.stage
import SbtNativePackager.Universal
import SbtNativePackager.{ Universal, Linux }

/**
* == Docker Plugin ==
Expand Down Expand Up @@ -65,7 +65,29 @@ object DockerPlugin extends AutoPlugin {
dockerExposedVolumes := Seq(),
dockerRepository := None,
dockerUpdateLatest := false,
dockerEntrypoint := Seq("bin/%s" format executableScriptName.value)
dockerEntrypoint := Seq("bin/%s" format executableScriptName.value),
dockerCmd := Seq(),
dockerCommands := {
val dockerBaseDirectory = (defaultLinuxInstallLocation in Docker).value
val user = (daemonUser in Docker).value
val group = (daemonGroup in Docker).value

val generalCommands = makeFrom(dockerBaseImage.value) +: makeMaintainer((maintainer in Docker).value).toSeq

generalCommands ++ Seq(
makeWorkdir(dockerBaseDirectory),
makeAdd(dockerBaseDirectory),
makeChown(user, group, "." :: Nil)
) ++
makeExposePorts(dockerExposedPorts.value) ++
makeVolumes(dockerExposedVolumes.value, user, group) ++
Seq(
makeUser(user),
makeEntrypoint(dockerEntrypoint.value),
makeCmd(dockerCmd.value)
)

}

) ++ mapGenericFilesToDocker ++ inConfig(Docker)(Seq(
executableScriptName := executableScriptName.value,
Expand All @@ -91,79 +113,128 @@ object DockerPlugin extends AutoPlugin {
target := target.value / "docker",

daemonUser := "daemon",
daemonGroup := daemonUser.value,
defaultLinuxInstallLocation := "/opt/docker",

dockerPackageMappings <<= sourceDirectory map { dir =>
MappingsHelper contentOf dir
},
dockerGenerateConfig <<= (dockerBaseImage, defaultLinuxInstallLocation,
maintainer, daemonUser, executableScriptName,
dockerExposedPorts, dockerExposedVolumes, target, dockerEntrypoint) map generateDockerConfig,
dockerGenerateConfig <<= (dockerCommands, target) map generateDockerConfig,
dockerTarget <<= (dockerRepository, packageName, version) map {
(repo, name, version) =>
repo.map(_ + "/").getOrElse("") + name + ":" + version
(repo, name, version) => repo.map(_ + "/").getOrElse("") + name + ":" + version
}
))

private[this] final def makeDockerContent(dockerBaseImage: String, dockerBaseDirectory: String, maintainer: String, daemonUser: String, execScript: String, exposedPorts: Seq[Int], exposedVolumes: Seq[String], entrypoint: Seq[String]) = {
val fromCommand = Cmd("FROM", dockerBaseImage)

val maintainerCommand: Option[Cmd] = {
if (maintainer.isEmpty)
None
else
Some(Cmd("MAINTAINER", maintainer))
}

/**
* @param maintainer (optional)
* @return MAINTAINER if defined
*/
private final def makeMaintainer(maintainer: String): Option[CmdLike] =
if (maintainer.isEmpty) None else Some(Cmd("MAINTAINER", maintainer))

/**
* @param dockerBaseImage
* @return FROM command
*/
private final def makeFrom(dockerBaseImage: String): CmdLike = Cmd("FROM", dockerBaseImage)

/**
* @param dockerBaseDirectory, the installation directory
* @param WORKDIR command, setting dockerBaseDirectory as cwd
*/
private final def makeWorkdir(dockerBaseDirectory: String): CmdLike = Cmd("WORKDIR", dockerBaseDirectory)

/**
* @param dockerBaseDirectory, the installation directory
* @return ADD command adding all files inside the installation directory
*/
private final def makeAdd(dockerBaseDirectory: String): CmdLike = {
val files = dockerBaseDirectory.split(java.io.File.separator)(1)
Cmd("ADD", s"$files /$files")
}

val dockerCommands = Seq(
Cmd("ADD", s"$files /$files"),
Cmd("WORKDIR", "%s" format dockerBaseDirectory),
ExecCmd("RUN", "chown", "-R", daemonUser, "."),
Cmd("USER", daemonUser),
ExecCmd("ENTRYPOINT", entrypoint: _*),
ExecCmd("CMD")
)

val exposeCommand: Option[CmdLike] = {
if (exposedPorts.isEmpty)
None
else
Some(Cmd("EXPOSE", exposedPorts.mkString(" ")))
}

// If the exposed volume does not exist, the volume is made available
// with root ownership. This may be too strict for some directories,
// and we lose the feature that all directories below the install path
// can be written to by the binary. Therefore the directories are
// created before the ownership is changed.
val volumeCommands: Seq[CmdLike] = {
if (exposedVolumes.isEmpty)
Seq()
else
Seq(
ExecCmd("RUN", Seq("mkdir", "-p") ++ exposedVolumes: _*),
ExecCmd("VOLUME", exposedVolumes: _*)
)
}

val commands =
Seq(fromCommand) ++ maintainerCommand ++ volumeCommands ++ exposeCommand ++ dockerCommands
/**
* @param daemonUser
* @param daemonGroup
* @param directory to chown recursively
* @return chown command, owning the installation directory with the daemonuser
*/
private final def makeChown(daemonUser: String, daemonGroup: String, directories: Seq[String]): CmdLike =
ExecCmd("RUN", Seq("chown", "-R", s"$daemonUser:$daemonGroup") ++ directories: _*)

/**
* @param daemonUser
* @return USER docker command
*/
private final def makeUser(daemonUser: String): CmdLike = Cmd("USER", daemonUser)

/**
* @param entrypoint
* @return ENTRYPOINT command
*/
private final def makeEntrypoint(entrypoint: Seq[String]): CmdLike = ExecCmd("ENTRYPOINT", entrypoint: _*)

/**
* Default CMD implementation as default parameters to ENTRYPOINT.
* @param args
* @return CMD with args in exec form
*/
private final def makeCmd(args: Seq[String]): CmdLike = ExecCmd("CMD", args: _*)

/**
* @param exposedPorts
* @return if ports are exposed the EXPOSE command
*/
private final def makeExposePorts(exposedPorts: Seq[Int]): Option[CmdLike] = {
if (exposedPorts.isEmpty) None else Some(Cmd("EXPOSE", exposedPorts mkString " "))
}

Dockerfile(commands: _*).makeContent
/**
* If the exposed volume does not exist, the volume is made available
* with root ownership. This may be too strict for some directories,
* and we lose the feature that all directories below the install path
* can be written to by the binary. Therefore the directories are
* created before the ownership is changed.
*
* All directories created afterwards are chowned.
*
* @param exposedVolumes
* @return commands to create, chown and declare volumes
* @see http://stackoverflow.com/questions/23544282/what-is-the-best-way-to-manage-permissions-for-docker-shared-volumes
* @see https://docs.docker.com/userguide/dockervolumes/
*/
private final def makeVolumes(exposedVolumes: Seq[String], daemonUser: String, daemonGroup: String): Seq[CmdLike] = {
if (exposedVolumes.isEmpty) Seq.empty
else Seq(
ExecCmd("RUN", Seq("mkdir", "-p") ++ exposedVolumes: _*),
makeChown(daemonUser, daemonGroup, exposedVolumes),
ExecCmd("VOLUME", exposedVolumes: _*)
)
}

private[this] final def generateDockerConfig(
dockerBaseImage: String, dockerBaseDirectory: String, maintainer: String, daemonUser: String, execScript: String, exposedPorts: Seq[Int], exposedVolumes: Seq[String], target: File, entrypoint: Seq[String]
) = {
val dockerContent = makeDockerContent(dockerBaseImage, dockerBaseDirectory, maintainer, daemonUser, execScript, exposedPorts, exposedVolumes, entrypoint)
/**
* @param commands representing the Dockerfile
* @return String representation of the Dockerfile described by commands
*/
private final def makeDockerContent(commands: Seq[CmdLike]): String = Dockerfile(commands: _*).makeContent

/**
* @param commands, docker content
* @param target directory for Dockerfile
* @return Dockerfile
*/
private[this] final def generateDockerConfig(commands: Seq[CmdLike], target: File): File = {
val dockerContent = makeDockerContent(commands)

val f = target / "Dockerfile"
IO.write(f, dockerContent)
f
}

/**
* uses the `mappings in Unversial` to generate the
* `mappings in Docker`.
*/
def mapGenericFilesToDocker: Seq[Setting[_]] = {
def renameDests(from: Seq[(File, String)], dest: String) = {
for {
Expand Down
3 changes: 3 additions & 0 deletions src/main/scala/com/typesafe/sbt/packager/docker/Keys.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,8 @@ trait DockerKeys {
val dockerRepository = SettingKey[Option[String]]("dockerRepository", "Repository for published Docker image")
val dockerUpdateLatest = SettingKey[Boolean]("dockerUpdateLatest", "Set to update latest tag")
val dockerEntrypoint = SettingKey[Seq[String]]("dockerEntrypoint", "Entrypoint arguments passed in exec form")
val dockerCmd = SettingKey[Seq[String]]("dockerCmd", "Docker CMD. Used together with dockerEntrypoint. Arguments passed in exec form")

val dockerCommands = TaskKey[Seq[CmdLike]]("dockerCommands", "List of docker commands that form the Dockerfile")
}

15 changes: 15 additions & 0 deletions src/sbt-test/docker/override-commands/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import com.typesafe.sbt.packager.docker._

enablePlugins(JavaAppPackaging)

name := "docker-commands"

version := "0.1.0"

maintainer := "Gary Coady <gary@lyranthe.org>"

dockerCommands := Seq(
Cmd("FROM", "dockerfile/java:latest"),
Cmd("MAINTAINER", maintainer.value),
ExecCmd("CMD", "echo", "Hello, World from Docker")
)
1 change: 1 addition & 0 deletions src/sbt-test/docker/override-commands/project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % sys.props("project.version"))
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
object Main extends App {
println("Hello world")
}
3 changes: 3 additions & 0 deletions src/sbt-test/docker/override-commands/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Generate the Docker image locally
> docker:publishLocal
$ exec bash -c 'docker run docker-test:latest | grep -q "Hello world"'
2 changes: 1 addition & 1 deletion src/sbt-test/docker/volumes/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ name := "simple-test"

version := "0.1.0"

dockerExposedVolumes in Docker := Seq("/opt/docker/logs", "/opt/docker/config")
dockerExposedVolumes := Seq("/opt/docker/logs", "/opt/docker/config")
1 change: 1 addition & 0 deletions src/sbt-test/docker/volumes/test
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Stage the distribution and ensure files show up.
> docker:stage
$ exec grep -q -F 'VOLUME ["/opt/docker/logs", "/opt/docker/config"]' target/docker/Dockerfile
$ exec grep -q -F 'RUN ["chown", "-R", "daemon:daemon", "/opt/docker/logs", "/opt/docker/config"]' target/docker/Dockerfile
$ exec grep -q -F 'RUN ["mkdir", "-p", "/opt/docker/logs", "/opt/docker/config"]' target/docker/Dockerfile
97 changes: 95 additions & 2 deletions src/sphinx/formats/docker.rst
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ The Docker support provides the following commands:
Customize
---------

There are some predefined settings, which you can easily customize. These
settings are explained in some detail in the next sections. If you want to
describe your Dockerfile completely yourself, you can provide your own
`docker commands` as described in `Custom Dockerfile`_.

Docker Image Name
~~~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -149,9 +154,13 @@ Docker Image Customization

.. code-block:: scala

dockerExposedPorts in Docker := Seq(9000, 9443)
dockerExposedPorts := Seq(9000, 9443)

dockerExposedVolumes := Seq("/opt/docker/logs")


dockerExposedVolumes in Docker := Seq("/opt/docker/logs")
In order to work properly with `USER daemon` the exposed volumes first
created (if not existend) and chowned.

Install Location
~~~~~~~~~~~~~~~~
Expand All @@ -161,3 +170,87 @@ The files from ``mappings in Docker`` are extracted underneath this directory.
.. code-block:: scala

defaultLinuxInstallLocation in Docker := "/opt/docker"

Custom Dockerfile
~~~~~~~~~~~~~~~~~

All settings before are used to create a single sequence of docker commands.
You have the option to write all of them on your own, filter or change existing
commands or simply add some.

First of all you should take a look what you docker commands look like.
In your sbt console type

.. code-block:: bash

> show dockerCommands
[info] List(Cmd(FROM,dockerfile/java:latest), Cmd(MAINTAINER,Your Name <y.n@yourcompany.com>), ...)



Remove Commands
===============

SBT Native Packager added some commands you may not need. For example
the chowning of a exposed volume.

.. code-block:: scala

import com.typesafe.sbt.packager.docker._

// we want to filter the chown command for '/data'
dockerExposedVolumes += "/data"

dockerCommands := dockerCommands.value.filterNot {

// ExecCmd is a case class, and args is a varargs variable, so you need to bind it with @
case ExecCmd("RUN", args @ _*) => args.contains("chown") && args.contains("/data")

// dont filter the rest
case cmd => false
}


Add Commands
============

Adding commands is as straigtforward as adding anything in a list.

.. code-block:: scala

import com.typesafe.sbt.packager.docker._

dockerCommands += Cmd("USER", daemonUser.value)

dockerCommands ++= Seq(
// setting the run script executable
ExecCmd("RUN",
"chmod", "u+x",
s"${(defaultLinuxInstallLocation in Docker).value}/bin/${executableScriptName.value}"),
// setting a daemon user
Cmd("USER", "daemon")
)


Write from Scratch
==================

You can simply wipe out all docker commands with

.. code-block:: scala

dockerCommands := Seq()


Now let's start adding some Docker commands.

.. code-block:: scala

import com.typesafe.sbt.packager.docker._

dockerCommands := Seq(
Cmd("FROM", "dockerfile/java:latest"),
Cmd("MAINTAINER", maintainer.value),
ExecCmd("CMD", "echo", "Hello, World from Docker")
)

2 changes: 2 additions & 0 deletions test-project-docker/build.sbt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import com.typesafe.sbt.packager.docker._

enablePlugins(JavaAppPackaging)

name := "docker-test"
Expand Down
Loading