import Data.List (find) import Data.Maybe (fromMaybe, isJust) import Language.Dockerfile.Normalize import Language.Dockerfile.Parser import Language.Dockerfile.Rules import Language.Dockerfile.Syntax import Test.Framework import Test.Framework.Providers.HUnit import Test.HUnit assertAst s ast = case parseString (s ++ "\n") of Left err -> assertFailure $ show err Right dockerfile -> assertEqual "ASTs are not equal" ast $ map instruction dockerfile assertChecks rule s f = case parseString (s ++ "\n") of Left err -> assertFailure $ show err Right dockerfile -> f $ analyze [rule] dockerfile -- Assert a failed check exists for rule ruleCatches :: Rule -> String -> Assertion ruleCatches rule s = assertChecks rule s f where f checks = assertEqual "No check for rule found" 1 $ length checks ruleCatchesNot :: Rule -> String -> Assertion ruleCatchesNot rule s = assertChecks rule s f where f checks = assertEqual "Found check of rule" 0 $ length checks normalizeTests = [ "join escaped lines" ~: assertEqual "Lines are not joined" expected $ normalizeEscapedLines dockerfile , "join long cmd" ~: assertEqual "Lines are not joined" longEscapedCmdExpected $ normalizeEscapedLines longEscapedCmd ] where expected = unlines ["ENV foo=bar baz=foz", ""] dockerfile = unlines ["ENV foo=bar \\", "baz=foz"] longEscapedCmd = unlines [ "RUN wget https://download.com/${version}.tar.gz -O /tmp/logstash.tar.gz && \\" , "(cd /tmp && tar zxf logstash.tar.gz && mv logstash-${version} /opt/logstash && \\" , "rm logstash.tar.gz) && \\" , "(cd /opt/logstash && \\" , "/opt/logstash/bin/plugin install contrib)" ] longEscapedCmdExpected = concat [ "RUN wget https://download.com/${version}.tar.gz -O /tmp/logstash.tar.gz && " , "(cd /tmp && tar zxf logstash.tar.gz && mv logstash-${version} /opt/logstash && " , "rm logstash.tar.gz) && " , "(cd /opt/logstash && " , "/opt/logstash/bin/plugin install contrib)\n" , "\n" , "\n" , "\n" , "\n" ] astTests = [ "from untagged" ~: assertAst "FROM busybox" [From (UntaggedImage "busybox")] , "env pair" ~: assertAst "ENV foo=bar" [Env [("foo", "bar")]] , "env space pair" ~: assertAst "ENV foo bar" [Env [("foo", "bar")] ] , "env quoted pair" ~: assertAst "ENV foo=\"bar\"" [Env [("foo", "bar")]] , "env multi raw pair" ~: assertAst "ENV foo=bar baz=foo" [Env [("foo", "bar"), ("baz", "foo")]] , "env multi quoted pair" ~: assertAst "ENV foo=\"bar\" baz=\"foo\"" [Env [("foo", "bar"), ("baz", "foo")]] , "one line cmd" ~: assertAst "CMD true" [Cmd ["true"]] , "multiline cmd" ~: assertAst "CMD true \\\n && true" [Cmd ["true", "&&", "true"], EOL] , "maintainer " ~: assertAst "MAINTAINER hudu@mail.com" [Maintainer "hudu@mail.com"] , "maintainer from" ~: assertAst maintainerFromProg maintainerFromAst , "quoted exec" ~: assertAst "CMD [\"echo\", \"1\"]" [Cmd ["echo", "1"]] , "env works with cmd" ~: assertAst envWorksCmdProg envWorksCmdAst , "multicomments first" ~: assertAst multiCommentsProg1 [Comment " line 1", Comment " line 2", Run ["apt-get", "update"], EOL] , "multicomments after" ~: assertAst multiCommentsProg2 [Run ["apt-get", "update"], Comment " line 1", Comment " line 2", EOL] , "escape with space" ~: assertAst escapedWithSpaceProgram [Run ["yum", "install", "-y", "imagemagick", "mysql"], EOL, EOL] , "scratch and maintainer" ~: assertAst "FROM scratch\nMAINTAINER hudu@mail.com" [From (UntaggedImage "scratch"), Maintainer "hudu@mail.com"] ] where maintainerFromProg = "FROM busybox\nMAINTAINER hudu@mail.com" maintainerFromAst = [ From (UntaggedImage "busybox") , Maintainer "hudu@mail.com" ] envWorksCmdProg = "ENV PATH=\"/root\"\nCMD [\"hadolint\",\"-i\"]" envWorksCmdAst = [ Env [("PATH", "/root")] , Cmd ["hadolint", "-i"] ] multiCommentsProg1 = unlines [ "# line 1" , "# line 2" , "RUN apt-get update" ] multiCommentsProg2 = unlines [ "RUN apt-get update" , "# line 1" , "# line 2" ] escapedWithSpaceProgram = unlines [ "RUN yum install -y \\ " , "imagemagick \\ " , "mysql" ] ruleTests = [ "untagged" ~: ruleCatches noUntagged "FROM debian" , "explicit latest" ~: ruleCatches noLatestTag "FROM debian:latest" , "explicit tagged" ~: ruleCatchesNot noLatestTag "FROM debian:jessie" , "sudo" ~: ruleCatches noSudo "RUN sudo apt-get update" , "no root" ~: ruleCatches noRootUser "USER root" , "install sudo" ~: ruleCatchesNot noSudo "RUN apt-get install sudo" , "sudo chained programs" ~: ruleCatches noSudo "RUN apt-get update && sudo apt-get install" , "invalid cmd" ~: ruleCatches invalidCmd "RUN top" , "install ssh" ~: ruleCatchesNot invalidCmd "RUN apt-get install ssh" , "apt upgrade" ~: ruleCatches noUpgrade "RUN apt-get update && apt-get upgrade" , "apt-get version pinning" ~: ruleCatches aptGetVersionPinned "RUN apt-get update && apt-get install python" , "apt-get no cleanup" ~: ruleCatches aptGetCleanup "RUN apt-get update && apt-get install python" , "apt-get cleanup" ~: ruleCatchesNot aptGetCleanup "RUN apt-get update && apt-get install python && rm -rf /var/lib/apt/lists/*" , "use add" ~: ruleCatches useAdd "COPY packaged-app.tar /usr/src/app" , "use not add" ~: ruleCatchesNot useAdd "COPY package.json /usr/src/app" , "invalid port" ~: ruleCatches invalidPort "EXPOSE 80000" , "valid port" ~: ruleCatchesNot invalidPort "EXPOSE 60000" , "maintainer address" ~: ruleCatches maintainerAddress "MAINTAINER Lukas" , "maintainer uri" ~: ruleCatchesNot maintainerAddress "MAINTAINER Lukas " , "maintainer uri" ~: ruleCatchesNot maintainerAddress "MAINTAINER John Doe " , "maintainer mail" ~: ruleCatchesNot maintainerAddress "MAINTAINER http://lukasmartinelli.ch" , "pip requirements" ~: ruleCatchesNot pipVersionPinned "RUN pip install -r requirements.txt" , "pip version not pinned" ~: ruleCatches pipVersionPinned "RUN pip install MySQL_python" , "pip version pinned" ~: ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2" , "apt-get auto yes" ~: ruleCatches aptGetYes "RUN apt-get install python" , "apt-get yes shortflag" ~: ruleCatchesNot aptGetYes "RUN apt-get install -yq python" , "apt-get yes different pos" ~: ruleCatchesNot aptGetYes "RUN apt-get install -y python" , "apt-get with auto yes" ~: ruleCatchesNot aptGetYes "RUN apt-get -y install python" , "apt-get with auto expanded yes" ~: ruleCatchesNot aptGetYes "RUN apt-get --yes install python" , "apt-get install recommends" ~: ruleCatchesNot aptGetNoRecommends "RUN apt-get install --no-install-recommends python" , "apt-get no install recommends" ~: ruleCatches aptGetNoRecommends "RUN apt-get install python" , "apt-get no install recommends" ~: ruleCatches aptGetNoRecommends "RUN apt-get -y install python" , "apt-get version" ~: ruleCatchesNot aptGetVersionPinned "RUN apt-get install -y python=1.2.2" , "apt-get pinned" ~: ruleCatchesNot aptGetVersionPinned "RUN apt-get -y --no-install-recommends install nodejs=0.10" , "apt-get pinned chained" ~: ruleCatchesNot aptGetVersionPinned $ unlines aptGetPinnedChainedProgram , "apt-get pinned regression" ~: ruleCatchesNot aptGetVersionPinned $ unlines aptGetPinnedRegressionProgram , "has maintainer named" ~: ruleCatchesNot hasMaintainer "FROM busybox\nMAINTAINER hudu@mail.com" , "has maintainer" ~: ruleCatchesNot hasMaintainer "FROM debian\nMAINTAINER Lukas" , "has maintainer first" ~: ruleCatchesNot hasMaintainer "MAINTAINER Lukas\nFROM DEBIAN" , "has no maintainer" ~: ruleCatches hasMaintainer "FROM debian" , "using add" ~: ruleCatches copyInsteadAdd "ADD file /usr/src/app/" , "add is ok for archive" ~: ruleCatchesNot copyInsteadAdd "ADD file.tar /usr/src/app/" , "add is ok for url" ~: ruleCatchesNot copyInsteadAdd "ADD http://file.com /usr/src/app/" , "many cmds" ~: ruleCatches multipleCmds "CMD /bin/true\nCMD /bin/true" , "single cmd" ~: ruleCatchesNot multipleCmds "CMD /bin/true" , "no cmd" ~: ruleCatchesNot multipleEntrypoints "FROM busybox" , "many entries" ~: ruleCatches multipleEntrypoints "ENTRYPOINT /bin/true\nENTRYPOINT /bin/true" , "single entry" ~: ruleCatchesNot multipleEntrypoints "ENTRYPOINT /bin/true" , "no entry" ~: ruleCatchesNot multipleEntrypoints "FROM busybox" , "workdir variable" ~: ruleCatchesNot absoluteWorkdir "WORKDIR ${work}" , "scratch" ~: ruleCatchesNot noUntagged "FROM scratch" ] where aptGetPinnedChainedProgram = [ "RUN apt-get update \\" , " && apt-get -y --no-install-recommends install nodejs=0.10 \\" , " && rm -rf /var/lib/apt/lists/*" ] aptGetPinnedRegressionProgram = [ "RUN apt-get update && apt-get install --no-install-recommends -y \\" , "python-demjson=2.2.2* \\" , "wget=1.16.1* \\" , "git=1:2.5.0* \\" , "ruby=1:2.1.*" ] tests = test $ ruleTests ++ astTests ++ normalizeTests main = defaultMain $ hUnitTestToTests tests