From 34c776dc868ca1bad546131c7433b38d74680adf Mon Sep 17 00:00:00 2001 From: eugene yokota Date: Thu, 24 Jan 2019 03:36:33 -0500 Subject: [PATCH] Implement dockerPermissionStrategy (#1190) * Validate Docker packaging * implement dockerPermissionStrategy Fixes #1189 This implements a non-root Docker container that's safer by default and compatible with Red Hat OpenShift. Current `ADD --chown=daemon:daemon opt /opt` nominally implements non-root image, but by giving ownership of the working directory to the `daemon` user, it reduces the safety. Instead we should use `chmod` to default to read-only access unless the build user opts into writable working directory. The challenge is calling `chmod` without incurring the fs layer overhead (#883). [Multi-stage builds](https://docs.docker.com/develop/develop-images/multistage-build/) can be used to pre-stage the files with desired file permissions. This adds new `dockerPermissionStrategy` setting which decides how file permissions are set for the working directory inside the Docker image generated by sbt-native-packager. The strategies are: - `DockerPermissionStrategy.MultiStage` (default): uses multi-stage Docker build to call chmod ahead of time. - `DockerPermissionStrategy.None`: does not attempt to change the file permissions, and use the host machine's file mode bits. - `DockerPermissionStrategy.Run`: calls `RUN` in the Dockerfile. This has regression on the resulting Docker image file size. - `DockerPermissionStrategy.CopyChown`: calls `COPY --chown` in the Dockerfile. Provided as a backward compatibility. For `MultiStage` and `Run` strategies, `dockerChmodType` is used in addition to call `chmod` during Docker build. - `DockerChmodType.UserGroupReadExecute` (default): chmod -R u=rX,g=rX - `DockerChmodType.UserGroupWriteExecute`: chmod -R u=rwX,g=rwX - `DockerChmodType.SyncGroupToUser`: chmod -R g=u - `DockerChmodType.Custom`: Custom argument provided by the user. Some application will require writing files to the working directory. In that case the setting should be changed as follows: ```scala import com.typesafe.sbt.packager.docker.DockerChmodType dockerChmodType := DockerChmodType.UserGroupWriteExecute ``` During `docker:stage`, Docker package validation is called to check if the selected strategy is compatible with the deteted Docker version. This fixes the current repeatability issue reported as #1187. If the incompatibility is detected, the user is advised to either upgrade their Docker, pick another strategy, or override the `dockerVersion` setting. `daemonGroup` is set to `root` instead of copying the value from the `daemonUser` setting. This matches the semantics of `USER` as well as OpenShift, which uses gid=0. * improve the names in file-permission scritped test * add comment on globalSettings --- .../docker/DockerPermissionStrategy.scala | 75 +++++++ .../sbt/packager/docker/DockerPlugin.scala | 190 +++++++++++++++--- .../sbt/packager/docker/DockerSupport.scala | 2 + .../typesafe/sbt/packager/docker/Keys.scala | 8 + .../sbt/packager/docker/dockerfile.scala | 8 + src/sbt-test/docker/entrypoint/build.sbt | 2 +- src/sbt-test/docker/envVars/build.sbt | 2 +- src/sbt-test/docker/file-permission/build.sbt | 96 +++++++++ .../file-permission/changes/dockerversion.sbt | 3 + .../changes/strategy-copychown.sbt | 3 + .../file-permission/changes/strategy-none.sbt | 3 + .../file-permission/changes/strategy-run.sbt | 3 + .../file-permission/changes/write-execute.sbt | 4 + .../file-permission/project/plugins.sbt | 1 + src/sbt-test/docker/file-permission/test | 27 +++ src/sbt-test/docker/labels/build.sbt | 2 +- src/sbt-test/docker/ports/build.sbt | 2 +- src/sbt-test/docker/rmi/test | 2 +- src/sbt-test/docker/staging/build.sbt | 2 +- .../docker/test-packageName/build.sbt | 1 + src/sbt-test/docker/udp-only-ports/build.sbt | 2 +- src/sbt-test/docker/volumes/build.sbt | 2 +- src/sbt-test/docker/volumes/test | 2 +- 23 files changed, 408 insertions(+), 34 deletions(-) create mode 100644 src/main/scala/com/typesafe/sbt/packager/docker/DockerPermissionStrategy.scala create mode 100644 src/sbt-test/docker/file-permission/build.sbt create mode 100644 src/sbt-test/docker/file-permission/changes/dockerversion.sbt create mode 100644 src/sbt-test/docker/file-permission/changes/strategy-copychown.sbt create mode 100644 src/sbt-test/docker/file-permission/changes/strategy-none.sbt create mode 100644 src/sbt-test/docker/file-permission/changes/strategy-run.sbt create mode 100644 src/sbt-test/docker/file-permission/changes/write-execute.sbt create mode 100644 src/sbt-test/docker/file-permission/project/plugins.sbt create mode 100644 src/sbt-test/docker/file-permission/test diff --git a/src/main/scala/com/typesafe/sbt/packager/docker/DockerPermissionStrategy.scala b/src/main/scala/com/typesafe/sbt/packager/docker/DockerPermissionStrategy.scala new file mode 100644 index 000000000..837bc9271 --- /dev/null +++ b/src/main/scala/com/typesafe/sbt/packager/docker/DockerPermissionStrategy.scala @@ -0,0 +1,75 @@ +package com.typesafe.sbt.packager.docker + +/** + * This represents a strategy to change the file permissions. + */ +sealed trait DockerPermissionStrategy +object DockerPermissionStrategy { + + /** + * `None` does not attempt to change the file permissions. + * This will inherit the host machine's group bits. + */ + case object None extends DockerPermissionStrategy + + /** + * `Run` calls `RUN` in the `Dockerfile`. + * This could double the size of the resulting Docker image + * because of the extra layer it creates. + */ + case object Run extends DockerPermissionStrategy + + /** + * `MultiStage` uses multi-stage Docker build to change + * the file permissions. + * https://docs.docker.com/develop/develop-images/multistage-build/ + */ + case object MultiStage extends DockerPermissionStrategy + + /** + * `CopyChown` calls `COPY --chown` in the `Dockerfile`. + * This option is provided for backward compatibility. + * This will inherit the host machine's file mode. + * Note that this option is not compatible with OpenShift which ignores + * USER command and uses an arbitrary user to run the container. + */ + case object CopyChown extends DockerPermissionStrategy +} + +/** + * This represents a type of file permission changes to run on the working directory. + * Note that group file mode bits must be effective to be OpenShift compatible. + */ +sealed trait DockerChmodType { + def argument: String +} +object DockerChmodType { + + /** + * Gives read permission to users and groups. + * Gives execute permission to users and groups, if +x flag is on for any. + */ + case object UserGroupReadExecute extends DockerChmodType { + def argument: String = "u=rX,g=rX" + } + + /** + * Gives read and write permissions to users and groups. + * Gives execute permission to users and groups, if +x flag is on for any. + */ + case object UserGroupWriteExecute extends DockerChmodType { + def argument: String = "u=rwX,g=rwX" + } + + /** + * Copies user file mode bits to group file mode bits. + */ + case object SyncGroupToUser extends DockerChmodType { + def argument: String = "g=u" + } + + /** + * Use custom argument. + */ + case class Custom(argument: String) extends DockerChmodType +} 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 93484f699..eaf08a81f 100644 --- a/src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala +++ b/src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala @@ -48,7 +48,7 @@ import scala.util.Try */ object DockerPlugin extends AutoPlugin { - object autoImport extends DockerKeys { + object autoImport extends DockerKeysEx { val Docker: Configuration = config("docker") val DockerAlias = com.typesafe.sbt.packager.docker.DockerAlias @@ -57,7 +57,7 @@ object DockerPlugin extends AutoPlugin { import autoImport._ /** - * The separator used by makeAdd should be always forced to UNIX separator. + * The separator used by makeCopy should be always forced to UNIX separator. * The separator doesn't depend on the OS where Dockerfile is being built. */ val UnixSeparatorChar = '/' @@ -66,6 +66,13 @@ object DockerPlugin extends AutoPlugin { override def projectConfigurations: Seq[Configuration] = Seq(Docker) + // Some of the default values are now provided in the global setting based on + // sbt plugin best practice: https://www.scala-sbt.org/release/docs/Plugins-Best-Practices.html#Provide+default+values+in + override lazy val globalSettings: Seq[Setting[_]] = Seq( + dockerPermissionStrategy := DockerPermissionStrategy.MultiStage, + dockerChmodType := DockerChmodType.UserGroupReadExecute + ) + override lazy val projectSettings: Seq[Setting[_]] = Seq( dockerBaseImage := "openjdk:8", dockerExposedPorts := Seq(), @@ -102,19 +109,48 @@ object DockerPlugin extends AutoPlugin { dockerRmiCommand := dockerExecCommand.value ++ Seq("rmi"), dockerBuildCommand := dockerExecCommand.value ++ Seq("build") ++ dockerBuildOptions.value ++ Seq("."), dockerCommands := { + val strategy = dockerPermissionStrategy.value val dockerBaseDirectory = (defaultLinuxInstallLocation in Docker).value val user = (daemonUser in Docker).value val group = (daemonGroup in Docker).value + val base = dockerBaseImage.value + val uid = 1001 + val gid = 0 + + 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), + makeWorkdir(dockerBaseDirectory), + makeUserAdd(user, uid, gid), + makeCopy(dockerBaseDirectory), + makeChmod(dockerChmodType.value, Seq(dockerBaseDirectory)), + DockerStageBreak + ) + case _ => Seq() + } - val generalCommands = makeFrom(dockerBaseImage.value) +: makeMaintainer((maintainer in Docker).value).toSeq - - generalCommands ++ - Seq(makeWorkdir(dockerBaseDirectory)) ++ makeAdd(dockerVersion.value, dockerBaseDirectory, user, group) ++ + val stage1: Seq[CmdLike] = generalCommands ++ + Seq(makeUserAdd(user, uid, gid), makeWorkdir(dockerBaseDirectory)) ++ + (strategy match { + case DockerPermissionStrategy.MultiStage => + Seq(makeCopyFrom(dockerBaseDirectory, stage0name, user, group)) + case DockerPermissionStrategy.Run => + Seq(makeCopy(dockerBaseDirectory), makeChmod(dockerChmodType.value, Seq(dockerBaseDirectory))) + case DockerPermissionStrategy.CopyChown => + Seq(makeCopyChown(dockerBaseDirectory, user, group)) + case DockerPermissionStrategy.None => + Seq(makeCopy(dockerBaseDirectory)) + }) ++ dockerLabels.value.map(makeLabel) ++ dockerEnvVars.value.map(makeEnvVar) ++ makeExposePorts(dockerExposedPorts.value, dockerExposedUdpPorts.value) ++ makeVolumes(dockerExposedVolumes.value, user, group) ++ - Seq(makeUser(user), makeEntrypoint(dockerEntrypoint.value), makeCmd(dockerCmd.value)) + Seq(makeUser(uid), makeEntrypoint(dockerEntrypoint.value), makeCmd(dockerCmd.value)) + + stage0 ++ stage1 } ) ++ mapGenericFilesToDocker ++ inConfig(Docker)( Seq( @@ -153,16 +189,22 @@ object DockerPlugin extends AutoPlugin { stagingDirectory := (target in Docker).value / "stage", target := target.value / "docker", daemonUser := "daemon", - daemonGroup := daemonUser.value, + daemonGroup := "root", defaultLinuxInstallLocation := "/opt/docker", + validatePackage := Validation + .runAndThrow(validatePackageValidators.value, streams.value.log), validatePackageValidators := Seq( nonEmptyMappings((mappings in Docker).value), filesExist((mappings in Docker).value), validateExposedPorts(dockerExposedPorts.value, dockerExposedUdpPorts.value), - validateDockerVersion(dockerVersion.value) + validateDockerVersion(dockerVersion.value), + validateDockerPermissionStrategy(dockerPermissionStrategy.value, dockerVersion.value) ), dockerPackageMappings := MappingsHelper.contentOf(sourceDirectory.value), - dockerGenerateConfig := generateDockerConfig(dockerCommands.value, stagingDirectory.value) + dockerGenerateConfig := { + val _ = validatePackage.value + generateDockerConfig(dockerCommands.value, stagingDirectory.value) + } ) ) @@ -180,6 +222,14 @@ object DockerPlugin extends AutoPlugin { private final def makeFrom(dockerBaseImage: String): CmdLike = Cmd("FROM", dockerBaseImage) + /** + * @param dockerBaseImage + * @param name + * @return FROM command + */ + private final def makeFromAs(dockerBaseImage: String, name: String): CmdLike = + Cmd("FROM", dockerBaseImage, "as", name) + /** * @param label * @return LABEL command @@ -205,16 +255,41 @@ object DockerPlugin extends AutoPlugin { Cmd("WORKDIR", dockerBaseDirectory) /** - * @param dockerVersion * @param dockerBaseDirectory the installation directory + * @return COPY command copying all files inside the installation directory + */ + private final def makeCopy(dockerBaseDirectory: String): CmdLike = { + + /** + * This is the file path of the file in the Docker image, and does not depend on the OS where the image + * is being built. This means that it needs to be the Unix file separator even when the image is built + * on e.g. Windows systems. + */ + val files = dockerBaseDirectory.split(UnixSeparatorChar)(1) + Cmd("COPY", s"$files /$files") + } + + /** + * @param dockerBaseDirectory the installation directory + * @param from files are copied from the given build stage + * @param daemonUser + * @param daemonGroup + * @return COPY command copying all files inside the directory from another build stage. + */ + private final def makeCopyFrom(dockerBaseDirectory: String, + from: String, + daemonUser: String, + daemonGroup: String): CmdLike = + Cmd("COPY", s"--from=$from --chown=$daemonUser:$daemonGroup $dockerBaseDirectory $dockerBaseDirectory") + + /** + * @param dockerBaseDirectory the installation directory + * @param from files are copied from the given build stage * @param daemonUser * @param daemonGroup - * @return ADD command adding all files inside the installation directory + * @return COPY command copying all files inside the directory from another build stage. */ - private final def makeAdd(dockerVersion: Option[DockerVersion], - dockerBaseDirectory: String, - daemonUser: String, - daemonGroup: String): Seq[CmdLike] = { + private final def makeCopyChown(dockerBaseDirectory: String, daemonUser: String, daemonGroup: String): CmdLike = { /** * This is the file path of the file in the Docker image, and does not depend on the OS where the image @@ -222,12 +297,7 @@ object DockerPlugin extends AutoPlugin { * on e.g. Windows systems. */ val files = dockerBaseDirectory.split(UnixSeparatorChar)(1) - - if (dockerVersion.exists(DockerSupport.chownFlag)) { - Seq(Cmd("ADD", s"--chown=$daemonUser:$daemonGroup $files /$files")) - } else { - Seq(Cmd("ADD", s"$files /$files"), makeChown(daemonUser, daemonGroup, "." :: Nil)) - } + Cmd("COPY", s"--chown=$daemonUser:$daemonGroup $files /$files") } /** @@ -238,12 +308,41 @@ object DockerPlugin extends AutoPlugin { private final def makeChown(daemonUser: String, daemonGroup: String, directories: Seq[String]): CmdLike = ExecCmd("RUN", Seq("chown", "-R", s"$daemonUser:$daemonGroup") ++ directories: _*) + /** + * @return chown command, owning the installation directory with the daemonuser + */ + private final def makeChmod(chmodType: DockerChmodType, directories: Seq[String]): CmdLike = + ExecCmd("RUN", Seq("chmod", "-R", chmodType.argument) ++ directories: _*) + /** * @param daemonUser + * @param userId + * @param groupId + * @return useradd to create the daemon user with the given userId and groupId + */ + private final def makeUserAdd(daemonUser: String, userId: Int, groupId: Int): CmdLike = + Cmd( + "RUN", + "id", + "-u", + daemonUser, + "||", + "useradd", + "--system", + "--create-home", + "--uid", + userId.toString, + "--gid", + groupId.toString, + daemonUser + ) + + /** + * @param userId userId of the daemon user * @return USER docker command */ - private final def makeUser(daemonUser: String): CmdLike = - Cmd("USER", daemonUser) + private final def makeUser(userId: Int): CmdLike = + Cmd("USER", userId.toString) /** * @param entrypoint @@ -462,11 +561,52 @@ object DockerPlugin extends AutoPlugin { |As a last resort you could hard code the docker version, but it's not recommended!! | | import com.typesafe.sbt.packager.docker.DockerVersion - | dockerVersion := Some(DockerVersion(17, 5, 0, Some("ce")) + | dockerVersion := Some(DockerVersion(18, 9, 0, Some("ce")) """.stripMargin ) ) } } + private[this] def validateDockerPermissionStrategy(strategy: DockerPermissionStrategy, + dockerVersion: Option[DockerVersion]): Validation.Validator = + () => { + (strategy, dockerVersion) match { + case (DockerPermissionStrategy.MultiStage, Some(ver)) if !DockerSupport.multiStage(ver) => + List( + ValidationError( + description = + s"The detected Docker version $ver is not compatible with DockerPermissionStrategy.MultiStage", + howToFix = + """|sbt-native packager tries to parse the `docker version` output. + |To use multi-stage build, upgrade your Docker, pick another strategy, or override dockerVersion: + | + | import com.typesafe.sbt.packager.docker.DockerPermissionStrategy + | dockerPermissionStrategy := DockerPermissionStrategy.Run + | + | import com.typesafe.sbt.packager.docker.DockerVersion + | dockerVersion := Some(DockerVersion(18, 9, 0, Some("ce")) + """.stripMargin + ) + ) + case (DockerPermissionStrategy.CopyChown, Some(ver)) if !DockerSupport.chownFlag(ver) => + List( + ValidationError( + description = + s"The detected Docker version $ver is not compatible with DockerPermissionStrategy.CopyChown", + howToFix = """|sbt-native packager tries to parse the `docker version` output. + |To use --chown flag, upgrade your Docker, pick another strategy, or override dockerVersion: + | + | import com.typesafe.sbt.packager.docker.DockerPermissionStrategy + | dockerPermissionStrategy := DockerPermissionStrategy.Run + | + | import com.typesafe.sbt.packager.docker.DockerVersion + | dockerVersion := Some(DockerVersion(18, 9, 0, Some("ce")) + """.stripMargin + ) + ) + case _ => List.empty + } + } + } diff --git a/src/main/scala/com/typesafe/sbt/packager/docker/DockerSupport.scala b/src/main/scala/com/typesafe/sbt/packager/docker/DockerSupport.scala index 024ca50e9..2ddc501f2 100644 --- a/src/main/scala/com/typesafe/sbt/packager/docker/DockerSupport.scala +++ b/src/main/scala/com/typesafe/sbt/packager/docker/DockerSupport.scala @@ -5,4 +5,6 @@ object DockerSupport { def chownFlag(version: DockerVersion): Boolean = (version.major == 17 && version.minor >= 9) || version.major > 17 + def multiStage(version: DockerVersion): Boolean = + (version.major == 17 && version.minor >= 5) || version.major > 17 } 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 e80085f3c..1966d7fb5 100644 --- a/src/main/scala/com/typesafe/sbt/packager/docker/Keys.scala +++ b/src/main/scala/com/typesafe/sbt/packager/docker/Keys.scala @@ -7,6 +7,7 @@ import sbt._ /** * Docker settings */ +@deprecated("Internal use only. Please don't extend this trait", "1.3.15") trait DockerKeys { val dockerGenerateConfig = TaskKey[File]("docker-generate-config", "Generates configuration file for Docker.") val dockerPackageMappings = @@ -42,3 +43,10 @@ trait DockerKeys { val dockerCommands = TaskKey[Seq[CmdLike]]("dockerCommands", "List of docker commands that form the Dockerfile") } + +// Workaround to pass mima. +// In the next version bump we should hide DockerKeys trait to package private. +private[packager] trait DockerKeysEx extends DockerKeys { + lazy val dockerPermissionStrategy = settingKey[DockerPermissionStrategy]("The strategy to change file permissions.") + lazy val dockerChmodType = settingKey[DockerChmodType]("The file permissions for the files copied into Docker image.") +} 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 e4fac23ad..34cf84163 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,14 @@ case class CombinedCmd(cmd: String, arg: CmdLike) extends CmdLike { def makeContent: String = "%s %s\n" format (cmd, arg.makeContent) } +/** + * A break in Dockerfile to express multi-stage build. + * https://docs.docker.com/develop/develop-images/multistage-build/ + */ +case object DockerStageBreak extends CmdLike { + def makeContent: String = "\n" +} + /** Represents dockerfile used by docker when constructing packages. */ case class Dockerfile(commands: CmdLike*) { def makeContent: String = { diff --git a/src/sbt-test/docker/entrypoint/build.sbt b/src/sbt-test/docker/entrypoint/build.sbt index e8f9be44e..c3dcaaddb 100644 --- a/src/sbt-test/docker/entrypoint/build.sbt +++ b/src/sbt-test/docker/entrypoint/build.sbt @@ -1,4 +1,4 @@ -enablePlugins(DockerPlugin) +enablePlugins(JavaAppPackaging, DockerPlugin) name := "simple-test" diff --git a/src/sbt-test/docker/envVars/build.sbt b/src/sbt-test/docker/envVars/build.sbt index 1b684e5ae..bec0a3872 100644 --- a/src/sbt-test/docker/envVars/build.sbt +++ b/src/sbt-test/docker/envVars/build.sbt @@ -1,4 +1,4 @@ -enablePlugins(DockerPlugin) +enablePlugins(DockerPlugin, JavaAppPackaging) name := "simple-test" diff --git a/src/sbt-test/docker/file-permission/build.sbt b/src/sbt-test/docker/file-permission/build.sbt new file mode 100644 index 000000000..5de1e031e --- /dev/null +++ b/src/sbt-test/docker/file-permission/build.sbt @@ -0,0 +1,96 @@ +lazy val checkDockerfileDefaults = taskKey[Unit]("") +lazy val checkDockerfileWithStrategyNone = taskKey[Unit]("") +lazy val checkDockerfileWithStrategyRun = taskKey[Unit]("") +lazy val checkDockerfileWithStrategyCopyChown = taskKey[Unit]("") +lazy val checkDockerfileWithWriteExecute = taskKey[Unit]("") + +lazy val root = (project in file(".")) + .enablePlugins(DockerPlugin, JavaAppPackaging) + .settings( + name := "file-permission-test", + version := "0.1.0", + checkDockerfileDefaults := { + val dockerfile = IO.read((stagingDirectory in Docker).value / "Dockerfile") + val lines = dockerfile.linesIterator.toList + assertEquals(lines, + """FROM openjdk:8 as stage0 + |WORKDIR /opt/docker + |RUN id -u daemon || useradd --system --create-home --uid 1001 --gid 0 daemon + |COPY opt /opt + |RUN ["chmod", "-R", "u=rX,g=rX", "/opt/docker"] + | + |FROM openjdk:8 + |RUN id -u daemon || useradd --system --create-home --uid 1001 --gid 0 daemon + |WORKDIR /opt/docker + |COPY --from=stage0 --chown=daemon:root /opt/docker /opt/docker + |USER 1001 + |ENTRYPOINT ["/opt/docker/bin/file-permission-test"] + |CMD []""".stripMargin.linesIterator.toList) + }, + + checkDockerfileWithStrategyNone := { + val dockerfile = IO.read((stagingDirectory in Docker).value / "Dockerfile") + val lines = dockerfile.linesIterator.toList + assertEquals(lines, + """FROM openjdk:8 + |RUN id -u daemon || useradd --system --create-home --uid 1001 --gid 0 daemon + |WORKDIR /opt/docker + |COPY opt /opt + |USER 1001 + |ENTRYPOINT ["/opt/docker/bin/file-permission-test"] + |CMD []""".stripMargin.linesIterator.toList) + }, + + checkDockerfileWithStrategyRun := { + val dockerfile = IO.read((stagingDirectory in Docker).value / "Dockerfile") + val lines = dockerfile.linesIterator.toList + assertEquals(lines, + """FROM openjdk:8 + |RUN id -u daemon || useradd --system --create-home --uid 1001 --gid 0 daemon + |WORKDIR /opt/docker + |COPY opt /opt + |RUN ["chmod", "-R", "u=rX,g=rX", "/opt/docker"] + |USER 1001 + |ENTRYPOINT ["/opt/docker/bin/file-permission-test"] + |CMD []""".stripMargin.linesIterator.toList) + }, + + checkDockerfileWithStrategyCopyChown := { + val dockerfile = IO.read((stagingDirectory in Docker).value / "Dockerfile") + val lines = dockerfile.linesIterator.toList + assertEquals(lines, + """FROM openjdk:8 + |RUN id -u daemon || useradd --system --create-home --uid 1001 --gid 0 daemon + |WORKDIR /opt/docker + |COPY --chown=daemon:root opt /opt + |USER 1001 + |ENTRYPOINT ["/opt/docker/bin/file-permission-test"] + |CMD []""".stripMargin.linesIterator.toList) + }, + + checkDockerfileWithWriteExecute := { + val dockerfile = IO.read((stagingDirectory in Docker).value / "Dockerfile") + val lines = dockerfile.linesIterator.toList + assertEquals(lines, + """FROM openjdk:8 as stage0 + |WORKDIR /opt/docker + |RUN id -u daemon || useradd --system --create-home --uid 1001 --gid 0 daemon + |COPY opt /opt + |RUN ["chmod", "-R", "u=rwX,g=rwX", "/opt/docker"] + | + |FROM openjdk:8 + |RUN id -u daemon || useradd --system --create-home --uid 1001 --gid 0 daemon + |WORKDIR /opt/docker + |COPY --from=stage0 --chown=daemon:root /opt/docker /opt/docker + |USER 1001 + |ENTRYPOINT ["/opt/docker/bin/file-permission-test"] + |CMD []""".stripMargin.linesIterator.toList) + } + ) + +def assertEquals(left: List[String], right: List[String]) = + assert(left == right, + "\n" + ((left zip right) flatMap { case (a: String, b: String) => + if (a == b) Nil + else List("- " + a, "+ " + b) + }).mkString("\n")) diff --git a/src/sbt-test/docker/file-permission/changes/dockerversion.sbt b/src/sbt-test/docker/file-permission/changes/dockerversion.sbt new file mode 100644 index 000000000..6d56d90a8 --- /dev/null +++ b/src/sbt-test/docker/file-permission/changes/dockerversion.sbt @@ -0,0 +1,3 @@ +import com.typesafe.sbt.packager.docker._ + +dockerVersion := Some(DockerVersion(1, 13, 0, None)) diff --git a/src/sbt-test/docker/file-permission/changes/strategy-copychown.sbt b/src/sbt-test/docker/file-permission/changes/strategy-copychown.sbt new file mode 100644 index 000000000..6919ff3eb --- /dev/null +++ b/src/sbt-test/docker/file-permission/changes/strategy-copychown.sbt @@ -0,0 +1,3 @@ +import com.typesafe.sbt.packager.docker._ + +dockerPermissionStrategy := DockerPermissionStrategy.CopyChown diff --git a/src/sbt-test/docker/file-permission/changes/strategy-none.sbt b/src/sbt-test/docker/file-permission/changes/strategy-none.sbt new file mode 100644 index 000000000..34ae7cb35 --- /dev/null +++ b/src/sbt-test/docker/file-permission/changes/strategy-none.sbt @@ -0,0 +1,3 @@ +import com.typesafe.sbt.packager.docker._ + +dockerPermissionStrategy := DockerPermissionStrategy.None diff --git a/src/sbt-test/docker/file-permission/changes/strategy-run.sbt b/src/sbt-test/docker/file-permission/changes/strategy-run.sbt new file mode 100644 index 000000000..07c41802f --- /dev/null +++ b/src/sbt-test/docker/file-permission/changes/strategy-run.sbt @@ -0,0 +1,3 @@ +import com.typesafe.sbt.packager.docker._ + +dockerPermissionStrategy := DockerPermissionStrategy.Run diff --git a/src/sbt-test/docker/file-permission/changes/write-execute.sbt b/src/sbt-test/docker/file-permission/changes/write-execute.sbt new file mode 100644 index 000000000..bad9cfbca --- /dev/null +++ b/src/sbt-test/docker/file-permission/changes/write-execute.sbt @@ -0,0 +1,4 @@ +import com.typesafe.sbt.packager.docker._ + +dockerPermissionStrategy := DockerPermissionStrategy.MultiStage +dockerChmodType := DockerChmodType.UserGroupWriteExecute diff --git a/src/sbt-test/docker/file-permission/project/plugins.sbt b/src/sbt-test/docker/file-permission/project/plugins.sbt new file mode 100644 index 000000000..b53de154c --- /dev/null +++ b/src/sbt-test/docker/file-permission/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % sys.props("project.version")) diff --git a/src/sbt-test/docker/file-permission/test b/src/sbt-test/docker/file-permission/test new file mode 100644 index 000000000..c655bfb25 --- /dev/null +++ b/src/sbt-test/docker/file-permission/test @@ -0,0 +1,27 @@ +# Stage the distribution and ensure files show up. +> docker:publishLocal +> checkDockerfileDefaults + +$ copy-file changes/strategy-none.sbt change.sbt +> reload +> docker:publishLocal +> checkDockerfileWithStrategyNone + +$ copy-file changes/strategy-run.sbt change.sbt +> reload +> docker:publishLocal +> checkDockerfileWithStrategyRun + +$ copy-file changes/dockerversion.sbt change.sbt +> reload +-> docker:stage + +$ copy-file changes/strategy-copychown.sbt change.sbt +> reload +> docker:publishLocal +> checkDockerfileWithStrategyCopyChown + +$ copy-file changes/write-execute.sbt change.sbt +> reload +> docker:publishLocal +> checkDockerfileWithWriteExecute diff --git a/src/sbt-test/docker/labels/build.sbt b/src/sbt-test/docker/labels/build.sbt index 818491b08..4a7a0dddf 100644 --- a/src/sbt-test/docker/labels/build.sbt +++ b/src/sbt-test/docker/labels/build.sbt @@ -1,4 +1,4 @@ -enablePlugins(DockerPlugin) +enablePlugins(JavaAppPackaging, DockerPlugin) name := "simple-test" diff --git a/src/sbt-test/docker/ports/build.sbt b/src/sbt-test/docker/ports/build.sbt index 3411d89ea..fc85171b2 100644 --- a/src/sbt-test/docker/ports/build.sbt +++ b/src/sbt-test/docker/ports/build.sbt @@ -1,4 +1,4 @@ -enablePlugins(DockerPlugin) +enablePlugins(JavaAppPackaging, DockerPlugin) name := "simple-test" diff --git a/src/sbt-test/docker/rmi/test b/src/sbt-test/docker/rmi/test index d56b456be..7e6941282 100644 --- a/src/sbt-test/docker/rmi/test +++ b/src/sbt-test/docker/rmi/test @@ -2,4 +2,4 @@ > docker:publishLocal $ exec bash -c 'docker images | grep -q rmi' > docker:clean -$ exec bash -c '[[ $(docker images) != *"rmi"* ]]' +$ exec bash -c '[[ $(docker images) != "rmi"* ]]' diff --git a/src/sbt-test/docker/staging/build.sbt b/src/sbt-test/docker/staging/build.sbt index 92a6f0a49..1a9958111 100644 --- a/src/sbt-test/docker/staging/build.sbt +++ b/src/sbt-test/docker/staging/build.sbt @@ -1,4 +1,4 @@ -enablePlugins(DockerPlugin) +enablePlugins(JavaAppPackaging, DockerPlugin) name := "simple-test" diff --git a/src/sbt-test/docker/test-packageName/build.sbt b/src/sbt-test/docker/test-packageName/build.sbt index 6ee89cbf3..4bf7c7698 100644 --- a/src/sbt-test/docker/test-packageName/build.sbt +++ b/src/sbt-test/docker/test-packageName/build.sbt @@ -1,5 +1,6 @@ enablePlugins(JavaAppPackaging) +organization := "com.example" name := "docker-test" // packageName := "docker-package" // sets the executable script, too diff --git a/src/sbt-test/docker/udp-only-ports/build.sbt b/src/sbt-test/docker/udp-only-ports/build.sbt index 1c8b47ec8..8c6fc5fde 100644 --- a/src/sbt-test/docker/udp-only-ports/build.sbt +++ b/src/sbt-test/docker/udp-only-ports/build.sbt @@ -1,4 +1,4 @@ -enablePlugins(DockerPlugin) +enablePlugins(JavaAppPackaging, DockerPlugin) name := "simple-test" diff --git a/src/sbt-test/docker/volumes/build.sbt b/src/sbt-test/docker/volumes/build.sbt index 04c46c910..2413c7dad 100644 --- a/src/sbt-test/docker/volumes/build.sbt +++ b/src/sbt-test/docker/volumes/build.sbt @@ -1,4 +1,4 @@ -enablePlugins(DockerPlugin) +enablePlugins(JavaAppPackaging, DockerPlugin) name := "simple-test" diff --git a/src/sbt-test/docker/volumes/test b/src/sbt-test/docker/volumes/test index 03bc826a9..5ad00d9be 100644 --- a/src/sbt-test/docker/volumes/test +++ b/src/sbt-test/docker/volumes/test @@ -1,5 +1,5 @@ # Stage the distribution and ensure files show up. > docker:stage $ exec grep -q -F 'VOLUME ["/opt/docker/logs", "/opt/docker/config"]' target/docker/stage/Dockerfile -$ exec grep -q -F 'RUN ["chown", "-R", "daemon:daemon", "/opt/docker/logs", "/opt/docker/config"]' target/docker/stage/Dockerfile +$ exec grep -q -F 'RUN ["chown", "-R", "daemon:root", "/opt/docker/logs", "/opt/docker/config"]' target/docker/stage/Dockerfile $ exec grep -q -F 'RUN ["mkdir", "-p", "/opt/docker/logs", "/opt/docker/config"]' target/docker/stage/Dockerfile