hadolint-2.0.0: app/Main.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE TemplateHaskell #-}
module Main where
import Control.Applicative
import qualified Data.Bifunctor as Bifunctor
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Map as Map
import Data.Maybe
import qualified Data.Sequence as Seq
import qualified Data.Set as Set
import Data.String (IsString (fromString))
import qualified Data.Text as Text
import qualified Data.Version
import qualified Development.GitRev
import qualified Hadolint
import qualified Hadolint.Rule as Rule
import Options.Applicative
( Parser,
action,
argument,
completeWith,
eitherReader,
execParser,
fullDesc,
header,
help,
helper,
info,
long,
maybeReader,
metavar,
option,
progDesc,
ReadM,
short,
showDefaultWith,
str,
strOption,
switch,
value,
)
-- version from hadolint.cabal file
import qualified Paths_hadolint as Meta
import System.Environment
import System.Exit (exitFailure, exitSuccess)
data CommandOptions = CommandOptions
{ showVersion :: Bool,
noFail :: Bool,
nocolor :: Bool,
configFile :: Maybe FilePath,
format :: Hadolint.OutputFormat,
dockerfiles :: [String],
lintingOptions :: Hadolint.LintOptions
}
toOutputFormat :: String -> Maybe Hadolint.OutputFormat
toOutputFormat "json" = Just Hadolint.Json
toOutputFormat "tty" = Just Hadolint.TTY
toOutputFormat "codeclimate" = Just Hadolint.CodeclimateJson
toOutputFormat "gitlab_codeclimate" = Just Hadolint.GitlabCodeclimateJson
toOutputFormat "checkstyle" = Just Hadolint.Checkstyle
toOutputFormat "codacy" = Just Hadolint.Codacy
toOutputFormat _ = Nothing
showFormat :: Hadolint.OutputFormat -> String
showFormat Hadolint.Json = "json"
showFormat Hadolint.TTY = "tty"
showFormat Hadolint.CodeclimateJson = "codeclimate"
showFormat Hadolint.GitlabCodeclimateJson = "gitlab_codeclimate"
showFormat Hadolint.Checkstyle = "checkstyle"
showFormat Hadolint.Codacy = "codacy"
parseOptions :: Parser CommandOptions
parseOptions =
CommandOptions
<$> version -- CLI options parser definition
<*> noFail
<*> nocolor
<*> configFile
<*> outputFormat
<*> files
<*> lintOptions
where
version = switch (long "version" <> short 'v' <> help "Show version")
noFail = switch (long "no-fail" <> help "Don't exit with a failure status code when any rule is violated")
nocolor = switch (long "no-color" <> help "Don't colorize output")
strictlabels = switch (long "strict-labels"
<> help "Do not permit labels other than specified in `label-schema`")
configFile =
optional
( strOption
( long "config" <> short 'c' <> metavar "FILENAME"
<> help "Path to the configuration file"
)
)
outputFormat =
option
(maybeReader toOutputFormat)
( long "format"
<> short 'f' -- options for the output format
<> help
"The output format for the results [tty | json | checkstyle | codeclimate | gitlab_codeclimate | codacy]"
<> value Hadolint.TTY
<> showDefaultWith showFormat -- The default value
<> completeWith ["tty", "json", "checkstyle", "codeclimate", "gitlab_codeclimate", "codacy"]
)
errorList =
many
( strOption
( long "error"
<> help "Make the rule `RULECODE` have the level `error`"
<> metavar "RULECODE"
)
)
warningList =
many
( strOption
( long "warning"
<> help "Make the rule `RULECODE` have the level `warning`"
<> metavar "RULECODE"
)
)
infoList =
many
( strOption
( long "info"
<> help "Make the rule `RULECODE` have the level `info`"
<> metavar "RULECODE"
)
)
styleList =
many
( strOption
( long "style"
<> help "Make the rule `RULECODE` have the level `style`"
<> metavar "RULECODE"
)
)
ignoreList =
many
( strOption
( long "ignore"
<> help "A rule to ignore. If present, the ignore list in the config file is ignored"
<> metavar "RULECODE"
)
)
files = many (argument str (metavar "DOCKERFILE..." <> action "file"))
lintOptions =
Hadolint.LintOptions
<$> errorList
<*> warningList
<*> infoList
<*> styleList
<*> ignoreList
<*> parseRulesConfig
labels = Map.fromList
<$> many
( option readSingleLabelSchema
( long "require-label"
<> help "The option --require-label=label:format makes Hadolint check that the label `label` conforms to format requirement `format`"
<> metavar "LABELSCHEMA (e.g. maintainer:text)"
)
)
parseRulesConfig =
Hadolint.RulesConfig
<$> parseAllowedRegistries
<*> labels
<*> strictlabels
parseAllowedRegistries = Set.fromList . fmap fromString
<$> many
( strOption
( long "trusted-registry"
<> help "A docker registry to allow to appear in FROM instructions"
<> metavar "REGISTRY (e.g. docker.io)"
)
)
type SingleLabelSchema = (Rule.LabelName, Rule.LabelType)
readSingleLabelSchema :: ReadM SingleLabelSchema
readSingleLabelSchema = eitherReader $ \s -> labelParser (Text.pack s)
labelParser :: Text.Text -> Either String (Rule.LabelName, Rule.LabelType)
labelParser l =
case Bifunctor.second (Rule.read . Text.drop 1) $ Text.breakOn ":" l of
(ln, Right lt) -> Right (ln, lt)
(_, Left e) -> Left $ Text.unpack e
noFailure :: Hadolint.Result s e -> Bool
noFailure (Hadolint.Result _ Seq.Empty Seq.Empty) = True
noFailure _ = False
exitProgram :: Foldable f => CommandOptions -> f (Hadolint.Result s e) -> IO ()
exitProgram cmd res
| noFail cmd = exitSuccess
| Hadolint.shallSkipErrorStatus (format cmd) = exitSuccess
| all noFailure res = exitSuccess
| otherwise = exitFailure
runLint :: CommandOptions -> Hadolint.LintOptions -> NonEmpty.NonEmpty String -> IO ()
runLint cmd conf files = do
res <- Hadolint.lintIO conf files
noColorEnv <- lookupEnv "NO_COLOR"
let noColor = nocolor cmd || isJust noColorEnv
Hadolint.printResults (format cmd) noColor res
exitProgram cmd res
main :: IO ()
main = do
cmd <- execParser opts
execute cmd
where
execute CommandOptions {showVersion = True} = putStrLn getVersion >> exitSuccess
execute CommandOptions {dockerfiles = []} =
putStrLn "Please provide a Dockerfile" >> exitFailure
execute cmd = do
lintConfig <- Hadolint.applyConfig (configFile cmd) (lintingOptions cmd)
let files = NonEmpty.fromList (dockerfiles cmd)
case lintConfig of
Left err -> error err
Right conf -> runLint cmd conf files
opts =
info
(helper <*> parseOptions)
( fullDesc <> progDesc "Lint Dockerfile for errors and best practices"
<> header "hadolint - Dockerfile Linter written in Haskell"
)
getVersion :: String
getVersion
| version == "UNKNOWN" =
"Haskell Dockerfile Linter " ++ Data.Version.showVersion Meta.version ++ "-no-git"
| otherwise = "Haskell Dockerfile Linter " ++ version
where
version = $(Development.GitRev.gitDescribe)