From 23583ebcc8cb31342ddf5ce48c6babc338296fa4 Mon Sep 17 00:00:00 2001 From: Nepomuk Seiler Date: Tue, 11 Sep 2018 14:12:55 +0200 Subject: [PATCH] Wip/1026 validate task (#1124) * FIX #1026 Add validatePackageConfiguration task This is an initial draft to provide helpful feedback to users during their configuration phase with sbt-native-packager. The intention is to give a short explanation why a warning or error is triggerd and a detailed description how to fix the issue. This is an initial draft to provide helpful feedback to users during their configuration phase with sbt-native-packager. The intention is to give a short explanation why a warning or error is triggerd and a detailed description how to fix the issue. * FIX #1026 Valiate mappings from linuxPackageMappings * Add scripted tests to check basic validation * Remove trailing comma --- .../com/typesafe/sbt/PackagerPlugin.scala | 11 ++- .../com/typesafe/sbt/packager/Keys.scala | 1 + .../sbt/packager/debian/DebianPlugin.scala | 6 ++ .../sbt/packager/docker/DockerPlugin.scala | 59 +++++++++++++++ .../typesafe/sbt/packager/rpm/RpmPlugin.scala | 8 +- .../packager/universal/UniversalPlugin.scala | 23 +++--- .../sbt/packager/validation/Validation.scala | 73 +++++++++++++++++++ .../packager/validation/ValidationKeys.scala | 22 ++++++ .../validation/ValidationResult.scala | 18 +++++ .../sbt/packager/validation/package.scala | 66 +++++++++++++++++ src/sbt-test/universal/validation/build.sbt | 5 ++ .../universal/validation/project/plugins.sbt | 1 + src/sbt-test/universal/validation/test | 6 ++ test-project-simple/build.sbt | 6 ++ test-project-simple/project/build.properties | 2 +- 15 files changed, 290 insertions(+), 17 deletions(-) create mode 100644 src/main/scala/com/typesafe/sbt/packager/validation/Validation.scala create mode 100644 src/main/scala/com/typesafe/sbt/packager/validation/ValidationKeys.scala create mode 100644 src/main/scala/com/typesafe/sbt/packager/validation/ValidationResult.scala create mode 100644 src/main/scala/com/typesafe/sbt/packager/validation/package.scala create mode 100644 src/sbt-test/universal/validation/build.sbt create mode 100644 src/sbt-test/universal/validation/project/plugins.sbt create mode 100644 src/sbt-test/universal/validation/test diff --git a/src/main/scala/com/typesafe/sbt/PackagerPlugin.scala b/src/main/scala/com/typesafe/sbt/PackagerPlugin.scala index 499cc7691..cc235c4d0 100644 --- a/src/main/scala/com/typesafe/sbt/PackagerPlugin.scala +++ b/src/main/scala/com/typesafe/sbt/PackagerPlugin.scala @@ -1,11 +1,11 @@ package com.typesafe.sbt import packager._ - import debian.DebianPlugin.autoImport.genChanges -import universal.UniversalPlugin.autoImport.{packageXzTarball, packageZipTarball} +import com.typesafe.sbt.packager.Keys.{packageXzTarball, packageZipTarball, validatePackage, validatePackageValidators} +import com.typesafe.sbt.packager.validation.Validation import sbt._ -import sbt.Keys.{name, normalizedName, packageBin} +import sbt.Keys.{name, normalizedName, packageBin, streams} /** * == SBT Native Packager Plugin == @@ -99,7 +99,10 @@ object SbtNativePackager extends AutoPlugin { packageSummary := name.value, packageName := normalizedName.value, executableScriptName := normalizedName.value, - maintainerScripts := Map() + maintainerScripts := Map(), + // no validation by default + validatePackageValidators := Seq.empty, + validatePackage := Validation.runAndThrow(validatePackageValidators.value, streams.value.log) ) object packageArchetype { diff --git a/src/main/scala/com/typesafe/sbt/packager/Keys.scala b/src/main/scala/com/typesafe/sbt/packager/Keys.scala index ef4b407dc..8ff0c3c03 100644 --- a/src/main/scala/com/typesafe/sbt/packager/Keys.scala +++ b/src/main/scala/com/typesafe/sbt/packager/Keys.scala @@ -55,3 +55,4 @@ object Keys with archetypes.systemloader.SystemloaderKeys with archetypes.scripts.BashStartScriptKeys with archetypes.scripts.BatStartScriptKeys + with validation.ValidationKeys diff --git a/src/main/scala/com/typesafe/sbt/packager/debian/DebianPlugin.scala b/src/main/scala/com/typesafe/sbt/packager/debian/DebianPlugin.scala index ca09bad0b..c4597e144 100644 --- a/src/main/scala/com/typesafe/sbt/packager/debian/DebianPlugin.scala +++ b/src/main/scala/com/typesafe/sbt/packager/debian/DebianPlugin.scala @@ -6,6 +6,7 @@ import com.typesafe.sbt.packager.archetypes.TemplateWriter import com.typesafe.sbt.packager.linux.LinuxPlugin.Users import com.typesafe.sbt.packager.linux.{LinuxFileMetaData, LinuxPackageMapping, LinuxPlugin, LinuxSymlink} import com.typesafe.sbt.packager.universal.Archives +import com.typesafe.sbt.packager.validation._ import com.typesafe.sbt.packager.{chmod, Hashing, SettingsHelper} import sbt.Keys._ import sbt._ @@ -89,6 +90,11 @@ object DebianPlugin extends AutoPlugin with DebianNativePackaging { packageDescription in Debian := (packageDescription in Linux).value, packageSummary in Debian := (packageSummary in Linux).value, maintainer in Debian := (maintainer in Linux).value, + validatePackageValidators in Debian := Seq( + nonEmptyMappings((linuxPackageMappings in Debian).value.flatMap(_.mappings)), + filesExist((linuxPackageMappings in Debian).value.flatMap(_.mappings)), + checkMaintainer((maintainer in Debian).value, asWarning = false) + ), // override the linux sourceDirectory setting sourceDirectory in Debian := sourceDirectory.value, /* ==== Debian configuration settings ==== */ 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 8db845a30..93484f699 100644 --- a/src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala +++ b/src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala @@ -11,6 +11,7 @@ import com.typesafe.sbt.packager.universal.UniversalPlugin import com.typesafe.sbt.packager.universal.UniversalPlugin.autoImport.stage import com.typesafe.sbt.SbtNativePackager.Universal import com.typesafe.sbt.packager.Compat._ +import com.typesafe.sbt.packager.validation._ import com.typesafe.sbt.packager.{MappingsHelper, Stager} import scala.sys.process.Process @@ -154,6 +155,12 @@ object DockerPlugin extends AutoPlugin { daemonUser := "daemon", daemonGroup := daemonUser.value, defaultLinuxInstallLocation := "/opt/docker", + validatePackageValidators := Seq( + nonEmptyMappings((mappings in Docker).value), + filesExist((mappings in Docker).value), + validateExposedPorts(dockerExposedPorts.value, dockerExposedUdpPorts.value), + validateDockerVersion(dockerVersion.value) + ), dockerPackageMappings := MappingsHelper.contentOf(sourceDirectory.value), dockerGenerateConfig := generateDockerConfig(dockerCommands.value, stagingDirectory.value) ) @@ -410,4 +417,56 @@ object DockerPlugin extends AutoPlugin { log.info("Published image " + tag) } + private[this] def validateExposedPorts(ports: Seq[Int], udpPorts: Seq[Int]): Validation.Validator = () => { + if (ports.isEmpty && udpPorts.isEmpty) { + List( + ValidationWarning( + description = "There are no exposed ports for your docker image", + howToFix = """| Configure the `dockerExposedPorts` or `dockerExposedUdpPorts` setting. E.g. + | + | // standard tcp ports + | dockerExposedPorts ++= Seq(9000, 9001) + | + | // for udp ports + | dockerExposedUdpPorts += 4444 + """.stripMargin + ) + ) + } else { + List.empty + } + } + + private[this] def validateDockerVersion(dockerVersion: Option[DockerVersion]): Validation.Validator = () => { + dockerVersion match { + case Some(_) => List.empty + case None => + List( + ValidationWarning( + description = + "sbt-native-packager wasn't able to identify the docker version. Some features may not be enabled", + howToFix = """|sbt-native packager tries to parse the `docker version` output. This can fail if + | + | - the output has changed: + | $ docker version --format '{{.Server.Version}}' + | + | - no `docker` executable is available + | $ which docker + | + | - you have not the required privileges to run `docker` + | + |You can display the parsed docker version in the sbt console with: + | + | $ sbt show dockerVersion + | + |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")) + """.stripMargin + ) + ) + } + } + } diff --git a/src/main/scala/com/typesafe/sbt/packager/rpm/RpmPlugin.scala b/src/main/scala/com/typesafe/sbt/packager/rpm/RpmPlugin.scala index c15fbd8a3..7b6ad8a2b 100644 --- a/src/main/scala/com/typesafe/sbt/packager/rpm/RpmPlugin.scala +++ b/src/main/scala/com/typesafe/sbt/packager/rpm/RpmPlugin.scala @@ -1,7 +1,7 @@ package com.typesafe.sbt.packager.rpm import sbt._ -import sbt.Keys.{isSnapshot, name, packageBin, sourceDirectory, streams, target, version} +import sbt.Keys._ import java.nio.charset.Charset import com.typesafe.sbt.SbtNativePackager.Linux @@ -9,6 +9,7 @@ import com.typesafe.sbt.packager.SettingsHelper import com.typesafe.sbt.packager.Keys._ import com.typesafe.sbt.packager.linux._ import com.typesafe.sbt.packager.Compat._ +import com.typesafe.sbt.packager.validation._ /** * Plugin containing all generic values used for packaging rpms. @@ -101,6 +102,11 @@ object RpmPlugin extends AutoPlugin { executableScriptName in Rpm := (executableScriptName in Linux).value, rpmDaemonLogFile := s"${(packageName in Linux).value}.log", daemonStdoutLogFile in Rpm := Some(rpmDaemonLogFile.value), + validatePackageValidators in Rpm := Seq( + nonEmptyMappings((linuxPackageMappings in Rpm).value.flatMap(_.mappings)), + filesExist((linuxPackageMappings in Rpm).value.flatMap(_.mappings)), + checkMaintainer((maintainer in Rpm).value, asWarning = false) + ), // override the linux sourceDirectory setting sourceDirectory in Rpm := sourceDirectory.value, packageArchitecture in Rpm := "noarch", diff --git a/src/main/scala/com/typesafe/sbt/packager/universal/UniversalPlugin.scala b/src/main/scala/com/typesafe/sbt/packager/universal/UniversalPlugin.scala index e733b9272..455dc3f2d 100644 --- a/src/main/scala/com/typesafe/sbt/packager/universal/UniversalPlugin.scala +++ b/src/main/scala/com/typesafe/sbt/packager/universal/UniversalPlugin.scala @@ -5,6 +5,7 @@ import sbt.Keys._ import Archives._ import com.typesafe.sbt.SbtNativePackager import com.typesafe.sbt.packager.Keys._ +import com.typesafe.sbt.packager.validation._ import com.typesafe.sbt.packager.{SettingsHelper, Stager} import sbt.Keys.TaskStreams @@ -50,7 +51,6 @@ object UniversalPlugin extends AutoPlugin { // For now, we provide delegates from dist/stage to universal... dist := (dist in Universal).value, stage := (stage in Universal).value, - // TODO - New default to naming, is this right? // TODO - We may need to do this for UniversalSrcs + UnviersalDocs name in Universal := name.value, name in UniversalDocs := (name in Universal).value, @@ -80,6 +80,7 @@ object UniversalPlugin extends AutoPlugin { ) ) ++ Seq( sourceDirectory in config := sourceDirectory.value / config.name, + validatePackageValidators in config := validatePackageValidators.value, target in config := target.value / config.name ) @@ -107,25 +108,25 @@ object UniversalPlugin extends AutoPlugin { inConfig(config)( Seq( universalArchiveOptions in packageKey := Nil, - mappings in packageKey := checkMappings(mappings.value), + mappings in packageKey := mappings.value, packageKey := packager( target.value, packageName.value, (mappings in packageKey).value, topLevelDirectory.value, (universalArchiveOptions in packageKey).value - ) + ), + validatePackageValidators in packageKey := (validatePackageValidators in config).value ++ Seq( + nonEmptyMappings((mappings in packageKey).value), + filesExist((mappings in packageKey).value), + checkMaintainer((maintainer in packageKey).value, asWarning = true) + ), + validatePackage in packageKey := Validation + .runAndThrow(validatePackageValidators.in(config, packageKey).value, streams.value.log), + packageKey := packageKey.dependsOn(validatePackage in packageKey).value ) ) - /** check that all mapped files actually exist */ - private[this] def checkMappings(mappings: Seq[(File, String)]): Seq[(File, String)] = - mappings collect { - case (f, p) => - if (f.exists) (f, p) - else sys.error("Mapped file " + f + " does not exist.") - } - /** Finds all sources in a source directory. */ private[this] def findSources(sourceDir: File): Seq[(File, String)] = ((PathFinder(sourceDir) ** AllPassFilter) --- sourceDir).pair(file => IO.relativize(sourceDir, file)) diff --git a/src/main/scala/com/typesafe/sbt/packager/validation/Validation.scala b/src/main/scala/com/typesafe/sbt/packager/validation/Validation.scala new file mode 100644 index 000000000..f25b7afae --- /dev/null +++ b/src/main/scala/com/typesafe/sbt/packager/validation/Validation.scala @@ -0,0 +1,73 @@ +package com.typesafe.sbt.packager.validation + +import sbt.Logger + +/** + * Validation result. + * + * @param errors all errors that were found during the validation + * @param warnings all warnings that were found during the validation + */ +final case class Validation(errors: List[ValidationError], warnings: List[ValidationWarning]) + +object Validation { + + /** + * A validator is a function that returns a list of validation results. + * + * + * @example Usually a validator is a function that captures some setting or task value, e.g. + * {{{ + * validatePackageValidators += { + * val universalMappings = (mappings in Universal).value + * () => { + * if (universalMappings.isEmpty) List(ValidationError(...)) else List.empt + * } + * } + * }}} + * + * The `validation` package object contains various standard validators. + * + */ + type Validator = () => List[ValidationResult] + + /** + * + * @param validators a list of validators that produce a `Validation` result + * @return aggregated result of all validator function + */ + def apply(validators: Seq[Validator]): Validation = validators.flatMap(_.apply()).foldLeft(Validation(Nil, Nil)) { + case (validation, error: ValidationError) => validation.copy(errors = validation.errors :+ error) + case (validation, warning: ValidationWarning) => validation.copy(warnings = validation.warnings :+ warning) + } + + /** + * Runs a list of validators and throws an exception after printing all + * errors and warnings with the provided logger. + * + * @param validators a list of validators that produce the validation result + * @param log used to print errors and warnings + */ + def runAndThrow(validators: Seq[Validator], log: Logger): Unit = { + val Validation(errors, warnings) = apply(validators) + + warnings.zipWithIndex.foreach { + case (warning, i) => + log.warn(s"[${i + 1}] ${warning.description}") + log.warn(warning.howToFix) + } + + errors.zipWithIndex.foreach { + case (error, i) => + log.error(s"[${i + 1}] ${error.description}") + log.error(error.howToFix) + } + + if (errors.nonEmpty) { + sys.error(s"${errors.length} error(s) found") + } + + log.success("All package validations passed") + } + +} diff --git a/src/main/scala/com/typesafe/sbt/packager/validation/ValidationKeys.scala b/src/main/scala/com/typesafe/sbt/packager/validation/ValidationKeys.scala new file mode 100644 index 000000000..d291a7018 --- /dev/null +++ b/src/main/scala/com/typesafe/sbt/packager/validation/ValidationKeys.scala @@ -0,0 +1,22 @@ +package com.typesafe.sbt.packager.validation + +import sbt._ + +trait ValidationKeys { + + /** + * A task that implements various validations for a format. + * Example usage: + * - `sbt universal:packageBin::validatePackage` + * - `sbt debian:packageBin::validatePackage` + * + * + * Each format should implement it's own validate. + * Implemented in #1026 + */ + val validatePackage = taskKey[Unit]("validates the package configuration") + + val validatePackageValidators = taskKey[Seq[Validation.Validator]]("validator functions") +} + +object ValidationKeys extends ValidationKeys diff --git a/src/main/scala/com/typesafe/sbt/packager/validation/ValidationResult.scala b/src/main/scala/com/typesafe/sbt/packager/validation/ValidationResult.scala new file mode 100644 index 000000000..7c6eb76c8 --- /dev/null +++ b/src/main/scala/com/typesafe/sbt/packager/validation/ValidationResult.scala @@ -0,0 +1,18 @@ +package com.typesafe.sbt.packager.validation + +sealed trait ValidationResult { + + /** + * Human readable and understandable description of the validation result. + */ + val description: String + + /** + * Help text on how to fix the issue. + */ + val howToFix: String + +} + +final case class ValidationError(description: String, howToFix: String) extends ValidationResult +final case class ValidationWarning(description: String, howToFix: String) extends ValidationResult diff --git a/src/main/scala/com/typesafe/sbt/packager/validation/package.scala b/src/main/scala/com/typesafe/sbt/packager/validation/package.scala new file mode 100644 index 000000000..02204fea1 --- /dev/null +++ b/src/main/scala/com/typesafe/sbt/packager/validation/package.scala @@ -0,0 +1,66 @@ +package com.typesafe.sbt.packager + +import sbt._ + +/** + * == validation == + * + * This package contains stanard validators that can be used by format and archetype plugins. + * + */ +package object validation { + + /** + * Basic validator to check if the resulting package will be empty or not. + * + * @param mappings the mappings that should be validated + * @return a validator that checks if the mappins are empty + */ + def nonEmptyMappings(mappings: Seq[(File, String)]): Validation.Validator = () => { + if (mappings.isEmpty) { + List( + ValidationError( + description = "You have no mappings defined! This will result in an empty package", + howToFix = "Try enabling an archetype, e.g. `enablePlugins(JavaAppPackaging)`" + ) + ) + } else { + List.empty + } + } + + /** + * + * @param mappings + * @return + */ + def filesExist(mappings: Seq[(File, String)]): Validation.Validator = () => { + // check that all files exist + mappings + .filter { + case (f, _) => !f.exists + } + .map { + case (file, dest) => + ValidationError( + description = s"Not found: ${file.getAbsolutePath} (mapped to $dest)", + howToFix = "Generate the file in the task/setting that adds it to the mappings task" + ) + } + .toList + } + + def checkMaintainer(maintainer: String, asWarning: Boolean): Validation.Validator = () => { + if (maintainer.isEmpty) { + val description = "The maintainer is empty" + val howToFix = """|Add this to your build.sbt + | maintainer := "your.name@company.org"""".stripMargin + + val result = if (asWarning) ValidationWarning(description, howToFix) else ValidationError(description, howToFix) + List(result) + } else { + List.empty + } + } + +} diff --git a/src/sbt-test/universal/validation/build.sbt b/src/sbt-test/universal/validation/build.sbt new file mode 100644 index 000000000..3615d803a --- /dev/null +++ b/src/sbt-test/universal/validation/build.sbt @@ -0,0 +1,5 @@ +enablePlugins(UniversalPlugin) + +name := "simple-test" + +version := "0.1.0" diff --git a/src/sbt-test/universal/validation/project/plugins.sbt b/src/sbt-test/universal/validation/project/plugins.sbt new file mode 100644 index 000000000..b53de154c --- /dev/null +++ b/src/sbt-test/universal/validation/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % sys.props("project.version")) diff --git a/src/sbt-test/universal/validation/test b/src/sbt-test/universal/validation/test new file mode 100644 index 000000000..d075c7a1b --- /dev/null +++ b/src/sbt-test/universal/validation/test @@ -0,0 +1,6 @@ +# Cannot create an empty distribution +-> universal:packageBin +$ mkdir src/universal +$ copy-file build.sbt src/universal/test-file +> universal:packageBin +$ exists target/universal/simple-test-0.1.0.zip diff --git a/test-project-simple/build.sbt b/test-project-simple/build.sbt index c6cf30b26..079c74976 100644 --- a/test-project-simple/build.sbt +++ b/test-project-simple/build.sbt @@ -22,3 +22,9 @@ javaOptions in Universal ++= Seq("-J-Xmx64m", "-J-Xms64m", "-jvm-debug 12345") //bashScriptConfigLocation := Some("${app_home}/../conf/jvmopts") mappings in UniversalSrc := (mappings in Universal).value + +maintainer in Universal := "" +mappings in Universal ++= Seq( + (baseDirectory.value / "foo.txt") -> "foo.txt", + (baseDirectory.value / "bar.txt") -> "bar.txt" +) \ No newline at end of file diff --git a/test-project-simple/project/build.properties b/test-project-simple/project/build.properties index 7b6213bdc..05313438a 100644 --- a/test-project-simple/project/build.properties +++ b/test-project-simple/project/build.properties @@ -1 +1 @@ -sbt.version=1.0.1 +sbt.version=1.1.2