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 support for HEALTHCHECK Dockerfile command #75

Merged
merged 1 commit into from
Aug 31, 2017
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
34 changes: 34 additions & 0 deletions src/main/scala/sbtdocker/DockerfileCommands.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import sbt._
import sbtdocker.Instructions._
import sbtdocker.staging.{CopyFile, SourceFile}

import scala.concurrent.duration.FiniteDuration

trait DockerfileLike extends DockerfileCommands {
type T <: DockerfileLike

Expand Down Expand Up @@ -244,4 +246,36 @@ trait DockerfileCommands {

def onBuild(instruction: DockerfileInstruction): T = addInstruction(Instructions.OnBuild(instruction))

def healthCheck(commands: Seq[String],
interval: Option[FiniteDuration] = None,
timeout: Option[FiniteDuration] = None,
startPeriod: Option[FiniteDuration] = None,
retries: Option[Int] = None): T = {
if (commands.nonEmpty) addInstruction(HealthCheck.exec(
commands = commands,
interval = interval,
timeout = timeout,
startPeriod = startPeriod,
retries = retries))
else self
}

def healthCheck(commands: String*): T = healthCheck(commands)

def healthCheckShell(commands: Seq[String],
interval: Option[FiniteDuration] = None,
timeout: Option[FiniteDuration] = None,
startPeriod: Option[FiniteDuration] = None,
retries: Option[Int] = None): T = {
if (commands.nonEmpty) addInstruction(HealthCheck.shell(commands = commands,
interval = interval,
timeout = timeout,
startPeriod = startPeriod,
retries = retries))
else self
}

def healthCheckShell(commands: String*): T = healthCheckShell(commands)

def healthCheckNone(): T = addInstruction(HealthCheckNone)
}
74 changes: 74 additions & 0 deletions src/main/scala/sbtdocker/Instructions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package sbtdocker
import org.apache.commons.lang3.StringEscapeUtils
import sbtdocker.staging.SourceFile

import scala.concurrent.duration.FiniteDuration

sealed trait Instruction

/**
Expand Down Expand Up @@ -298,6 +300,78 @@ object Instructions {
*/
case class StageFiles(sources: Seq[SourceFile], destination: String) extends FileStagingInstruction

object HealthCheck {
/**
* Command to execute for health check.
* @param commands Command list.
*/
def exec(commands: Seq[String],
interval: Option[FiniteDuration],
timeout: Option[FiniteDuration],
startPeriod: Option[FiniteDuration],
retries: Option[Int]) =
HealthCheck(
command = jsonArrayString(commands),
interval = interval,
timeout = timeout,
startPeriod = startPeriod,
retries = retries)

/**
* Command to execute through a shell (`/bin/sh`) for health check.
* @param commands Command list.
*/
def shell(commands: Seq[String],
interval: Option[FiniteDuration],
timeout: Option[FiniteDuration],
startPeriod: Option[FiniteDuration],
retries: Option[Int]) =
HealthCheck(
command = shellCommandString(commands),
interval = interval,
timeout = timeout,
startPeriod = startPeriod,
retries = retries)

def none() = HealthCheckNone
}

/**
* Defines health check command to run inside the container to check that it is still working.
* @param command Command to execute for health check
* @param interval The health check will first run interval seconds after the container is started,
* and then again interval seconds after each previous check completes.
* @param timeout If a single run of the check takes longer than specified then the check is considered to have failed.
* @param startPeriod Provides initialization time for containers that need time to bootstrap.
* @param retries How many consecutive failures of the health check for the container to be considered unhealthy.
*/
case class HealthCheck(
command: String,
interval: Option[FiniteDuration] = None,
timeout: Option[FiniteDuration] = None,
startPeriod: Option[FiniteDuration] = None,
retries: Option[Int] = None
) extends DockerfileInstruction {

override def instructionName = "HEALTHCHECK"

override def arguments: String = Seq(
interval.map(x => s"--interval=${x.toSeconds}s"),
timeout.map(x => s"--timeout=${x.toSeconds}s"),
startPeriod.map(x => s"--start-period=${x.toSeconds}s"),
retries.map(x => s"--retries=$x"),
Some(s"CMD $command")
).flatten.mkString(" ")
}

/**
* Disable any health check inherited from the base image.
*/
case object HealthCheckNone extends DockerfileInstruction {
override def instructionName = "HEALTHCHECK"
override def arguments = "NONE"
}

/**
* This class allows the user to specify a raw Dockerfile instruction.
* Example:
Expand Down
31 changes: 26 additions & 5 deletions src/test/scala/sbtdocker/DockerfileLikeSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package sbtdocker
import org.scalatest.{FunSuite, Matchers}
import sbt.file
import sbtdocker.Instructions._
import staging.{CopyFile, DefaultDockerfileProcessor, StagedDockerfile}
import sbtdocker.staging.{CopyFile, DefaultDockerfileProcessor, StagedDockerfile}

import scala.concurrent.duration._

class DockerfileLikeSuite extends FunSuite with Matchers {
val allInstructions = Seq(
Expand All @@ -23,7 +25,12 @@ class DockerfileLikeSuite extends FunSuite with Matchers {
Volume("mountpoint"),
User("marcus"),
WorkDir("path"),
OnBuild(Run.exec(Seq("echo", "123")))
OnBuild(Run.exec(Seq("echo", "123"))),
HealthCheck.exec(Seq("cmd", "arg"), interval = Some(20.seconds), timeout = Some(10.seconds),
startPeriod = Some(1.second), retries = Some(3)),
HealthCheck.shell(Seq("cmd", "arg"), interval = Some(20.seconds), timeout = Some(10.seconds),
startPeriod = Some(1.second), retries = Some(3)),
HealthCheck.none()
)

test("Instructions string is in correct order and matches instructions") {
Expand All @@ -46,7 +53,10 @@ class DockerfileLikeSuite extends FunSuite with Matchers {
|VOLUME ["mountpoint"]
|USER marcus
|WORKDIR path
|ONBUILD RUN ["echo", "123"]""".stripMargin
|ONBUILD RUN ["echo", "123"]
|HEALTHCHECK --interval=20s --timeout=10s --start-period=1s --retries=3 CMD ["cmd", "arg"]
|HEALTHCHECK --interval=20s --timeout=10s --start-period=1s --retries=3 CMD cmd arg
|HEALTHCHECK NONE""".stripMargin
}

def staged(dockerfile: immutable.Dockerfile): StagedDockerfile = {
Expand Down Expand Up @@ -85,26 +95,35 @@ class DockerfileLikeSuite extends FunSuite with Matchers {
.user("marcus")
.workDir("path")
.onBuild(Run.exec(Seq("echo", "123")))
.healthCheck(Seq("cmd", "arg"), interval = Some(20.seconds), timeout = Some(10.seconds),
startPeriod = Some(1.second), retries = Some(3))
.healthCheckShell(Seq("cmd", "arg"), interval = Some(20.seconds), timeout = Some(10.seconds),
startPeriod = Some(1.second), retries = Some(3))
.healthCheckNone()

withMethods shouldEqual predefined
}

test("Run, Cmd and EntryPoint instructions should handle arguments with whitespace") {
test("Run, Cmd, EntryPoint and HealthCheck instructions should handle arguments with whitespace") {
val dockerfile = immutable.Dockerfile.empty
.run("echo", "arg \"with\t\nspaces")
.runShell("echo", "arg \"with\t\nspaces")
.cmd("echo", "arg \"with\t\nspaces")
.cmdShell("echo", "arg \"with\t\nspaces")
.entryPoint("echo", "arg \"with\t\nspaces")
.entryPointShell("echo", "arg \"with\t\nspaces")
.healthCheck(Seq("echo", "arg \"with\t\nspaces"))
.healthCheckShell(Seq("echo", "arg \"with\t\nspaces"))

staged(dockerfile).instructionsString shouldEqual
"""RUN ["echo", "arg \"with\t\nspaces"]
|RUN echo arg\ \"with\t\nspaces
|CMD ["echo", "arg \"with\t\nspaces"]
|CMD echo arg\ \"with\t\nspaces
|ENTRYPOINT ["echo", "arg \"with\t\nspaces"]
|ENTRYPOINT echo arg\ \"with\t\nspaces""".stripMargin
|ENTRYPOINT echo arg\ \"with\t\nspaces
|HEALTHCHECK CMD ["echo", "arg \"with\t\nspaces"]
|HEALTHCHECK CMD echo arg\ \"with\t\nspaces""".stripMargin
}

test("Add and copy a file to /") {
Expand Down Expand Up @@ -149,6 +168,8 @@ class DockerfileLikeSuite extends FunSuite with Matchers {
.entryPointShell()
.volume()
.maintainer("test")
.healthCheck()
.healthCheckShell()

dockerfile shouldEqual immutable.Dockerfile.empty.maintainer("test")
}
Expand Down
20 changes: 20 additions & 0 deletions src/test/scala/sbtdocker/InstructionsSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package sbtdocker
import org.scalatest.{FlatSpec, Matchers}
import sbtdocker.Instructions._

import scala.concurrent.duration._

class InstructionsSpec extends FlatSpec with Matchers {
"From" should "create a correct string" in {
From("image").toString shouldEqual "FROM image"
Expand Down Expand Up @@ -78,6 +80,24 @@ class InstructionsSpec extends FlatSpec with Matchers {
OnBuild(Run.exec(Seq("echo", "123"))).toString shouldEqual "ONBUILD RUN [\"echo\", \"123\"]"
}

"HealthCheck" should "create a correct string with exec format" in {
HealthCheck.exec(Seq("cmd", "arg"), interval = Some(20.seconds), timeout = Some(10.seconds),
startPeriod = Some(1.second), retries = Some(3)).toString shouldEqual
"HEALTHCHECK --interval=20s --timeout=10s --start-period=1s --retries=3 CMD [\"cmd\", \"arg\"]"
HealthCheck.exec(Seq("1 \t3", "\"ö'\\a"), None, None, None, None).toString shouldEqual "HEALTHCHECK CMD [\"1 \\t3\", \"\\\"\\u00F6'\\\\a\"]"
}

it should "create a correct string with shell format" in {
HealthCheck.shell(Seq("cmd", "arg"), interval = Some(20.seconds), timeout = Some(10.seconds),
startPeriod = Some(1.second), retries = Some(3)).toString shouldEqual
"HEALTHCHECK --interval=20s --timeout=10s --start-period=1s --retries=3 CMD cmd arg"
HealthCheck.shell(Seq("1 \t3", "\"ö'\\a"), None, None, None, None).toString shouldEqual "HEALTHCHECK CMD 1\\ \\t3 \\\"ö'\\a"
}

it should "create a correct string for NONE parameter" in {
HealthCheck.none().toString shouldEqual "HEALTHCHECK NONE"
}

"Label" should "create a correct label string" in {
Label("foo", "bar").toString shouldEqual """LABEL foo="bar""""
Label("com.example.bar", "foo").toString shouldEqual """LABEL com.example.bar="foo""""
Expand Down
14 changes: 13 additions & 1 deletion src/test/scala/sbtdocker/immutable/ImmutableDockerfileSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import sbt.file
import sbtdocker.ImageName
import sbtdocker.staging.CopyFile

import scala.concurrent.duration._

class ImmutableDockerfileSpec extends FlatSpec with Matchers {

import sbtdocker.Instructions._
Expand Down Expand Up @@ -49,6 +51,11 @@ class ImmutableDockerfileSpec extends FlatSpec with Matchers {
.user("marcus")
.workDir("/srv")
.onBuild(Run.exec(Seq("echo", "text")))
.healthCheck(Seq("healthcheck.sh", "1"), interval = Some(20.seconds), timeout = Some(10.seconds),
startPeriod = Some(1.second), retries = Some(3))
.healthCheckShell(Seq("healthcheck.sh", "2"), interval = Some(20.seconds), timeout = Some(10.seconds),
startPeriod = Some(1.second), retries = Some(3))
.healthCheckNone()

val instructions = Seq(
From("image"),
Expand All @@ -72,7 +79,12 @@ class ImmutableDockerfileSpec extends FlatSpec with Matchers {
Volume("/srv"),
User("marcus"),
WorkDir("/srv"),
OnBuild(Run.exec(Seq("echo", "text"))))
OnBuild(Run.exec(Seq("echo", "text"))),
HealthCheck.exec(Seq("healthcheck.sh", "1"), interval = Some(20.seconds), timeout = Some(10.seconds),
startPeriod = Some(1.second), retries = Some(3)),
HealthCheck.shell(Seq("healthcheck.sh", "2"), interval = Some(20.seconds), timeout = Some(10.seconds),
startPeriod = Some(1.second), retries = Some(3)),
HealthCheckNone)

dockerfile.instructions should contain theSameElementsInOrderAs instructions
}
Expand Down
14 changes: 13 additions & 1 deletion src/test/scala/sbtdocker/mutable/MutableDockerfileSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import sbt._
import sbtdocker.ImageName
import sbtdocker.staging.CopyFile

import scala.concurrent.duration._

class MutableDockerfileSpec extends FlatSpec with Matchers {

import sbtdocker.Instructions._
Expand Down Expand Up @@ -48,6 +50,11 @@ class MutableDockerfileSpec extends FlatSpec with Matchers {
user("marcus")
workDir("/srv")
onBuild(Run.exec(Seq("echo", "text")))
healthCheck(Seq("healthcheck.sh", "1"), interval = Some(20.seconds), timeout = Some(10.seconds),
startPeriod = Some(1.second), retries = Some(3))
healthCheckShell(Seq("healthcheck.sh", "2"), interval = Some(20.seconds), timeout = Some(10.seconds),
startPeriod = Some(1.second), retries = Some(3))
healthCheckNone()
}

val instructions = Seq(
Expand All @@ -72,7 +79,12 @@ class MutableDockerfileSpec extends FlatSpec with Matchers {
Volume("/srv"),
User("marcus"),
WorkDir("/srv"),
OnBuild(Run.exec(Seq("echo", "text"))))
OnBuild(Run.exec(Seq("echo", "text"))),
HealthCheck.exec(Seq("healthcheck.sh", "1"), interval = Some(20.seconds), timeout = Some(10.seconds),
startPeriod = Some(1.second), retries = Some(3)),
HealthCheck.shell(Seq("healthcheck.sh", "2"), interval = Some(20.seconds), timeout = Some(10.seconds),
startPeriod = Some(1.second), retries = Some(3)),
HealthCheckNone)

dockerfile.instructions should contain theSameElementsInOrderAs instructions
}
Expand Down