Skip to content

Commit

Permalink
Add More Config Options to Docker Contrib Module (#1456)
Browse files Browse the repository at this point in the history
* Add more dockerfile options to the docker contrib module

Add exposedPorts, exposedUdpPorts, volumes, envVars and user as
overridable methods to DockerConfig trait.

* Add tests for docker module

Add tests for a docker config with defaults (no options) and
all options specified by building them (though only on
machines with docker installed).  Add tests which check the
contents of the generated dockerfiles as a fallback.

* Update docker contrib module documentation

* Enable use of alternate docker executables in docker plugin

Pull request: #1456
  • Loading branch information
LaurenceWarne authored Sep 18, 2021
1 parent cd7c663 commit 7f282a6
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 7 deletions.
6 changes: 5 additions & 1 deletion build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -583,9 +583,13 @@ object contrib extends MillModule {
override def ivyDeps = Agg(Deps.flywayCore)
}


object docker extends MillModule {
override def compileModuleDeps = Seq(scalalib)
override def testArgs = T {
Seq("-Djna.nosys=true") ++
scalalib.worker.testArgs() ++
scalalib.backgroundwrapper.testArgs()
}
}

object bloop extends MillModule {
Expand Down
80 changes: 74 additions & 6 deletions contrib/docker/src/DockerModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,60 @@ trait DockerModule { outer: JavaModule =>
def labels: T[Map[String, String]] = Map.empty[String, String]
def baseImage: T[String] = "gcr.io/distroless/java:latest"
def pullBaseImage: T[Boolean] = T(baseImage().endsWith(":latest"))
/**
* TCP Ports the container will listen to at runtime.
*
* See also the Docker docs on
* [[https://docs.docker.com/engine/reference/builder/#expose ports]] for
* more information.
*/
def exposedPorts: T[Seq[Int]] = Seq.empty[Int]
/**
* UDP Ports the container will listen to at runtime.
*
* See also the Docker docs on
* [[https://docs.docker.com/engine/reference/builder/#expose ports]] for
* more information.
*/
def exposedUdpPorts: T[Seq[Int]] = Seq.empty[Int]
/**
* The names of mount points.
*
* See also the Docker docs on
* [[https://docs.docker.com/engine/reference/builder/#volume volumes]]
* for more information.
*/
def volumes: T[Seq[String]] = Seq.empty[String]
/**
* Environment variables to be set in the container.
*
* See also the Docker docs on
* [[https://docs.docker.com/engine/reference/builder/#env ENV]]
* for more information.
*/
def envVars: T[Map[String, String]] = Map.empty[String, String]
/**
* Commands to add as RUN instructions.
*
* See also the Docker docs on
* [[https://docs.docker.com/engine/reference/builder/#run RUN]]
* for more information.
*/
def run: T[Seq[String]] = Seq.empty[String]
/**
* Any applicable string to the USER instruction.
*
* An empty string will be ignored and will result in USER not being
* specified. See also the Docker docs on
* [[https://docs.docker.com/engine/reference/builder/#user USER]]
* for more information.
*/
def user: T[String] = ""
/**
* The name of the executable to use, the default is "docker".
*/
def executable: T[String] = "docker"

private def baseImageCacheBuster: T[(Boolean, Double)] = T.input {
val pull = pullBaseImage()
if (pull) (pull, Math.random()) else (pull, 0d)
Expand All @@ -35,14 +89,28 @@ trait DockerModule { outer: JavaModule =>
}
.mkString(" ")

val labelLine = if (labels().isEmpty) "" else s"LABEL $labelRhs"
val lines = List(
if (labels().isEmpty) "" else s"LABEL $labelRhs",
if (exposedPorts().isEmpty) ""
else exposedPorts().map(port => s"$port/tcp")
.mkString("EXPOSE ", " ", ""),
if (exposedUdpPorts().isEmpty) ""
else exposedUdpPorts().map(port => s"$port/udp")
.mkString("EXPOSE ", " ", ""),
envVars().map { case (env, value) =>
s"ENV $env=$value"
}.mkString("\n"),
if (volumes().isEmpty) ""
else volumes().map(v => s"\"$v\"").mkString("VOLUME [", ", ", "]"),
run().map(c => s"RUN $c").mkString("\n"),
if (user().isEmpty) "" else s"USER ${user()}"
).filter(_.nonEmpty).mkString(sys.props("line.separator"))

s"""
|FROM ${baseImage()}
|$labelLine
|$lines
|COPY $jarName /$jarName
|ENTRYPOINT ["java", "-jar", "/$jarName"]
""".stripMargin
|ENTRYPOINT ["java", "-jar", "/$jarName"]""".stripMargin
}

final def build = T {
Expand All @@ -61,7 +129,7 @@ trait DockerModule { outer: JavaModule =>
val pullLatestBase = IterableShellable(if (pull) Some("--pull") else None)

val result = os
.proc("docker", "build", tagArgs, pullLatestBase, dest)
.proc(executable(), "build", tagArgs, pullLatestBase, dest)
.call(stdout = os.Inherit, stderr = os.Inherit)

log.info(s"Docker build completed ${if (result.exitCode == 0) "successfully"
Expand All @@ -71,7 +139,7 @@ trait DockerModule { outer: JavaModule =>

final def push() = T.command {
val tags = build()
tags.foreach(t => os.proc("docker", "push", t).call(stdout = os.Inherit, stderr = os.Inherit))
tags.foreach(t => os.proc(executable(), "push", t).call(stdout = os.Inherit, stderr = os.Inherit))
}
}
}
1 change: 1 addition & 0 deletions contrib/docker/test/resources/docker/src/Main.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
object Main extends App
138 changes: 138 additions & 0 deletions contrib/docker/test/src/DockerModuleTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package mill
package contrib.docker

import mill.api.PathRef
import mill.scalalib.JavaModule
import mill.util.{TestEvaluator, TestUtil}
import os.Path
import utest._
import utest.framework.TestPath

object DockerModuleTest extends TestSuite {

private def testExecutable =
if (isInstalled("podman")) "podman"
else "docker"

object Docker extends TestUtil.BaseModule with JavaModule with DockerModule {

override def millSourcePath = TestUtil.getSrcPathStatic()
override def artifactName = testArtifactName

object dockerDefault extends DockerConfig {
override def executable = testExecutable
}

object dockerAll extends DockerConfig {
override def baseImage = "docker.io/openjdk:11"
override def labels = Map("version" -> "1.0")
override def exposedPorts = Seq(8080, 443)
override def exposedUdpPorts = Seq(80)
override def volumes = Seq("/v1", "/v2")
override def envVars = Map("foo" -> "bar", "foobar" -> "barfoo")
override def run = Seq(
"/bin/bash -c 'echo Hello World!'",
"useradd -ms /bin/bash user1"
)
override def user = "user1"
override def executable = testExecutable
}
}

val testArtifactName = "mill-docker-contrib-test"

val testModuleSourcesPath: Path =
os.pwd / "contrib" / "docker" / "test" / "resources" / "docker"

val multineRegex = "\\R+".r

private def isInstalled(executable: String): Boolean = {
val getPathCmd = if (scala.util.Properties.isWin) "where" else "which"
os.proc(getPathCmd, executable).call(check = false).exitCode == 0
}

private def workspaceTest(m: TestUtil.BaseModule)(t: TestEvaluator => Unit)(
implicit tp: TestPath
): Unit = {
if (isInstalled(testExecutable) && !scala.util.Properties.isWin) {
val eval = new TestEvaluator(m)
os.remove.all(m.millSourcePath)
os.remove.all(eval.outPath)
os.makeDir.all(m.millSourcePath / os.up)
os.copy(testModuleSourcesPath, m.millSourcePath)
t(eval)
} else {
val identifier = tp.value.mkString("/")
println(s"Skipping '$identifier' since no docker installation was found")
assert(true)
}
}

override def utestAfterAll(): Unit = {
if (isInstalled(testExecutable) && !scala.util.Properties.isWin)
os
.proc(testExecutable, "rmi", testArtifactName)
.call(stdout = os.Inherit, stderr = os.Inherit)
else ()
}

def tests = Tests {

test("docker build") {
"default options" - workspaceTest(Docker) { eval =>
val Right((imageName :: Nil, _)) = eval(Docker.dockerDefault.build)
assert(imageName == testArtifactName)
}

"all options" - workspaceTest(Docker) { eval =>
val Right((imageName :: Nil, _)) = eval(Docker.dockerAll.build)
assert(imageName == testArtifactName)
}
}

test("dockerfile contents") {
"default options" - {
val eval = new TestEvaluator(Docker)
val Right((dockerfileString, _)) = eval(Docker.dockerDefault.dockerfile)
val expected = multineRegex.replaceAllIn(
"""
|FROM gcr.io/distroless/java:latest
|COPY out.jar /out.jar
|ENTRYPOINT ["java", "-jar", "/out.jar"]""".stripMargin,
sys.props("line.separator")
)
val dockerfileStringRefined = multineRegex.replaceAllIn(
dockerfileString,
sys.props("line.separator")
)
assert(dockerfileStringRefined == expected)
}

"all options" - {
val eval = new TestEvaluator(Docker)
val Right((dockerfileString, _)) = eval(Docker.dockerAll.dockerfile)
val expected = multineRegex.replaceAllIn(
"""
|FROM docker.io/openjdk:11
|LABEL "version"="1.0"
|EXPOSE 8080/tcp 443/tcp
|EXPOSE 80/udp
|ENV foo=bar
|ENV foobar=barfoo
|VOLUME ["/v1", "/v2"]
|RUN /bin/bash -c 'echo Hello World!'
|RUN useradd -ms /bin/bash user1
|USER user1
|COPY out.jar /out.jar
|ENTRYPOINT ["java", "-jar", "/out.jar"]""".stripMargin,
sys.props("line.separator")
)
val dockerfileStringRefined = multineRegex.replaceAllIn(
dockerfileString,
sys.props("line.separator")
)
assert(dockerfileStringRefined == expected)
}
}
}
}
19 changes: 19 additions & 0 deletions docs/antora/modules/ROOT/pages/Contrib_Modules.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,25 @@ object docker extends DockerConfig {
// Configure whether the docker build should check the remote registry for a new version of the base image before building.
// By default this is true if the base image is using a latest tag
def pullBaseImage = true
// Add container metadata via the LABEL instruction
def labels = Map("version" -> "1.0")
// TCP ports the container will listen to
def exposedPorts = Seq(8080, 443)
// UDP ports the container will listen to
def exposedUdpPorts = Seq(80)
// The names of mount points, these will be translated to VOLUME instructions
def volumes = Seq("/v1", "/v2")
// Environment variables to be set in the container (ENV instructions)
def envVars = Map("foo" -> "bar", "foobar" -> "barfoo")
// Add RUN instructions
def run = Seq(
"/bin/bash -c 'echo Hello World!'",
"useradd -ms /bin/bash new-user"
)
// User to use when running the image
def user = "new-user"
// Optionally override the docker executable to use something else
def executable = "podman"
}
----

Expand Down

0 comments on commit 7f282a6

Please sign in to comment.