Skip to content

Commit

Permalink
Wip/1026 validate task (#1124)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
muuki88 authored Sep 11, 2018
1 parent 1d71681 commit 23583eb
Show file tree
Hide file tree
Showing 15 changed files with 290 additions and 17 deletions.
11 changes: 7 additions & 4 deletions src/main/scala/com/typesafe/sbt/PackagerPlugin.scala
Original file line number Diff line number Diff line change
@@ -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 ==
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions src/main/scala/com/typesafe/sbt/packager/Keys.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,4 @@ object Keys
with archetypes.systemloader.SystemloaderKeys
with archetypes.scripts.BashStartScriptKeys
with archetypes.scripts.BatStartScriptKeys
with validation.ValidationKeys
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down Expand Up @@ -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 ==== */
Expand Down
59 changes: 59 additions & 0 deletions src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
)
Expand Down Expand Up @@ -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
)
)
}
}

}
8 changes: 7 additions & 1 deletion src/main/scala/com/typesafe/sbt/packager/rpm/RpmPlugin.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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
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.
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
)

Expand Down Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
}

}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 23583eb

Please sign in to comment.