From a167541b0a1fe53d50a9a43dbdd808a2e9bd9254 Mon Sep 17 00:00:00 2001 From: Nepomuk Seiler Date: Tue, 10 Feb 2015 17:28:46 +0100 Subject: [PATCH 1/2] Initial refactoring on #453 --- .../sbt/packager/docker/DockerPlugin.scala | 176 ++++++++++++------ .../typesafe/sbt/packager/docker/Keys.scala | 3 + 2 files changed, 125 insertions(+), 54 deletions(-) diff --git a/src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala b/src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala index 77849c7f1..76cc5e0c4 100644 --- a/src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala +++ b/src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala @@ -65,7 +65,28 @@ 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( + makeAdd(dockerBaseDirectory), + makeWorkdir(dockerBaseDirectory), + makeChown(user, group, "."), + makeUser(user), + makeEntrypoint(dockerEntrypoint.value), + makeCmd(dockerCmd.value) + ) ++ + makeExposePorts(dockerExposedPorts.value) ++ + makeVolumes(dockerExposedVolumes.value, user, group) + + } ) ++ mapGenericFilesToDocker ++ inConfig(Docker)(Seq( executableScriptName := executableScriptName.value, @@ -91,79 +112,126 @@ 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 == null || 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, directory: String): CmdLike = + ExecCmd("RUN", "chown", "-R", s"$daemonUser:$daemonGroup", directory) + + /** + * @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 + */ + private final def makeVolumes(exposedVolumes: Seq[String], daemonUser: String, daemonGroup: String): Seq[CmdLike] = { + if (exposedVolumes.isEmpty) Seq() + else Seq( + ExecCmd("RUN", Seq("mkdir", "-p") ++ exposedVolumes: _*), + makeChown(daemonUser, daemonGroup, exposedVolumes mkString " "), + 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 { diff --git a/src/main/scala/com/typesafe/sbt/packager/docker/Keys.scala b/src/main/scala/com/typesafe/sbt/packager/docker/Keys.scala index c8042d921..0f04af174 100644 --- a/src/main/scala/com/typesafe/sbt/packager/docker/Keys.scala +++ b/src/main/scala/com/typesafe/sbt/packager/docker/Keys.scala @@ -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") } From 5f101aced72a744f17e68d0f0a95568fdc77a6ad Mon Sep 17 00:00:00 2001 From: Nepomuk Seiler Date: Wed, 11 Feb 2015 12:26:05 +0100 Subject: [PATCH 2/2] Adding documentation, examples and tests. --- .../sbt/packager/docker/DockerPlugin.scala | 29 +++--- .../docker/override-commands/build.sbt | 15 +++ .../override-commands/project/plugins.sbt | 1 + .../src/main/scala/Main.scala | 3 + src/sbt-test/docker/override-commands/test | 3 + src/sbt-test/docker/volumes/build.sbt | 2 +- src/sbt-test/docker/volumes/test | 1 + src/sphinx/formats/docker.rst | 97 ++++++++++++++++++- test-project-docker/build.sbt | 2 + test-project-docker/src/main/scala/Main.scala | 8 ++ 10 files changed, 145 insertions(+), 16 deletions(-) create mode 100644 src/sbt-test/docker/override-commands/build.sbt create mode 100644 src/sbt-test/docker/override-commands/project/plugins.sbt create mode 100644 src/sbt-test/docker/override-commands/src/main/scala/Main.scala create mode 100644 src/sbt-test/docker/override-commands/test diff --git a/src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala b/src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala index 76cc5e0c4..e50ffc699 100644 --- a/src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala +++ b/src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala @@ -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 == @@ -74,17 +74,18 @@ object DockerPlugin extends AutoPlugin { val generalCommands = makeFrom(dockerBaseImage.value) +: makeMaintainer((maintainer in Docker).value).toSeq - generalCommands ++ + generalCommands ++ Seq( + makeWorkdir(dockerBaseDirectory), + makeAdd(dockerBaseDirectory), + makeChown(user, group, "." :: Nil) + ) ++ + makeExposePorts(dockerExposedPorts.value) ++ + makeVolumes(dockerExposedVolumes.value, user, group) ++ Seq( - makeAdd(dockerBaseDirectory), - makeWorkdir(dockerBaseDirectory), - makeChown(user, group, "."), makeUser(user), makeEntrypoint(dockerEntrypoint.value), makeCmd(dockerCmd.value) - ) ++ - makeExposePorts(dockerExposedPorts.value) ++ - makeVolumes(dockerExposedVolumes.value, user, group) + ) } @@ -129,7 +130,7 @@ object DockerPlugin extends AutoPlugin { * @return MAINTAINER if defined */ private final def makeMaintainer(maintainer: String): Option[CmdLike] = - if (maintainer == null || maintainer.isEmpty) None else Some(Cmd("MAINTAINER", maintainer)) + if (maintainer.isEmpty) None else Some(Cmd("MAINTAINER", maintainer)) /** * @param dockerBaseImage @@ -158,8 +159,8 @@ object DockerPlugin extends AutoPlugin { * @param directory to chown recursively * @return chown command, owning the installation directory with the daemonuser */ - private final def makeChown(daemonUser: String, daemonGroup: String, directory: String): CmdLike = - ExecCmd("RUN", "chown", "-R", s"$daemonUser:$daemonGroup", directory) + private final def makeChown(daemonUser: String, daemonGroup: String, directories: Seq[String]): CmdLike = + ExecCmd("RUN", Seq("chown", "-R", s"$daemonUser:$daemonGroup") ++ directories: _*) /** * @param daemonUser @@ -199,12 +200,14 @@ object DockerPlugin extends AutoPlugin { * * @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() + if (exposedVolumes.isEmpty) Seq.empty else Seq( ExecCmd("RUN", Seq("mkdir", "-p") ++ exposedVolumes: _*), - makeChown(daemonUser, daemonGroup, exposedVolumes mkString " "), + makeChown(daemonUser, daemonGroup, exposedVolumes), ExecCmd("VOLUME", exposedVolumes: _*) ) } diff --git a/src/sbt-test/docker/override-commands/build.sbt b/src/sbt-test/docker/override-commands/build.sbt new file mode 100644 index 000000000..e5884f3f3 --- /dev/null +++ b/src/sbt-test/docker/override-commands/build.sbt @@ -0,0 +1,15 @@ +import com.typesafe.sbt.packager.docker._ + +enablePlugins(JavaAppPackaging) + +name := "docker-commands" + +version := "0.1.0" + +maintainer := "Gary Coady " + +dockerCommands := Seq( + Cmd("FROM", "dockerfile/java:latest"), + Cmd("MAINTAINER", maintainer.value), + ExecCmd("CMD", "echo", "Hello, World from Docker") +) diff --git a/src/sbt-test/docker/override-commands/project/plugins.sbt b/src/sbt-test/docker/override-commands/project/plugins.sbt new file mode 100644 index 000000000..b53de154c --- /dev/null +++ b/src/sbt-test/docker/override-commands/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % sys.props("project.version")) diff --git a/src/sbt-test/docker/override-commands/src/main/scala/Main.scala b/src/sbt-test/docker/override-commands/src/main/scala/Main.scala new file mode 100644 index 000000000..61471c658 --- /dev/null +++ b/src/sbt-test/docker/override-commands/src/main/scala/Main.scala @@ -0,0 +1,3 @@ +object Main extends App { + println("Hello world") +} diff --git a/src/sbt-test/docker/override-commands/test b/src/sbt-test/docker/override-commands/test new file mode 100644 index 000000000..c666e2a58 --- /dev/null +++ b/src/sbt-test/docker/override-commands/test @@ -0,0 +1,3 @@ +# Generate the Docker image locally +> docker:publishLocal +$ exec bash -c 'docker run docker-test:latest | grep -q "Hello world"' diff --git a/src/sbt-test/docker/volumes/build.sbt b/src/sbt-test/docker/volumes/build.sbt index 4ca9e37cd..04c46c910 100644 --- a/src/sbt-test/docker/volumes/build.sbt +++ b/src/sbt-test/docker/volumes/build.sbt @@ -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") diff --git a/src/sbt-test/docker/volumes/test b/src/sbt-test/docker/volumes/test index 108c02a86..302f8845a 100644 --- a/src/sbt-test/docker/volumes/test +++ b/src/sbt-test/docker/volumes/test @@ -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 diff --git a/src/sphinx/formats/docker.rst b/src/sphinx/formats/docker.rst index f2d09e4ec..03e51fe0c 100644 --- a/src/sphinx/formats/docker.rst +++ b/src/sphinx/formats/docker.rst @@ -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 ~~~~~~~~~~~~~~~~~ @@ -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 ~~~~~~~~~~~~~~~~ @@ -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 ), ...) + + + +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") + ) + diff --git a/test-project-docker/build.sbt b/test-project-docker/build.sbt index f812fe56d..218e819c0 100644 --- a/test-project-docker/build.sbt +++ b/test-project-docker/build.sbt @@ -1,3 +1,5 @@ +import com.typesafe.sbt.packager.docker._ + enablePlugins(JavaAppPackaging) name := "docker-test" diff --git a/test-project-docker/src/main/scala/Main.scala b/test-project-docker/src/main/scala/Main.scala index 61471c658..42b023d6c 100644 --- a/test-project-docker/src/main/scala/Main.scala +++ b/test-project-docker/src/main/scala/Main.scala @@ -1,3 +1,11 @@ +import java.nio.file._ +import scala.util._ + object Main extends App { println("Hello world") + + val path = Paths get "/data/test01" + val result = Try(Files createFile path) + + println(result) }