hadolint 1.12.0 → 1.13.0
raw patch · 5 files changed
+230/−175 lines, 5 files
Files
- app/Main.hs +43/−170
- hadolint.cabal +8/−5
- src/Hadolint.hs +9/−0
- src/Hadolint/Config.hs +84/−0
- src/Hadolint/Lint.hs +86/−0
app/Main.hs view
@@ -5,91 +5,49 @@ module Main where import Control.Applicative-import Control.Monad (filterM)-import Data.Coerce (coerce)-import Data.Maybe (fromMaybe, listToMaybe)+import qualified Data.List.NonEmpty as NonEmpty import Data.Semigroup ((<>)) import qualified Data.Set as Set import Data.String-import Data.Text (Text) import qualified Data.Version-import qualified Data.Yaml as Yaml import qualified Development.GitRev-import GHC.Generics-import qualified Language.Docker as Docker-import Language.Docker.Syntax (Dockerfile) import Options.Applicative hiding (ParseError) import qualified Paths_hadolint -- version from hadolint.cabal file-import System.Directory- (XdgDirectory(..), doesFileExist, getCurrentDirectory,- getXdgDirectory) import System.Exit (exitFailure, exitSuccess)-import System.FilePath ((</>)) -import qualified Hadolint.Formatter.Checkstyle as Checkstyle-import qualified Hadolint.Formatter.Codeclimate as Codeclimate-import qualified Hadolint.Formatter.Format as Format-import qualified Hadolint.Formatter.Json as Json-import qualified Hadolint.Formatter.TTY as TTY-import qualified Hadolint.Formatter.Codacy as Codacy-import qualified Hadolint.Rules as Rules--type IgnoreRule = Text--type TrustedRegistry = Text--data OutputFormat- = Json- | TTY- | CodeclimateJson- | Checkstyle- | Codacy- deriving (Show, Eq)+import qualified Hadolint -data LintOptions = LintOptions+data CommandOptions = CommandOptions { showVersion :: Bool , configFile :: Maybe FilePath- , format :: OutputFormat- , ignoreRules :: [IgnoreRule]+ , format :: Hadolint.OutputFormat , dockerfiles :: [String]- , rulesConfig :: Rules.RulesConfig- } deriving (Show)--data ConfigFile = ConfigFile- { ignored :: Maybe [IgnoreRule]- , trustedRegistries :: Maybe [TrustedRegistry]- } deriving (Show, Eq, Generic)--instance Yaml.FromJSON ConfigFile--ignoreFilter :: [IgnoreRule] -> Rules.RuleCheck -> Bool-ignoreFilter ignoredRules (Rules.RuleCheck (Rules.Metadata code _ _) _ _ _) =- code `notElem` ignoredRules+ , lintingOptions :: Hadolint.LintOptions+ } -toOutputFormat :: String -> Maybe OutputFormat-toOutputFormat "json" = Just Json-toOutputFormat "tty" = Just TTY-toOutputFormat "codeclimate" = Just CodeclimateJson-toOutputFormat "checkstyle" = Just Checkstyle-toOutputFormat "codacy" = Just Codacy+toOutputFormat :: String -> Maybe Hadolint.OutputFormat+toOutputFormat "json" = Just Hadolint.Json+toOutputFormat "tty" = Just Hadolint.TTY+toOutputFormat "codeclimate" = Just Hadolint.CodeclimateJson+toOutputFormat "checkstyle" = Just Hadolint.Checkstyle+toOutputFormat "codacy" = Just Hadolint.Codacy toOutputFormat _ = Nothing -showFormat :: OutputFormat -> String-showFormat Json = "json"-showFormat TTY = "tty"-showFormat CodeclimateJson = "codeclimate"-showFormat Checkstyle = "checkstyle"-showFormat Codacy = "codacy"+showFormat :: Hadolint.OutputFormat -> String+showFormat Hadolint.Json = "json"+showFormat Hadolint.TTY = "tty"+showFormat Hadolint.CodeclimateJson = "codeclimate"+showFormat Hadolint.Checkstyle = "checkstyle"+showFormat Hadolint.Codacy = "codacy" -parseOptions :: Parser LintOptions+parseOptions :: Parser CommandOptions parseOptions =- LintOptions <$> -- CLI options parser definition+ CommandOptions <$> -- CLI options parser definition version <*> configFile <*> outputFormat <*>- ignoreList <*> files <*>- parseRulesConfig+ lintOptions where version = switch (long "version" <> short 'v' <> help "Show version") --@@ -106,8 +64,9 @@ (maybeReader toOutputFormat) (long "format" <> -- options for the output format short 'f' <>- help "The output format for the results [tty | json | checkstyle | codeclimate | codacy]" <>- value TTY <> -- The default value+ help+ "The output format for the results [tty | json | checkstyle | codeclimate | codacy]" <>+ value Hadolint.TTY <> -- The default value showDefaultWith showFormat <> completeWith ["tty", "json", "checkstyle", "codeclimate", "codacy"]) --@@ -122,9 +81,12 @@ -- | Parse a list of dockerfile names files = many (argument str (metavar "DOCKERFILE..." <> action "file")) --- -- | Parses all the optional rules configuration+ -- | Parse the rule ignore list and the rules configuration into a LintOptions+ lintOptions = Hadolint.LintOptions <$> ignoreList <*> parseRulesConfig+ --+ -- | Parse all the optional rules configuration parseRulesConfig =- Rules.RulesConfig . Set.fromList . fmap fromString <$>+ Hadolint.RulesConfig . Set.fromList . fmap fromString <$> many (strOption (long "trusted-registry" <>@@ -132,118 +94,29 @@ metavar "REGISTRY (e.g. docker.io)")) main :: IO ()-main = execParser opts >>= applyConfig >>= lint+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 -> do+ res <- Hadolint.lint conf files+ Hadolint.printResultsAndExit (format cmd) res opts = info (helper <*> parseOptions) (fullDesc <> progDesc "Lint Dockerfile for errors and best practices" <> header "hadolint - Dockerfile Linter written in Haskell") -applyConfig :: LintOptions -> IO LintOptions-applyConfig o- | not (null (ignoreRules o)) && rulesConfig o /= mempty = return o- | otherwise = do- theConfig <-- case configFile o of- Nothing -> findConfig- c -> return c- case theConfig of- Nothing -> return o- Just config -> parseAndApply config- where- findConfig = do- localConfigFile <- (</> ".hadolint.yaml") <$> getCurrentDirectory- configFile <- getXdgDirectory XdgConfig "hadolint.yaml"- listToMaybe <$> filterM doesFileExist [localConfigFile, configFile]- parseAndApply config = do- result <- Yaml.decodeFileEither config- case result of- Left err -> printError err config- Right (ConfigFile ignore trusted) -> return (override ignore trusted)- -- | Applies the configuration found in the file to the passed LintOptions- override ignore trusted = applyTrusted trusted . applyIgnore ignore $ o- applyIgnore ignore opts =- case ignoreRules opts of- [] -> opts {ignoreRules = fromMaybe [] ignore}- _ -> opts- applyTrusted trusted opts- | null (Rules.allowedRegistries (rulesConfig opts)) =- opts {rulesConfig = toRules trusted <> rulesConfig opts}- | otherwise = opts- -- | Converts a list of TrustedRegistry to a RulesConfig record- toRules (Just trusted) = Rules.RulesConfig (Set.fromList . coerce $ trusted)- toRules _ = mempty- printError err config =- case err of- Yaml.AesonException e ->- error $- unlines- [ "Error parsing your config file in '" ++ config ++ "':"- , "It should contain one of the keys 'ignored' or 'trustedRegistries'. For example:\n"- , "ignored:"- , "\t- DL3000"- , "\t- SC1099\n\n"- , "The key 'trustedRegistries' should contain the names of the allowed docker registries:\n"- , "allowedRegistries:"- , "\t- docker.io"- , "\t- my-company.com"- , ""- , e- ]- _ ->- error $- "Error parsing your config file in '" ++- config ++ "': " ++ Yaml.prettyPrintParseException err---- | Support UNIX convention of passing "-" instead of "/dev/stdin"-parseFilename :: String -> String-parseFilename "-" = "/dev/stdin"-parseFilename s = s- getVersion :: String getVersion | $(Development.GitRev.gitDescribe) == "UNKNOWN" = "Haskell Dockerfile Linter " ++ Data.Version.showVersion Paths_hadolint.version ++ "-no-git" | otherwise = "Haskell Dockerfile Linter " ++ $(Development.GitRev.gitDescribe)---- | Performs the process of parsing the dockerfile and analyzing it with all the applicable--- rules, depending on the list of ignored rules.--- Depending on the preferred printing format, it will output the results to stdout-lint :: LintOptions -> IO ()-lint LintOptions {showVersion = True} = putStrLn getVersion >> exitSuccess-lint LintOptions {dockerfiles = []} = putStrLn "Please provide a Dockerfile" >> exitFailure-lint LintOptions {ignoreRules = ignoreList, dockerfiles = dFiles, format, rulesConfig} = do- processedFiles <- mapM (lintDockerfile ignoreList) dFiles- let allResults = results processedFiles- printResult allResults- if allResults /= mempty- then exitFailure- else exitSuccess- where- results = foldMap Format.toResult -- Parse and check rules for each dockerfile,- -- then convert them to a Result and combine with- -- the result of the previous dockerfile results- printResult res =- case format of- TTY -> TTY.printResult res- Json -> Json.printResult res- Checkstyle -> Checkstyle.printResult res- CodeclimateJson -> Codeclimate.printResult res >> exitSuccess- Codacy -> Codacy.printResult res >> exitSuccess- lintDockerfile ignoreRules dockerFile = do- ast <- Docker.parseFile (parseFilename dockerFile)- return (processedFile ast)- where- processedFile = fmap processRules- processRules fileLines = filter ignoredRules (analyzeAll rulesConfig fileLines)- ignoredRules = ignoreFilter ignoreRules---- | Returns the result of applying all the rules to the given dockerfile-analyzeAll :: Rules.RulesConfig -> Dockerfile -> [Rules.RuleCheck]-analyzeAll config = Rules.analyze (Rules.rules ++ Rules.optionalRules config)---- | Helper to analyze AST quickly in GHCI-analyzeEither :: Rules.RulesConfig -> Either t Dockerfile -> [Rules.RuleCheck]-analyzeEither _ (Left _) = []-analyzeEither config (Right dockerFile) = analyzeAll config dockerFile
hadolint.cabal view
@@ -2,10 +2,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 4ff12972c5a652d9e4a6df3e276fdb1a565eaf408b15b14f6e7f9c22a20ade5d+-- hash: a69d376d8be88ae5432a97710688afbe46ad227c9262d12796a4357211b33238 name: hadolint-version: 1.12.0+version: 1.13.0 synopsis: Dockerfile Linter JavaScript API description: A smarter Dockerfile linter that helps you build best practice Docker images. category: Development@@ -34,19 +34,25 @@ , base >=4.8 && <5 , bytestring , containers+ , directory >=1.3.0+ , filepath , language-docker >=6.0.4 && <7 , megaparsec >=6.4 , mtl , split >=0.2 , text , void+ , yaml exposed-modules:+ Hadolint+ Hadolint.Config Hadolint.Formatter.Checkstyle Hadolint.Formatter.Codacy Hadolint.Formatter.Codeclimate Hadolint.Formatter.Format Hadolint.Formatter.Json Hadolint.Formatter.TTY+ Hadolint.Lint Hadolint.Rules Hadolint.Shell other-modules:@@ -61,15 +67,12 @@ build-depends: base >=4.8 && <5 , containers- , directory >=1.3.0- , filepath , gitrev >=1.3.1 , hadolint , language-docker >=6.0.4 && <7 , megaparsec >=6.4 , optparse-applicative >=0.14.0 , text- , yaml if !(os(osx)) ld-options: -static -pthread other-modules:
+ src/Hadolint.hs view
@@ -0,0 +1,9 @@+module Hadolint+ ( module Hadolint.Lint+ , module Hadolint.Rules+ , module Hadolint.Config+ ) where++import Hadolint.Config+import Hadolint.Lint+import Hadolint.Rules
+ src/Hadolint/Config.hs view
@@ -0,0 +1,84 @@+{-# LANGUAGE DeriveGeneric #-}++module Hadolint.Config (applyConfig) where++import Control.Monad (filterM)+import Data.Coerce (coerce)+import Data.Maybe (fromMaybe, listToMaybe)+import qualified Data.Set as Set+import qualified Data.Yaml as Yaml+import GHC.Generics+import qualified Language.Docker as Docker+import System.Directory+ (XdgDirectory(..), doesFileExist, getCurrentDirectory,+ getXdgDirectory)+import System.FilePath ((</>))++import qualified Hadolint.Lint as Lint+import qualified Hadolint.Rules as Rules++data ConfigFile = ConfigFile+ { ignored :: Maybe [Lint.IgnoreRule]+ , trustedRegistries :: Maybe [Lint.TrustedRegistry]+ } deriving (Show, Eq, Generic)++instance Yaml.FromJSON ConfigFile++-- | If both the ignoreRules and rulesConfig properties of Lint options are empty+-- then this function will fill them with the default found in the passed config+-- file. If there is an error parsing the default config file, this function will+-- return the error string.+applyConfig :: Maybe FilePath -> Lint.LintOptions -> IO (Either String Lint.LintOptions)+applyConfig maybeConfig o+ | not (null (Lint.ignoreRules o)) && Lint.rulesConfig o /= mempty = return (Right o)+ | otherwise = do+ theConfig <-+ case maybeConfig of+ Nothing -> findConfig+ c -> return c+ case theConfig of+ Nothing -> return (Right o)+ Just config -> parseAndApply config+ where+ findConfig = do+ localConfigFile <- (</> ".hadolint.yaml") <$> getCurrentDirectory+ configFile <- getXdgDirectory XdgConfig "hadolint.yaml"+ listToMaybe <$> filterM doesFileExist [localConfigFile, configFile]+ parseAndApply :: FilePath -> IO (Either String Lint.LintOptions)+ parseAndApply configFile = do+ result <- Yaml.decodeFileEither configFile+ case result of+ Left err -> return $ Left (formatError err configFile)+ Right (ConfigFile ignore trusted) -> return (Right (override ignore trusted))+ -- | Applies the configuration found in the file to the passed Lint.LintOptions+ override ignore trusted = applyTrusted trusted . applyIgnore ignore $ o+ applyIgnore ignore opts =+ case Lint.ignoreRules opts of+ [] -> opts {Lint.ignoreRules = fromMaybe [] ignore}+ _ -> opts+ applyTrusted trusted opts+ | null (Rules.allowedRegistries (Lint.rulesConfig opts)) =+ opts {Lint.rulesConfig = toRules trusted <> Lint.rulesConfig opts}+ | otherwise = opts+ -- | Converts a list of TrustedRegistry to a RulesConfig record+ toRules (Just trusted) = Rules.RulesConfig (Set.fromList . coerce $ trusted)+ toRules _ = mempty+ formatError err config =+ case err of+ Yaml.AesonException e ->+ unlines+ [ "Error parsing your config file in '" ++ config ++ "':"+ , "It should contain one of the keys 'ignored' or 'trustedRegistries'. For example:\n"+ , "ignored:"+ , "\t- DL3000"+ , "\t- SC1099\n\n"+ , "The key 'trustedRegistries' should contain the names of the allowed docker registries:\n"+ , "allowedRegistries:"+ , "\t- docker.io"+ , "\t- my-company.com"+ , ""+ , e+ ]+ _ ->+ "Error parsing your config file in '" +++ config ++ "': " ++ Yaml.prettyPrintParseException err
+ src/Hadolint/Lint.hs view
@@ -0,0 +1,86 @@+{-# LANGUAGE NamedFieldPuns #-}++module Hadolint.Lint where++import qualified Data.List.NonEmpty as NonEmpty+import Data.Text (Text)+import qualified Language.Docker as Docker+import Language.Docker.Parser (DockerfileError)+import Language.Docker.Syntax (Dockerfile)+import System.Exit (exitFailure, exitSuccess)++import qualified Hadolint.Formatter.Checkstyle as Checkstyle+import qualified Hadolint.Formatter.Codacy as Codacy+import qualified Hadolint.Formatter.Codeclimate as Codeclimate+import qualified Hadolint.Formatter.Format as Format+import qualified Hadolint.Formatter.Json as Json+import qualified Hadolint.Formatter.TTY as TTY+import qualified Hadolint.Rules as Rules++type IgnoreRule = Text++type TrustedRegistry = Text++data LintOptions = LintOptions+ { ignoreRules :: [IgnoreRule]+ , rulesConfig :: Rules.RulesConfig+ } deriving (Show)++data OutputFormat+ = Json+ | TTY+ | CodeclimateJson+ | Checkstyle+ | Codacy+ deriving (Show, Eq)++printResultsAndExit :: OutputFormat -> Format.Result Char DockerfileError -> IO ()+printResultsAndExit format allResults = do+ printResult allResults+ if allResults /= mempty+ then exitFailure+ else exitSuccess+ where+ printResult res =+ case format of+ TTY -> TTY.printResult res+ Json -> Json.printResult res+ Checkstyle -> Checkstyle.printResult res+ CodeclimateJson -> Codeclimate.printResult res >> exitSuccess+ Codacy -> Codacy.printResult res >> exitSuccess++-- | Performs the process of parsing the dockerfile and analyzing it with all the applicable+-- rules, depending on the list of ignored rules.+-- Depending on the preferred printing format, it will output the results to stdout+lint :: LintOptions -> NonEmpty.NonEmpty String -> IO (Format.Result Char DockerfileError)+lint LintOptions {ignoreRules = ignoreList, rulesConfig} dFiles = do+ processedFiles <- mapM (lintDockerfile ignoreList) (NonEmpty.toList dFiles)+ return (results processedFiles)+ where+ results = foldMap Format.toResult -- Parse and check rules for each dockerfile,+ -- then convert them to a Result and combine with+ -- the result of the previous dockerfile results+ lintDockerfile ignoreRules dockerFile = do+ ast <- Docker.parseFile (parseFilename dockerFile)+ return (processedFile ast)+ where+ processedFile = fmap processRules+ processRules fileLines = filter ignoredRules (analyzeAll rulesConfig fileLines)+ ignoredRules = ignoreFilter ignoreRules+ -- | Returns true if the rule should be ignored+ ignoreFilter :: [IgnoreRule] -> Rules.RuleCheck -> Bool+ ignoreFilter rules (Rules.RuleCheck (Rules.Metadata code _ _) _ _ _) =+ code `notElem` rules+ -- | Support UNIX convention of passing "-" instead of "/dev/stdin"+ parseFilename :: String -> String+ parseFilename "-" = "/dev/stdin"+ parseFilename s = s++-- | Returns the result of applying all the rules to the given dockerfile+analyzeAll :: Rules.RulesConfig -> Dockerfile -> [Rules.RuleCheck]+analyzeAll config = Rules.analyze (Rules.rules ++ Rules.optionalRules config)++-- | Helper to analyze AST quickly in GHCI+analyzeEither :: Rules.RulesConfig -> Either t Dockerfile -> [Rules.RuleCheck]+analyzeEither _ (Left _) = []+analyzeEither config (Right dockerFile) = analyzeAll config dockerFile