hadolint 1.5.0 → 1.6.0
raw patch · 4 files changed
+157/−42 lines, 4 filesdep ~language-dockerdep ~optparse-applicativePVP ok
version bump matches the API change (PVP)
Dependency ranges changed: language-docker, optparse-applicative
API changes (from Hackage documentation)
+ Hadolint.Rules: ignored :: Dockerfile -> [(Linenumber, [String])]
Files
- README.md +21/−5
- hadolint.cabal +7/−7
- src/Hadolint/Rules.hs +71/−29
- test/Spec.hs +58/−1
README.md view
@@ -79,6 +79,22 @@ In windows, the `%LOCALAPPDATA%` environment variable is used instead of `XDG_CONFIG_HOME` +## Inline ignores++It is also possible to ignore rules by using a special comment directly above the Dockerfile+instruction you want to make an exception for. Ignore rule comments look like+`# hadolint ignore=DL3001,SC1081`. For example:++```dockerfile+# hadolint ignore=DL3006+FROM ubuntu++# hadolint ignore=DL3003,SC1035+RUN cd /tmp && echo "hello!"+```++Inline ignores will only work if place directly above the instruction.+ ## Integrations To get most of `hadolint` it is useful to integrate it as a check to your CI@@ -99,7 +115,7 @@ Please [create an issue][] if you have an idea for a good rule. | Rule | Description |-|--------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|+|:-------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------| | [DL3000](https://github.com/hadolint/hadolint/wiki/DL3000) | Use absolute WORKDIR. | | [DL3001](https://github.com/hadolint/hadolint/wiki/DL3001) | For some bash commands it makes no sense running them in a Docker container like ssh, vim, shutdown, service, ps, free, top, kill, mount, ifconfig. | | [DL3002](https://github.com/hadolint/hadolint/wiki/DL3002) | Do not switch to root USER. |@@ -121,7 +137,7 @@ | [DL3018](https://github.com/hadolint/hadolint/wiki/DL3018) | Pin versions in apk add. Instead of `apk add <package>` use `apk add <package>=<version>`. | | [DL3019](https://github.com/hadolint/hadolint/wiki/DL3019) | Use the `--no-cache` switch to avoid the need to use `--update` and remove `/var/cache/apk/*` when done installing packages. | | [DL3020](https://github.com/hadolint/hadolint/wiki/DL3020) | Use `COPY` instead of `ADD` for files and folders. |-| [DL3021](https://github.com/hadolint/hadolint/wiki/DL3021) | `COPY` with more than 2 arguments requires the last argument to end with `/` |+| [DL3021](https://github.com/hadolint/hadolint/wiki/DL3021) | `COPY` with more than 2 arguments requires the last argument to end with `/` | | [DL3022](https://github.com/hadolint/hadolint/wiki/DL3022) | `COPY --from` should reference a previously defined `FROM` alias | | [DL3023](https://github.com/hadolint/hadolint/wiki/DL3023) | `COPY --from` cannot reference its own `FROM` alias | | [DL3024](https://github.com/hadolint/hadolint/wiki/DL3024) | `FROM` aliases (stage names) must be unique |@@ -209,8 +225,7 @@ ### AST Dockerfile syntax is fully described in the [Dockerfile reference][]. Just take-a look at [Syntax.hs](https://www.stackage.org/haddock/nightly-2018-01-07/language-docker-2.0.1/Language-Docker-Syntax.html)-in the `language-docker` project to see the AST definition.+a look at [Syntax.hs][] in the `language-docker` project to see the AST definition. ## Alternatives @@ -223,7 +238,7 @@ [appveyor-img]: https://ci.appveyor.com/api/projects/status//github/hadolint/hadolint?svg=true&branch=master [appveyor]: https://ci.appveyor.com/project/hadolint/hadolint/branch/master [license-img]: https://img.shields.io/badge/license-GPL--3-blue.svg-[license]: https://tldrlegal.com/license/gnu-general-public-license-v3-(gpl-3)+[license]: https://tldrlegal.com/l/gpl-3.0 [release-img]: https://img.shields.io/github/release/hadolint/hadolint.svg [release]: https://github.com/hadolint/hadolint/releases/latest [downloads-img]: https://img.shields.io/github/downloads/hadolint/hadolint/total.svg@@ -235,3 +250,4 @@ [integration]: docs/INTEGRATION.md [create an issue]: https://github.com/hadolint/hadolint/issues/new [dockerfile reference]: http://docs.docker.com/engine/reference/builder/+[syntax.hs]: https://www.stackage.org/haddock/nightly-2018-01-07/language-docker-2.0.1/Language-Docker-Syntax.html
hadolint.cabal view
@@ -1,11 +1,11 @@--- This file has been generated from package.yaml by hpack version 0.20.0.+-- This file has been generated from package.yaml by hpack version 0.21.2. -- -- see: https://github.com/sol/hpack ----- hash: 0e73d59878f7e4b0a47b9ea2607d8a8de24ade73e92179f9c23870e7e0364c35+-- hash: 8953f4990fda4e78c53418a6f7426bc5f60f367bf771ed9e7bf015eac0868247 name: hadolint-version: 1.5.0+version: 1.6.0 synopsis: Dockerfile Linter JavaScript API description: A smarter Dockerfile linter that helps you build best practice Docker images. category: Development@@ -34,7 +34,7 @@ , base >=4.8 && <5 , bytestring , dlist- , language-docker >=2.0.0+ , language-docker >=3.0.0 , parsec >=3.1 , split >=0.2 , text@@ -60,8 +60,8 @@ , filepath , gitrev >=1.3.1 , hadolint- , language-docker >=2.0.0- , optparse-applicative <=0.14.0.0+ , language-docker >=3.0.0+ , optparse-applicative , parsec >=3.1 , yaml if !(os(osx))@@ -83,7 +83,7 @@ , bytestring >=0.10 , hadolint , hspec- , language-docker >=2.0.0+ , language-docker >=3.0.0 , parsec >=3.1 , split >=0.2 other-modules:
src/Hadolint/Rules.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE NamedFieldPuns #-}+ module Hadolint.Rules where import Control.Arrow ((&&&))@@ -7,7 +9,10 @@ import Hadolint.Bash import Language.Docker.Syntax -import ShellCheck.Interface+import qualified ShellCheck.Interface+import ShellCheck.Interface (Severity(..))+import qualified Text.Parsec as Parsec+import Text.Parsec ((<|>)) data Metadata = Metadata { code :: String@@ -70,7 +75,12 @@ -> Rule instructionRuleState code severity message = mapInstructions (Metadata code severity message) -dockerfileRule :: String -> Severity -> String -> ([Instruction] -> Bool) -> Rule+dockerfileRule ::+ String -- ^ The rule code name+ -> ShellCheck.Interface.Severity -- ^ The rule severity+ -> String -- ^ The error message associated with the rule+ -> ([Instruction] -> Bool) -- ^ The function that will check the rule+ -> Rule dockerfileRule code severity message f = rule where rule dockerfile =@@ -82,8 +92,31 @@ analyze :: [Rule] -> Dockerfile -> [RuleCheck] analyze rules dockerfile = filter failed $ concat [r dockerfile | r <- rules] where- failed (RuleCheck _ _ _ success) = not success+ failed RuleCheck {metadata = Metadata {code}, linenumber, success} =+ not success && not (wasIgnored code linenumber)+ wasIgnored c ln = not $ null [line | (line, codes) <- allIgnores, line == ln, c `elem` codes]+ allIgnores = ignored dockerfile +ignored :: Dockerfile -> [(Linenumber, [String])]+ignored dockerfile =+ [(l + 1, ignores) | (l, Just ignores) <- map (lineNumber &&& extractIgnored) dockerfile]+ where+ extractIgnored = ignoreFromInstruction . instruction+ ignoreFromInstruction (Comment comment) = either (const Nothing) Just (parseComment comment)+ ignoreFromInstruction _ = Nothing+ -- | Parses the comment text and extracts the ignored rule names+ parseComment :: String -> Either Parsec.ParseError [String]+ parseComment = Parsec.parse commentParser ""+ commentParser =+ Parsec.skipMany space >> -- The parser for the ignored rules+ Parsec.string "hadolint" >>+ Parsec.skipMany1 space >>+ Parsec.string "ignore=" >>+ Parsec.skipMany space >>+ Parsec.sepBy1 ruleName (Parsec.many space >> Parsec.char ',' >> Parsec.many space)+ space = Parsec.char ' ' <|> Parsec.char '\t'+ ruleName = Parsec.many1 (Parsec.choice $ map Parsec.char "DLSC0123456789")+ rules = [ absoluteWorkdir , shellcheckBash@@ -124,7 +157,7 @@ shellcheckBash :: Dockerfile -> [RuleCheck] shellcheckBash = concatMap check where- check (InstructionPos (Run args) source linenumber) =+ check (InstructionPos (Run (Arguments args)) source linenumber) = rmDup [RuleCheck m source linenumber False | m <- convert args] check _ = [] convert args = [commentMetadata c | c <- shellcheck $ unwords args]@@ -187,7 +220,7 @@ severity = WarningC message = "Multiple `CMD` instructions found. If you list more than one `CMD` then only the last \- \`CMD` will take effect."+ \`CMD` will take effect" check dockerfile = 1 >= length (filter (True ==) $ map isCmd dockerfile) isCmd (Cmd _) = True isCmd _ = False@@ -198,7 +231,7 @@ severity = ErrorC message = "Multiple `ENTRYPOINT` instructions found. If you list more than one `ENTRYPOINT` then \- \only the last `ENTRYPOINT` will take effect."+ \only the last `ENTRYPOINT` will take effect" check dockerfile = 1 >= length (filter (True ==) $ map isEntrypoint dockerfile) isEntrypoint (Entrypoint _) = True isEntrypoint _ = False@@ -211,7 +244,7 @@ check dockerfile = not $ anyCurl dockerfile && anyWget dockerfile anyCurl = any $ usingCmd "curl" anyWget = any $ usingCmd "wget"- usingCmd cmd (Run args) = cmd `elem` args+ usingCmd cmd (Run (Arguments args)) = cmd `elem` args usingCmd _ _ = False invalidCmd = instructionRule code severity message check@@ -221,7 +254,7 @@ message = "For some bash commands it makes no sense running them in a Docker container like `ssh`, \ \`vim`, `shutdown`, `service`, `ps`, `free`, `top`, `kill`, `mount`, `ifconfig`"- check (Run args) = head args `notElem` invalidCmds+ check (Run (Arguments args)) = head args `notElem` invalidCmds check _ = True invalidCmds = ["ssh", "vim", "shutdown", "service", "ps", "free", "top", "kill", "mount"] @@ -239,7 +272,7 @@ code = "DL3003" severity = WarningC message = "Use WORKDIR to switch to a directory"- check (Run args) = not $ usingProgram "cd" args+ check (Run (Arguments args)) = not $ usingProgram "cd" args check _ = True noSudo = instructionRule code severity message check@@ -248,25 +281,26 @@ severity = ErrorC message = "Do not use sudo as it leads to unpredictable behavior. Use a tool like gosu to enforce \- \root."- check (Run args) = not $ usingProgram "sudo" args+ \root"+ check (Run (Arguments args)) = not $ usingProgram "sudo" args check _ = True noAptGetUpgrade = instructionRule code severity message check where code = "DL3005" severity = ErrorC- message = "Do not use apt-get upgrade or dist-upgrade."- check (Run args) = not $ isInfixOf ["apt-get", "upgrade"] args+ message = "Do not use apt-get upgrade or dist-upgrade"+ check (Run (Arguments args)) = not $ isInfixOf ["apt-get", "upgrade"] args check _ = True noUntagged dockerfile = instructionRuleLine code severity message check dockerfile where code = "DL3006" severity = WarningC- message = "Always tag the version of an image explicitly."- check line (From (UntaggedImage "scratch" _)) = True- check line (From (UntaggedImage i _)) = i `elem` previouslyDefinedAliases line dockerfile+ message = "Always tag the version of an image explicitly"+ check line (From (UntaggedImage (Image _ "scratch") _)) = True+ check line (From (UntaggedImage (Image _ i) _)) =+ i `elem` previouslyDefinedAliases line dockerfile check _ _ = True noLatestTag = instructionRule code severity message check@@ -275,7 +309,7 @@ severity = WarningC message = "Using latest is prone to errors if the image will ever update. Pin the version explicitly \- \to a release tag."+ \to a release tag" check (From (TaggedImage _ tag _)) = tag /= "latest" check _ = True @@ -286,12 +320,17 @@ message = "Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get \ \install <package>=<version>`"- check (Run args) = and [versionFixed p | p <- aptGetPackages args]+ check (Run (Arguments args)) = and [versionFixed p | p <- aptGetPackages args] check _ = True versionFixed package = "=" `isInfixOf` package aptGetPackages :: [String] -> [String]-aptGetPackages args = concat [filter noOption cmd | cmd <- bashCommands args, isAptGetInstall cmd]+aptGetPackages args =+ concat+ [ filter noOption (dropOptionsWithArg ["-t", "--target-release"] cmd)+ | cmd <- bashCommands args+ , isAptGetInstall cmd+ ] where noOption arg = arg `notElem` options && not ("--" `isPrefixOf` arg) options = ["apt-get", "install", "-d", "-f", "-m", "-q", "-y", "-qq"]@@ -301,7 +340,7 @@ code = "DL3009" severity = InfoC message = "Delete the apt-get lists after installing something"- check (Run args) = not (hasUpdate args) || hasCleanup args+ check (Run (Arguments args)) = not (hasUpdate args) || hasCleanup args check _ = True hasCleanup cmd = ["rm", "-rf", "/var/lib/apt/lists/*"] `isInfixOf` cmd hasUpdate cmd = ["apt-get", "update"] `isInfixOf` cmd@@ -317,7 +356,7 @@ code = "DL3017" severity = ErrorC message = "Do not use apk upgrade"- check (Run args) = not $ isInfixOf ["apk", "upgrade"] args+ check (Run (Arguments args)) = not $ isInfixOf ["apk", "upgrade"] args check _ = True isApkAdd :: [String] -> Bool@@ -329,7 +368,7 @@ severity = WarningC message = "Pin versions in apk add. Instead of `apk add <package>` use `apk add <package>=<version>`"- check (Run args) = and [versionFixed p | p <- apkAddPackages args]+ check (Run (Arguments args)) = and [versionFixed p | p <- apkAddPackages args] check _ = True versionFixed package = "=" `isInfixOf` package @@ -351,7 +390,7 @@ message = "Use the `--no-cache` switch to avoid the need to use `--update` and remove \ \`/var/cache/apk/*` when done installing packages"- check (Run args) = not (isApkAdd args) || hasNoCacheOption args+ check (Run (Arguments args)) = not (isApkAdd args) || hasNoCacheOption args check _ = True hasNoCacheOption cmd = ["--no-cache"] `isInfixOf` cmd @@ -386,8 +425,9 @@ message = "Pin versions in pip. Instead of `pip install <package>` use `pip install \ \<package>==<version>`"- check (Run args) =- not (isPipInstall args && not (isRecursiveInstall args)) || all versionFixed (packages args)+ check (Run (Arguments args)) =+ not (isPipInstall args) ||+ (isRecursiveInstall args || isSetupPyInstall args || all versionFixed (packages args)) check _ = True isVersionedGit :: String -> Bool isVersionedGit package = "git+http" `isInfixOf` package && "@" `isInfixOf` package@@ -402,6 +442,8 @@ ["pip3", "install"] `isInfixOf` cmd || ["pip2", "install"] `isInfixOf` cmd isRecursiveInstall :: [String] -> Bool isRecursiveInstall cmd = ["-r"] `isInfixOf` cmd+ isSetupPyInstall :: [String] -> Bool+ isSetupPyInstall cmd = ["."] `isInfixOf` cmd -- | Returns all the packages after pip install packages :: [String] -> [String] packages args = concat [filter noOption cmd | cmd <- bashCommands args, isPipInstall cmd]@@ -428,7 +470,7 @@ message = "Pin versions in npm. Instead of `npm install <package>` use `npm install \ \<package>@<version>`"- check (Run args) = all versionFixed (packages args)+ check (Run (Arguments args)) = all versionFixed (packages args) check _ = True packages :: [String] -> [String] packages args = concat [filter noOption cmd | cmd <- bashCommands args, isNpmInstall cmd]@@ -462,7 +504,7 @@ code = "DL3014" severity = WarningC message = "Use the `-y` switch to avoid manual input `apt-get -y install <package>`"- check (Run args) = not (isAptGetInstall args) || hasYesOption args+ check (Run (Arguments args)) = not (isAptGetInstall args) || hasYesOption args check _ = True hasYesOption cmd = ["-y"] `isInfixOf` cmd ||@@ -474,7 +516,7 @@ code = "DL3015" severity = InfoC message = "Avoid additional packages by specifying `--no-install-recommends`"- check (Run args) = not (isAptGetInstall args) || hasNoRecommendsOption args+ check (Run (Arguments args)) = not (isAptGetInstall args) || hasNoRecommendsOption args check _ = True hasNoRecommendsOption cmd = ["--no-install-recommends"] `isInfixOf` cmd @@ -559,6 +601,6 @@ code = "DL4005" severity = WarningC message = "Use SHELL to change the default shell"- check (Run args) = not $ any shellSymlink (bashCommands args)+ check (Run (Arguments args)) = not $ any shellSymlink (bashCommands args) check _ = True shellSymlink args = usingProgram "ln" args && isInfixOf ["/bin/sh"] args
test/Spec.hs view
@@ -20,7 +20,7 @@ it "explicit tagged" $ ruleCatchesNot noLatestTag "FROM debian:jessie" it "explicit SHA" $ ruleCatchesNot noLatestTag- "FROM debian@sha256:\+ "FROM hub.docker.io/debian@sha256:\ \7959ed6f7e35f8b1aaa06d1d8259d4ee25aa85a086d5c125480c333183f9deeb" it "explicit tagged with name" $ ruleCatchesNot noLatestTag "FROM debian:jessie AS builder"@@ -145,6 +145,8 @@ ruleCatchesNot pipVersionPinned "RUN pip3 install MySQL_python==1.2.2" it "pip install requirements" $ ruleCatchesNot pipVersionPinned "RUN pip install -r requirements.txt"+ it "pip install use setup.py" $+ ruleCatchesNot pipVersionPinned "RUN pip install ." it "pip version not pinned" $ ruleCatches pipVersionPinned "RUN pip install MySQL_python" it "pip version pinned" $@@ -259,6 +261,18 @@ ruleCatchesNot aptGetVersionPinned "RUN apt-get -y --no-install-recommends install nodejs=0.10"+ it "apt-get tolerate target-release" $+ let dockerfile =+ [ "RUN set -e &&\\"+ , " echo \"deb http://http.debian.net/debian jessie-backports main\" \+ \> /etc/apt/sources.list.d/jessie-backports.list &&\\"+ , " apt-get update &&\\"+ , " apt-get install -y --no-install-recommends -t jessie-backports \+ \openjdk-8-jdk=8u131-b11-1~bpo8+1 &&\\"+ , " rm -rf /var/lib/apt/lists/*"+ ]+ in ruleCatchesNot aptGetVersionPinned $ unlines dockerfile+ it "has maintainer" $ ruleCatches hasNoMaintainer "FROM debian\nMAINTAINER Lukas" it "has maintainer first" $ ruleCatches hasNoMaintainer "MAINTAINER Lukas\nFROM DEBIAN" it "has no maintainer" $ ruleCatchesNot hasNoMaintainer "FROM debian"@@ -349,6 +363,49 @@ case ast of Left err -> assertEqual "Unexpected error msg" expectedMsg (formatError err) Right _ -> assertFailure "AST should fail parsing"+ --+ describe "Rules can be ignored with inline comments" $ do+ it "ignores single rule" $+ let dockerfile =+ [ "FROM ubuntu"+ , "# hadolint ignore=DL3002"+ , "USER root"+ ]+ in ruleCatchesNot noRootUser $ unlines dockerfile+ it "ignores only the given rule" $+ let dockerfile =+ [ "# hadolint ignore=DL3001"+ , "USER root"+ ]+ in ruleCatches noRootUser $ unlines dockerfile+ it "ignores only the given rule, when multiple passed" $+ let dockerfile =+ [ "# hadolint ignore=DL3001,DL3002"+ , "USER root"+ ]+ in ruleCatchesNot noRootUser $ unlines dockerfile+ it "ignores the rule only if directly above the instruction" $+ let dockerfile =+ [ "# hadolint ignore=DL3001,DL3002"+ , "FROM ubuntu"+ , "USER root"+ ]+ in ruleCatches noRootUser $ unlines dockerfile+ it "won't ignore the rule if passed invalid rule names" $+ let dockerfile =+ [ "# hadolint ignore=crazy,DL3002"+ , "USER root"+ ]+ in ruleCatches noRootUser $ unlines dockerfile+ it "ignores multiple rules correctly, even with some extra whitespace" $+ let dockerfile =+ [ "FROM node as foo"+ , "# hadolint ignore=DL3023, DL3021"+ , "COPY --from=foo bar baz ."+ ]+ in do+ ruleCatchesNot copyFromAnother $ unlines dockerfile+ ruleCatchesNot copyEndingSlash $ unlines dockerfile assertChecks rule s f = case parseString (s ++ "\n") of