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

Add More Config Options to Docker Contrib Module #1456

Merged
merged 4 commits into from
Sep 18, 2021
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
6 changes: 5 additions & 1 deletion build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -579,9 +579,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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've written these based on the other contrib module test suites, let me know if there's anything untoward here.


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 = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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