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 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
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