Skip to content

Commit

Permalink
Autoremove multi-stage intermediate image(s) (#1279)
Browse files Browse the repository at this point in the history
* Autoremove multi-stage intermediate image(s)

* Scripted test

* Documentation

* Make MiMa happy

* Look for "LABEL ..." instead of comment to find id

* Fix test
  • Loading branch information
mkurz authored and muuki88 committed Nov 18, 2019
1 parent 37d5535 commit 7ed21b2
Show file tree
Hide file tree
Showing 10 changed files with 117 additions and 6 deletions.
10 changes: 10 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ mimaBinaryIssueFilters ++= {
),
ProblemFilters.exclude[ReversedMissingMethodProblem](
"com.typesafe.sbt.packager.graalvmnativeimage.GraalVMNativeImageKeys.com$typesafe$sbt$packager$graalvmnativeimage$GraalVMNativeImageKeys$_setter_$graalVMNativeImageGraalVersion_="
),
// added via #1279
ProblemFilters.exclude[ReversedMissingMethodProblem](
"com.typesafe.sbt.packager.docker.DockerKeys.com$typesafe$sbt$packager$docker$DockerKeys$_setter_$dockerAutoremoveMultiStageIntermediateImages_="
),
ProblemFilters.exclude[ReversedMissingMethodProblem](
"com.typesafe.sbt.packager.docker.DockerKeys.dockerAutoremoveMultiStageIntermediateImages"
),
ProblemFilters.exclude[DirectMissingMethodProblem](
"com.typesafe.sbt.packager.docker.DockerPlugin.publishLocalDocker"
)
)
}
Expand Down
56 changes: 54 additions & 2 deletions src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.typesafe.sbt.packager.docker

import java.io.File
import java.util.UUID
import java.util.concurrent.atomic.AtomicBoolean

