From f7fbde609a02cf2495e133c52e346ffe2ed3bb44 Mon Sep 17 00:00:00 2001 From: Matthias Kurz Date: Thu, 14 Nov 2019 23:51:41 +0100 Subject: [PATCH 1/6] Autoremove multi-stage intermediate image(s) --- .../sbt/packager/docker/DockerPlugin.scala | 57 ++++++++++++++++++- .../typesafe/sbt/packager/docker/Keys.scala | 5 ++ .../sbt/packager/docker/dockerfile.scala | 7 +++ 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala b/src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala index 404a820fc..b74aa630b 100644 --- a/src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala +++ b/src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala @@ -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._ @@ -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) { @@ -134,6 +136,7 @@ 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" @@ -141,6 +144,8 @@ object DockerPlugin extends AutoPlugin { case DockerPermissionStrategy.MultiStage => Seq( makeFromAs(base, stage0name), + makeLabel("sbt-native-packager-multi-stage" -> "intermediate"), + makeLabel("sbt-native-packager-multi-stage-id" -> uniqueDockerfileId), makeWorkdir(dockerBaseDirectory), makeCopy(dockerBaseDirectory), makeUser("root"), @@ -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( @@ -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(", ")}]" ) @@ -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 @@ -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)) + + 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) } diff --git a/src/main/scala/com/typesafe/sbt/packager/docker/Keys.scala b/src/main/scala/com/typesafe/sbt/packager/docker/Keys.scala index 0c516a051..22a2eb9b2 100644 --- a/src/main/scala/com/typesafe/sbt/packager/docker/Keys.scala +++ b/src/main/scala/com/typesafe/sbt/packager/docker/Keys.scala @@ -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", diff --git a/src/main/scala/com/typesafe/sbt/packager/docker/dockerfile.scala b/src/main/scala/com/typesafe/sbt/packager/docker/dockerfile.scala index 34cf84163..074e94439 100644 --- a/src/main/scala/com/typesafe/sbt/packager/docker/dockerfile.scala +++ b/src/main/scala/com/typesafe/sbt/packager/docker/dockerfile.scala @@ -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/ From 5a3d5436c2546bc3bb551e462ba655ad18d83c6e Mon Sep 17 00:00:00 2001 From: Matthias Kurz Date: Thu, 14 Nov 2019 23:51:52 +0100 Subject: [PATCH 2/6] Scripted test --- .../build.sbt | 7 +++++++ .../project/plugins.sbt | 1 + .../src/main/scala/Main.scala | 3 +++ .../autoremove-multi-stage-intermediate-images/test | 13 +++++++++++++ 4 files changed, 24 insertions(+) create mode 100644 src/sbt-test/docker/autoremove-multi-stage-intermediate-images/build.sbt create mode 100644 src/sbt-test/docker/autoremove-multi-stage-intermediate-images/project/plugins.sbt create mode 100644 src/sbt-test/docker/autoremove-multi-stage-intermediate-images/src/main/scala/Main.scala create mode 100644 src/sbt-test/docker/autoremove-multi-stage-intermediate-images/test diff --git a/src/sbt-test/docker/autoremove-multi-stage-intermediate-images/build.sbt b/src/sbt-test/docker/autoremove-multi-stage-intermediate-images/build.sbt new file mode 100644 index 000000000..5f859ffcb --- /dev/null +++ b/src/sbt-test/docker/autoremove-multi-stage-intermediate-images/build.sbt @@ -0,0 +1,7 @@ +enablePlugins(JavaAppPackaging) + +name := "docker-autoremove-multi-stage-intermediate-images-test" + +version := "0.1.0" + +maintainer := "Matthias Kurz " diff --git a/src/sbt-test/docker/autoremove-multi-stage-intermediate-images/project/plugins.sbt b/src/sbt-test/docker/autoremove-multi-stage-intermediate-images/project/plugins.sbt new file mode 100644 index 000000000..b53de154c --- /dev/null +++ b/src/sbt-test/docker/autoremove-multi-stage-intermediate-images/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % sys.props("project.version")) diff --git a/src/sbt-test/docker/autoremove-multi-stage-intermediate-images/src/main/scala/Main.scala b/src/sbt-test/docker/autoremove-multi-stage-intermediate-images/src/main/scala/Main.scala new file mode 100644 index 000000000..61471c658 --- /dev/null +++ b/src/sbt-test/docker/autoremove-multi-stage-intermediate-images/src/main/scala/Main.scala @@ -0,0 +1,3 @@ +object Main extends App { + println("Hello world") +} diff --git a/src/sbt-test/docker/autoremove-multi-stage-intermediate-images/test b/src/sbt-test/docker/autoremove-multi-stage-intermediate-images/test new file mode 100644 index 000000000..038949d13 --- /dev/null +++ b/src/sbt-test/docker/autoremove-multi-stage-intermediate-images/test @@ -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 ""' +# 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 ""' +# 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 ""' From 4f4410c3122c5099f2369355a763c3660cd2c923 Mon Sep 17 00:00:00 2001 From: Matthias Kurz Date: Thu, 14 Nov 2019 23:52:00 +0100 Subject: [PATCH 3/6] Documentation --- src/sphinx/formats/docker.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/sphinx/formats/docker.rst b/src/sphinx/formats/docker.rst index 4974fc745..48aad3397 100644 --- a/src/sphinx/formats/docker.rst +++ b/src/sphinx/formats/docker.rst @@ -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: From 3fc79e72dbef0d2cdaa6d82adaca734248334de8 Mon Sep 17 00:00:00 2001 From: Matthias Kurz Date: Fri, 15 Nov 2019 00:17:49 +0100 Subject: [PATCH 4/6] Make MiMa happy --- build.sbt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/build.sbt b/build.sbt index 40b4d2218..24ae0e454 100644 --- a/build.sbt +++ b/build.sbt @@ -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" ) ) } From 733befb8ad6df4e61961bfc460dba47729774f72 Mon Sep 17 00:00:00 2001 From: Matthias Kurz Date: Mon, 18 Nov 2019 10:12:27 +0100 Subject: [PATCH 5/6] Look for "LABEL ..." instead of comment to find id --- .../sbt/packager/docker/DockerPlugin.scala | 19 ++++++++++--------- .../test | 10 +++++----- src/sphinx/formats/docker.rst | 4 ++-- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala b/src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala index b74aa630b..40c10f13a 100644 --- a/src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala +++ b/src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala @@ -136,7 +136,7 @@ object DockerPlugin extends AutoPlugin { val gidOpt = (daemonGroupGid in Docker).value val base = dockerBaseImage.value val addPerms = dockerAdditionalPermissions.value - val uniqueDockerfileId = UUID.randomUUID().toString + val multiStageId = UUID.randomUUID().toString val generalCommands = makeFrom(base) +: makeMaintainer((maintainer in Docker).value).toSeq val stage0name = "stage0" @@ -144,8 +144,8 @@ object DockerPlugin extends AutoPlugin { case DockerPermissionStrategy.MultiStage => Seq( makeFromAs(base, stage0name), - makeLabel("sbt-native-packager-multi-stage" -> "intermediate"), - makeLabel("sbt-native-packager-multi-stage-id" -> uniqueDockerfileId), + makeLabel("snp-multi-stage" -> "intermediate"), + makeLabel("snp-multi-stage-id" -> multiStageId), makeWorkdir(dockerBaseDirectory), makeCopy(dockerBaseDirectory), makeUser("root"), @@ -185,7 +185,7 @@ object DockerPlugin extends AutoPlugin { // Seq(ExecCmd("RUN", Seq("ls", "-l", "/opt/docker/bin/"): _*)) ++ Seq(makeEntrypoint(dockerEntrypoint.value), makeCmd(dockerCmd.value)) - Seq(makeComment(s"id=${uniqueDockerfileId}")) ++ stage0 ++ stage1 + stage0 ++ stage1 } ) ++ mapGenericFilesToDocker ++ inConfig(Docker)( Seq( @@ -527,16 +527,17 @@ object DockerPlugin extends AutoPlugin { 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)) - if (removeIntermediateImages) { + val labelKey = "snp-multi-stage-id" + val labelCmd = s"LABEL ${labelKey}=" strategy match { case DockerPermissionStrategy.MultiStage => - headComments.find(_.startsWith("id=")).map(_.substring(3).trim) match { + 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"sbt-native-packager-multi-stage-id=${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(" ") diff --git a/src/sbt-test/docker/autoremove-multi-stage-intermediate-images/test b/src/sbt-test/docker/autoremove-multi-stage-intermediate-images/test index 038949d13..7fdf12493 100644 --- a/src/sbt-test/docker/autoremove-multi-stage-intermediate-images/test +++ b/src/sbt-test/docker/autoremove-multi-stage-intermediate-images/test @@ -1,13 +1,13 @@ # First make sure we start clean -$ exec bash -c 'docker image prune -f --filter label=sbt-native-packager-multi-stage=intermediate' +$ 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=sbt-native-packager-multi-stage=intermediate | grep -q ""' +-$ exec bash -c 'docker images --filter label=snp-multi-stage=intermediate | grep -q ""' # 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 ""' +$ exec bash -c 'docker images --filter label=snp-multi-stage=intermediate | grep -q ""' # 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 ""' +$ 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 ""' diff --git a/src/sphinx/formats/docker.rst b/src/sphinx/formats/docker.rst index 48aad3397..08da886d5 100644 --- a/src/sphinx/formats/docker.rst +++ b/src/sphinx/formats/docker.rst @@ -185,9 +185,9 @@ Publishing Settings ``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``. + 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=sbt-native-packager-multi-stage=intermediate`` + ``docker image prune -f --filter label=snp-multi-stage=intermediate`` Tasks ----- From d84628b95ee979aec77fcf30cec7a4c443e7bf3d Mon Sep 17 00:00:00 2001 From: Matthias Kurz Date: Mon, 18 Nov 2019 10:13:01 +0100 Subject: [PATCH 6/6] Fix test --- src/sbt-test/docker/file-permission/build.sbt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/sbt-test/docker/file-permission/build.sbt b/src/sbt-test/docker/file-permission/build.sbt index cbea0ac2a..873d9aa33 100644 --- a/src/sbt-test/docker/file-permission/build.sbt +++ b/src/sbt-test/docker/file-permission/build.sbt @@ -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"] @@ -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"]