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

Autoremove multi-stage intermediate image(s) #1279

Merged
merged 6 commits into from
Nov 18, 2019
Merged
Show file tree
Hide file tree
Changes from 4 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
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
57 changes: 54 additions & 3 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 uniqueDockerfileId = 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("sbt-native-packager-multi-stage" -> "intermediate"),
makeLabel("sbt-native-packager-multi-stage-id" -> uniqueDockerfileId),
muuki88 marked this conversation as resolved.
Show resolved Hide resolved
makeWorkdir(dockerBaseDirectory),
makeCopy(dockerBaseDirectory),
makeUser("root"),
Expand Down Expand Up @@ -180,7 +185,7 @@ object DockerPlugin extends AutoPlugin {
// Seq(ExecCmd("RUN", Seq("ls", "-l", "/opt/docker/bin/"): _*)) ++
Seq(makeEntrypoint(dockerEntrypoint.value), makeCmd(dockerCmd.value))

stage0 ++ stage1
Seq(makeComment(s"id=${uniqueDockerfileId}")) ++ stage0 ++ stage1
}
) ++ mapGenericFilesToDocker ++ inConfig(Docker)(
Seq(
Expand All @@ -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,44 @@ 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)

// First let's see if there was a comment that tells us the id of the generated dockerfile
val headComments = IO.readLines(context / "Dockerfile").takeWhile(_.startsWith("# ")).map(_.substring(2))
muuki88 marked this conversation as resolved.
Show resolved Hide resolved

if (removeIntermediateImages) {
strategy match {
case DockerPermissionStrategy.MultiStage =>
headComments.find(_.startsWith("id=")).map(_.substring(3).trim) match {
// No matter if the build process succeeded or failed, we try to remove the intermediate images
case Some(id) => {
val label = s"sbt-native-packager-multi-stage-id=${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=sbt-native-packager-multi-stage=intermediate'
# Generate the Docker image locally
> docker:publishLocal
# By default intermediate images will be removed
-$ exec bash -c 'docker images --filter label=sbt-native-packager-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=sbt-native-packager-multi-stage=intermediate | grep -q "<none>"'
# Alright, now let's remove them by hand
$ exec bash -c 'docker image prune -f --filter label=sbt-native-packager-multi-stage=intermediate'
-$ exec bash -c 'docker images --filter label=sbt-native-packager-multi-stage=intermediate | grep -q "<none>"'
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 ``sbt-native-packager-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=sbt-native-packager-multi-stage=intermediate``

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