import sbt._
Expand Down Expand Up @@ -96,6 +97,7 @@ object DockerPlugin extends AutoPlugin {
Option((version in Docker).value)
),
dockerUpdateLatest := false,
dockerAutoremoveMultiStageIntermediateImages := true,
dockerAliases := {
val alias = dockerAlias.value
if (dockerUpdateLatest.value) {
Expand Down Expand Up @@ -134,13 +136,16 @@ object DockerPlugin extends AutoPlugin {
val gidOpt = (daemonGroupGid in Docker).value
val base = dockerBaseImage.value
val addPerms = dockerAdditionalPermissions.value
val multiStageId = UUID.randomUUID().toString

val generalCommands = makeFrom(base) +: makeMaintainer((maintainer in Docker).value).toSeq
val stage0name = "stage0"
val stage0: Seq[CmdLike] = strategy match {
case DockerPermissionStrategy.MultiStage =>
Seq(
makeFromAs(base, stage0name),
makeLabel("snp-multi-stage" -> "intermediate"),
makeLabel("snp-multi-stage-id" -> multiStageId),
makeWorkdir(dockerBaseDirectory),
makeCopy(dockerBaseDirectory),
makeUser("root"),
Expand Down Expand Up @@ -190,7 +195,14 @@ object DockerPlugin extends AutoPlugin {
packageName := packageName.value,
publishLocal := {
val log = streams.value.log
publishLocalDocker(stage.value, dockerBuildCommand.value, log)
publishLocalDocker(
stage.value,
dockerBuildCommand.value,
dockerExecCommand.value,
dockerPermissionStrategy.value,
dockerAutoremoveMultiStageIntermediateImages.value,
log
)
log.info(
s"Built image ${dockerAlias.value.withTag(None).toString} with tags [${dockerAliases.value.flatMap(_.tag).mkString(", ")}]"
)
Expand Down Expand Up @@ -242,6 +254,13 @@ object DockerPlugin extends AutoPlugin {
)
)

/**
* @param comment
* @return # comment
*/
private final def makeComment(comment: String): CmdLike =
Comment(comment)

/**
* @param maintainer (optional)
* @return LABEL MAINTAINER if defined
Expand Down Expand Up @@ -497,12 +516,45 @@ object DockerPlugin extends AutoPlugin {
override def buffer[T](f: => T): T = f
}

def publishLocalDocker(context: File, buildCommand: Seq[String], log: Logger): Unit = {
def publishLocalDocker(context: File,
buildCommand: Seq[String],
execCommand: Seq[String],
strategy: DockerPermissionStrategy,
removeIntermediateImages: Boolean,
log: Logger): Unit = {
log.debug("Executing Native " + buildCommand.mkString(" "))
log.debug("Working directory " + context.toString)

val ret = sys.process.Process(buildCommand, context) ! publishLocalLogger(log)

if (removeIntermediateImages) {
val labelKey = "snp-multi-stage-id"
val labelCmd = s"LABEL ${labelKey}="
strategy match {
case DockerPermissionStrategy.MultiStage =>
IO.readLines(context / "Dockerfile")
.find(_.startsWith(labelCmd))
.map(_.substring(labelCmd.size).stripPrefix("\"").stripSuffix("\"")) match {
// No matter if the build process succeeded or failed, we try to remove the intermediate images
case Some(id) => {
val label = s"${labelKey}=${id}"
log.info(s"""Removing intermediate image(s) (labeled "${label}") """)
val retImageClean = sys.process.Process(
execCommand ++ s"image prune -f --filter label=${label}".split(" ")
) ! publishLocalLogger(log)
// FYI: "docker image prune" returns 0 (success) no matter if images were removed or not
if (retImageClean != 0)
log.err("Something went wrong while removing multi-stage intermediate image(s)") // no exception, just let the user know
}
case None =>
log.info(
"""Not removing multi-stage intermediate image(s) because id is missing in Dockerfile (Comment: "# id=...")"""
)
}
case _ => // Intermediate images are not generated when using other strategies
}
}

if (ret != 0)
throw new RuntimeException("Nonzero exit value: " + ret)
}
Expand Down
5 changes: 5 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 @@ -26,6 +26,11 @@ trait DockerKeys {
SettingKey[Seq[DockerAlias]]("dockerAliases", "Docker aliases for the built image")
val dockerUpdateLatest =
SettingKey[Boolean]("dockerUpdateLatest", "Set to update latest tag")
val dockerAutoremoveMultiStageIntermediateImages =
SettingKey[Boolean](
"dockerAutoremoveMultiStageIntermediateImages",
"Automatically remove multi-stage intermediate images"
)
val dockerEntrypoint = SettingKey[Seq[String]]("dockerEntrypoint", "Entrypoint arguments passed in exec form")
val dockerCmd = SettingKey[Seq[String]](
"dockerCmd",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ case class CombinedCmd(cmd: String, arg: CmdLike) extends CmdLike {
def makeContent: String = "%s %s\n" format (cmd, arg.makeContent)
}

/**
* A comment
*/
case class Comment(comment: String) extends CmdLike {
def makeContent: String = "# %s\n" format (comment)
}

/**
* A break in Dockerfile to express multi-stage build.
* https://docs.docker.com/develop/develop-images/multistage-build/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
enablePlugins(JavaAppPackaging)

name := "docker-autoremove-multi-stage-intermediate-images-test"

version := "0.1.0"

maintainer := "Matthias Kurz <m.kurz@irregular.at>"
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")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# First make sure we start clean
$ exec bash -c 'docker image prune -f --filter label=snp-multi-stage=intermediate'
# Generate the Docker image locally
> docker:publishLocal
# By default intermediate images will be removed
-$ exec bash -c 'docker images --filter label=snp-multi-stage=intermediate | grep -q "<none>"'
# Now lets change the default so we keep those images
> set dockerAutoremoveMultiStageIntermediateImages := false
> docker:publishLocal
$ exec bash -c 'docker images --filter label=snp-multi-stage=intermediate | grep -q "<none>"'
# Alright, now let's remove them by hand
$ exec bash -c 'docker image prune -f --filter label=snp-multi-stage=intermediate'
-$ exec bash -c 'docker images --filter label=snp-multi-stage=intermediate | grep -q "<none>"'
14 changes: 10 additions & 4 deletions src/sbt-test/docker/file-permission/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ lazy val root = (project in file("."))
checkDockerfileDefaults := {
val dockerfile = IO.read((stagingDirectory in Docker).value / "Dockerfile")
val lines = dockerfile.linesIterator.toList
assertEquals(lines,
assertEquals(lines.take(2),
"""FROM fabric8/java-centos-openjdk8-jdk as stage0
|WORKDIR /opt/docker
|LABEL snp-multi-stage="intermediate"""".stripMargin.linesIterator.toList)
assert(lines(2).substring(0, 25) == "LABEL snp-multi-stage-id=") // random generated id is hard to test
assertEquals(lines.drop(3),
"""WORKDIR /opt/docker
|COPY opt /opt
|USER root
|RUN ["chmod", "-R", "u=rX,g=rX", "/opt/docker"]
Expand Down Expand Up @@ -90,9 +93,12 @@ lazy val root = (project in file("."))
checkDockerfileWithWriteExecute := {
val dockerfile = IO.read((stagingDirectory in Docker).value / "Dockerfile")
val lines = dockerfile.linesIterator.toList
assertEquals(lines,
assertEquals(lines.take(2),
"""FROM fabric8/java-centos-openjdk8-jdk as stage0
|WORKDIR /opt/docker
|LABEL snp-multi-stage="intermediate"""".stripMargin.linesIterator.toList)
assert(lines(2).substring(0, 25) == "LABEL snp-multi-stage-id=") // random generated id is hard to test
assertEquals(lines.drop(3),
"""WORKDIR /opt/docker
|COPY opt /opt
|USER root
|RUN ["chmod", "-R", "u=rwX,g=rwX", "/opt/docker"]
Expand Down
7 changes: 7 additions & 0 deletions src/sphinx/formats/docker.rst
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,13 @@ Publishing Settings
Overrides the default Docker rmi command. This may be used if force flags or other options need to be passed to the command ``docker rmi``.
Defaults to ``Seq("[dockerExecCommand]", "rmi")`` and will be directly appended with the image name and tag.

``dockerAutoremoveMultiStageIntermediateImages``
If intermediate images should be automatically removed when ``MultiStage`` strategy is used.
Intermediate images usually aren't needed after packaging is finished and therefore defaults to ``true``.
All intermediate images are labeled ``snp-multi-stage=intermediate``.
If set to ``false`` and you want to remove all intermediate images at a later point, you can therefore do that by filtering for this label:
``docker image prune -f --filter label=snp-multi-stage=intermediate``

Tasks
-----
The Docker plugin provides the following commands:
Expand Down

0 comments on commit 7ed21b2

Please sign in to comment.