language-docker 6.0.4 → 15.0.0
raw patch · 51 files changed
Files
- README.md +5/−98
- language-docker.cabal +117/−81
- src/Language/Docker.hs +42/−99
- src/Language/Docker/EDSL.hs +0/−385
- src/Language/Docker/EDSL/Quasi.hs +0/−49
- src/Language/Docker/EDSL/Types.hs +0/−71
- src/Language/Docker/Normalize.hs +0/−94
- src/Language/Docker/Parser.hs +69/−587
- src/Language/Docker/Parser/Arguments.hs +31/−0
- src/Language/Docker/Parser/Cmd.hs +13/−0
- src/Language/Docker/Parser/Copy.hs +173/−0
- src/Language/Docker/Parser/Expose.hs +81/−0
- src/Language/Docker/Parser/From.hs +72/−0
- src/Language/Docker/Parser/Healthcheck.hs +104/−0
- src/Language/Docker/Parser/Instruction.hs +147/−0
- src/Language/Docker/Parser/Pairs.hs +58/−0
- src/Language/Docker/Parser/Prelude.hs +380/−0
- src/Language/Docker/Parser/Run.hs +343/−0
- src/Language/Docker/PrettyPrint.hs +284/−155
- src/Language/Docker/Syntax.hs +368/−135
- src/Language/Docker/Syntax/Lift.hs +0/−67
- src/Language/Docker/Syntax/Port.hs +19/−0
- src/Language/Docker/Syntax/PortRange.hs +20/−0
- src/Language/Docker/Syntax/Protocol.hs +15/−0
- test/Language/Docker/EDSL/QuasiSpec.hs +0/−40
- test/Language/Docker/EDSLSpec.hs +0/−133
- test/Language/Docker/ExamplesSpec.hs +0/−20
- test/Language/Docker/IntegrationSpec.hs +163/−0
- test/Language/Docker/ParseAddSpec.hs +137/−0
- test/Language/Docker/ParseCmdSpec.hs +64/−0
- test/Language/Docker/ParseCopySpec.hs +267/−0
- test/Language/Docker/ParseExposeSpec.hs +146/−0
- test/Language/Docker/ParseHealthcheckSpec.hs +89/−0
- test/Language/Docker/ParsePragmaSpec.hs +71/−0
- test/Language/Docker/ParseRunSpec.hs +736/−0
- test/Language/Docker/ParserSpec.hs +238/−363
- test/Language/Docker/PrettyPrintSpec.hs +164/−0
- test/TestHelper.hs +44/−0
- test/fixtures/1.Dockerfile +17/−0
- test/fixtures/2.Dockerfile +16/−0
- test/fixtures/3.Dockerfile +6/−0
- test/fixtures/4.Dockerfile +8/−0
- test/fixtures/5.Dockerfile +8/−0
- test/fixtures/6.Dockerfile +3/−0
- test/fixtures/7.Dockerfile +3/−0
- test/fixtures/8.Dockerfile +4/−0
- test/fixtures/Dockerfile.bom.utf16be binary
- test/fixtures/Dockerfile.bom.utf16le binary
- test/fixtures/Dockerfile.bom.utf32be binary
- test/fixtures/Dockerfile.bom.utf32le binary
- test/fixtures/Dockerfile.bom.utf8 +3/−0
README.md view
@@ -1,4 +1,4 @@-[![Build Status][travis-img]][travis]+[![Build Status][actions-img]][actions] [![Hackage][hackage-img]][hackage] [![GPL-3 licensed][license-img]][license] @@ -12,10 +12,6 @@ - [Parsing files](#parsing-files) - [Parsing strings](#parsing-strings) - [Pretty-printing files](#pretty-printing-files)-- [Writing Dockerfiles in Haskell](#writing-dockerfiles-in-haskell)-- [Using the QuasiQuoter](#using-the-quasiquoter)-- [Templating Dockerfiles in Haskell](#templating-dockerfiles-in-haskell)-- [Using IO in the DSL](#using-io-in-the-dsl) ## Parsing files @@ -35,102 +31,13 @@ print (parseString c) ``` -## Pretty-printing files--```haskell-import Language.Docker-main = do- Right d <- parseFile "./Dockerfile"- putStr (prettyPrint d)-```--## Writing Dockerfiles in Haskell--```haskell-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE OverloadedLists #-}-import Language.Docker--main = putDockerfileStr $ do- from "node"- run "apt-get update"- run ["apt-get", "install", "something"]- -- ...-```--## Using the QuasiQuoter--```haskell-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE QuasiQuotes #-}-import Language.Docker-main = putDockerfileStr $ do- from "node"- run "apt-get update"- [edockerfile|- RUN apt-get update- CMD node something.js- |]- -- ...-```--## Templating Dockerfiles in Haskell--```haskell-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE OverloadedLists #-}-import Control.Monad-import Language.Docker-import Data.String (fromString)-import qualified Data.Text.Lazy.IO as L--tags = ["7.8", "7.10", "8"]-cabalSandboxBuild packageName = do- let cabalFile = packageName ++ ".cabal"- run "cabal sandbox init"- run "cabal update"- add [fromString cabalFile] (fromString $ "/app/" ++ cabalFile)- run "cabal install --only-dep -j"- add "." "/app/"- run "cabal build"-main =- forM_ tags $ \tag -> do- let df = toDockerfileText $ do- from ("haskell" `tagged` tag)- cabalSandboxBuild "mypackage"- L.writeFile ("./examples/templating-" ++ tag ++ ".dockerfile") df-```--## Using IO in the DSL-By default the DSL runs in the `Identity` monad. By running in IO we can-support more features like file globbing:--```haskell-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE OverloadedLists #-}-import Language.Docker-import qualified System.Directory as Directory-import qualified System.FilePath as FilePath-import qualified System.FilePath.Glob as Glob-import Data.List.NonEmpty (fromList)-import qualified Data.Text.Lazy.IO as L+## Create Dockerfiles -main = do- str <- toDockerfileTextIO $ do- fs <- liftIO $ do- cwd <- Directory.getCurrentDirectory- fs <- Glob.glob "./test/*.hs"- let relativeFiles = map (FilePath.makeRelative cwd) fs- return (fromList relativeFiles)- from "ubuntu"- copy $ (toSources fs) `to` "/app/"- L.putStr str-```+Use the [dockerfile-creator package](https://github.com/hadolint/dockerfile-creator) [hackage-img]: https://img.shields.io/hackage/v/language-docker.svg [hackage]: https://hackage.haskell.org/package/language-docker-[travis-img]: https://travis-ci.org/hadolint/language-docker.svg?branch=master-[travis]: https://travis-ci.org/hadolint/language-docker+[actions-img]: https://github.com/hadolint/language-docker/actions/workflows/haskell.yml/badge.svg+[actions]: https://github.com/hadolint/language-docker/actions/workflows/haskell.yml [license-img]: https://img.shields.io/badge/license-GPL--3-blue.svg [license]: https://tldrlegal.com/license/gnu-general-public-license-v3-(gpl-3)
language-docker.cabal view
@@ -1,96 +1,132 @@--- This file has been generated from package.yaml by hpack version 0.28.2.------ see: https://github.com/sol/hpack------ hash: 64e893f351e7689307e2cbe6595ac2c624f727f4e02b9ff0aae6ec166f4cf5d4+cabal-version: 3.4+name: language-docker+version: 15.0.0+synopsis: Dockerfile parser, pretty-printer and embedded DSL+description:+ All functions for parsing and pretty-printing Dockerfiles are exported through @Language.Docker@. For more fine-grained operations look for specific modules that implement a certain functionality.+ See the <https://github.com/hadolint/language-docker GitHub project> for the source-code and examples. -name: language-docker-version: 6.0.4-synopsis: Dockerfile parser, pretty-printer and embedded DSL-description: All functions for parsing, printing and writting Dockerfiles are exported through @Language.Docker@. For more fine-grained operations look for specific modules that implement a certain functionality.- See the <https://github.com/hadolint/language-docker GitHub project> for the source-code and examples.-category: Development-homepage: https://github.com/hadolint/language-docker#readme-bug-reports: https://github.com/hadolint/language-docker/issues-author: Lukas Martinelli,- Pedro Tacla Yamada,- José Lorenzo Rodríguez-maintainer: lorenzo@seatgeek.com-copyright: Lukas Martinelli, Copyright (c) 2016,- Pedro Tacla Yamada, Copyright (c) 2016,- José Lorenzo Rodríguez, Copyright (c) 2017-license: GPL-3-license-file: LICENSE-build-type: Simple-cabal-version: >= 1.10+category: Development+homepage: https://github.com/hadolint/language-docker#readme+bug-reports: https://github.com/hadolint/language-docker/issues+author:+ Lukas Martinelli,+ Pedro Tacla Yamada,+ José Lorenzo Rodríguez++maintainer: lorenzo@seatgeek.com+copyright:+ Lukas Martinelli, Copyright (c) 2016,+ Pedro Tacla Yamada, Copyright (c) 2016,+ José Lorenzo Rodríguez, Copyright (c) 2017++license: GPL-3.0-or-later+license-file: LICENSE+build-type: Simple extra-source-files:- README.md+ README.md+ test/fixtures/1.Dockerfile+ test/fixtures/2.Dockerfile+ test/fixtures/3.Dockerfile+ test/fixtures/4.Dockerfile+ test/fixtures/5.Dockerfile+ test/fixtures/6.Dockerfile+ test/fixtures/7.Dockerfile+ test/fixtures/8.Dockerfile+ test/fixtures/Dockerfile.bom.utf16be+ test/fixtures/Dockerfile.bom.utf16le+ test/fixtures/Dockerfile.bom.utf32be+ test/fixtures/Dockerfile.bom.utf32le+ test/fixtures/Dockerfile.bom.utf8 source-repository head- type: git+ type: git location: https://github.com/hadolint/language-docker +common deps+ build-depends:+ , base >=4.8 && <5+ , bytestring >=0.11.5 && <0.13+ , containers >=0.6.7 && <0.8+ , data-default >=0.8.0 && <0.10+ , data-default-class >=0.2.0 && <0.4+ , megaparsec >=9.7.0 && <9.9+ , prettyprinter >=1.7.1 && <1.9+ , split >=0.2.5 && <0.4+ , text >=2.0.2 && <2.2+ , time >=1.12.2 && <1.14+ library+ import: deps exposed-modules:- Language.Docker- Language.Docker.Parser- Language.Docker.PrettyPrint- Language.Docker.Normalize- Language.Docker.Syntax- Language.Docker.Syntax.Lift- Language.Docker.EDSL- Language.Docker.EDSL.Quasi- Language.Docker.EDSL.Types+ Language.Docker+ Language.Docker.Parser+ Language.Docker.PrettyPrint+ Language.Docker.Syntax+ other-modules:- Paths_language_docker- hs-source-dirs:- src- ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -fno-warn-unused-do-bind -fno-warn-orphans- build-depends:- base >=4.8 && <5- , bytestring >=0.10- , containers- , free- , megaparsec >=6.4- , mtl- , prettyprinter- , split >=0.2- , template-haskell- , text- , th-lift- , time- default-language: Haskell2010+ Language.Docker.Parser.Arguments+ Language.Docker.Parser.Cmd+ Language.Docker.Parser.Copy+ Language.Docker.Parser.Expose+ Language.Docker.Parser.From+ Language.Docker.Parser.Healthcheck+ Language.Docker.Parser.Instruction+ Language.Docker.Parser.Pairs+ Language.Docker.Parser.Prelude+ Language.Docker.Parser.Run+ Language.Docker.Syntax.Port+ Language.Docker.Syntax.PortRange+ Language.Docker.Syntax.Protocol + hs-source-dirs: src+ default-extensions:+ ImplicitParams+ OverloadedStrings+ Rank2Types++ ghc-options:+ -Wall -Wcompat -Wincomplete-record-updates+ -Wincomplete-uni-patterns -Wredundant-constraints+ -fno-warn-unused-do-bind -fno-warn-orphans++ default-language: GHC2021+ test-suite hspec- type: exitcode-stdio-1.0- main-is: Spec.hs+ import: deps+ type: exitcode-stdio-1.0+ main-is: Spec.hs other-modules:- Language.Docker.EDSL.QuasiSpec- Language.Docker.EDSLSpec- Language.Docker.ExamplesSpec- Language.Docker.ParserSpec- Paths_language_docker- hs-source-dirs:- test+ Language.Docker.IntegrationSpec+ Language.Docker.ParseAddSpec+ Language.Docker.ParseCmdSpec+ Language.Docker.ParseCopySpec+ Language.Docker.ParseExposeSpec+ Language.Docker.ParseHealthcheckSpec+ Language.Docker.ParsePragmaSpec+ Language.Docker.ParserSpec+ Language.Docker.ParseRunSpec+ Language.Docker.PrettyPrintSpec+ TestHelper++ hs-source-dirs: test+ default-extensions:+ ImplicitParams+ OverloadedLists+ OverloadedStrings+ Rank2Types++ ghc-options:+ -Wall -Wcompat -Wincomplete-record-updates+ -Wincomplete-uni-patterns -Wredundant-constraints+ -fno-warn-unused-do-bind -fno-warn-orphans+ build-depends:- Glob- , HUnit >=1.2- , QuickCheck- , base >=4.8 && <5- , bytestring >=0.10- , containers- , directory- , filepath- , free , hspec+ , hspec-megaparsec+ , HUnit >=1.2 , language-docker- , megaparsec >=6.4- , mtl- , prettyprinter- , process- , split >=0.2- , template-haskell- , text- , th-lift- , time- default-language: Haskell2010+ , QuickCheck++ default-language: GHC2021+ build-tool-depends: hspec-discover:hspec-discover >=2 && <3
src/Language/Docker.hs view
@@ -1,105 +1,48 @@+{-# LANGUAGE DuplicateRecordFields #-}+ module Language.Docker- ( Language.Docker.Syntax.Dockerfile -- * Parsing Dockerfiles (@Language.Docker.Syntax@ and @Language.Docker.Parser@)- , parseText- , parseFile+ ( Language.Docker.Syntax.Dockerfile,++ -- * Parsing Dockerfiles (@Language.Docker.Syntax@ and @Language.Docker.Parser@)+ parseText,+ parseFile,+ parseStdin,+ -- * Re-exports from @megaparsec@- , Text.Megaparsec.parseErrorPretty- -- * Pretty-printing Dockerfiles (@Language.Docker.PrettyPrint@)- , prettyPrint- , prettyPrintDockerfile- -- * Writting Dockerfiles (@Language.Docker.EDSL@)- , Language.Docker.EDSL.toDockerfileText- , Language.Docker.EDSL.toDockerfile- , Language.Docker.EDSL.putDockerfileStr- , Language.Docker.EDSL.writeDockerFile- , Language.Docker.EDSL.toDockerfileTextIO- , Language.Docker.EDSL.toDockerfileIO- , Language.Docker.EDSL.runDockerfileIO- , Language.Docker.EDSL.runDockerfileTextIO- , Control.Monad.IO.Class.liftIO- , Language.Docker.EDSL.from- -- ** Constructing base images- , Language.Docker.EDSL.tagged- , Language.Docker.EDSL.untagged- , Language.Docker.EDSL.digested- , Language.Docker.EDSL.aliased- -- ** Syntax- , Language.Docker.EDSL.add- , Language.Docker.EDSL.user- , Language.Docker.EDSL.label- , Language.Docker.EDSL.stopSignal- , Language.Docker.EDSL.copy- , Language.Docker.EDSL.copyFromStage- , Language.Docker.EDSL.to- , Language.Docker.EDSL.fromStage- , Language.Docker.EDSL.ownedBy- , Language.Docker.EDSL.toSources- , Language.Docker.EDSL.toTarget- , Language.Docker.EDSL.run- , Language.Docker.EDSL.runArgs- , Language.Docker.EDSL.cmd- , Language.Docker.EDSL.cmdArgs- , Language.Docker.EDSL.healthcheck- , Language.Docker.EDSL.check- , Language.Docker.EDSL.interval- , Language.Docker.EDSL.timeout- , Language.Docker.EDSL.startPeriod- , Language.Docker.EDSL.retries- , Language.Docker.EDSL.workdir- , Language.Docker.EDSL.expose- , Language.Docker.EDSL.ports- , Language.Docker.EDSL.tcpPort- , Language.Docker.EDSL.udpPort- , Language.Docker.EDSL.variablePort- , Language.Docker.EDSL.portRange- , Language.Docker.EDSL.udpPortRange- , Language.Docker.EDSL.volume- , Language.Docker.EDSL.entrypoint- , Language.Docker.EDSL.entrypointArgs- , Language.Docker.EDSL.maintainer- , Language.Docker.EDSL.env- , Language.Docker.EDSL.arg- , Language.Docker.EDSL.comment- , Language.Docker.EDSL.onBuild- , Language.Docker.EDSL.onBuildRaw- , Language.Docker.EDSL.embed- , Language.Docker.EDSL.Quasi.edockerfile- -- ** Support types for the EDSL- , Language.Docker.EDSL.EDockerfileM- , Language.Docker.EDSL.EDockerfileTM- , Language.Docker.EDSL.Types.EBaseImage(..)- -- * QuasiQuoter (@Language.Docker.EDSL.Quasi@)- , Language.Docker.EDSL.Quasi.dockerfile- -- * Types (@Language.Docker.Syntax@)- , Language.Docker.Syntax.Instruction(..)- , Language.Docker.Syntax.InstructionPos(..)- , Language.Docker.Syntax.BaseImage(..)- , Language.Docker.Syntax.SourcePath(..)- , Language.Docker.Syntax.TargetPath(..)- , Language.Docker.Syntax.Chown(..)- , Language.Docker.Syntax.CopySource(..)- , Language.Docker.Syntax.CopyArgs(..)- , Language.Docker.Syntax.AddArgs(..)- , Language.Docker.Syntax.Check(..)- , Language.Docker.Syntax.CheckArgs(..)- , Language.Docker.Syntax.Image(..)- , Language.Docker.Syntax.Registry(..)- , Language.Docker.Syntax.ImageAlias(..)- , Language.Docker.Syntax.Tag(..)- , Language.Docker.Syntax.Ports- , Language.Docker.Syntax.Directory- , Language.Docker.Syntax.Arguments- , Language.Docker.Syntax.Pairs- , Language.Docker.Syntax.Filename- , Language.Docker.Syntax.Linenumber- -- * Instruction and InstructionPos helpers- , Language.Docker.EDSL.instructionPos- ) where+ Text.Megaparsec.parseErrorPretty,+ Text.Megaparsec.errorBundlePretty, -import qualified Control.Monad.IO.Class-import qualified Language.Docker.EDSL-import qualified Language.Docker.EDSL.Quasi-import qualified Language.Docker.EDSL.Types+ -- * Pretty-printing Dockerfiles (@Language.Docker.PrettyPrint@)+ prettyPrint,+ prettyPrintDockerfile,++ -- * Types (@Language.Docker.Syntax@)+ Language.Docker.Syntax.Instruction (..),+ Language.Docker.Syntax.InstructionPos (..),+ Language.Docker.Syntax.BaseImage (..),+ Language.Docker.Syntax.SourcePath (..),+ Language.Docker.Syntax.TargetPath (..),+ Language.Docker.Syntax.Chown (..),+ Language.Docker.Syntax.CopySource (..),+ Language.Docker.Syntax.CopyArgs (..),+ Language.Docker.Syntax.AddArgs (..),+ Language.Docker.Syntax.Check (..),+ Language.Docker.Syntax.CheckArgs (..),+ Language.Docker.Syntax.Image (..),+ Language.Docker.Syntax.Registry (..),+ Language.Docker.Syntax.ImageAlias (..),+ Language.Docker.Syntax.Tag (..),+ Language.Docker.Syntax.Digest (..),+ Language.Docker.Syntax.Ports,+ Language.Docker.Syntax.Directory,+ Language.Docker.Syntax.Arguments,+ Language.Docker.Syntax.Pairs,+ Language.Docker.Syntax.Filename,+ Language.Docker.Syntax.Platform,+ Language.Docker.Syntax.Linenumber,+ )+where+ import Language.Docker.Parser import Language.Docker.PrettyPrint import qualified Language.Docker.Syntax
− src/Language/Docker/EDSL.hs
@@ -1,385 +0,0 @@-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE OverloadedLists #-}-{-# LANGUAGE TemplateHaskell #-}--module Language.Docker.EDSL where--import Control.Monad.Free-import Control.Monad.Free.TH-import Control.Monad.Trans.Free (FreeT, iterTM)-import Control.Monad.Writer-import qualified Data.ByteString.Lazy as BL-import qualified Data.ByteString.Lazy.Char8 as B8-import Data.List.NonEmpty (NonEmpty)-import Data.String (fromString)-import Data.Text (Text)-import qualified Data.Text as Text-import qualified Data.Text.Lazy as L-import qualified Data.Text.Lazy.Encoding as E--import qualified Language.Docker.PrettyPrint as PrettyPrint-import qualified Language.Docker.Syntax as Syntax--import Language.Docker.EDSL.Types---- | The type of 'Identity' based EDSL blocks-type EDockerfileM = Free EInstruction---- | The type of free monad EDSL blocks-type EDockerfileTM = FreeT EInstruction--type EInstructionM = Free EInstruction--type EInstructionTM = FreeT EInstruction--makeFree ''EInstruction--runDockerWriter :: (MonadWriter [Syntax.Instruction Text] m) => EDockerfileM a -> m a-runDockerWriter = iterM runD--runDockerWriterIO ::- (Monad m, MonadTrans t, MonadWriter [Syntax.Instruction Text] (t m))- => EDockerfileTM m a- -> t m a-runDockerWriterIO = iterTM runD--runDef :: MonadWriter [t] m => (t1 -> t) -> t1 -> m b -> m b-runDef f a n = tell [f a] >> n--runDef2 :: MonadWriter [t] m => (t1 -> t2 -> t) -> t1 -> t2 -> m b -> m b-runDef2 f a b n = tell [f a b] >> n--runD :: MonadWriter [Syntax.Instruction Text] m => EInstruction (m b) -> m b-runD (From bi n) =- case bi of- EUntaggedImage bi' alias -> runDef Syntax.From (Syntax.UntaggedImage bi' alias) n- ETaggedImage bi' tg alias -> runDef Syntax.From (Syntax.TaggedImage bi' tg alias) n- EDigestedImage bi' d alias -> runDef Syntax.From (Syntax.DigestedImage bi' d alias) n-runD (CmdArgs as n) = runDef Syntax.Cmd as n-runD (Shell as n) = runDef Syntax.Shell as n-runD (AddArgs s d c n) = runDef Syntax.Add (Syntax.AddArgs s d c) n-runD (User u n) = runDef Syntax.User u n-runD (Label ps n) = runDef Syntax.Label ps n-runD (StopSignal s n) = runDef Syntax.Stopsignal s n-runD (CopyArgs s d c f n) = runDef Syntax.Copy (Syntax.CopyArgs s d c f) n-runD (RunArgs as n) = runDef Syntax.Run as n-runD (Workdir d n) = runDef Syntax.Workdir d n-runD (Expose ps n) = runDef Syntax.Expose ps n-runD (Volume v n) = runDef Syntax.Volume v n-runD (EntrypointArgs e n) = runDef Syntax.Entrypoint e n-runD (Maintainer m n) = runDef Syntax.Maintainer m n-runD (Env ps n) = runDef Syntax.Env ps n-runD (Arg k v n) = runDef2 Syntax.Arg k v n-runD (Comment c n) = runDef Syntax.Comment c n-runD (Healthcheck c n) = runDef Syntax.Healthcheck c n-runD (OnBuildRaw i n) = runDef Syntax.OnBuild i n-runD (Embed is n) = do- tell (map Syntax.instruction is)- n--instructionPos :: Syntax.Instruction args -> Syntax.InstructionPos args-instructionPos i = Syntax.InstructionPos i "" 0---- | Runs the Dockerfile EDSL and returns a 'Dockerfile' you can pretty print--- or manipulate-toDockerfile :: EDockerfileM a -> Syntax.Dockerfile-toDockerfile e =- let (_, w) = runWriter (runDockerWriter e)- in map instructionPos w---- | runs the Dockerfile EDSL and returns a 'Data.Text.Lazy' using--- 'Language.Docker.PrettyPrint'------ @--- import Language.Docker------ main :: IO ()--- main = print $ toDockerfileText $ do--- from (tagged "fpco/stack-build" "lts-6.9")--- add ["."] "/app/language-docker"--- workdir "/app/language-docker"--- run "stack build --test --only-dependencies"--- cmd "stack test"--- @-toDockerfileText :: EDockerfileM a -> L.Text-toDockerfileText = PrettyPrint.prettyPrint . toDockerfile---- | Writes the dockerfile to the given file path after pretty-printing it------ @--- import Language.Docker------ main :: IO ()--- main = writeDockerFile "build.Dockerfile" $ toDockerfile $ do--- from (tagged "fpco/stack-build" "lts-6.9")--- add ["."] "/app/language-docker"--- workdir "/app/language-docker"--- run "stack build --test --only-dependencies"--- cmd "stack test"--- @-writeDockerFile :: Text -> Syntax.Dockerfile -> IO ()-writeDockerFile filename =- BL.writeFile (Text.unpack filename) . E.encodeUtf8 . PrettyPrint.prettyPrint---- | Prints the dockerfile to stdout. Mainly used for debugging purposes------ @--- import Language.Docker------ main :: IO ()--- main = putDockerfileStr $ do--- from (tagged "fpco/stack-build" "lts-6.9")--- add ["."] "/app/language-docker"--- workdir "/app/language-docker"--- run "stack build --test --only-dependencies"--- cmd "stack test"--- @-putDockerfileStr :: EDockerfileM a -> IO ()-putDockerfileStr = B8.putStrLn . E.encodeUtf8 . PrettyPrint.prettyPrint . toDockerfile---- | Use a docker image in a FROM instruction without a tag------ The following two examples are equivalent------ @--- from $ untagged "fpco/stack-build"--- @------ Is equivalent to, when having OverloadedStrings:------ @--- from "fpco/stack-build"--- @-untagged :: Text -> EBaseImage-untagged = flip EUntaggedImage Nothing . fromString . Text.unpack---- | Use a specific tag for a docker image. This function is meant--- to be used as an infix operator.------ @--- from $ "fpco/stack-build" `tagged` "lts-10.3"--- @-tagged :: Syntax.Image -> Syntax.Tag -> EBaseImage-tagged imageName tag = ETaggedImage imageName tag Nothing--digested :: Syntax.Image -> Text -> EBaseImage-digested imageName hash = EDigestedImage imageName hash Nothing---- | Alias a FROM instruction to be used as a build stage.--- This function is meant to be used as an infix operator.------ @--- from $ "fpco/stack-build" `aliased` "builder"--- @-aliased :: EBaseImage -> Text -> EBaseImage-aliased image alias =- case image of- EUntaggedImage n _ -> EUntaggedImage n (Just $ Syntax.ImageAlias alias)- ETaggedImage n t _ -> ETaggedImage n t (Just $ Syntax.ImageAlias alias)- EDigestedImage n h _ -> EDigestedImage n h (Just $ Syntax.ImageAlias alias)---- | Create a RUN instruction with the given arguments.------ @--- run "apt-get install wget"--- @-run :: MonadFree EInstruction m => Syntax.Arguments Text -> m ()-run = runArgs---- | Create an ENTRYPOINT instruction with the given arguments.------ @--- entrypoint "/usr/local/bin/program --some-flag"--- @-entrypoint :: MonadFree EInstruction m => Syntax.Arguments Text -> m ()-entrypoint = entrypointArgs---- | Create a CMD instruction with the given arguments.------ @--- cmd "my-program --some-flag"--- @-cmd :: MonadFree EInstruction m => Syntax.Arguments Text -> m ()-cmd = cmdArgs---- | Create a COPY instruction. This function is meant to be--- used with the compinators 'to', 'fromStage' and 'ownedBy'------ @--- copy $ ["foo.js", "bar.js"] `to` "."--- copy $ ["some_file"] `to` "/some/path" `fromStage` "builder"--- @-copy :: MonadFree EInstruction m => Syntax.CopyArgs -> m ()-copy (Syntax.CopyArgs sources dest ch src) = copyArgs sources dest ch src---- | Create a COPY instruction from a given build stage.--- This is a shorthand version of using 'copy' with combinators.------ @--- copyFromStage "builder" ["foo.js", "bar.js"] "."--- @-copyFromStage ::- MonadFree EInstruction m- => Syntax.CopySource- -> NonEmpty Syntax.SourcePath- -> Syntax.TargetPath- -> m ()-copyFromStage stage source dest = copy $ Syntax.CopyArgs source dest Syntax.NoChown stage---- | Create an ADD instruction. This is often used as a shorthand version--- of copy when no extra options are needed. Currently there is no way to--- pass extra options to ADD, so you are encouraged to use 'copy' instead.------ @--- add ["foo.js", "bar.js"] "."--- @-add :: MonadFree EInstruction m => NonEmpty Syntax.SourcePath -> Syntax.TargetPath -> m ()-add sources dest = addArgs sources dest Syntax.NoChown---- | Converts a NonEmpty list of strings to a NonEmpty list of 'Syntax.SourcePath'------ This is a convenience function when you need to pass a non-static list of--- strings that you build somewhere as an argument for 'copy' or 'add'------ @--- someFiles <- glob "*.js"--- copy $ (toSources someFiles) `to` "."--- @-toSources :: NonEmpty Text -> NonEmpty Syntax.SourcePath-toSources = fmap Syntax.SourcePath---- | Converts a Text into a 'Syntax.TargetPath'------ This is a convenience function when you need to pass a string variable--- as an argument for 'copy' or 'add'------ @--- let destination = buildSomePath pwd--- add ["foo.js"] (toTarget destination)--- @-toTarget :: Text -> Syntax.TargetPath-toTarget = Syntax.TargetPath---- | Adds the --from= option to a COPY instruction.------ This function is meant to be used as an infix operator:------ @--- copy $ ["foo.js"] `to` "." `fromStage` "builder"--- @-fromStage :: Syntax.CopyArgs -> Syntax.CopySource -> Syntax.CopyArgs-fromStage args src = args {Syntax.sourceFlag = src}---- | Adds the --chown= option to a COPY instruction.------ This function is meant to be used as an infix operator:------ @--- copy $ ["foo.js"] `to` "." `ownedBy` "www-data:www-data"--- @-ownedBy :: Syntax.CopyArgs -> Syntax.Chown -> Syntax.CopyArgs-ownedBy args owner = args {Syntax.chownFlag = owner}---- | Usedto join source paths with atarget path as an arguments for 'copy'------ This function is meant to be used as an infix operator:------ @--- copy $ ["foo.js"] `to` "." `ownedBy`--- @-to :: NonEmpty Syntax.SourcePath -> Syntax.TargetPath -> Syntax.CopyArgs-to sources dest = Syntax.CopyArgs sources dest Syntax.NoChown Syntax.NoSource--ports :: [Syntax.Port] -> Syntax.Ports-ports = Syntax.Ports--tcpPort :: Int -> Syntax.Port-tcpPort = flip Syntax.Port Syntax.TCP--udpPort :: Int -> Syntax.Port-udpPort = flip Syntax.Port Syntax.UDP--variablePort :: Text -> Syntax.Port-variablePort varName = Syntax.PortStr ("$" <> varName)--portRange :: Int -> Int -> Syntax.Port-portRange a b = Syntax.PortRange a b Syntax.TCP--udpPortRange :: Int -> Int -> Syntax.Port-udpPortRange a b = Syntax.PortRange a b Syntax.UDP--check :: Syntax.Arguments args -> Syntax.Check args-check command =- Syntax.Check- Syntax.CheckArgs- { Syntax.checkCommand = command- , Syntax.interval = Nothing- , Syntax.timeout = Nothing- , Syntax.startPeriod = Nothing- , Syntax.retries = Nothing- }--interval :: Syntax.Check args -> Integer -> Syntax.Check args-interval ch secs =- case ch of- Syntax.NoCheck -> Syntax.NoCheck- Syntax.Check chArgs -> Syntax.Check chArgs {Syntax.interval = Just $ fromInteger secs}--timeout :: Syntax.Check args -> Integer -> Syntax.Check args-timeout ch secs =- case ch of- Syntax.NoCheck -> Syntax.NoCheck- Syntax.Check chArgs -> Syntax.Check chArgs {Syntax.timeout = Just $ fromInteger secs}--startPeriod :: Syntax.Check args -> Integer -> Syntax.Check args-startPeriod ch secs =- case ch of- Syntax.NoCheck -> Syntax.NoCheck- Syntax.Check chArgs -> Syntax.Check chArgs {Syntax.startPeriod = Just $ fromInteger secs}--retries :: Syntax.Check args -> Integer -> Syntax.Check args-retries ch tries =- case ch of- Syntax.NoCheck -> Syntax.NoCheck- Syntax.Check chArgs -> Syntax.Check chArgs {Syntax.retries = Just $ fromInteger tries}--noCheck :: Syntax.Check args-noCheck = Syntax.NoCheck---- | ONBUILD Dockerfile instruction------ Each nested instruction gets emitted as a separate @ONBUILD@ block------ @--- 'toDockerfile' $ do--- from "node"--- run "apt-get update"--- onBuild $ do--- run "echo more-stuff"--- run "echo here"--- @-onBuild :: MonadFree EInstruction m => EDockerfileM a -> m ()-onBuild b = mapM_ (onBuildRaw . Syntax.instruction) (toDockerfile b)---- | A version of 'toDockerfile' which allows IO actions-toDockerfileIO :: MonadIO m => EDockerfileTM m t -> m Syntax.Dockerfile-toDockerfileIO e = fmap snd (runDockerfileIO e)---- | A version of 'toDockerfileText' which allows IO actions-toDockerfileTextIO :: MonadIO m => EDockerfileTM m t -> m L.Text-toDockerfileTextIO e = fmap snd (runDockerfileTextIO e)---- | Just runs the EDSL's writer monad-runDockerfileIO :: MonadIO m => EDockerfileTM m t -> m (t, Syntax.Dockerfile)-runDockerfileIO e = do- (r, w) <- runWriterT (runDockerWriterIO e)- return (r, map instructionPos w)---- | Runs the EDSL's writer monad and pretty-prints the result-runDockerfileTextIO :: MonadIO m => EDockerfileTM m t -> m (t, L.Text)-runDockerfileTextIO e = do- (r, w) <- runDockerfileIO e- return (r, PrettyPrint.prettyPrint w)
− src/Language/Docker/EDSL/Quasi.hs
@@ -1,49 +0,0 @@-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE TemplateHaskell #-}--module Language.Docker.EDSL.Quasi where--import Language.Haskell.TH-import Language.Haskell.TH.Quote-import Language.Haskell.TH.Syntax--import qualified Data.Text as Text-import Language.Docker.EDSL-import qualified Language.Docker.Parser as Parser-import Language.Docker.Syntax.Lift ()-import Text.Megaparsec (parseErrorPretty)---- | Quasiquoter for embedding dockerfiles on the EDSL------ @--- putStr $ 'toDockerfile' $ do--- from "node"--- run "apt-get update"--- [edockerfile|--- RUN apt-get update--- CMD node something.js--- |]--- @-edockerfile :: QuasiQuoter-edockerfile = dockerfile {quoteExp = edockerfileE}--edockerfileE :: String -> ExpQ-edockerfileE e =- case Parser.parseText (Text.pack e) of- Left err -> fail (parseErrorPretty err)- Right d -> [|embed d|]--dockerfile :: QuasiQuoter-dockerfile =- QuasiQuoter- { quoteExp = dockerfileE- , quoteDec = error "Can't use Dockerfile as a declaration"- , quotePat = error "Can't use Dockerfile as a pattern"- , quoteType = error "Can't use Dockerfile as a type"- }--dockerfileE :: String -> ExpQ-dockerfileE e =- case Parser.parseText (Text.pack e) of- Left err -> fail (parseErrorPretty err)- Right d -> lift d
− src/Language/Docker/EDSL/Types.hs
@@ -1,71 +0,0 @@-{-# LANGUAGE DeriveFunctor #-}--module Language.Docker.EDSL.Types where--import Data.List.NonEmpty (NonEmpty)-import Data.String-import Data.Text (Text)-import qualified Language.Docker.Syntax as Syntax--data EBaseImage- = EUntaggedImage Syntax.Image- (Maybe Syntax.ImageAlias)- | ETaggedImage Syntax.Image- Syntax.Tag- (Maybe Syntax.ImageAlias)- | EDigestedImage Syntax.Image- Text- (Maybe Syntax.ImageAlias)- deriving (Show, Eq, Ord)--instance IsString EBaseImage where- fromString = flip EUntaggedImage Nothing . fromString--data EInstruction next- = From EBaseImage- next- | AddArgs (NonEmpty Syntax.SourcePath)- Syntax.TargetPath- Syntax.Chown- next- | User Text- next- | Label Syntax.Pairs- next- | StopSignal Text- next- | CopyArgs (NonEmpty Syntax.SourcePath)- Syntax.TargetPath- Syntax.Chown- Syntax.CopySource- next- | RunArgs (Syntax.Arguments Text)- next- | CmdArgs (Syntax.Arguments Text)- next- | Shell (Syntax.Arguments Text)- next- | Workdir Syntax.Directory- next- | Expose Syntax.Ports- next- | Volume Text- next- | EntrypointArgs (Syntax.Arguments Text)- next- | Maintainer Text- next- | Env Syntax.Pairs- next- | Arg Text- (Maybe Text)- next- | Comment Text- next- | Healthcheck (Syntax.Check Text)- next- | OnBuildRaw (Syntax.Instruction Text)- next- | Embed [Syntax.InstructionPos Text]- next- deriving (Functor)
− src/Language/Docker/Normalize.hs
@@ -1,94 +0,0 @@-module Language.Docker.Normalize- ( normalizeEscapedLines- ) where--import Data.List (mapAccumL)-import Data.Maybe (catMaybes)-import Data.Semigroup ((<>))-import Data.Text (Text)-import qualified Data.Text as Text-import Data.Text.Lazy (toStrict)-import qualified Data.Text.Lazy.Builder as Builder--data NormalizedLine- = Continue- | Joined !Builder.Builder- !Int---- Finds all lines ending with \ and joins them with the next line using--- a single space. If the next line is a comment, then the comment line is--- deleted. It finally adds the same amount of new lines for each of the--- lines it joined, in order to preserve the line count in the document.-normalize :: Text -> Text-normalize allLines =- let (lastState, res) -- mapAccumL is the idea of a for loop with a variable holding- -- some state and another variable where we append the final result- -- of the looping operation. For each line in lines, apply the transform- -- function. This function always returns a new state, and another element- -- to append to the final result. The ending result of mapAccumL is the final- -- state variale and the resulting list of values. We initialize the loop with- -- the 'Continue' state, which means "no special action to do next"- = mapAccumL transform Continue (Text.lines allLines)- in case lastState of- Continue -- The last line of the document is a normal line, cleanup and return- -> Text.unlines . catMaybes $ res- Joined l times -- The last line contains a \, so we need to add the buffered- -- line back to the result, pad with newlines and cleanup- -> Text.unlines (catMaybes res <> [toText (l <> padNewlines times)])- where- toText = toStrict . Builder.toLazyText- -- | Checks the result of the previous operation in the loop (first argument)- --- -- If the previous result is a 'Joined' operation, then we merge the previous- -- and the current line in a single line and return it.- --- -- If the current line ends with a \, then we produce a 'Joined' state as result- -- of this looping operation.- --- -- If the previous 2 conditions are true at the same time, then we produce a new- -- 'Joined' state holding the concatenation of the Joined buffer and the previous line- -- and we return 'Nothing' as an indication that this line does not form part of the- -- final result.- transform :: NormalizedLine -> Text -> (NormalizedLine, Maybe Text)- transform (Joined prev times) rawLine- -- If we are buffering lines and the next one is empty or it starts with a comment- -- we simply ignore the comment and remember to add a newline- | Text.null line || isComment line = (Joined prev (times + 1), Nothing)- -- If we are buffering lines, then we check whether the current line end with \,- -- if it does, then we merged it into the buffered state- | endsWithEscape line = (Joined (prev <> normalizeLast line) (times + 1), Nothing)- -- otherwise we just yield- -- the concatanation of the buffer and the current line as result, after padding with- -- newlines- | otherwise = (Continue, Just (toText (prev <> Builder.fromText line <> padNewlines times)))- where- line = Text.stripEnd rawLine- -- When not buffering lines, then we just check if we need to start doing it by checking- -- whether or not the current line ends with \. If it does not, then we just yield the- -- current line as part of the result- transform Continue rawLine- | endsWithEscape line = (Joined (normalizeLast line) 1, Nothing)- | otherwise = (Continue, Just line)- where- line = Text.stripEnd rawLine- --- endsWithEscape t- | Text.null t = False- | otherwise = Text.last t == '\\'- --- padNewlines times = Builder.fromText (Text.replicate times (Text.singleton '\n'))- --- normalizeLast = Builder.fromText . Text.dropWhileEnd (== '\\')- --- isComment line =- case (Text.uncons . Text.stripStart) line of- Just ('#', _) -> True- _ -> False---- | Remove new line escapes and join escaped lines together on one line--- to simplify parsing later on. Escapes are replaced with line breaks--- to not alter the line numbers.-normalizeEscapedLines :: Text -> Text-normalizeEscapedLines = normalize--{-# INLINE normalizeEscapedLines #-}
src/Language/Docker/Parser.hs view
@@ -1,604 +1,86 @@-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE DeriveDataTypeable #-}- module Language.Docker.Parser- ( parseText- , parseFile- , Parser- , Error- , DockerfileError(..)- ) where+ ( parseText,+ parseFile,+ parseStdin,+ Parser,+ Error,+ DockerfileError (..),+ )+where -import Control.Monad (void) import qualified Data.ByteString as B-import Data.Data-import Data.List.NonEmpty (NonEmpty, fromList)-import Data.Maybe (listToMaybe)-import Data.Semigroup ((<>))-import qualified Data.Set as S-import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Encoding as E import qualified Data.Text.Encoding.Error as E-import Data.Time.Clock (secondsToDiffTime)-import Text.Megaparsec hiding (Label, label)-import Text.Megaparsec.Char hiding (eol)-import qualified Text.Megaparsec.Char.Lexer as L--import Language.Docker.Normalize+import Language.Docker.Parser.Instruction (parseInstruction, parseComment)+import Language.Docker.Parser.Prelude import Language.Docker.Syntax -data DockerfileError- = DuplicateFlagError String- | NoValueFlagError String- | InvalidFlagError String- | FileListError String- | QuoteError String- String- deriving (Eq, Data, Typeable, Ord, Read, Show)--type Parser = Parsec DockerfileError Text--type Error = ParseError Char DockerfileError--type Instr = Instruction Text--data CopyFlag- = FlagChown Chown- | FlagSource CopySource- | FlagInvalid (Text, Text)--data CheckFlag- = FlagInterval Duration- | FlagTimeout Duration- | FlagStartPeriod Duration- | FlagRetries Retries- | CFlagInvalid (Text, Text)--instance ShowErrorComponent DockerfileError where- showErrorComponent (DuplicateFlagError f) = "duplicate flag: " ++ f- showErrorComponent (FileListError f) =- "unexpected end of line. At least two arguments are required for " ++ f- showErrorComponent (NoValueFlagError f) = "unexpected flag " ++ f ++ " with no value"- showErrorComponent (InvalidFlagError f) = "invalid flag: " ++ f- showErrorComponent (QuoteError t str) =- "unexpected end of " ++ t ++ " quoted string " ++ str ++ " (unmatched quote)"----------------------------------------- Utilities---------------------------------------- | End parsing signaling a “conversion error”.-customError :: DockerfileError -> Parser a-customError = fancyFailure . S.singleton . ErrorCustom--eol :: Parser ()-eol = void $ takeWhile1P (Just "whitespace") isSpaceNl--reserved :: Text -> Parser ()-reserved name = void (lexeme (string' name) <?> T.unpack name)--natural :: Parser Integer-natural = L.decimal <?> "positive number"--commaSep :: Parser a -> Parser [a]-commaSep p = sepBy p (symbol ",")--stringLiteral :: Parser Text-stringLiteral = do- void (char '"')- lit <- manyTill L.charLiteral (char '"')- return (T.pack lit)--brackets :: Parser a -> Parser a-brackets = between (symbol "[" *> spaces) (spaces *> symbol "]")--spaces1 :: Parser ()-spaces1 = void (takeWhile1P (Just "at least one space") (\c -> c == ' ' || c == '\t'))--spaces :: Parser ()-spaces = void (takeWhileP (Just "at least one space") (\c -> c == ' ' || c == '\t'))--symbol :: Text -> Parser Text-symbol name = do- x <- string name- spaces- return x--caseInsensitiveString :: Text -> Parser Text-caseInsensitiveString = string'--charsWithEscapedSpaces :: String -> Parser Text-charsWithEscapedSpaces stopChars = do- buf <- takeWhile1P Nothing (`notElem` ("\n\t\\ " ++ stopChars))- try (jumpEscapeSequence buf) <|> try (backslashFollowedByChars buf) <|> return buf- where- backslashFollowedByChars buf = do- backslashes <- takeWhile1P Nothing (== '\\')- notFollowedBy (char ' ')- rest <- charsWithEscapedSpaces stopChars- return $ T.concat [buf, backslashes, rest]- jumpEscapeSequence buf = do- void $ string "\\ "- rest <- charsWithEscapedSpaces stopChars- return $ T.concat [buf, " ", rest]--lexeme :: Parser a -> Parser a-lexeme p = do- x <- p- spaces1- return x--isNl :: Char -> Bool-isNl c = c == '\n'--isSpaceNl :: Char -> Bool-isSpaceNl c = c == ' ' || c == '\t' || c == '\n'--anyUnless :: (Char -> Bool) -> Parser Text-anyUnless predicate = takeWhileP Nothing (\c -> not (isSpaceNl c || predicate c))--someUnless :: String -> (Char -> Bool) -> Parser Text-someUnless name predicate = takeWhile1P (Just name) (\c -> not (isSpaceNl c || predicate c))----------------------------------------- DOCKER INSTRUCTIONS PARSER--------------------------------------comment :: Parser Instr-comment = do- void $ char '#'- text <- takeWhileP Nothing (not . isNl)- return $ Comment text--parseRegistry :: Parser Registry-parseRegistry = do- domain <- someUnless "a domain name" (== '.')- void $ char '.'- tld <- someUnless "a TLD" (== '/')- void $ char '/'- return $ Registry (domain <> "." <> tld)--taggedImage :: Parser BaseImage-taggedImage = do- registryName <- (Just <$> try parseRegistry) <|> return Nothing- name <- someUnless "the image name with a tag" (\c -> c == '@' || c == ':')- void $ char ':'- tag <- someUnless "the image tag" (== ':')- maybeAlias <- maybeImageAlias- return $ TaggedImage (Image registryName name) (Tag tag) maybeAlias--digestedImage :: Parser BaseImage-digestedImage = do- name <- someUnless "the image name with a digest" (\c -> c == '@' || c == ':')- void $ char '@'- digest <- someUnless "the image digest" (== '@')- maybeAlias <- maybeImageAlias- return $ DigestedImage (Image Nothing name) digest maybeAlias--untaggedImage :: Parser BaseImage-untaggedImage = do- registryName <- (Just <$> try parseRegistry) <|> return Nothing- name <- someUnless "just the image name" (\c -> c == '@' || c == ':')- notInvalidTag name- notInvalidDigest name- maybeAlias <- maybeImageAlias- return $ UntaggedImage (Image registryName name) maybeAlias- where- notInvalidTag :: Text -> Parser ()- notInvalidTag name =- try (notFollowedBy $ string ":") <?> "no ':' or a valid image tag string (example: " ++- T.unpack name ++ ":valid-tag)"- notInvalidDigest :: Text -> Parser ()- notInvalidDigest name =- try (notFollowedBy $ string "@") <?> "no '@' or a valid digest hash (example: " ++- T.unpack name ++ "@a3f42f2de)"--maybeImageAlias :: Parser (Maybe ImageAlias)-maybeImageAlias = Just <$> (spaces1 >> imageAlias) <|> return Nothing--imageAlias :: Parser ImageAlias-imageAlias = do- void (try (reserved "AS") <?> "AS followed by the image alias")- alias <- someUnless "the image alias" (== '\n')- return $ ImageAlias alias--baseImage :: Parser BaseImage-baseImage =- try digestedImage <|> -- Let's try each version- try taggedImage <|>- untaggedImage--from :: Parser Instr-from = do- reserved "FROM"- image <- baseImage- return $ From image--cmd :: Parser Instr-cmd = do- reserved "CMD"- args <- arguments- return $ Cmd args--copy :: Parser Instr-copy = do- reserved "COPY"- flags <- copyFlag `sepEndBy` spaces1- let chownFlags = [c | FlagChown c <- flags]- let sourceFlags = [f | FlagSource f <- flags]- let invalid = [i | FlagInvalid i <- flags]- -- Let's do some validation on the flags- case (invalid, chownFlags, sourceFlags) of- ((k, v):_, _, _) -> unexpectedFlag k v- (_, _:_:_, _) -> customError $ DuplicateFlagError "--chown"- (_, _, _:_:_) -> customError $ DuplicateFlagError "--from"- _ -> do- let ch =- case chownFlags of- [] -> NoChown- c:_ -> c- let fr =- case sourceFlags of- [] -> NoSource- f:_ -> f- fileList "COPY" (\src dest -> Copy (CopyArgs src dest ch fr))--copyFlag :: Parser CopyFlag-copyFlag =- (FlagChown <$> try chown <?> "only one --chown") <|>- (FlagSource <$> try copySource <?> "only one --from") <|>- (FlagInvalid <$> try anyFlag <?> "no other flags")--chown :: Parser Chown-chown = do- void $ string "--chown="- ch <- someUnless "the user and group for chown" (== ' ')- return $ Chown ch--copySource :: Parser CopySource-copySource = do- void $ string "--from="- src <- someUnless "the copy source path" isNl- return $ CopySource src--anyFlag :: Parser (Text, Text)-anyFlag = do- void $ string "--"- name <- someUnless "the flag value" (== '=')- void $ char '='- val <- anyUnless (== ' ')- return (T.append "--" name, val)--fileList :: Text -> (NonEmpty SourcePath -> TargetPath -> Instr) -> Parser Instr-fileList name constr = do- paths <-- (try stringList <?> "an array of strings [\"src_file\", \"dest_file\"]") <|>- (try spaceSeparated <?> "a space separated list of file paths")- case paths of- [_] -> customError $ FileListError (T.unpack name)- _ -> return $ constr (SourcePath <$> fromList (init paths)) (TargetPath $ last paths)- where- spaceSeparated = anyUnless (== ' ') `sepBy1` (try spaces1 <?> "at least another file path")- stringList = brackets $ commaSep stringLiteral--unexpectedFlag :: Text -> Text -> Parser a-unexpectedFlag name "" = customFailure $ NoValueFlagError (T.unpack name)-unexpectedFlag name _ = customFailure $ InvalidFlagError (T.unpack name)--shell :: Parser Instr-shell = do- reserved "SHELL"- args <- arguments- return $ Shell args--stopsignal :: Parser Instr-stopsignal = do- reserved "STOPSIGNAL"- args <- untilEol "the stop signal"- return $ Stopsignal args---- We cannot use string literal because it swallows space--- and therefore have to implement quoted values by ourselves-doubleQuotedValue :: Parser Text-doubleQuotedValue =- between (string "\"") (string "\"") (takeWhileP Nothing (\c -> c /= '"' && c /= '\n'))--singleQuotedValue :: Parser Text-singleQuotedValue =- between (string "'") (string "'") (takeWhileP Nothing (\c -> c /= '\'' && c /= '\n'))--unquotedString :: String -> Parser Text-unquotedString stopChars = do- str <- charsWithEscapedSpaces stopChars- checkFaults str- where- checkFaults str- | T.null str = return str- | T.head str == '\'' = customError $ QuoteError "single" (T.unpack str)- | T.head str == '\"' = customError $ QuoteError "double" (T.unpack str)- | otherwise = return str--singleValue :: String -> Parser Text-singleValue stopChars =- try doubleQuotedValue <|> -- Quotes or no quotes are fine- try singleQuotedValue <|>- (try (unquotedString stopChars) <?> "a string with no quotes")--pair :: Parser (Text, Text)-pair = do- key <- singleValue "="- void $ char '='- value <- singleValue ""- return (key, value)--pairsList :: Parser Pairs-pairsList = pair `sepBy1` spaces1--label :: Parser Instr-label = do- reserved "LABEL"- p <- pairs- return $ Label p--arg :: Parser Instr-arg = do- reserved "ARG"- (try nameWithDefault <?> "the arg name") <|>- Arg <$> untilEol "the argument name" <*> pure Nothing- where- nameWithDefault = do- name <- someUnless "the argument name" (== '=')- void $ char '='- def <- untilEol "the argument value"- return $ Arg name (Just def)--env :: Parser Instr-env = do- reserved "ENV"- p <- pairs- return $ Env p--pairs :: Parser Pairs-pairs = try pairsList <|> try singlePair--singlePair :: Parser Pairs-singlePair = do- key <- anyUnless (== '=')- spaces1 <?> "a space followed by the value for the variable '" ++ T.unpack key ++ "'"- val <- untilEol "the variable value"- return [(key, val)]--user :: Parser Instr-user = do- reserved "USER"- username <- untilEol "the user"- return $ User username--add :: Parser Instr-add = do- reserved "ADD"- flag <- lexeme copyFlag <|> return (FlagChown NoChown)- notFollowedBy (string "--") <?> "only the --chown flag or the src and dest paths"- case flag of- FlagChown ch -> fileList "ADD" (\src dest -> Add (AddArgs src dest ch))- FlagSource _ -> customError $ InvalidFlagError "--from"- FlagInvalid (k, v) -> unexpectedFlag k v--expose :: Parser Instr-expose = do- reserved "EXPOSE"- ps <- ports- return $ Expose ps--port :: Parser Port-port =- (try portVariable <?> "a variable") <|> -- There a many valid representations of ports- (try portRange <?> "a port range optionally followed by the protocol (udp/tcp)") <|>- (try portWithProtocol <?> "a port with its protocol (udp/tcp)") <|>- (try portInt <?> "a valid port number")--ports :: Parser Ports-ports = Ports <$> port `sepEndBy1` (char ' ' <|> char '\t')--portRange :: Parser Port-portRange = do- start <- natural- void $ char '-'- finish <- try natural- proto <- try protocol <|> return TCP- return $ PortRange (fromIntegral start) (fromIntegral finish) proto--protocol :: Parser Protocol-protocol = do- void (char '/')- tcp <|> udp- where- tcp = caseInsensitiveString "tcp" >> return TCP- udp = caseInsensitiveString "udp" >> return UDP--portInt :: Parser Port-portInt = do- portNumber <- natural- notFollowedBy (string "/" <|> string "-")- return $ Port (fromIntegral portNumber) TCP--portWithProtocol :: Parser Port-portWithProtocol = do- portNumber <- natural- proto <- protocol- return $ Port (fromIntegral portNumber) proto--portVariable :: Parser Port-portVariable = do- void (char '$')- variable <- someUnless "the variable name" (== '$')- return $ PortStr (T.append "$" variable)--run :: Parser Instr-run = do- reserved "RUN"- c <- arguments- return $ Run c---- Parse value until end of line is reached-untilEol :: String -> Parser Text-untilEol name = takeWhile1P (Just name) (not . isNl)--workdir :: Parser Instr-workdir = do- reserved "WORKDIR"- directory <- untilEol "the workdir path"- return $ Workdir directory--volume :: Parser Instr-volume = do- reserved "VOLUME"- directory <- untilEol "the volume path"- return $ Volume directory--maintainer :: Parser Instr-maintainer = do- reserved "MAINTAINER"- name <- untilEol "the maintainer name"- return $ Maintainer name---- Parse arguments of a command in the exec form-argumentsExec :: Parser (Arguments Text)-argumentsExec = do- args <- brackets $ commaSep stringLiteral- return $ ArgumentsList (T.unwords args)---- Parse arguments of a command in the shell form-argumentsShell :: Parser (Arguments Text)-argumentsShell = ArgumentsText <$> toEnd- where- toEnd = untilEol "the shell arguments"--arguments :: Parser (Arguments Text)-arguments = try argumentsExec <|> try argumentsShell--entrypoint :: Parser Instr-entrypoint = do- reserved "ENTRYPOINT"- args <- arguments- return $ Entrypoint args--onbuild :: Parser Instr-onbuild = do- reserved "ONBUILD"- i <- parseInstruction- return $ OnBuild i--healthcheck :: Parser Instr-healthcheck = do- reserved "HEALTHCHECK"- Healthcheck <$> (fullCheck <|> noCheck)- where- noCheck = string "NONE" >> return NoCheck- allFlags = do- flags <- someFlags- spaces1 <?> "another flag"- return flags- someFlags = do- x <- checkFlag- cont <- try (spaces1 >> lookAhead (string "--") >> return True) <|> return False- if cont- then do- xs <- someFlags- return (x : xs)- else return [x]- fullCheck = do- flags <- allFlags <|> return []- let intervals = [x | FlagInterval x <- flags]- let timeouts = [x | FlagTimeout x <- flags]- let startPeriods = [x | FlagStartPeriod x <- flags]- let retriesD = [x | FlagRetries x <- flags]- let invalid = [x | CFlagInvalid x <- flags]- -- Let's do some validation on the flags- case (invalid, intervals, timeouts, startPeriods, retriesD) of- ((k, v):_, _, _, _, _) -> unexpectedFlag k v- (_, _:_:_, _, _, _) -> customError $ DuplicateFlagError "--interval"- (_, _, _:_:_, _, _) -> customError $ DuplicateFlagError "--timeout"- (_, _, _, _:_:_, _) -> customError $ DuplicateFlagError "--start-period"- (_, _, _, _, _:_:_) -> customError $ DuplicateFlagError "--retries"- _ -> do- Cmd checkCommand <- cmd- let interval = listToMaybe intervals- let timeout = listToMaybe timeouts- let startPeriod = listToMaybe startPeriods- let retries = listToMaybe retriesD- return $ Check CheckArgs {..}--checkFlag :: Parser CheckFlag-checkFlag =- (FlagInterval <$> durationFlag "--interval=" <?> "--interval") <|>- (FlagTimeout <$> durationFlag "--timeout=" <?> "--timeout") <|>- (FlagStartPeriod <$> durationFlag "--start-period=" <?> "--start-period") <|>- (FlagRetries <$> retriesFlag <?> "--retries") <|>- (CFlagInvalid <$> anyFlag <?> "no flags")--durationFlag :: Text -> Parser Duration-durationFlag flagName = do- void $ try (string flagName)- scale <- natural- unit <- char 's' <|> char 'm' <|> char 'h' <?> "either 's', 'm' or 'h' as the unit"- case unit of- 's' -> return $ Duration (secondsToDiffTime scale)- 'm' -> return $ Duration (secondsToDiffTime (scale * 60))- 'h' -> return $ Duration (secondsToDiffTime (scale * 60 * 60))- _ -> fail "only 's', 'm' or 'h' are allowed as the duration"--retriesFlag :: Parser Retries-retriesFlag = do- void $ try (string "--retries=")- n <- try natural <?> "the number of retries"- return $ Retries (fromIntegral n)----------------------------------------- Main Parser--------------------------------------parseInstruction :: Parser Instr-parseInstruction =- onbuild <|> -- parse all main instructions- from <|>- copy <|>- run <|>- workdir <|>- entrypoint <|>- volume <|>- expose <|>- env <|>- arg <|>- user <|>- label <|>- stopsignal <|>- cmd <|>- shell <|>- maintainer <|>- add <|>- comment <|>- healthcheck- contents :: Parser a -> Parser a contents p = do- void $ takeWhileP Nothing isSpaceNl- r <- p- eof- return r+ void onlyWhitespaces+ r <- p+ eof+ return r -dockerfile :: Parser Dockerfile+dockerfile :: (?esc :: Char) => Parser Dockerfile dockerfile =- many $ do- pos <- getPosition- i <- parseInstruction- eol <|> eof <?> "a new line followed by the next instruction"- return $ InstructionPos i (T.pack . sourceName $ pos) (unPos . sourceLine $ pos)+ many $ do+ pos <- getSourcePos+ i <- parseInstruction+ eol <|> eof <?> "a new line followed by the next instruction"+ return $ InstructionPos i (T.pack . sourceName $ pos) (unPos . sourceLine $ pos) parseText :: Text -> Either Error Dockerfile-parseText s = parse (contents dockerfile) "<string>" $ normalizeEscapedLines s+parseText txt = do+ let ?esc = findEscapePragma (T.lines (dos2unix txt))+ in parse (contents dockerfile) "<string>" (dos2unix txt) parseFile :: FilePath -> IO (Either Error Dockerfile)-parseFile file = doParse <$> B.readFile file+parseFile file = doParse file <$> B.readFile file++-- | Reads the standard input until the end and parses the contents as a Dockerfile+parseStdin :: IO (Either Error Dockerfile)+parseStdin = doParse "/dev/stdin" <$> B.getContents++-- | Parses a list of lines from a dockerfile one by one until either the escape+-- | pragma has been found, or pragmas are no longer expected.+-- | Pragmas can occur only until a comment, an empty line or another+-- | instruction occurs (i.e. they have to be the first lines of a Dockerfile).+findEscapePragma :: [Text] -> Char+findEscapePragma [] = defaultEsc+findEscapePragma (l:ls) =+ case parse (contents parseComment) "<line>" l of+ Left _ -> defaultEsc+ Right (Pragma (Escape (EscapeChar c))) -> c+ Right (Pragma _) -> findEscapePragma ls+ Right _ -> defaultEsc where- doParse =- parse (contents dockerfile) file . normalizeEscapedLines . E.decodeUtf8With E.lenientDecode+ ?esc = defaultEsc++doParse :: FilePath -> B.ByteString -> Either Error Dockerfile+doParse path txt = do+ let ?esc = findEscapePragma (T.lines src)+ in parse (contents dockerfile) path src+ where+ src =+ case B.take 4 txt of+ "\255\254\NUL\NUL" ->+ dos2unix (E.decodeUtf32LEWith E.lenientDecode $ B.drop 4 txt)+ "\NUL\NUL\254\255" ->+ dos2unix (E.decodeUtf32BEWith E.lenientDecode $ B.drop 4 txt)+ _ ->+ case B.take 2 txt of+ "\255\254" ->+ dos2unix (E.decodeUtf16LEWith E.lenientDecode $ B.drop 2 txt)+ "\254\255" ->+ dos2unix (E.decodeUtf16BEWith E.lenientDecode $ B.drop 2 txt)+ _ ->+ case B.take 3 txt of+ "\239\187\191" ->+ dos2unix (E.decodeUtf8With E.lenientDecode $ B.drop 3 txt)+ _ -> dos2unix (E.decodeUtf8With E.lenientDecode txt)++-- | Changes crlf line endings to simple line endings+dos2unix :: T.Text -> T.Text+dos2unix = T.replace "\r\n" "\n"
+ src/Language/Docker/Parser/Arguments.hs view
@@ -0,0 +1,31 @@+module Language.Docker.Parser.Arguments+ ( arguments,+ )+where++import qualified Data.Text as T+import Language.Docker.Parser.Prelude+import Language.Docker.Syntax++-- Parse arguments of a command in the exec form+argumentsExec :: (?esc :: Char) => Parser (Arguments Text)+argumentsExec = do+ args <- brackets $ commaSep doubleQuotedStringEscaped+ return $ ArgumentsList (T.unwords args)++-- Parse arguments of a command in the shell form+argumentsShell :: (?esc :: Char) => Parser (Arguments Text)+argumentsShell =+ try (ArgumentsText <$> untilHeredoc)+ <|> (ArgumentsText <$> toEnd)+ where+ toEnd = untilEol "the shell arguments"++-- Parse arguments of a command in the heredoc format+argumentsHeredoc :: (?esc :: Char) => Parser (Arguments Text)+argumentsHeredoc = ArgumentsText <$> heredoc++arguments :: (?esc :: Char) => Parser (Arguments Text)+arguments = try argumentsHeredoc+ <|> try argumentsExec+ <|> try argumentsShell
+ src/Language/Docker/Parser/Cmd.hs view
@@ -0,0 +1,13 @@+module Language.Docker.Parser.Cmd+ ( parseCmd,+ )+where++import Language.Docker.Parser.Arguments+import Language.Docker.Parser.Prelude+import Language.Docker.Syntax++parseCmd :: (?esc :: Char) => Parser (Instruction Text)+parseCmd = do+ reserved "CMD"+ Cmd <$> arguments
+ src/Language/Docker/Parser/Copy.hs view
@@ -0,0 +1,173 @@+module Language.Docker.Parser.Copy+ ( parseCopy,+ parseAdd,+ )+where++import Data.List.NonEmpty (NonEmpty, fromList)+import qualified Data.Text as T+import Language.Docker.Parser.Prelude+import Language.Docker.Syntax++data Flag+ = FlagChecksum Checksum+ | FlagChown Chown+ | FlagChmod Chmod+ | FlagLink Link+ | FlagSource CopySource+ | FlagExclude Exclude+ | FlagInvalid (Text, Text)++parseCopy :: (?esc :: Char) => Parser (Instruction Text)+parseCopy = do+ reserved "COPY"+ flags <- copyFlag `sepEndBy` requiredWhitespace+ let chownFlags = [c | FlagChown c <- flags]+ let chmodFlags = [c | FlagChmod c <- flags]+ let linkFlags = [l | FlagLink l <- flags]+ let sourceFlags = [f | FlagSource f <- flags]+ let excludeFlags = [e | FlagExclude e <- flags]+ let invalid = [i | FlagInvalid i <- flags]+ -- Let's do some validation on the flags+ case (invalid, chownFlags, chmodFlags, linkFlags, sourceFlags, excludeFlags) of+ ((k, v) : _, _, _, _, _, _) -> unexpectedFlag k v+ (_, _ : _ : _, _, _, _, _) -> customError $ DuplicateFlagError "--chown"+ (_, _, _ : _ : _, _, _, _) -> customError $ DuplicateFlagError "--chmod"+ (_, _, _, _ : _ : _, _, _) -> customError $ DuplicateFlagError "--link"+ (_, _, _, _, _ : _ : _, _) -> customError $ DuplicateFlagError "--from"+ _ -> do+ let cho =+ case chownFlags of+ [] -> NoChown+ c : _ -> c+ let chm =+ case chmodFlags of+ [] -> NoChmod+ c : _ -> c+ let lnk =+ case linkFlags of+ [] -> NoLink+ l : _ -> l+ let fr =+ case sourceFlags of+ [] -> NoSource+ f : _ -> f+ try (heredocList (\src dest -> Copy (CopyArgs src dest) (CopyFlags cho chm lnk fr excludeFlags)))+ <|> fileList "COPY" (\src dest -> Copy (CopyArgs src dest) (CopyFlags cho chm lnk fr excludeFlags))++parseAdd :: (?esc :: Char) => Parser (Instruction Text)+parseAdd = do+ reserved "ADD"+ flags <- addFlag `sepEndBy` requiredWhitespace+ let checksumFlags = [c | FlagChecksum c <- flags]+ let chownFlags = [c | FlagChown c <- flags]+ let chmodFlags = [c | FlagChmod c <- flags]+ let linkFlags = [l | FlagLink l <- flags]+ let excludeFlags = [e | FlagExclude e <- flags]+ let invalidFlags = [i | FlagInvalid i <- flags]+ notFollowedBy (string "--") <?>+ "only the --checksum, --chown, --chmod, --link, --exclude flags or the src and dest paths"+ case (invalidFlags, checksumFlags, chownFlags, linkFlags, chmodFlags, excludeFlags) of+ ((k, v) : _, _, _, _, _, _) -> unexpectedFlag k v+ (_, _ : _ : _, _, _, _, _) -> customError $ DuplicateFlagError "--checksum"+ (_, _, _ : _ : _, _, _, _) -> customError $ DuplicateFlagError "--chown"+ (_, _, _, _ : _ : _, _, _) -> customError $ DuplicateFlagError "--chmod"+ (_, _, _, _, _ : _ : _, _) -> customError $ DuplicateFlagError "--link"+ _ -> do+ let chk = case checksumFlags of+ [] -> NoChecksum+ c : _ -> c+ let cho = case chownFlags of+ [] -> NoChown+ c : _ -> c+ let chm = case chmodFlags of+ [] -> NoChmod+ c : _ -> c+ let lnk = case linkFlags of+ [] -> NoLink+ l : _ -> l+ fileList "ADD" (\src dest -> Add (AddArgs src dest) (AddFlags chk cho chm lnk excludeFlags))++heredocList :: (?esc :: Char) =>+ (NonEmpty SourcePath -> TargetPath -> Instruction Text) ->+ Parser (Instruction Text)+heredocList constr = do+ markers <- try (spaceSep1 heredocMarker) <?> "a list of heredoc markers"+ target <- untilEol "target path"+ case reverse markers of+ [] -> customError $ FileListError "empty list of heredoc markers"+ m:_ -> void $ heredocContent m+ return $ constr (SourcePath <$> fromList markers) (TargetPath target)++fileList :: (?esc :: Char) => Text ->+ (NonEmpty SourcePath -> TargetPath -> Instruction Text) ->+ Parser (Instruction Text)+fileList name constr = do+ paths <-+ (try stringList <?> "an array of strings [\"src_file\", \"dest_file\"]")+ <|> (try spaceSeparated <?> "a space separated list of file paths")+ case paths of+ [_] -> customError $ FileListError (T.unpack name)+ _ -> return $ constr (SourcePath <$> fromList (init paths)) (TargetPath $ last paths)+ where+ spaceSeparated =+ someUnless "a file" (== ' ') `sepEndBy1` (try requiredWhitespace <?> "at least another file path")+ stringList = brackets $ commaSep doubleQuotedString++unexpectedFlag :: Text -> Text -> Parser a+unexpectedFlag name "" = customFailure $ NoValueFlagError (T.unpack name)+unexpectedFlag name _ = customFailure $ InvalidFlagError (T.unpack name)++copyFlag :: (?esc :: Char) => Parser Flag+copyFlag = (FlagSource <$> try copySource <?> "only one --from") <|> addFlag++addFlag :: (?esc :: Char) => Parser Flag+addFlag = (FlagChecksum <$> try checksum <?> "--checksum")+ <|> (FlagChown <$> try chown <?> "--chown")+ <|> (FlagChmod <$> try chmod <?> "--chmod")+ <|> (FlagLink <$> try link <?> "--link")+ <|> (FlagExclude <$> try exclude <?> "--exclude")+ <|> (FlagInvalid <$> try anyFlag <?> "other flag")++checksum :: (?esc :: Char) => Parser Checksum+checksum = do+ void $ string "--checksum="+ chk <- someUnless "the remote file checksum" (== ' ')+ return $ Checksum chk++chown :: (?esc :: Char) => Parser Chown+chown = do+ void $ string "--chown="+ cho <- someUnless "the user and group for chown" (== ' ')+ return $ Chown cho++chmod :: (?esc :: Char) => Parser Chmod+chmod = do+ void $ string "--chmod="+ chm <- someUnless "the mode for chmod" (== ' ')+ return $ Chmod chm++link :: Parser Link+link = do+ void $ string "--link"+ return Link++copySource :: (?esc :: Char) => Parser CopySource+copySource = do+ void $ string "--from="+ src <- someUnless "the copy source path" isNl+ return $ CopySource src++exclude :: (?esc :: Char) => Parser Exclude+exclude = do+ void $ string "--exclude="+ exc <- someUnless "the exclude pattern" (== ' ')+ return $ Exclude exc++anyFlag :: (?esc :: Char) => Parser (Text, Text)+anyFlag = do+ void $ string "--"+ name <- someUnless "the flag value" (== '=')+ void $ char '='+ val <- anyUnless (== ' ')+ return (T.append "--" name, val)
+ src/Language/Docker/Parser/Expose.hs view
@@ -0,0 +1,81 @@+module Language.Docker.Parser.Expose+ ( parseExpose,+ )+where++import qualified Data.Text as T+import Language.Docker.Parser.Prelude+import Language.Docker.Syntax++parseExpose :: (?esc :: Char) => Parser (Instruction Text)+parseExpose = do+ reserved "EXPOSE"+ Expose <$> ports++ports :: (?esc :: Char) => Parser Ports+ports = Ports <$> portspec `sepEndBy` requiredWhitespace++portspec :: (?esc :: Char) => Parser PortSpec+portspec =+ ( try parsePortRangeSpec <?> "A range of ports optionally followed by the protocol" )+ <|> ( parsePortSpec <?> "A port optionally followed by the protocol" )++parsePortRangeSpec :: (?esc :: Char) => Parser PortSpec+parsePortRangeSpec = PortRangeSpec <$> portRange++parsePortSpec :: (?esc :: Char) => Parser PortSpec+parsePortSpec = PortSpec <$> port++port :: (?esc :: Char) => Parser Port+port = (try portVariable <?> "a variable")+ <|> (try portWithProtocol <?> "a port with its protocol (udp/tcp)")+ <|> (portInt <?> "a valid port number")++portRangeLimit :: (?esc :: Char) => Parser Port+portRangeLimit = number <|> variable+ where+ number = do+ num <- natural+ return $ Port (fromIntegral num) TCP++ variable = do+ void (char '$')+ var <- someUnless "the variable name" (\c -> c == '-' || c == '/')+ return $ PortStr (T.append "$" var)++portRange :: (?esc :: Char) => Parser PortRange+portRange = do+ start <- portRangeLimit+ void $ char '-'+ finish <- try portRangeLimit+ proto <- try protocol <|> return TCP+ return $ PortRange (setProto start proto) (setProto finish proto)+ where+ setProto :: Port -> Protocol -> Port+ setProto (Port p _) prot = Port p prot+ setProto (PortStr s) _ = PortStr s++protocol :: Parser Protocol+protocol = do+ void (char '/')+ try (tcp <|> udp) <|> fail "invalid protocol"+ where+ tcp = caseInsensitiveString "tcp" >> return TCP+ udp = caseInsensitiveString "udp" >> return UDP++portInt :: Parser Port+portInt = do+ portNumber <- natural+ notFollowedBy (string "/" <|> string "-")+ return $ Port (fromIntegral portNumber) TCP++portWithProtocol :: Parser Port+portWithProtocol = do+ portNumber <- natural+ Port (fromIntegral portNumber) <$> protocol++portVariable :: (?esc :: Char) => Parser Port+portVariable = do+ void (char '$')+ variable <- someUnless "the variable name" (== '$')+ return $ PortStr (T.append "$" variable)
+ src/Language/Docker/Parser/From.hs view
@@ -0,0 +1,72 @@+module Language.Docker.Parser.From+ ( parseFrom,+ )+where++import qualified Data.Text as T+import Language.Docker.Parser.Prelude+import Language.Docker.Syntax++parseRegistry :: (?esc :: Char) => Parser Registry+parseRegistry = do+ domain <- someUnless "a domain name" (== '.')+ void $ char '.'+ tld <- someUnless "a TLD" (== '/')+ void $ char '/'+ return $ Registry (domain <> "." <> tld)++parsePlatform :: (?esc :: Char) => Parser Platform+parsePlatform = do+ void $ string "--platform="+ p <- someUnless "the platform for the FROM image" (== ' ')+ requiredWhitespace+ return p++parseBaseImage :: (?esc :: Char) => (Text -> Parser (Maybe Tag)) -> Parser BaseImage+parseBaseImage tagParser = do+ maybePlatform <- (Just <$> try parsePlatform) <|> return Nothing+ notFollowedBy (string "--")+ regName <- (Just <$> try parseRegistry) <|> return Nothing+ name <- someUnless "the image name with a tag" (\c -> c == '@' || c == ':')+ maybeTag <- tagParser name <|> return Nothing+ maybeDigest <- (Just <$> try parseDigest) <|> return Nothing+ maybeAlias <- (Just <$> try (requiredWhitespace *> imageAlias)) <|> return Nothing+ return $ BaseImage (Image regName name) maybeTag maybeDigest maybeAlias maybePlatform++taggedImage :: (?esc :: Char) => Parser BaseImage+taggedImage = parseBaseImage tagParser+ where+ tagParser _ = do+ void $ char ':'+ t <- someUnless "the image tag" (\c -> c == '@' || c == ':')+ return (Just . Tag $ t)++parseDigest :: (?esc :: Char) => Parser Digest+parseDigest = do+ void $ char '@'+ d <- someUnless "the image digest" (== '@')+ return $ Digest d++untaggedImage :: (?esc :: Char) => Parser BaseImage+untaggedImage = parseBaseImage notInvalidTag+ where+ notInvalidTag :: Text -> Parser (Maybe Tag)+ notInvalidTag name = do+ try (notFollowedBy $ string ":") <?> "no ':' or a valid image tag string (example: "+ ++ T.unpack name+ ++ ":valid-tag)"+ return Nothing++imageAlias :: (?esc :: Char) => Parser ImageAlias+imageAlias = do+ void (try (reserved "AS") <?> "'AS' followed by the image alias")+ aka <- someUnless "the image alias" (== '\n')+ return $ ImageAlias aka++baseImage :: (?esc :: Char) => Parser BaseImage+baseImage = try taggedImage <|> untaggedImage++parseFrom :: (?esc :: Char) => Parser (Instruction Text)+parseFrom = do+ reserved "FROM"+ From <$> baseImage
+ src/Language/Docker/Parser/Healthcheck.hs view
@@ -0,0 +1,104 @@+{-# LANGUAGE RecordWildCards #-}++module Language.Docker.Parser.Healthcheck+ ( parseHealthcheck,+ )+where++import Data.Maybe (listToMaybe)+import qualified Data.Text as T+import Data.Time.Clock (secondsToDiffTime)+import Language.Docker.Parser.Cmd (parseCmd)+import Language.Docker.Parser.Prelude+import Language.Docker.Syntax++data CheckFlag+ = FlagInterval Duration+ | FlagTimeout Duration+ | FlagStartPeriod Duration+ | FlagStartInterval Duration+ | FlagRetries Retries+ | CFlagInvalid (Text, Text)++parseHealthcheck :: (?esc :: Char) => Parser (Instruction Text)+parseHealthcheck = do+ reserved "HEALTHCHECK"+ Healthcheck <$> (fullCheck <|> noCheck)+ where+ noCheck = string "NONE" >> return NoCheck+ allFlags = do+ flags <- someFlags+ requiredWhitespace <?> "another flag"+ return flags+ someFlags = do+ x <- checkFlag+ cont <- try (requiredWhitespace >> lookAhead (string "--") >> return True) <|> return False+ if cont+ then do+ xs <- someFlags+ return (x : xs)+ else return [x]+ fullCheck = do+ flags <- allFlags <|> return []+ let intervals = [x | FlagInterval x <- flags]+ let timeouts = [x | FlagTimeout x <- flags]+ let startPeriods = [x | FlagStartPeriod x <- flags]+ let startIntervals = [x | FlagStartInterval x <- flags]+ let retriesD = [x | FlagRetries x <- flags]+ let invalid = [x | CFlagInvalid x <- flags]+ -- Let's do some validation on the flags+ case (invalid, intervals, timeouts, startPeriods, startIntervals, retriesD) of+ ((k, v) : _, _, _, _, _, _) -> unexpectedFlag k v+ (_, _ : _ : _, _, _, _, _) -> customError $ DuplicateFlagError "--interval"+ (_, _, _ : _ : _, _, _, _) -> customError $ DuplicateFlagError "--timeout"+ (_, _, _, _ : _ : _, _, _) -> customError $ DuplicateFlagError "--start-period"+ (_, _, _, _, _ : _ : _, _) -> customError $ DuplicateFlagError "--start-interval"+ (_, _, _, _, _, _ : _ : _) -> customError $ DuplicateFlagError "--retries"+ _ -> do+ Cmd checkCommand <- parseCmd+ let interval = listToMaybe intervals+ let timeout = listToMaybe timeouts+ let startPeriod = listToMaybe startPeriods+ let startInterval = listToMaybe startIntervals+ let retries = listToMaybe retriesD+ return $ Check CheckArgs {..}++checkFlag :: (?esc :: Char) => Parser CheckFlag+checkFlag =+ (FlagInterval <$> durationFlag "--interval=" <?> "--interval")+ <|> (FlagTimeout <$> durationFlag "--timeout=" <?> "--timeout")+ <|> (FlagStartPeriod <$> durationFlag "--start-period=" <?> "--start-period")+ <|> (FlagStartInterval <$> durationFlag "--start-interval=" <?> "--start-interval")+ <|> (FlagRetries <$> retriesFlag <?> "--retries")+ <|> (CFlagInvalid <$> anyFlag <?> "no flags")++durationFlag :: Text -> Parser Duration+durationFlag flagName = do+ void $ try (string flagName)+ value <- try ( fromRational . realToFrac <$> fractional )+ <|> ( secondsToDiffTime . fromInteger <$> natural )+ <?> "a natural or fractional number"+ unit <- char 's' <|> char 'm' <|> char 'h' <?> "either 's', 'm' or 'h' as the unit"+ case unit of+ 's' -> return $ Duration value+ 'm' -> return $ Duration (value * 60)+ 'h' -> return $ Duration (value * 60 * 60)+ _ -> fail "only 's', 'm' or 'h' are allowed as the duration"++retriesFlag :: Parser Retries+retriesFlag = do+ void $ try (string "--retries=")+ n <- try natural <?> "the number of retries"+ return $ Retries (fromIntegral n)++anyFlag :: (?esc :: Char) => Parser (Text, Text)+anyFlag = do+ void $ string "--"+ name <- someUnless "the flag value" (== '=')+ void $ char '='+ val <- anyUnless (== ' ')+ return (T.append "--" name, val)++unexpectedFlag :: Text -> Text -> Parser a+unexpectedFlag name "" = customFailure $ NoValueFlagError (T.unpack name)+unexpectedFlag name _ = customFailure $ InvalidFlagError (T.unpack name)
+ src/Language/Docker/Parser/Instruction.hs view
@@ -0,0 +1,147 @@+module Language.Docker.Parser.Instruction+ ( parseArg,+ parseComment,+ parseEntryPoint,+ parseEscapePragma,+ parseInstruction,+ parseMaintainer,+ parseOnbuild,+ parsePragma,+ parseShell,+ parseStopSignal,+ parseSyntaxPragma,+ parseUser,+ parseVolume,+ parseWorkdir,+ )+where++import Language.Docker.Parser.Arguments (arguments)+import Language.Docker.Parser.Cmd (parseCmd)+import Language.Docker.Parser.Copy (parseAdd, parseCopy)+import Language.Docker.Parser.Expose (parseExpose)+import Language.Docker.Parser.From (parseFrom)+import Language.Docker.Parser.Healthcheck (parseHealthcheck)+import Language.Docker.Parser.Pairs (parseEnv, parseLabel)+import Language.Docker.Parser.Prelude+import Language.Docker.Parser.Run (parseRun)+import Language.Docker.Syntax++parseShell :: (?esc :: Char) => Parser (Instruction Text)+parseShell = do+ reserved "SHELL"+ Shell <$> arguments++parseStopSignal :: (?esc :: Char) => Parser (Instruction Text)+parseStopSignal = do+ reserved "STOPSIGNAL"+ args <- untilEol "the stop signal"+ return $ Stopsignal args++parseArg :: (?esc :: Char) => Parser (Instruction Text)+parseArg = do+ reserved "ARG"+ (try nameWithDefault <?> "the arg name")+ <|> (try nameWithoutDefault <?> "the arg name")+ <|> Arg <$> untilEol "the argument name" <*> pure Nothing+ where+ nameWithoutDefault = do+ name <- someUnless "the argument name" (== '=')+ void $ untilEol "the rest"+ return $ Arg name Nothing+ nameWithDefault = do+ name <- someUnless "the argument name" (== '=')+ void $ char '='+ df <- untilEol "the argument value"+ return $ Arg name (Just df)++parseUser :: (?esc :: Char) => Parser (Instruction Text)+parseUser = do+ reserved "USER"+ username <- untilEol "the user"+ return $ User username++parseWorkdir :: (?esc :: Char) => Parser (Instruction Text)+parseWorkdir = do+ reserved "WORKDIR"+ directory <- untilEol "the workdir path"+ return $ Workdir directory++parseVolume :: (?esc :: Char) => Parser (Instruction Text)+parseVolume = do+ reserved "VOLUME"+ directory <- untilEol "the volume path"+ return $ Volume directory++parseMaintainer :: (?esc :: Char) => Parser (Instruction Text)+parseMaintainer = do+ reserved "MAINTAINER"+ name <- untilEol "the maintainer name"+ return $ Maintainer name++parseEntryPoint :: (?esc :: Char) => Parser (Instruction Text)+parseEntryPoint = do+ reserved "ENTRYPOINT"+ Entrypoint <$> arguments++parseOnbuild :: (?esc :: Char) => Parser (Instruction Text)+parseOnbuild = do+ reserved "ONBUILD"+ OnBuild <$> parseInstruction++parsePragma :: (?esc :: Char) => Parser (Instruction Text)+parsePragma = do+ void $ lexeme' (char '#')+ choice+ [ parseEscapePragma <?> "an escape",+ parseSyntaxPragma <?> "a syntax"+ ]++parseEscapePragma :: Parser (Instruction Text)+parseEscapePragma = do+ void $ lexeme' (string "escape")+ void $ lexeme' (string "=")+ Pragma . Escape . EscapeChar <$> charLiteral++parseSyntaxPragma :: (?esc :: Char) => Parser (Instruction Text)+parseSyntaxPragma = do+ void $ lexeme' (string "syntax")+ void $ lexeme' (string "=")+ img <- untilEol "the syntax"+ return $ Pragma+ ( Syntax+ ( SyntaxImage+ ( Image+ { registryName = Nothing,+ imageName = img+ }+ )+ )+ )++parseComment :: (?esc :: Char) => Parser (Instruction Text)+parseComment = (try parsePragma <?> "a pragma") <|> Comment <$> comment++parseInstruction :: (?esc :: Char) => Parser (Instruction Text)+parseInstruction =+ choice+ [ parseOnbuild,+ parseFrom,+ parseCopy,+ parseRun,+ parseWorkdir,+ parseEntryPoint,+ parseVolume,+ parseExpose,+ parseEnv,+ parseArg,+ parseUser,+ parseLabel,+ parseStopSignal,+ parseCmd,+ parseShell,+ parseMaintainer,+ parseAdd,+ parseComment,+ parseHealthcheck+ ]
+ src/Language/Docker/Parser/Pairs.hs view
@@ -0,0 +1,58 @@+module Language.Docker.Parser.Pairs+ ( parseEnv,+ parseLabel,+ )+where++import qualified Data.Text as T+import Language.Docker.Parser.Prelude+import Language.Docker.Syntax+++unquotedString :: (?esc :: Char) => (Char -> Bool) -> Parser Text+unquotedString acceptCondition = do+ str <- stringWithEscaped [' ', '\t'] (Just (\c -> acceptCondition c && c /= '"' && c /= '\''))+ checkFaults str+ where+ checkFaults str+ | T.null str = fail "a non empty string"+ | T.head str == '\'' = customError $ QuoteError "single" (T.unpack str)+ | T.head str == '\"' = customError $ QuoteError "double" (T.unpack str)+ | otherwise = return str++singleValue :: (?esc :: Char) => (Char -> Bool) -> Parser Text+singleValue acceptCondition = mconcat <$> variants+ where+ variants =+ many $+ choice+ [ doubleQuotedStringEscaped <?> "a string inside double quotes",+ singleQuotedStringEscaped <?> "a string inside single quotes",+ unquotedString acceptCondition <?> "a string with no quotes"+ ]++pair :: (?esc :: Char) => Parser (Text, Text)+pair = do+ key <- singleValue (/= '=')+ value <- withEqualSign <|> withoutEqualSign+ return (key, value)+ where+ withEqualSign = do+ void $ char '='+ singleValue (\c -> c /= ' ' && c /= '\t')+ withoutEqualSign = do+ requiredWhitespace+ untilEol "value"++pairs :: (?esc :: Char) => Parser Pairs+pairs = (pair <?> "a key value pair (key=value)") `sepEndBy1` requiredWhitespace++parseLabel :: (?esc :: Char) => Parser (Instruction Text)+parseLabel = do+ reserved "LABEL"+ Label <$> pairs++parseEnv :: (?esc :: Char) => Parser (Instruction Text)+parseEnv = do+ reserved "ENV"+ Env <$> pairs
+ src/Language/Docker/Parser/Prelude.hs view
@@ -0,0 +1,380 @@+{-# LANGUAGE DeriveDataTypeable #-}++module Language.Docker.Parser.Prelude+ (+ DockerfileError (..),+ Error,+ Parser,+ anyUnless,+ brackets,+ caseInsensitiveString,+ commaSep,+ comment,+ customError,+ doubleQuotedString,+ doubleQuotedStringEscaped,+ eol,+ escapedLineBreaks',+ fractional,+ heredoc,+ heredocContent,+ heredocMarker,+ isNl,+ isSpaceNl,+ lexeme',+ lexeme,+ natural,+ onlySpaces,+ onlyWhitespaces,+ requiredWhitespace,+ reserved,+ singleQuotedString,+ singleQuotedStringEscaped,+ someUnless,+ spaceSep1,+ stringWithEscaped,+ symbol,+ untilEol,+ untilHeredoc,+ whitespace,+ module Megaparsec,+ char,+ L.charLiteral,+ string,+ string',+ void,+ when,+ Text,+ module Data.Default.Class+ )+where++import Control.Monad (void, when)+import Data.Data+import Data.Maybe (fromMaybe)+import qualified Data.Set as S+import Data.Text (Text)+import qualified Data.Text as T+import Text.Megaparsec as Megaparsec hiding (Label)+import Text.Megaparsec.Char hiding (eol)+import qualified Text.Megaparsec.Char.Lexer as L+import Data.Default.Class (Default(def))++data DockerfileError+ = DuplicateFlagError String+ | NoValueFlagError String+ | InvalidFlagError String+ | FileListError String+ | MissingArgument [Text]+ | DuplicateArgument Text+ | UnexpectedArgument Text Text+ | QuoteError+ String+ String+ deriving (Eq, Data, Typeable, Ord, Read, Show)++type Parser = Parsec DockerfileError Text++type Error = ParseErrorBundle Text DockerfileError++instance ShowErrorComponent DockerfileError where+ showErrorComponent (DuplicateFlagError f) = "duplicate flag: " ++ f+ showErrorComponent (FileListError f) =+ "unexpected end of line. At least two arguments are required for " ++ f+ showErrorComponent (NoValueFlagError f) = "unexpected flag " ++ f ++ " with no value"+ showErrorComponent (InvalidFlagError f) = "invalid flag: " ++ f+ showErrorComponent (MissingArgument f) = "missing required argument(s) for mount flag: " ++ show f+ showErrorComponent (DuplicateArgument f) = "duplicate argument for mount flag: " ++ T.unpack f+ showErrorComponent (UnexpectedArgument a b) = "unexpected argument '" ++ T.unpack a ++ "' for mount of type '" ++ T.unpack b ++ "'"+ showErrorComponent (QuoteError t str) =+ "unexpected end of " ++ t ++ " quoted string " ++ str ++ " (unmatched quote)"++-- Spaces are sometimes significant information in a dockerfile, this type records+-- thee presence of lack of such whitespace in certain lines.+data FoundWhitespace+ = FoundWhitespace+ | MissingWhitespace+ deriving (Eq, Show)++-- There is no need to remember how many spaces we found in a line, so we can+-- cheaply remmeber that we already whitenessed some significant whitespace while+-- parsing an expression by concatenating smaller results+instance Semigroup FoundWhitespace where+ FoundWhitespace <> _ = FoundWhitespace+ _ <> a = a++instance Monoid FoundWhitespace where+ mempty = MissingWhitespace++------------------------------------+-- Utilities+------------------------------------++-- | End parsing signaling a “conversion error”.+customError :: DockerfileError -> Parser a+customError = fancyFailure . S.singleton . ErrorCustom++castToSpace :: FoundWhitespace -> Text+castToSpace FoundWhitespace = " "+castToSpace MissingWhitespace = ""++eol :: (?esc :: Char) => Parser ()+eol = void ws <?> "end of line"+ where+ ws =+ some $+ choice+ [ void onlySpaces1,+ void $ takeWhile1P Nothing (== '\n'),+ void escapedLineBreaks+ ]++reserved :: (?esc :: Char) => Text -> Parser ()+reserved name = void (lexeme (string' name) <?> T.unpack name)++natural :: Parser Integer+natural = L.decimal <?> "positive number"++fractional :: Parser Float+fractional = L.float <?> "fractional number"++commaSep :: (?esc :: Char) => Parser a -> Parser [a]+commaSep p = sepBy (p <* whitespace) (symbol ",")++spaceSep1 :: Parser a -> Parser [a]+spaceSep1 p = sepEndBy1 p onlySpaces++singleQuotedString :: Parser Text+singleQuotedString = quotedString '\''++doubleQuotedString :: Parser Text+doubleQuotedString = quotedString '\"'++-- | Special variants of the string parsers dealing with escaped line breaks+-- and escaped quote characters well.+singleQuotedStringEscaped :: (?esc :: Char) => Parser Text+singleQuotedStringEscaped = quotedStringEscaped '\''++doubleQuotedStringEscaped :: (?esc :: Char) => Parser Text+doubleQuotedStringEscaped = quotedStringEscaped '\"'++quotedString :: Char -> Parser Text+quotedString c = do+ lit <- char c >> manyTill L.charLiteral (char c)+ return $ T.pack lit++quotedStringEscaped :: (?esc :: Char) => Char -> Parser Text+quotedStringEscaped q =+ between (char q) (char q) $ stringWithEscaped [q] Nothing++brackets :: (?esc :: Char) => Parser a -> Parser a+brackets = between (symbol "[" *> whitespace) (whitespace *> symbol "]")++untilWS :: Parser Text+untilWS = do+ s <- manyTill anySingle spaceChar+ return $ T.pack s++heredocMarker :: (?esc :: Char) => Parser Text+heredocMarker = do+ void $ string "<<"+ void $ takeWhileP (Just "dash") (== '-')+ m <- try doubleQuotedString <|> try singleQuotedString <|> untilWS+ optional heredocRedirect+ pure m++heredocRedirect :: (?esc :: Char) => Parser Text+heredocRedirect = do+ void $ ( string "|" <|> string ">" <|> string ">>" ) *> onlySpaces+ untilEol "heredoc path"++-- | This tries to parse everything until there is the just the heredoc marker+-- on its own on a line. Making provisions for the case that the marker is+-- followed by the end of the file rather than another newline.+heredocContent :: Text -> Parser Text+heredocContent marker = do+ emptyHeredoc <- observing delimiter+ doc <- case emptyHeredoc of+ Left _ -> manyTill anySingle termination+ Right _ -> pure ""+ return $ T.strip $ T.pack doc+ where+ termination :: Parser Text+ termination = try terEOL <|> terEOF++ terEOL :: Parser Text+ terEOL = string $ "\n" <> marker <> "\n"++ terEOF :: Parser Text+ terEOF = do+ t <- string $ "\n" <> marker+ hidden eof+ pure t++ delimiter :: Parser Text+ delimiter = try delEOL <|> delEOF++ delEOL :: Parser Text+ delEOL = string $ marker <> "\n"++ delEOF :: Parser Text+ delEOF = do+ t <- string marker+ hidden eof+ pure t++heredoc :: (?esc :: Char) => Parser Text+heredoc = do+ m <- heredocMarker+ heredocContent m++-- | Parses text until a heredoc or newline is found. Will also consume the+-- heredoc. It will however respect escaped newlines.+untilHeredoc :: (?esc :: Char) => Parser Text+untilHeredoc = do+ txt <- manyTill chars heredoc+ return $ T.strip $ mconcat txt+ where+ chars =+ choice+ [ castToSpace <$> escapedLineBreaks,+ charToTxt <$> anySingleBut '\n'+ ]+ charToTxt c = T.pack [c]++onlySpaces :: Parser Text+onlySpaces = takeWhileP (Just "spaces") (\c -> c == ' ' || c == '\t')++onlySpaces1 :: Parser Text+onlySpaces1 = takeWhile1P (Just "at least one space") (\c -> c == ' ' || c == '\t')++onlyWhitespaces :: Parser Text+onlyWhitespaces = takeWhileP+ (Just "whitespaces")+ (\c -> c == ' ' || c == '\t' || c == '\n' || c == '\r')++escapedLineBreaks :: (?esc :: Char) => Parser FoundWhitespace+escapedLineBreaks = mconcat <$> breaks+ where+ breaks =+ some $ do+ try (char ?esc *> onlySpaces *> newlines)+ skipMany . try $ onlySpaces *> comment *> newlines+ -- Spaces before the next '\' have a special significance+ -- so we remembeer the fact that we found some+ FoundWhitespace <$ onlySpaces1 <|> pure MissingWhitespace+ newlines = takeWhile1P Nothing isNl++-- | This converts escaped line breaks, but keeps _all_ spaces before and after+escapedLineBreaks' :: (?esc :: Char) => Parser Text+escapedLineBreaks' = mconcat <$> breaks+ where+ breaks =+ some $ do+ try ( char ?esc *> onlySpaces *> newlines )+ skipMany . try $ onlySpaces *> comment *> newlines+ onlySpaces1+ newlines = takeWhile1P Nothing isNl++foundWhitespace :: (?esc :: Char) => Parser FoundWhitespace+foundWhitespace = mconcat <$> found+ where+ found = many $ choice [FoundWhitespace <$ onlySpaces1, escapedLineBreaks]++whitespace :: (?esc :: Char) => Parser ()+whitespace = void foundWhitespace++requiredWhitespace :: (?esc :: Char) => Parser ()+requiredWhitespace = do+ ws <- foundWhitespace+ case ws of+ FoundWhitespace -> pure ()+ MissingWhitespace -> fail "missing whitespace"++-- Parse value until end of line is reached+-- after consuming all escaped newlines+untilEol :: (?esc :: Char) => String -> Parser Text+untilEol name = do+ res <- mconcat <$> predicate+ when (res == "") $ fail ("expecting " ++ name)+ pure res+ where+ predicate =+ many $+ choice+ [ castToSpace <$> escapedLineBreaks,+ takeWhile1P (Just name) (\c -> c /= '\n' && c /= ?esc),+ takeWhile1P Nothing (== ?esc) <* notFollowedBy (char '\n')+ ]++symbol :: (?esc :: Char) => Text -> Parser Text+symbol name = do+ x <- string name+ whitespace+ return x++caseInsensitiveString :: Text -> Parser Text+caseInsensitiveString = string'++stringWithEscaped :: (?esc :: Char) => [Char] -> Maybe (Char -> Bool) -> Parser Text+stringWithEscaped quoteChars maybeAcceptCondition = mconcat <$> sequences+ where+ sequences =+ many $+ choice+ [ mconcat <$> inner,+ try $ takeWhile1P Nothing (== ?esc) <* notFollowedBy quoteParser,+ string (T.singleton ?esc) *> quoteParser+ ]+ inner =+ some $+ choice+ [ castToSpace <$> escapedLineBreaks,+ takeWhile1P+ Nothing+ (\c -> c /= ?esc && c /= '\n' && c `notElem` quoteChars && acceptCondition c)+ ]+ quoteParser = T.singleton <$> choice (fmap char quoteChars)+ acceptCondition = fromMaybe (const True) maybeAcceptCondition++lexeme :: (?esc :: Char) => Parser a -> Parser a+lexeme p = do+ x <- p+ requiredWhitespace+ return x++lexeme' :: Parser a -> Parser a+lexeme' p = do+ x <- p+ void onlySpaces+ return x++isNl :: Char -> Bool+isNl c = c == '\n'++isSpaceNl :: (?esc :: Char) => Char -> Bool+isSpaceNl c = c == ' ' || c == '\t' || c == '\n' || c == ?esc++anyUnless :: (?esc :: Char) => (Char -> Bool) -> Parser Text+anyUnless predicate = someUnless "" predicate <|> pure ""++someUnless :: (?esc :: Char) => String -> (Char -> Bool) -> Parser Text+someUnless name predicate = do+ res <- applyPredicate+ case res of+ [] -> fail ("expecting " ++ name)+ _ -> pure (mconcat res)+ where+ applyPredicate =+ many $+ choice+ [ castToSpace <$> escapedLineBreaks,+ takeWhile1P (Just name) (\c -> not (isSpaceNl c || predicate c)),+ takeWhile1P Nothing (\c -> c == ?esc && not (predicate c))+ <* notFollowedBy (char '\n')+ ]++comment :: Parser Text+comment = do+ void $ char '#'+ takeWhileP Nothing (not . isNl)
+ src/Language/Docker/Parser/Run.hs view
@@ -0,0 +1,343 @@+{-# LANGUAGE DuplicateRecordFields #-}++module Language.Docker.Parser.Run+ ( parseRun,+ runFlags,+ )+where++import Data.Functor (($>))+import qualified Data.Set as Set+import Language.Docker.Parser.Arguments (arguments)+import Language.Docker.Parser.Prelude+import Language.Docker.Syntax++data RunFlag+ = RunFlagMount RunMount+ | RunFlagSecurity RunSecurity+ | RunFlagNetwork RunNetwork+ deriving (Show)++data RunMountArg+ = MountArgEnv Text+ | MountArgFromImage Text+ | MountArgId Text+ | MountArgMode Text+ | MountArgReadOnly Bool+ | MountArgRequired Bool+ | MountArgSharing CacheSharing+ | MountArgSource SourcePath+ | MountArgTarget TargetPath+ | MountArgType MountType+ | MountArgUid Text+ | MountArgGid Text+ | MountArgRelabel Relabel+ deriving (Show)++data MountType+ = Bind+ | Cache+ | Tmpfs+ | Secret+ | Ssh+ deriving (Show)++parseRun :: (?esc :: Char) => Parser (Instruction Text)+parseRun = do+ reserved "RUN"+ Run <$> runArguments++runArguments :: (?esc :: Char) => Parser (RunArgs Text)+runArguments = do+ presentFlags <- choice [runFlags <* requiredWhitespace, pure (RunFlags mempty Nothing Nothing)]+ args <- arguments+ return $ RunArgs args presentFlags++runFlags :: (?esc :: Char) => Parser RunFlags+runFlags = do+ flags <- runFlag `sepBy` flagSeparator+ return $ foldr toRunFlags emptyFlags flags+ where+ flagSeparator = try (requiredWhitespace *> lookAhead (string "--")) <|> fail "expected flag"+ emptyFlags = RunFlags mempty Nothing Nothing+ toRunFlags (RunFlagMount m) rf@RunFlags { mount = mnt } = rf {mount = Set.insert m mnt}+ toRunFlags (RunFlagNetwork n) rf = rf {network = Just n}+ toRunFlags (RunFlagSecurity s) rf = rf {security = Just s}++runFlag :: (?esc :: Char) => Parser RunFlag+runFlag =+ choice+ [ RunFlagMount <$> runFlagMount,+ RunFlagSecurity <$> runFlagSecurity,+ RunFlagNetwork <$> runFlagNetwork+ ]++runFlagSecurity :: Parser RunSecurity+runFlagSecurity = do+ void $ string "--security="+ choice [Insecure <$ string "insecure", Sandbox <$ string "sandbox"]++runFlagNetwork :: Parser RunNetwork+runFlagNetwork = do+ void $ string "--network="+ choice [NetworkNone <$ string "none", NetworkHost <$ string "host", NetworkDefault <$ string "default"]++runFlagMount :: (?esc :: Char) => Parser RunMount+runFlagMount = do+ void $ string "--mount="+ args <- mountArgs `sepBy1` string ","+ mt <- parseTypeFromArgs args+ case mt of+ Bind -> BindMount <$> bindMount (filter (not . isMountArgType) args)+ Cache -> CacheMount <$> cacheMount (filter (not . isMountArgType) args)+ Tmpfs -> TmpfsMount <$> tmpfsMount (filter (not . isMountArgType) args)+ Secret -> SecretMount <$> secretMount (filter (not . isMountArgType) args)+ Ssh -> SshMount <$> secretMount (filter (not . isMountArgType) args)++parseTypeFromArgs :: [RunMountArg] -> Parser MountType+parseTypeFromArgs args =+ -- `notFollowedBy eof` is a trivially succeeding parser that isn't supposed to+ -- consume any input here. It's a hack that converts a simple type into a+ -- parser. This allows this function to emit parse errors after analyzing its+ -- input arguments and not consume any input.+ case filter isMountArgType args of+ [] -> Bind <$ notFollowedBy eof+ [MountArgType t] -> t <$ notFollowedBy eof+ _:_ -> fail "--mount with multiple `type` arguments"++isMountArgType :: RunMountArg -> Bool+isMountArgType (MountArgType _) = True+isMountArgType _ = False++bindMount :: [RunMountArg] -> Parser BindOpts+bindMount args =+ case validArgs "bind" allowed required args of+ Left e -> customError e+ Right as -> return $ foldr bindOpts def as+ where+ allowed = Set.fromList ["target", "source", "from", "ro", "relabel"]+ required = Set.singleton "target"+ bindOpts :: RunMountArg -> BindOpts -> BindOpts+ bindOpts (MountArgTarget path) bo = bo {bTarget = path}+ bindOpts (MountArgSource path) bo = bo {bSource = Just path}+ bindOpts (MountArgFromImage img) bo = bo {bFromImage = Just img}+ bindOpts (MountArgReadOnly ro) bo = bo {bReadOnly = Just ro}+ bindOpts (MountArgRelabel re) bo = bo {bRelabel = Just re}+ bindOpts invalid _ = error $ "unhandled " <> show invalid <> " please report this bug"++cacheMount :: [RunMountArg] -> Parser CacheOpts+cacheMount args =+ case validArgs "cache" allowed required args of+ Left e -> customError e+ Right as -> return $ foldr cacheOpts def as+ where+ allowed = Set.fromList ["target", "sharing", "id", "ro", "from", "source", "mode", "uid", "gid"]+ required = Set.singleton "target"+ cacheOpts :: RunMountArg -> CacheOpts -> CacheOpts+ cacheOpts (MountArgTarget path) co = co {cTarget = path}+ cacheOpts (MountArgSharing sh) co = co {cSharing = Just sh}+ cacheOpts (MountArgId i) co = co {cCacheId = Just i}+ cacheOpts (MountArgReadOnly ro) co = co {cReadOnly = Just ro}+ cacheOpts (MountArgFromImage img) co = co {cFromImage = Just img}+ cacheOpts (MountArgSource path) co = co {cSource = Just path}+ cacheOpts (MountArgMode m) co = co {cMode = Just m}+ cacheOpts (MountArgUid u) co = co {cUid = Just u}+ cacheOpts (MountArgGid g) co = co {cGid = Just g}+ cacheOpts invalid _ = error $ "unhandled " <> show invalid <> " please report this bug"++tmpfsMount :: [RunMountArg] -> Parser TmpOpts+tmpfsMount args =+ case validArgs "tmpfs" required required args of+ Left e -> customError e+ Right as -> return $ foldr tmpOpts def as+ where+ required = Set.singleton "target"+ tmpOpts :: RunMountArg -> TmpOpts -> TmpOpts+ tmpOpts (MountArgTarget path) t = t {tTarget = path}+ tmpOpts invalid _ = error $ "unhandled " <> show invalid <> " please report this bug"++secretMount :: [RunMountArg] -> Parser SecretOpts+secretMount args =+ case validArgs "secret" allowed required args of+ Left e -> customError e+ Right as -> return $ foldr secretOpts def as+ where+ allowed = Set.fromList ["target", "id", "required", "source", "mode", "uid", "gid", "env"]+ required = Set.empty+ secretOpts :: RunMountArg -> SecretOpts -> SecretOpts+ secretOpts (MountArgTarget path) co = co {sTarget = Just path}+ secretOpts (MountArgId i) co = co {sCacheId = Just i}+ secretOpts (MountArgRequired r) co = co {sIsRequired = Just r}+ secretOpts (MountArgSource path) co = co {sSource = Just path}+ secretOpts (MountArgEnv e) co = co {sEnv = Just e}+ secretOpts (MountArgMode m) co = co {sMode = Just m}+ secretOpts (MountArgUid u) co = co {sUid = Just u}+ secretOpts (MountArgGid g) co = co {sGid = Just g}+ secretOpts invalid _ = error $ "unhandled " <> show invalid <> " please report this bug"++validArgs ::+ Foldable t =>+ Text ->+ Set.Set Text ->+ Set.Set Text ->+ t RunMountArg ->+ Either DockerfileError [RunMountArg]+validArgs typeName allowed required args =+ let (result, seen) = foldr checkValidArg (Right [], Set.empty) args+ in case Set.toList (Set.difference required seen) of+ [] -> result+ missing -> Left $ MissingArgument missing+ where+ checkValidArg :: RunMountArg -> (Either DockerfileError [RunMountArg], Set.Set Text) -> (Either DockerfileError [RunMountArg], Set.Set Text)+ checkValidArg _ x@(Left _, _) = x+ checkValidArg a (Right as, seen) =+ let name = toArgName a+ in case (Set.member name allowed, Set.member name seen) of+ (False, _) -> (Left (UnexpectedArgument name typeName), seen)+ (_, True) -> (Left (DuplicateArgument name), seen)+ (True, False) -> (Right (a : as), Set.insert name seen)++mountArgs :: (?esc :: Char) => Parser RunMountArg+mountArgs =+ choice+ [ mountArgEnv,+ mountArgFromImage,+ mountArgGid,+ mountArgId,+ mountArgMode,+ mountArgReadOnly,+ mountArgRelabel,+ mountArgRequired,+ mountArgSharing,+ mountArgSource,+ mountArgTarget,+ mountArgType,+ mountArgUid+ ]++stringArg :: (?esc :: Char) => Parser Text+stringArg = choice [doubleQuotedString, someUnless "a string" (== ',')]++key :: Text -> Parser a -> Parser a+key name p = string (name <> "=") *> p++tryKeyValue' :: Text -> Text -> Parser Text+tryKeyValue' k v = try $ string' (k <> "=") *> string' v++cacheSharing :: Parser CacheSharing+cacheSharing =+ choice [Private <$ string "private", Shared <$ string "shared", Locked <$ string "locked"]++mountArgEnv :: (?esc :: Char) => Parser RunMountArg+mountArgEnv = MountArgEnv <$> key "env" stringArg++mountArgFromImage :: (?esc :: Char) => Parser RunMountArg+mountArgFromImage = MountArgFromImage <$> key "from" stringArg++mountArgGid :: (?esc :: Char) => Parser RunMountArg+mountArgGid = MountArgGid <$> key "gid" stringArg++mountArgId :: (?esc :: Char) => Parser RunMountArg+mountArgId = MountArgId <$> key "id" stringArg++mountArgMode :: (?esc :: Char) => Parser RunMountArg+mountArgMode = MountArgMode <$> key "mode" stringArg++mountArgReadOnly :: Parser RunMountArg+mountArgReadOnly =+ MountArgReadOnly+ <$> choice+ [ choiceRoExplicit,+ choiceRwExplicit,+ choiceRo, -- these two must come last and be separate+ choiceRw+ ]+ where+ choiceRoExplicit =+ choice+ [ tryKeyValue' "ro" "true",+ tryKeyValue' "rw" "false",+ tryKeyValue' "readonly" "true",+ tryKeyValue' "readwrite" "false"+ ] $> True++ choiceRwExplicit =+ choice+ [ tryKeyValue' "rw" "true",+ tryKeyValue' "ro" "false",+ tryKeyValue' "readwrite" "true",+ tryKeyValue' "readonly" "false"+ ] $> False++ choiceRo =+ choice+ [ string' "ro",+ string' "readonly"+ ] $> True++ choiceRw =+ choice+ [ string' "rw",+ string' "readwrite"+ ] $> False++mountArgRequired :: Parser RunMountArg+mountArgRequired = MountArgRequired <$> choice+ [ choice ["required=true",+ "required=True"+ ] $> True,+ choice ["required=false",+ "required=False"+ ] $> False,+ string "required" $> True -- This must come last in the list!+ ]++mountArgSharing :: Parser RunMountArg+mountArgSharing = MountArgSharing <$> key "sharing" cacheSharing++mountArgSource :: (?esc :: Char) => Parser RunMountArg+mountArgSource = do+ label "source=" $ choice [string "source=", string "src="]+ MountArgSource . SourcePath <$> stringArg++mountArgTarget :: (?esc :: Char) => Parser RunMountArg+mountArgTarget = do+ label "target=" $ choice [string "target=", string "dst=", string "destination="]+ MountArgTarget . TargetPath <$> stringArg++mountArgType :: Parser RunMountArg+mountArgType = MountArgType <$> key "type" mountType++mountType :: Parser MountType+mountType =+ choice+ [ Bind <$ string "bind",+ Cache <$ string "cache",+ Tmpfs <$ string "tmpfs",+ Secret <$ string "secret",+ Ssh <$ string "ssh"+ ]++mountArgUid :: (?esc :: Char) => Parser RunMountArg+mountArgUid = MountArgUid <$> key "uid" stringArg++mountArgRelabel :: Parser RunMountArg+mountArgRelabel = MountArgRelabel <$> key "relabel" relabel++relabel :: Parser Relabel+relabel = choice [RelabelShared <$ string "shared", RelabelPrivate <$ string "private"]++toArgName :: RunMountArg -> Text+toArgName (MountArgEnv _) = "env"+toArgName (MountArgFromImage _) = "from"+toArgName (MountArgGid _) = "gid"+toArgName (MountArgId _) = "id"+toArgName (MountArgMode _) = "mode"+toArgName (MountArgReadOnly _) = "ro"+toArgName (MountArgRequired _) = "required"+toArgName (MountArgSharing _) = "sharing"+toArgName (MountArgSource _) = "source"+toArgName (MountArgTarget _) = "target"+toArgName (MountArgType _) = "type"+toArgName (MountArgUid _) = "uid"+toArgName (MountArgRelabel _) = "relabel"
src/Language/Docker/PrettyPrint.hs view
@@ -1,37 +1,33 @@-{-# LANGUAGE NoMonomorphismRestriction #-}-{-# LANGUAGE RebindableSyntax #-}-{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE MultiWayIf #-}-{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE RebindableSyntax #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE NoMonomorphismRestriction #-} module Language.Docker.PrettyPrint where -import Data.List.NonEmpty as NonEmpty (NonEmpty(..), toList)-import Data.Maybe (Maybe(..))-import Data.Semigroup ((<>))+import Data.List.NonEmpty as NonEmpty (NonEmpty (..), toList)+import Data.Set (Set) import Data.String (fromString) import Data.Text (Text)+import Prettyprinter+import Prettyprinter.Internal (Doc (Empty))+import Prettyprinter.Render.Text (renderLazy)+import Language.Docker.Syntax+import Prelude hiding ((<>), (>>))+import qualified Data.Set as Set import qualified Data.Text as Text import qualified Data.Text.Lazy as L import qualified Data.Text.Lazy.Builder as B-import Data.Text.Prettyprint.Doc-import Data.Text.Prettyprint.Doc.Internal (Doc(Empty))-import Data.Text.Prettyprint.Doc.Render.Text (renderLazy)-import Language.Docker.Syntax-import Prelude hiding ((<>), (>>), return) -data EscapeAccum = EscapeAccum- { buffer :: !B.Builder- , count :: !Int- , escaping :: !Bool- }--instance Pretty (Arguments Text) where- pretty = prettyPrintArguments+data EscapeAccum+ = EscapeAccum+ { buffer :: !B.Builder,+ count :: !Int,+ escaping :: !Bool+ } -- | Pretty print a 'Dockerfile' to a 'Text' prettyPrint :: Dockerfile -> L.Text@@ -39,13 +35,23 @@ where opts = LayoutOptions Unbounded -prettyPrintDockerfile :: Pretty (Arguments args) => [InstructionPos args] -> Doc ann+prettyPrintDockerfile :: [InstructionPos Text] -> Doc ann prettyPrintDockerfile instr = doPrint instr <> "\n" where- doPrint = vsep . fmap prettyPrintInstructionPos+ doPrint ips =+ let ?esc = findEscapeChar ips+ in (vsep . fmap prettyPrintInstructionPos ) ips +findEscapeChar :: [InstructionPos args] -> Char+findEscapeChar [] = defaultEsc+findEscapeChar (i:is) =+ case i of+ InstructionPos {instruction = (Pragma (Escape (EscapeChar c)))} -> c+ InstructionPos {instruction = (Pragma _)} -> findEscapeChar is+ _ -> defaultEsc+ -- | Pretty print a 'InstructionPos' to a 'Doc'-prettyPrintInstructionPos :: Pretty (Arguments args) => InstructionPos args -> Doc ann+prettyPrintInstructionPos :: (?esc :: Char) => InstructionPos Text -> Doc ann prettyPrintInstructionPos (InstructionPos i _ _) = prettyPrintInstruction i prettyPrintImage :: Image -> Doc ann@@ -53,94 +59,112 @@ prettyPrintImage (Image (Just (Registry reg)) name) = pretty reg <> "/" <> pretty name prettyPrintBaseImage :: BaseImage -> Doc ann-prettyPrintBaseImage b =- case b of- DigestedImage img digest alias -> do- prettyPrintImage img- pretty '@'- pretty digest- prettyAlias alias- UntaggedImage img alias -> do- prettyPrintImage img- prettyAlias alias- TaggedImage img (Tag tag) alias -> do- prettyPrintImage img- pretty ':'- pretty tag- prettyAlias alias+prettyPrintBaseImage BaseImage {..} = do+ prettyPlatform platform+ prettyPrintImage image+ prettyTag tag+ prettyDigest digest+ prettyAlias alias where (>>) = (<>)- return = (mempty <>)+ prettyPlatform maybePlatform =+ case maybePlatform of+ Nothing -> mempty+ Just p -> "--platform=" <> pretty p <> " "+ prettyTag maybeTag =+ case maybeTag of+ Nothing -> mempty+ Just (Tag p) -> ":" <> pretty p prettyAlias maybeAlias =- case maybeAlias of- Nothing -> mempty- Just (ImageAlias alias) -> " AS " <> pretty alias+ case maybeAlias of+ Nothing -> mempty+ Just (ImageAlias a) -> " AS " <> pretty a+ prettyDigest maybeDigest =+ case maybeDigest of+ Nothing -> mempty+ Just (Digest d) -> "@" <> pretty d -prettyPrintPairs :: Pairs -> Doc ann-prettyPrintPairs ps = hsep $ fmap prettyPrintPair ps+prettyPrintPairs :: (?esc :: Char) => Pairs -> Doc ann+prettyPrintPairs ps = align $ sepLine $ fmap prettyPrintPair ps+ where+ sepLine = concatWith (\x y -> x <> " " <> pretty ?esc <> line <> y) -prettyPrintPair :: (Text, Text) -> Doc ann+prettyPrintPair :: (?esc :: Char) => (Text, Text) -> Doc ann prettyPrintPair (k, v) = pretty k <> pretty '=' <> doubleQoute v -prettyPrintArguments :: Arguments Text -> Doc ann+prettyPrintArguments :: (?esc :: Char) => Arguments Text -> Doc ann prettyPrintArguments (ArgumentsList as) = prettyPrintJSON (Text.words as) prettyPrintArguments (ArgumentsText as) = hsep (fmap helper (Text.words as)) where- helper "&&" = "\\\n &&"+ helper "&&" = pretty ?esc <> "\n &&" helper a = pretty a -prettyPrintJSON :: [Text] -> Doc ann+prettyPrintJSON :: (?esc :: Char) => [Text] -> Doc ann prettyPrintJSON args = list (fmap doubleQoute args) -doubleQoute :: Text -> Doc ann+doubleQoute :: (?esc :: Char) => Text -> Doc ann doubleQoute w = enclose dquote dquote (pretty (escapeQuotes w)) -escapeQuotes :: Text -> L.Text+escapeQuotes :: (?esc :: Char) => Text -> L.Text escapeQuotes text =- case Text.foldr accumulate (EscapeAccum mempty 0 False) text of- EscapeAccum buffer _ False -> B.toLazyText buffer- EscapeAccum buffer count True ->- case count `mod` 2 of- 0 -> B.toLazyText (B.singleton '\\' <> buffer)- _ -> B.toLazyText buffer+ case Text.foldr accumulate (EscapeAccum mempty 0 False) text of+ EscapeAccum buffer _ False -> B.toLazyText buffer+ EscapeAccum buffer count True ->+ case count `mod` 2 of+ 0 -> B.toLazyText (B.singleton ?esc <> buffer)+ _ -> B.toLazyText buffer where accumulate '"' EscapeAccum {buffer, escaping = False} =- EscapeAccum (B.singleton '"' <> buffer) 0 True- accumulate '\\' EscapeAccum {buffer, escaping = True, count} =- EscapeAccum (B.singleton '\\' <> buffer) (count + 1) True+ EscapeAccum (B.singleton '"' <> buffer) 0 True accumulate c EscapeAccum {buffer, escaping = True, count}- | count `mod` 2 == 0 = EscapeAccum (B.singleton c <> B.singleton '\\' <> buffer) 0 False- | otherwise = EscapeAccum (B.singleton c <> buffer) 0 False -- It was already escaped+ | c == ?esc = EscapeAccum (B.singleton ?esc <> buffer) (count + 1) True+ | even count = EscapeAccum (B.singleton c <> B.singleton ?esc <> buffer) 0 False+ | otherwise = EscapeAccum (B.singleton c <> buffer) 0 False -- It was already escaped accumulate c EscapeAccum {buffer, escaping = False} =- EscapeAccum (B.singleton c <> buffer) 0 False+ EscapeAccum (B.singleton c <> buffer) 0 False -prettyPrintPort :: Port -> Doc ann-prettyPrintPort (PortStr str) = pretty str-prettyPrintPort (PortRange start stop TCP) = pretty start <> "-" <> pretty stop-prettyPrintPort (PortRange start stop UDP) = pretty start <> "-" <> pretty stop <> "/udp"-prettyPrintPort (Port num TCP) = pretty num <> "/tcp"-prettyPrintPort (Port num UDP) = pretty num <> "/udp"+prettyPrintPortSpec :: PortSpec -> Doc ann+prettyPrintPortSpec (PortSpec port) = pretty port+prettyPrintPortSpec (PortRangeSpec portrange) = pretty portrange prettyPrintFileList :: NonEmpty SourcePath -> TargetPath -> Doc ann prettyPrintFileList sources (TargetPath dest) =- let ending =- case (Text.isSuffixOf "/" dest, sources) of- (True, _) -> "" -- If the target ends with / then no extra ending is needed- (_, _fst :| _snd:_) -> "/" -- More than one source means that the target should end in /- _ -> ""- in hsep $ [pretty s | SourcePath s <- toList sources] ++ [pretty dest <> ending]+ let ending =+ case (Text.isSuffixOf "/" dest, sources) of+ (True, _) -> "" -- If the target ends with / then no extra ending is needed+ (_, _fst :| _snd : _) -> "/" -- More than one source means that the target should end in /+ _ -> ""+ in hsep $ [pretty s | SourcePath s <- toList sources] ++ [pretty dest <> ending] +prettyPrintChecksum :: Checksum -> Doc ann+prettyPrintChecksum checksum =+ case checksum of+ Checksum c -> "--checksum=" <> pretty c+ NoChecksum -> mempty+ prettyPrintChown :: Chown -> Doc ann prettyPrintChown chown =- case chown of- Chown c -> "--chown=" <> pretty c- NoChown -> mempty+ case chown of+ Chown c -> "--chown=" <> pretty c+ NoChown -> mempty +prettyPrintChmod :: Chmod -> Doc ann+prettyPrintChmod chmod =+ case chmod of+ Chmod c -> "--chmod=" <> pretty c+ NoChmod -> mempty++prettyPrintLink :: Link -> Doc ann+prettyPrintLink link =+ case link of+ Link -> "--link"+ NoLink -> mempty+ prettyPrintCopySource :: CopySource -> Doc ann prettyPrintCopySource source =- case source of- CopySource c -> "--from=" <> pretty c- NoSource -> mempty+ case source of+ CopySource c -> "--from=" <> pretty c+ NoSource -> mempty prettyPrintDuration :: Text -> Maybe Duration -> Doc ann prettyPrintDuration flagName = maybe mempty pp@@ -152,81 +176,186 @@ where pp (Retries r) = "--retries=" <> pretty r -prettyPrintInstruction :: Pretty (Arguments args) => Instruction args -> Doc ann+prettyPrintRunMount :: (?esc :: Char) => Set RunMount -> Doc ann+prettyPrintRunMount set =+ foldl (<>) "" (map printSingleMount (Set.toList set))+ where+ printSingleMount mount = "--mount="+ <> case mount of+ BindMount BindOpts {..} ->+ "type=bind"+ <> printTarget bTarget+ <> maybe mempty printSource bSource+ <> maybe mempty printFromImage bFromImage+ <> maybe mempty printReadOnly bReadOnly+ <> maybe mempty printRelabel bRelabel+ CacheMount CacheOpts {..} ->+ "type=cache"+ <> printTarget cTarget+ <> maybe mempty printSharing cSharing+ <> maybe mempty printId cCacheId+ <> maybe mempty printFromImage cFromImage+ <> maybe mempty printSource cSource+ <> maybe mempty printMode cMode+ <> maybe mempty printUid cUid+ <> maybe mempty printGid cGid+ <> maybe mempty printReadOnly cReadOnly+ SshMount SecretOpts {..} ->+ "type=ssh"+ <> maybe mempty printTarget sTarget+ <> maybe mempty printId sCacheId+ <> maybe mempty printSource sSource+ <> maybe mempty printMode sMode+ <> maybe mempty printUid sUid+ <> maybe mempty printGid sGid+ <> maybe mempty printRequired sIsRequired+ SecretMount SecretOpts {..} ->+ "type=secret"+ <> maybe mempty printTarget sTarget+ <> maybe mempty printId sCacheId+ <> maybe mempty printSource sSource+ <> maybe mempty printMode sMode+ <> maybe mempty printUid sUid+ <> maybe mempty printGid sGid+ <> maybe mempty printRequired sIsRequired+ TmpfsMount TmpOpts {..} -> "type=tmpfs" <> printTarget tTarget+ printQuotable str+ | Text.any (== '"') str = doubleQoute str+ | otherwise = pretty str+ printTarget (TargetPath t) = ",target=" <> printQuotable t+ printSource (SourcePath s) = ",source=" <> printQuotable s+ printFromImage f = ",from=" <> printQuotable f+ printSharing sharing = ",sharing="+ <> case sharing of+ Shared -> "shared"+ Private -> "private"+ Locked -> "locked"+ printId i = ",id=" <> printQuotable i+ printMode m = ",mode=" <> pretty m+ printUid uid = ",uid=" <> pretty uid+ printGid gid = ",gid=" <> pretty gid+ printReadOnly True = ",ro"+ printReadOnly False = ",rw"+ printRequired True = ",required"+ printRequired False = mempty+ printRelabel r = ",relabel="+ <> case r of+ RelabelShared -> printQuotable "shared"+ RelabelPrivate -> printQuotable "private"++prettyPrintRunNetwork :: Maybe RunNetwork -> Doc ann+prettyPrintRunNetwork Nothing = mempty+prettyPrintRunNetwork (Just NetworkHost) = "--network=host"+prettyPrintRunNetwork (Just NetworkNone) = "--network=none"+prettyPrintRunNetwork (Just NetworkDefault) = "--network=default"++prettyPrintRunSecurity :: Maybe RunSecurity -> Doc ann+prettyPrintRunSecurity Nothing = mempty+prettyPrintRunSecurity (Just Sandbox) = "--security=sandbox"+prettyPrintRunSecurity (Just Insecure) = "--security=insecure"++prettyPrintPragma :: PragmaDirective -> Doc ann+prettyPrintPragma (Escape (EscapeChar esc)) = "escape = " <> pretty esc+prettyPrintPragma (Syntax (SyntaxImage img)) = "syntax = " <> prettyPrintImage img++prettyPrintInstruction :: (?esc :: Char) => Instruction Text -> Doc ann prettyPrintInstruction i =- case i of- Maintainer m -> do- "MAINTAINER"- pretty m- Arg a Nothing -> do- "ARG"- pretty a- Arg k (Just v) -> do- "ARG"- pretty k <> "=" <> pretty v- Entrypoint e -> do- "ENTRYPOINT"- pretty e- Stopsignal s -> do- "STOPSIGNAL"- pretty s- Workdir w -> do- "WORKDIR"- pretty w- Expose (Ports ps) -> do- "EXPOSE"- hsep (fmap prettyPrintPort ps)- Volume dir -> do- "VOLUME"- pretty dir- Run c -> do- "RUN"- pretty c- Copy CopyArgs {sourcePaths, targetPath, chownFlag, sourceFlag} -> do- "COPY"- prettyPrintChown chownFlag- prettyPrintCopySource sourceFlag- prettyPrintFileList sourcePaths targetPath- Cmd c -> do- "CMD"- pretty c- Label l -> do- "LABEL"- prettyPrintPairs l- Env ps -> do- "ENV"- prettyPrintPairs ps- User u -> do- "USER"- pretty u- Comment s -> do- pretty '#'- pretty s- OnBuild i' -> do- "ONBUILD"- prettyPrintInstruction i'- From b -> do- "FROM"- prettyPrintBaseImage b- Add AddArgs {sourcePaths, targetPath, chownFlag} -> do- "ADD"- prettyPrintChown chownFlag- prettyPrintFileList sourcePaths targetPath- Shell args -> do- "SHELL"- pretty args- Healthcheck NoCheck -> "HEALTHCHECK NONE"- Healthcheck (Check CheckArgs {..}) -> do- "HEALTHCHECK"- prettyPrintDuration "--interval=" interval- prettyPrintDuration "--timeout=" timeout- prettyPrintDuration "--start-period=" startPeriod- prettyPrintRetries retries- "CMD"- pretty checkCommand+ case i of+ Maintainer m -> do+ "MAINTAINER"+ pretty m+ Arg a Nothing -> do+ "ARG"+ pretty a+ Arg k (Just v) -> do+ "ARG"+ pretty k <> "=" <> pretty v+ Entrypoint e -> do+ "ENTRYPOINT"+ prettyPrintArguments e+ Stopsignal s -> do+ "STOPSIGNAL"+ pretty s+ Workdir w -> do+ "WORKDIR"+ pretty w+ Expose (Ports ps) -> do+ "EXPOSE"+ hsep (fmap prettyPrintPortSpec ps)+ Volume dir -> do+ "VOLUME"+ pretty dir+ Run (RunArgs c RunFlags {mount, network, security}) -> do+ "RUN"+ prettyPrintRunMount mount+ prettyPrintRunNetwork network+ prettyPrintRunSecurity security+ prettyPrintArguments c+ Copy+ CopyArgs {sourcePaths, targetPath}+ CopyFlags {chmodFlag, chownFlag, linkFlag, sourceFlag, excludeFlags} -> do+ "COPY"+ prettyPrintChown chownFlag+ prettyPrintChmod chmodFlag+ prettyPrintLink linkFlag+ prettyPrintCopySource sourceFlag+ prettyPrintExcludes excludeFlags+ prettyPrintFileList sourcePaths targetPath+ Cmd c -> do+ "CMD"+ prettyPrintArguments c+ Label l -> do+ "LABEL"+ prettyPrintPairs l+ Env ps -> do+ "ENV"+ prettyPrintPairs ps+ User u -> do+ "USER"+ pretty u+ Pragma p -> do+ pretty '#'+ prettyPrintPragma p+ Comment s -> do+ pretty '#'+ pretty s+ OnBuild i' -> do+ "ONBUILD"+ prettyPrintInstruction i'+ From b -> do+ "FROM"+ prettyPrintBaseImage b+ Add+ AddArgs {sourcePaths, targetPath}+ AddFlags {checksumFlag, chownFlag, chmodFlag, linkFlag, excludeFlags} -> do+ "ADD"+ prettyPrintChecksum checksumFlag+ prettyPrintChown chownFlag+ prettyPrintChmod chmodFlag+ prettyPrintLink linkFlag+ prettyPrintExcludes excludeFlags+ prettyPrintFileList sourcePaths targetPath+ Shell args -> do+ "SHELL"+ prettyPrintArguments args+ Healthcheck NoCheck -> "HEALTHCHECK NONE"+ Healthcheck (Check CheckArgs {..}) -> do+ "HEALTHCHECK"+ prettyPrintDuration "--interval=" interval+ prettyPrintDuration "--timeout=" timeout+ prettyPrintDuration "--start-period=" startPeriod+ prettyPrintDuration "--start-interval" startInterval+ prettyPrintRetries retries+ "CMD"+ prettyPrintArguments checkCommand where (>>) = spaceCat- return a = a++prettyPrintExcludes :: [Exclude] -> Doc ann+prettyPrintExcludes excludes = hsep (fmap prettyPrintExclude excludes)++prettyPrintExclude :: Exclude -> Doc ann+prettyPrintExclude (Exclude e) = "--exclude=" <> pretty e spaceCat :: Doc ann -> Doc ann -> Doc ann spaceCat a Empty = a
src/Language/Docker/Syntax.hs view
@@ -1,187 +1,415 @@-{-# LANGUAGE GeneralizedNewtypeDeriving, TypeFamilies,- DuplicateRecordFields, FlexibleInstances, DeriveFunctor #-}+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE TypeFamilies #-} -module Language.Docker.Syntax where+module Language.Docker.Syntax+ ( module Language.Docker.Syntax,+ module Language.Docker.Syntax.Port,+ module Language.Docker.Syntax.PortRange,+ module Language.Docker.Syntax.Protocol+ )+where +import Data.Default.Class (Default (..)) import Data.List (intercalate, isInfixOf) import Data.List.NonEmpty (NonEmpty) import Data.List.Split (endBy)-import Data.String (IsString(..))+import Data.String (IsString (..)) import Data.Text (Text)+import Data.Set (Set) import qualified Data.Text as Text import Data.Time.Clock (DiffTime)-import GHC.Exts (IsList(..))+import GHC.Exts (IsList (..))+import Text.Printf -data Image = Image- { registryName :: !(Maybe Registry)- , imageName :: !Text- } deriving (Show, Eq, Ord) +import Language.Docker.Syntax.Port+import Language.Docker.Syntax.PortRange+import Language.Docker.Syntax.Protocol+++data Image+ = Image+ { registryName :: !(Maybe Registry),+ imageName :: !Text+ }+ deriving (Show, Eq, Ord)+ instance IsString Image where- fromString img =- if "/" `isInfixOf` img- then let parts = endBy "/" img- in if "." `isInfixOf` head parts- then Image- (Just (Registry (Text.pack (head parts))))- (Text.pack . intercalate "/" $ tail parts)- else Image Nothing (Text.pack img)- else Image Nothing (Text.pack img)+ fromString img =+ if "/" `isInfixOf` img+ then+ let parts = endBy "/" img+ in if "." `isInfixOf` head parts+ then+ Image+ (Just (Registry (Text.pack (head parts))))+ (Text.pack . intercalate "/" $ tail parts)+ else Image Nothing (Text.pack img)+ else Image Nothing (Text.pack img) -newtype Registry = Registry- { unRegistry :: Text- } deriving (Show, Eq, Ord, IsString)+newtype Registry+ = Registry+ { unRegistry :: Text+ }+ deriving (Show, Eq, Ord, IsString) -newtype Tag = Tag- { unTag :: Text- } deriving (Show, Eq, Ord, IsString)+newtype Tag+ = Tag+ { unTag :: Text+ }+ deriving (Show, Eq, Ord, IsString) -data Protocol- = TCP- | UDP- deriving (Show, Eq, Ord)+newtype Digest+ = Digest+ { unDigest :: Text+ }+ deriving (Show, Eq, Ord, IsString) -data Port- = Port !Int- !Protocol- | PortStr !Text- | PortRange !Int- !Int- !Protocol- deriving (Show, Eq, Ord)+data PortSpec+ = PortSpec !Port+ | PortRangeSpec !PortRange+ deriving (Show, Eq, Ord) -newtype Ports = Ports- { unPorts :: [Port]- } deriving (Show, Eq, Ord)+newtype Ports+ = Ports+ { unPorts :: [PortSpec]+ }+ deriving (Show, Eq, Ord) instance IsList Ports where- type Item Ports = Port- fromList = Ports- toList (Ports ps) = ps+ type Item Ports = PortSpec+ fromList = Ports+ toList (Ports ps) = ps type Directory = Text -newtype ImageAlias = ImageAlias- { unImageAlias :: Text- } deriving (Show, Eq, Ord, IsString)+type Platform = Text +newtype ImageAlias+ = ImageAlias+ { unImageAlias :: Text+ }+ deriving (Show, Eq, Ord, IsString)+ data BaseImage- = UntaggedImage !Image- !(Maybe ImageAlias)- | TaggedImage !Image- !Tag- !(Maybe ImageAlias)- | DigestedImage !Image- !Text- !(Maybe ImageAlias)- deriving (Eq, Ord, Show)+ = BaseImage+ { image :: !Image,+ tag :: !(Maybe Tag),+ digest :: !(Maybe Digest),+ alias :: !(Maybe ImageAlias),+ platform :: !(Maybe Platform)+ }+ deriving (Eq, Ord, Show) -- | Type of the Dockerfile AST type Dockerfile = [InstructionPos Text] -newtype SourcePath = SourcePath- { unSourcePath :: Text- } deriving (Show, Eq, Ord, IsString)+newtype SourcePath+ = SourcePath+ { unSourcePath :: Text+ }+ deriving (Show, Eq, Ord, IsString) -newtype TargetPath = TargetPath- { unTargetPath :: Text- } deriving (Show, Eq, Ord, IsString)+newtype TargetPath+ = TargetPath+ { unTargetPath :: Text+ }+ deriving (Show, Eq, Ord, IsString) +data Relabel+ = RelabelShared+ | RelabelPrivate+ deriving (Show, Eq, Ord)++data Checksum+ = Checksum !Text+ | NoChecksum+ deriving (Show, Eq, Ord)++instance IsString Checksum where+ fromString ch =+ case ch of+ "" -> NoChecksum+ _ -> Checksum (Text.pack ch)+ data Chown- = Chown !Text- | NoChown- deriving (Show, Eq, Ord)+ = Chown !Text+ | NoChown+ deriving (Show, Eq, Ord) instance IsString Chown where- fromString ch =- case ch of- "" -> NoChown- _ -> Chown (Text.pack ch)+ fromString ch =+ case ch of+ "" -> NoChown+ _ -> Chown (Text.pack ch) +data Chmod+ = Chmod !Text+ | NoChmod+ deriving (Show, Eq, Ord)++instance IsString Chmod where+ fromString ch =+ case ch of+ "" -> NoChmod+ _ -> Chmod (Text.pack ch)++data Link+ = Link+ | NoLink+ deriving (Show, Eq, Ord)+ data CopySource- = CopySource !Text- | NoSource- deriving (Show, Eq, Ord)+ = CopySource !Text+ | NoSource+ deriving (Show, Eq, Ord) instance IsString CopySource where- fromString src =- case src of- "" -> NoSource- _ -> CopySource (Text.pack src)+ fromString src =+ case src of+ "" -> NoSource+ _ -> CopySource (Text.pack src) -newtype Duration = Duration- { durationTime :: DiffTime- } deriving (Show, Eq, Ord, Num)+newtype Duration+ = Duration+ { durationTime :: DiffTime+ }+ deriving (Show, Eq, Ord, Num, Fractional) -newtype Retries = Retries- { times :: Int- } deriving (Show, Eq, Ord, Num)+newtype Retries+ = Retries+ { times :: Int+ }+ deriving (Show, Eq, Ord, Num) -data CopyArgs = CopyArgs- { sourcePaths :: NonEmpty SourcePath- , targetPath :: !TargetPath- , chownFlag :: !Chown- , sourceFlag :: !CopySource- } deriving (Show, Eq, Ord)+data CopyArgs+ = CopyArgs+ { sourcePaths :: NonEmpty SourcePath,+ targetPath :: !TargetPath+ }+ deriving (Show, Eq, Ord) -data AddArgs = AddArgs- { sourcePaths :: NonEmpty SourcePath- , targetPath :: !TargetPath- , chownFlag :: !Chown- } deriving (Show, Eq, Ord)+data CopyFlags+ = CopyFlags+ { chownFlag :: !Chown,+ chmodFlag :: !Chmod,+ linkFlag :: !Link,+ sourceFlag :: !CopySource,+ excludeFlags :: ![Exclude]+ }+ deriving (Show, Eq, Ord) +instance Default CopyFlags where+ def = CopyFlags NoChown NoChmod NoLink NoSource []++data AddArgs+ = AddArgs+ { sourcePaths :: NonEmpty SourcePath,+ targetPath :: !TargetPath+ }+ deriving (Show, Eq, Ord)++data AddFlags+ = AddFlags+ { checksumFlag :: !Checksum,+ chownFlag :: !Chown,+ chmodFlag :: !Chmod,+ linkFlag :: !Link,+ excludeFlags :: ![Exclude]+ }+ deriving (Show, Eq, Ord)++instance Default AddFlags where+ def = AddFlags NoChecksum NoChown NoChmod NoLink []++newtype Exclude+ = Exclude+ { unExclude :: Text+ }+ deriving (Show, Eq, Ord, IsString)+ data Check args- = Check !(CheckArgs args)- | NoCheck- deriving (Show, Eq, Ord, Functor)+ = Check !(CheckArgs args)+ | NoCheck+ deriving (Show, Eq, Ord, Functor) data Arguments args- = ArgumentsText args- | ArgumentsList args- deriving (Show, Eq, Ord, Functor)+ = ArgumentsText args+ | ArgumentsList args+ deriving (Show, Eq, Ord, Functor) instance IsString (Arguments Text) where- fromString = ArgumentsText . Text.pack+ fromString = ArgumentsText . Text.pack instance IsList (Arguments Text) where- type Item (Arguments Text) = Text- fromList = ArgumentsList . Text.unwords- toList (ArgumentsText ps) = Text.words ps- toList (ArgumentsList ps) = Text.words ps+ type Item (Arguments Text) = Text+ fromList = ArgumentsList . Text.unwords+ toList (ArgumentsText ps) = Text.words ps+ toList (ArgumentsList ps) = Text.words ps -data CheckArgs args = CheckArgs- { checkCommand :: !(Arguments args)- , interval :: !(Maybe Duration)- , timeout :: !(Maybe Duration)- , startPeriod :: !(Maybe Duration)- , retries :: !(Maybe Retries)- } deriving (Show, Eq, Ord, Functor)+data CheckArgs args+ = CheckArgs+ { checkCommand :: !(Arguments args),+ interval :: !(Maybe Duration),+ timeout :: !(Maybe Duration),+ startPeriod :: !(Maybe Duration),+ startInterval :: !(Maybe Duration),+ retries :: !(Maybe Retries)+ }+ deriving (Show, Eq, Ord, Functor) type Pairs = [(Text, Text)] +data RunMount+ = BindMount !BindOpts+ | CacheMount !CacheOpts+ | TmpfsMount !TmpOpts+ | SecretMount !SecretOpts+ | SshMount !SecretOpts+ deriving (Eq, Show, Ord)++data BindOpts+ = BindOpts+ { bTarget :: !TargetPath,+ bSource :: !(Maybe SourcePath),+ bFromImage :: !(Maybe Text),+ bReadOnly :: !(Maybe Bool),+ bRelabel :: !(Maybe Relabel)+ }+ deriving (Show, Eq, Ord)++instance Default BindOpts where+ def = BindOpts "" Nothing Nothing Nothing Nothing++data CacheOpts+ = CacheOpts+ { cTarget :: !TargetPath,+ cSharing :: !(Maybe CacheSharing),+ cCacheId :: !(Maybe Text),+ cReadOnly :: !(Maybe Bool),+ cFromImage :: !(Maybe Text),+ cSource :: !(Maybe SourcePath),+ cMode :: !(Maybe Text),+ cUid :: !(Maybe Text),+ cGid :: !(Maybe Text)+ }+ deriving (Show, Eq, Ord)++instance Default CacheOpts where+ def = CacheOpts "" Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing++newtype TmpOpts = TmpOpts {tTarget :: TargetPath} deriving (Eq, Show, Ord)++instance Default TmpOpts where+ def = TmpOpts ""++data SecretOpts+ = SecretOpts+ { sTarget :: !(Maybe TargetPath),+ sCacheId :: !(Maybe Text),+ sIsRequired :: !(Maybe Bool),+ sSource :: !(Maybe SourcePath),+ sEnv :: !(Maybe Text),+ sMode :: !(Maybe Text),+ sUid :: !(Maybe Text),+ sGid :: !(Maybe Text)+ }+ deriving (Eq, Show, Ord)++instance Default SecretOpts where+ def = SecretOpts Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing++data CacheSharing+ = Shared+ | Private+ | Locked+ deriving (Show, Eq, Ord)++data RunSecurity+ = Insecure+ | Sandbox+ deriving (Show, Eq, Ord)++data RunNetwork+ = NetworkNone+ | NetworkHost+ | NetworkDefault+ deriving (Show, Eq, Ord)++data RunFlags+ = RunFlags+ { mount :: !(Set RunMount),+ security :: !(Maybe RunSecurity),+ network :: !(Maybe RunNetwork)+ }+ deriving (Show, Eq, Ord)++instance Default RunFlags where+ def = RunFlags mempty Nothing Nothing++data RunArgs args = RunArgs (Arguments args) RunFlags+ deriving (Show, Eq, Ord, Functor)++instance IsString (RunArgs Text) where+ fromString s =+ RunArgs+ (ArgumentsText . Text.pack $ s)+ RunFlags+ { mount = mempty,+ security = Nothing,+ network = Nothing+ }++newtype EscapeChar+ = EscapeChar+ { escape :: Char+ }+ deriving (Show, Eq, Ord)++instance IsChar EscapeChar where+ fromChar c =+ EscapeChar {escape = c}+ toChar e = escape e++newtype SyntaxImage+ = SyntaxImage+ { syntax :: Image+ }+ deriving (Show, Eq, Ord)++data PragmaDirective+ = Escape !EscapeChar+ | Syntax !SyntaxImage+ deriving (Show, Eq, Ord)+ -- | All commands available in Dockerfiles data Instruction args- = From !BaseImage- | Add !AddArgs- | User !Text- | Label !Pairs- | Stopsignal !Text- | Copy !CopyArgs- | Run !(Arguments args)- | Cmd !(Arguments args)- | Shell !(Arguments args)- | Workdir !Directory- | Expose !Ports- | Volume !Text- | Entrypoint !(Arguments args)- | Maintainer !Text- | Env !Pairs- | Arg !Text- !(Maybe Text)- | Healthcheck !(Check args)- | Comment !Text- | OnBuild !(Instruction args)- deriving (Eq, Ord, Show, Functor)+ = From !BaseImage+ | Add !AddArgs !AddFlags+ | User !Text+ | Label !Pairs+ | Stopsignal !Text+ | Copy !CopyArgs !CopyFlags+ | Run !(RunArgs args)+ | Cmd !(Arguments args)+ | Shell !(Arguments args)+ | Workdir !Directory+ | Expose !Ports+ | Volume !Text+ | Entrypoint !(Arguments args)+ | Maintainer !Text+ | Env !Pairs+ | Arg+ !Text+ !(Maybe Text)+ | Healthcheck !(Check args)+ | Pragma !PragmaDirective+ | Comment !Text+ | OnBuild !(Instruction args)+ deriving (Eq, Ord, Show, Functor) type Filename = Text @@ -189,8 +417,13 @@ -- | 'Instruction' with additional location information required for creating -- good check messages-data InstructionPos args = InstructionPos- { instruction :: !(Instruction args)- , sourcename :: !Filename- , lineNumber :: !Linenumber- } deriving (Eq, Ord, Show, Functor)+data InstructionPos args+ = InstructionPos+ { instruction :: !(Instruction args),+ sourcename :: !Filename,+ lineNumber :: !Linenumber+ }+ deriving (Eq, Ord, Show, Functor)++defaultEsc :: Char+defaultEsc = '\\'
− src/Language/Docker/Syntax/Lift.hs
@@ -1,67 +0,0 @@-{-# LANGUAGE TemplateHaskell #-}--module Language.Docker.Syntax.Lift where--import qualified Data.ByteString as ByteString-import Data.Fixed (Fixed)-import Data.List.NonEmpty (NonEmpty)-import qualified Data.Text as Text-import Data.Time.Clock (DiffTime)-import Language.Haskell.TH.Lift-import Language.Haskell.TH.Syntax ()--import Language.Docker.Syntax--instance Lift ByteString.ByteString where- lift b = [|ByteString.pack $(lift $ ByteString.unpack b)|]--instance Lift Text.Text where- lift b = [|Text.pack $(lift $ Text.unpack b)|]--deriveLift ''NonEmpty--deriveLift ''Fixed--deriveLift ''DiffTime--deriveLift ''Protocol--deriveLift ''Port--deriveLift ''Ports--deriveLift ''Registry--deriveLift ''Image--deriveLift ''ImageAlias--deriveLift ''Tag--deriveLift ''BaseImage--deriveLift ''Arguments--deriveLift ''Instruction--deriveLift ''InstructionPos--deriveLift ''SourcePath--deriveLift ''TargetPath--deriveLift ''Chown--deriveLift ''CopySource--deriveLift ''CopyArgs--deriveLift ''AddArgs--deriveLift ''Duration--deriveLift ''Retries--deriveLift ''CheckArgs--deriveLift ''Check
+ src/Language/Docker/Syntax/Port.hs view
@@ -0,0 +1,19 @@+module Language.Docker.Syntax.Port where+++import Data.Text+import Prettyprinter+import Language.Docker.Syntax.Protocol+++-- | A port can either be a number (plus a protocol, tcp by default) or a+-- variable.+data Port+ = Port !Int !Protocol+ | PortStr !Text+ deriving (Show, Eq, Ord)+++instance Pretty Port where+ pretty (Port num proto) = pretty num <> pretty proto+ pretty (PortStr str) = pretty str
+ src/Language/Docker/Syntax/PortRange.hs view
@@ -0,0 +1,20 @@+module Language.Docker.Syntax.PortRange where+++import Prettyprinter+import Language.Docker.Syntax.Port+import Language.Docker.Syntax.Protocol+++-- | A port range starts and ends with either a number or a variable and can+-- have a protocol associated (tcp by default). The protocol of the start and+-- end port shall be ignored.+data PortRange+ = PortRange !Port !Port+ deriving (Show, Eq, Ord)+++instance Pretty PortRange where+ pretty (PortRange (Port start UDP) (Port end UDP)) = pretty start <> "-" <> pretty end <> "/udp"+ pretty (PortRange (Port start UDP) end) = pretty start <> "-" <> pretty end <> "/udp"+ pretty (PortRange start end) = pretty start <> "-" <> pretty end
+ src/Language/Docker/Syntax/Protocol.hs view
@@ -0,0 +1,15 @@+module Language.Docker.Syntax.Protocol where+++import Prettyprinter+++data Protocol+ = TCP+ | UDP+ deriving (Show, Eq, Ord)+++instance Pretty Protocol where+ pretty TCP = ""+ pretty UDP = "/udp"
− test/Language/Docker/EDSL/QuasiSpec.hs
@@ -1,40 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE OverloadedLists #-}-{-# LANGUAGE QuasiQuotes #-}-module Language.Docker.EDSL.QuasiSpec- where--import Language.Docker.EDSL-import Language.Docker.EDSL.Quasi-import Language.Docker.Syntax-import Test.Hspec--spec :: Spec-spec = do- describe "dockerfile" $- it "parses a dockerfile and returns its ast" $ do- let df = map instruction [dockerfile|- FROM node- RUN apt-get update- CMD ["node", "something.js"]- |]- df `shouldBe` [ From (UntaggedImage "node" Nothing)- , Run "apt-get update"- , Cmd ["node", "something.js"]- ]-- describe "edockerfile" $- it "lets us use parsed dockerfiles seamlessly in our DSL" $ do- let d = do- from ("node" `aliased` "node-build")- expose (ports [tcpPort 8080, variablePort "PORT"])- [edockerfile|- RUN apt-get update- CMD node something.js- |]- df = map instruction (toDockerfile d)- df `shouldBe` [ From (UntaggedImage "node" (Just $ ImageAlias "node-build"))- , Expose (Ports [Port 8080 TCP, PortStr "$PORT"])- , Run "apt-get update"- , Cmd "node something.js"- ]
− test/Language/Docker/EDSLSpec.hs
@@ -1,133 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE OverloadedLists #-}-module Language.Docker.EDSLSpec where--import Control.Monad.IO.Class-import Data.List (sort)-import Language.Docker.EDSL-import Language.Docker.PrettyPrint-import qualified Language.Docker.Syntax as Syntax-import System.Directory-import System.FilePath-import System.FilePath.Glob-import Test.Hspec-import qualified Data.Text.Lazy as L-import qualified Data.Text as Text-import Data.Semigroup ((<>))--printed :: [L.Text] -> L.Text-printed = L.unlines--spec :: Spec-spec = do- describe "toDockerfile s" $- it "allows us to write haskell code that represents Dockerfiles" $ do- let r = map Syntax.instruction $ toDockerfile (do- from "node"- cmdArgs ["node", "-e", "'console.log(\'hey\')'"])- r `shouldBe` [ Syntax.From $- Syntax.UntaggedImage "node"- Nothing- , Syntax.Cmd ["node", "-e", "'console.log(\'hey\')'"]- ]-- describe "prettyPrint $ toDockerfile s" $ do- it "allows us to write haskell code that represents Dockerfiles" $ do- let r = prettyPrint $ toDockerfile (do- from "node"- shell ["cmd", "/S"]- entrypoint ["/tini", "--"]- cmdArgs ["node", "-e", "'console.log(\'hey\')'"]- healthcheck $ check "curl -f http://localhost/ || exit 1" `interval` 300)- r `shouldBe` printed [ "FROM node"- , "SHELL [\"cmd\", \"/S\"]"- , "ENTRYPOINT [\"/tini\", \"--\"]"- , "CMD [\"node\", \"-e\", \"'console.log(\'hey\')'\"]"- , "HEALTHCHECK --interval=300s CMD curl -f http://localhost/ || exit 1"- ]- it "print expose instructions correctly" $ do- let r = prettyPrint $ toDockerfile (do- from "scratch"- expose $ ports [variablePort "PORT", tcpPort 80, udpPort 51]- expose $ ports [portRange 90 100]- expose $ ports [udpPortRange 190 200])- r `shouldBe` printed [ "FROM scratch"- , "EXPOSE $PORT 80/tcp 51/udp"- , "EXPOSE 90-100"- , "EXPOSE 190-200/udp"- ]-- it "onBuild let's us nest statements" $ do- let r = prettyPrint $ toDockerfile $ do- from "node"- cmdArgs ["node", "-e", "'console.log(\'hey\')'"]- onBuild $ do- run "echo \"hello world\""- run "echo \"hello world2\""- r `shouldBe` printed [ "FROM node"- , "CMD [\"node\", \"-e\", \"'console.log(\'hey\')'\"]"- , "ONBUILD RUN echo \"hello world\""- , "ONBUILD RUN echo \"hello world2\""- ]-- it "parses and prints from aliases correctly" $ do- let r = prettyPrint $ toDockerfile $ do- from $ "node" `tagged` "10.1" `aliased` "node-build"- run "echo foo"- r `shouldBe` printed [ "FROM node:10.1 AS node-build"- , "RUN echo foo"- ]-- it "parses and prints from with a registry" $ do- let r = prettyPrint $ toDockerfile $ do- from "opensuse/tumbleweed"- run "echo foo"- r `shouldBe` printed [ "FROM opensuse/tumbleweed"- , "RUN echo foo"- ]-- it "parses and prints copy instructions" $ do- let r = prettyPrint $ toDockerfile $ do- from "scratch"- copy $ ["foo.js"] `to` "bar.js"- copy $ ["foo.js", "bar.js"] `to` "."- copy $ ["foo.js", "bar.js"] `to` "baz/"- copy $ ["something"] `to` "crazy" `fromStage` "builder"- copy $ ["this"] `to` "that" `fromStage` "builder" `ownedBy` "www-data"- r `shouldBe` printed [ "FROM scratch"- , "COPY foo.js bar.js"- , "COPY foo.js bar.js ./"- , "COPY foo.js bar.js baz/"- , "COPY --from=builder something crazy"- , "COPY --chown=www-data --from=builder this that"- ]- it "quotes label and env correctly" $ do- let r = prettyPrint $ toDockerfile $ do- from "scratch"- label [("email", "Example <example@example.com>")]- label [("escape", "Escape this\" thing")]- env [("foo", "bar baz")]- env [("double_escape", "escape this \\\"")]- r `shouldBe` printed [ "FROM scratch"- , "LABEL email=\"Example <example@example.com>\""- , "LABEL escape=\"Escape this\\\" thing\""- , "ENV foo=\"bar baz\""- , "ENV double_escape=\"escape this \\\"\""- ]-- describe "toDockerfileTextIO" $- it "let's us run in the IO monad" $ do- -- TODO - "glob" is a really useful combinator- str <- toDockerfileTextIO $ do- fs <- liftIO $ do- cwd <- getCurrentDirectory- fs <- glob "./test/Language/Docker/*.hs"- return (map (makeRelative cwd) (sort fs))- from "ubuntu"- let file = Text.pack . takeFileName- mapM_ (\f -> add [Syntax.SourcePath (Text.pack f)] (Syntax.TargetPath $ "/app/" <> file f)) fs- str `shouldBe` printed [ "FROM ubuntu"- , "ADD ./test/Language/Docker/EDSLSpec.hs /app/EDSLSpec.hs"- , "ADD ./test/Language/Docker/ExamplesSpec.hs /app/ExamplesSpec.hs"- , "ADD ./test/Language/Docker/ParserSpec.hs /app/ParserSpec.hs"- ]
− test/Language/Docker/ExamplesSpec.hs
@@ -1,20 +0,0 @@-module Language.Docker.ExamplesSpec where--import Control.Monad-import Data.Monoid-import System.Directory-import System.FilePath-import System.FilePath.Glob-import System.Process-import Test.Hspec--stackRunGhc e = callProcess "stack" ["runghc", "--package", "language-docker", e]--spec :: Spec-spec = do- cwd <- runIO getCurrentDirectory- exampleSources <- runIO $ glob "./examples/*.hs"- forM_ exampleSources $ \exampleSource -> do- let exampleSource' = makeRelative cwd exampleSource- describe exampleSource $ it ("stack runghc " <> exampleSource') $- stackRunGhc exampleSource
+ test/Language/Docker/IntegrationSpec.hs view
@@ -0,0 +1,163 @@+module Language.Docker.IntegrationSpec where++import qualified Data.Text as Text+import qualified Data.Text.IO+import qualified Data.Text.Lazy.IO as L+import Language.Docker.Parser+import Language.Docker.PrettyPrint (prettyPrint, prettyPrintDockerfile)+import Test.HUnit hiding (Label)+import Test.Hspec+import Text.Megaparsec hiding (Label)++spec :: Spec+spec = do+ describe "parse file" $ do+ it "1.Dockerfile" $ do+ parsed <- parseFile "test/fixtures/1.Dockerfile"+ case parsed of+ Right a -> L.putStr $ prettyPrint a+ Left err -> assertFailure $ errorBundlePretty err++ it "2.Dockerfile" $ do+ parsed <- parseFile "test/fixtures/2.Dockerfile"+ case parsed of+ Right a -> L.putStr $ prettyPrint a+ Left err -> assertFailure $ errorBundlePretty err++ it "3.Dockerfile" $ do+ parsed <- parseFile "test/fixtures/3.Dockerfile"+ case parsed of+ Right a -> L.putStr $ prettyPrint a+ Left err -> assertFailure $ errorBundlePretty err++ it "4.Dockerfile" $ do+ parsed <- parseFile "test/fixtures/4.Dockerfile"+ case parsed of+ Right a -> L.putStr $ prettyPrint a+ Left err -> assertFailure $ errorBundlePretty err++ it "5.Dockerfile" $ do+ parsed <- parseFile "test/fixtures/5.Dockerfile"+ case parsed of+ Right a -> L.putStr $ prettyPrint a+ Left err -> assertFailure $ errorBundlePretty err++ describe "escape character detection logic" $ do+ it "ensure the pretty printer respects escape pragmas" $ do+ fromDisk <- Data.Text.IO.readFile "test/fixtures/6.Dockerfile"+ let ast = parseText fromDisk+ in case ast of+ Right a ->+ let fromAst = prettyPrintDockerfile a+ in assertEqual "error" fromDisk (Text.pack . show $ fromAst)+ Left err -> assertFailure $ errorBundlePretty err++ it "ensure escape character '\\' is used as default" $ do+ fromDisk <- Data.Text.IO.readFile "test/fixtures/7.Dockerfile"+ let ast = parseText fromDisk+ in case ast of+ Right a ->+ let fromAst = prettyPrintDockerfile a+ in assertEqual "error" fromDisk (Text.pack . show $ fromAst)+ Left err -> assertFailure $ errorBundlePretty err++ it "ensure the printer ignores escape pragmas in the wrong place" $ do+ fromDisk <- Data.Text.IO.readFile "test/fixtures/8.Dockerfile"+ let ast = parseText fromDisk+ in case ast of+ Right a ->+ let fromAst = prettyPrintDockerfile a+ in assertEqual "error" fromDisk (Text.pack . show $ fromAst)+ Left err -> assertFailure $ errorBundlePretty err++ describe "parse text" $ do+ it "1.Dockerfile" $ do+ contents <- Data.Text.IO.readFile "test/fixtures/1.Dockerfile"+ case parseText (Text.replace "\n" "\r\n" contents) of+ Right _ -> return ()+ Left err -> assertFailure $ errorBundlePretty err++ it "2.Dockerfile" $ do+ contents <- Data.Text.IO.readFile "test/fixtures/2.Dockerfile"+ case parseText contents of+ Right _ -> return ()+ Left err -> assertFailure $ errorBundlePretty err++ it "3.Dockerfile" $ do+ contents <- Data.Text.IO.readFile "test/fixtures/3.Dockerfile"+ case parseText contents of+ Right _ -> return ()+ Left err -> assertFailure $ errorBundlePretty err++ it "4.Dockerfile" $ do+ contents <- Data.Text.IO.readFile "test/fixtures/4.Dockerfile"+ case parseText contents of+ Right _ -> return ()+ Left err -> assertFailure $ errorBundlePretty err++ it "5.Dockerfile" $ do+ contents <- Data.Text.IO.readFile "test/fixtures/5.Dockerfile"+ case parseText contents of+ Right _ -> return ()+ Left err -> assertFailure $ errorBundlePretty err++ it "1.Dockerfile crlf" $ do+ contents <- Data.Text.IO.readFile "test/fixtures/1.Dockerfile"+ case parseText contents of+ Right _ -> return ()+ Left err -> assertFailure $ errorBundlePretty err++ it "2.Dockerfile crlf" $ do+ contents <- Data.Text.IO.readFile "test/fixtures/2.Dockerfile"+ case parseText (Text.replace "\n" "\r\n" contents) of+ Right _ -> return ()+ Left err -> assertFailure $ errorBundlePretty err++ it "3.Dockerfile crlf" $ do+ contents <- Data.Text.IO.readFile "test/fixtures/3.Dockerfile"+ case parseText (Text.replace "\n" "\r\n" contents) of+ Right _ -> return ()+ Left err -> assertFailure $ errorBundlePretty err++ it "4.Dockerfile crlf" $ do+ contents <- Data.Text.IO.readFile "test/fixtures/4.Dockerfile"+ case parseText (Text.replace "\n" "\r\n" contents) of+ Right _ -> return ()+ Left err -> assertFailure $ errorBundlePretty err++ it "5.Dockerfile crlf" $ do+ contents <- Data.Text.IO.readFile "test/fixtures/5.Dockerfile"+ case parseText (Text.replace "\n" "\r\n" contents) of+ Right _ -> return ()+ Left err -> assertFailure $ errorBundlePretty err++ describe "parse utf encoded files with byte order mark" $ do+ it "Dockerfile UTF-8 with BOM" $ do+ parsed <- parseFile "test/fixtures/Dockerfile.bom.utf8"+ case parsed of+ Right _ -> return ()+ Left err -> assertFailure $ errorBundlePretty err++ it "Dockerfile UTF-16 Little Endian with BOM" $ do+ parsed <- parseFile "test/fixtures/Dockerfile.bom.utf16le"+ case parsed of+ Right _ -> return ()+ Left err -> assertFailure $ errorBundlePretty err++ it "Dockerfile UTF-16 Big Endian with BOM" $ do+ parsed <- parseFile "test/fixtures/Dockerfile.bom.utf16be"+ case parsed of+ Right _ -> return ()+ Left err -> assertFailure $ errorBundlePretty err++ it "Dockerfile UTF-32 Little Endian with BOM" $ do+ parsed <- parseFile "test/fixtures/Dockerfile.bom.utf32le"+ case parsed of+ Right _ -> return ()+ Left err -> assertFailure $ errorBundlePretty err++ it "Dockerfile UTF-32 Big Endian with BOM" $ do+ parsed <- parseFile "test/fixtures/Dockerfile.bom.utf32be"+ case parsed of+ Right _ -> return ()+ Left err -> assertFailure $ errorBundlePretty err
+ test/Language/Docker/ParseAddSpec.hs view
@@ -0,0 +1,137 @@+module Language.Docker.ParseAddSpec (spec) where++import Data.Default+import qualified Data.Text as Text+import Language.Docker.Syntax+import TestHelper+import Test.Hspec+++spec :: Spec+spec = do+ describe "ADD" $ do+ it "simple ADD" $+ let file = Text.unlines ["ADD . /app", "ADD http://foo.bar/baz ."]+ in assertAst+ file+ [ Add ( AddArgs [SourcePath "."] (TargetPath "/app") ) def,+ Add+ ( AddArgs [SourcePath "http://foo.bar/baz"] (TargetPath ".") )+ def+ ]+ it "multifiles ADD" $+ let file = Text.unlines ["ADD foo bar baz /app"]+ in assertAst+ file+ [ Add+ ( AddArgs+ (fmap SourcePath ["foo", "bar", "baz"])+ (TargetPath "/app")+ )+ def+ ]+ it "list of quoted files" $+ let file = Text.unlines ["ADD [\"foo\", \"bar\", \"baz\", \"/app\"]"]+ in assertAst+ file+ [ Add+ ( AddArgs+ (fmap SourcePath ["foo", "bar", "baz"])+ (TargetPath "/app")+ )+ def+ ]+ it "with checksum flag" $+ let file = Text.unlines ["ADD --checksum=sha256:24454f830cdd http://www.example.com/foo bar"]+ in assertAst+ file+ [ Add+ ( AddArgs (fmap SourcePath ["http://www.example.com/foo"]) (TargetPath "bar") )+ ( AddFlags (Checksum "sha256:24454f830cdd") NoChown NoChmod NoLink [] )+ ]+ it "with chown flag" $+ let file = Text.unlines ["ADD --chown=root:root foo bar"]+ in assertAst+ file+ [ Add+ ( AddArgs (fmap SourcePath ["foo"]) (TargetPath "bar") )+ ( AddFlags NoChecksum (Chown "root:root") NoChmod NoLink [] )+ ]+ it "with chmod flag" $+ let file = Text.unlines ["ADD --chmod=640 foo bar"]+ in assertAst+ file+ [ Add+ ( AddArgs (fmap SourcePath ["foo"]) (TargetPath "bar") )+ ( AddFlags NoChecksum NoChown (Chmod "640") NoLink [] )+ ]+ it "with link flag" $+ let file = Text.unlines ["ADD --link foo bar"]+ in assertAst+ file+ [ Add+ ( AddArgs (fmap SourcePath ["foo"]) (TargetPath "bar") )+ ( AddFlags NoChecksum NoChown NoChmod Link [])+ ]+ it "with chown and chmod flag" $+ let file = Text.unlines ["ADD --chown=root:root --chmod=640 foo bar"]+ in assertAst+ file+ [ Add+ ( AddArgs (fmap SourcePath ["foo"]) (TargetPath "bar") )+ ( AddFlags NoChecksum (Chown "root:root") (Chmod "640") NoLink [] )+ ]+ it "with chown and chmod flag other order" $+ let file = Text.unlines ["ADD --chmod=640 --chown=root:root foo bar"]+ in assertAst+ file+ [ Add+ ( AddArgs (fmap SourcePath ["foo"]) (TargetPath "bar") )+ ( AddFlags NoChecksum (Chown "root:root") (Chmod "640") NoLink [] )+ ]+ it "with all flags" $+ let file =+ Text.unlines ["ADD --chmod=640 --chown=root:root --checksum=sha256:24454f830cdd --link foo bar"]+ in assertAst+ file+ [ Add+ ( AddArgs (fmap SourcePath ["foo"]) (TargetPath "bar") )+ ( AddFlags (Checksum "sha256:24454f830cdd") (Chown "root:root") (Chmod "640") Link [] )+ ]+ it "list of quoted files and chown" $+ let file =+ Text.unlines+ [ "ADD --chown=user:group [\"foo\", \"bar\", \"baz\", \"/app\"]" ]+ in assertAst+ file+ [ Add+ ( AddArgs+ (fmap SourcePath ["foo", "bar", "baz"])+ (TargetPath "/app")+ )+ ( AddFlags NoChecksum (Chown "user:group") NoChmod NoLink [] )+ ]+ it "with exclude flag" $+ let file = Text.unlines ["ADD --exclude=*.tmp foo bar"]+ in assertAst+ file+ [ Add+ ( AddArgs (fmap SourcePath ["foo"]) (TargetPath "bar") )+ ( AddFlags NoChecksum NoChown NoChmod NoLink [Exclude "*.tmp"] )+ ]+ it "with multiple exclude flags" $+ let file = Text.unlines ["ADD --exclude=*.tmp --exclude=*.log foo bar"]+ in assertAst+ file+ [ Add+ ( AddArgs (fmap SourcePath ["foo"]) (TargetPath "bar") )+ ( AddFlags NoChecksum NoChown NoChmod NoLink [Exclude "*.tmp", Exclude "*.log"] )+ ]+ it "with exclude and other flags" $+ let file = Text.unlines ["ADD --chown=root:root --exclude=*.tmp foo bar"]+ in assertAst+ file+ [ Add+ ( AddArgs (fmap SourcePath ["foo"]) (TargetPath "bar") )+ ( AddFlags NoChecksum (Chown "root:root") NoChmod NoLink [Exclude "*.tmp"] )+ ]
+ test/Language/Docker/ParseCmdSpec.hs view
@@ -0,0 +1,64 @@+module Language.Docker.ParseCmdSpec where++import Language.Docker.Syntax+import Test.Hspec+import TestHelper+import qualified Data.Text as Text+++spec :: Spec+spec = do+ describe "parse CMD instructions" $ do+ it "one line cmd" $ assertAst "CMD true" [Cmd "true"]+ it "cmd over several lines" $+ assertAst "CMD true \\\n && true" [Cmd "true && true"]+ it "quoted command params" $ assertAst "CMD [\"echo\", \"1\"]" [Cmd ["echo", "1"]]+ it "Parses commas correctly" $ assertAst "CMD [ \"echo\" ,\"-e\" , \"1\"]" [Cmd ["echo", "-e", "1"]]++ ---------------------------------------------------------------------------+ -- This is the Dockefile statement under test (cleaned of Haskell escapes):+ --+ -- CMD [ "/bin/sh", "-c", \+ -- "echo foo && \+ -- echo bar" \+ -- ]+ it "parse exec style CMD with long broken lines" $ do+ let cmd =+ Text.unlines+ [ "CMD [ \"/bin/sh\", \"-c\", \\",+ " \"echo foo && \\",+ " echo bar\" \\",+ " ]"+ ]+ in assertAst cmd [ Cmd [ "/bin/sh", "-c", "echo foo && echo bar" ] ]+ it "parse exec style CMD with long broken lines" $ do+ let cmd =+ Text.unlines+ [ "# escape = `",+ "CMD [ \"/bin/sh\", \"-c\", `",+ " \"echo foo && `",+ " echo bar\" `",+ " ]"+ ]+ in assertAst+ cmd+ [ Pragma ( Escape ( EscapeChar '`' ) ),+ Cmd [ "/bin/sh", "-c", "echo foo && echo bar" ]+ ]+ ---------------------------------------------------------------------------++ it "parse exec style CMD with escaped characters" $ do+ let cmd =+ Text.unlines+ [ "CMD [ \"sbt\", \"set reStart / mainClass := Some(\\\"Main\\\");~reStart\" ]" ]+ in assertAst cmd [ Cmd [ "sbt", "set reStart / mainClass := Some(\"Main\");~reStart" ] ]+ it "parse exec style CMD with windows style escaped characters" $ do+ let cmd =+ Text.unlines+ [ "# escape = `",+ "CMD [ \"sbt\", \"set reStart / mainClass := Some(`\"Main`\");~reStart\" ]" ]+ in assertAst+ cmd+ [ Pragma ( Escape ( EscapeChar '`' ) ),+ Cmd [ "sbt", "set reStart / mainClass := Some(\"Main\");~reStart" ]+ ]
+ test/Language/Docker/ParseCopySpec.hs view
@@ -0,0 +1,267 @@+module Language.Docker.ParseCopySpec where++import Data.Default+import qualified Data.Text as Text+import Language.Docker.Syntax+import TestHelper+import Test.Hspec+++spec :: Spec+spec = do+ describe "regular Copy" $ do+ it "simple COPY" $+ let file = Text.unlines ["COPY . /app", "COPY baz /some/long/path"]+ in assertAst+ file+ [ Copy ( CopyArgs [ SourcePath "." ] ( TargetPath "/app" ) ) def,+ Copy+ ( CopyArgs+ [ SourcePath "baz" ]+ ( TargetPath "/some/long/path" )+ )+ def+ ]+ it "multifiles COPY" $+ let file = Text.unlines ["COPY foo bar baz /app"]+ in assertAst+ file+ [ Copy+ ( CopyArgs+ (fmap SourcePath ["foo", "bar", "baz"])+ (TargetPath "/app")+ )+ def+ ]+ it "list of quoted files" $+ let file = Text.unlines ["COPY [\"foo\", \"bar\", \"baz\", \"/app\"]"]+ in assertAst+ file+ [ Copy+ ( CopyArgs+ (fmap SourcePath ["foo", "bar", "baz"])+ (TargetPath "/app")+ )+ def+ ]+ it "supports windows paths" $+ let file = Text.unlines ["COPY C:\\\\go C:\\\\go"]+ in assertAst+ file+ [ Copy+ ( CopyArgs+ (fmap SourcePath ["C:\\\\go"])+ (TargetPath "C:\\\\go")+ )+ def+ ]+ it "does not get confused with trailing whitespace" $+ let file = Text.unlines ["COPY a b "]+ in assertAst+ file+ [ Copy ( CopyArgs [SourcePath "a"] (TargetPath "b") ) def ]++ describe "Copy with flags" $ do+ it "with chown flag" $+ let file = Text.unlines ["COPY --chown=user:group foo bar"]+ in assertAst+ file+ [ Copy+ ( CopyArgs [ SourcePath "foo" ] (TargetPath "bar") )+ ( CopyFlags ( Chown "user:group" ) NoChmod NoLink NoSource [])+ ]+ it "with chmod flag" $+ let file = Text.unlines ["COPY --chmod=777 foo bar"]+ in assertAst+ file+ [ Copy+ ( CopyArgs [ SourcePath "foo" ] (TargetPath "bar") )+ ( CopyFlags NoChown ( Chmod "777" ) NoLink NoSource [])+ ]+ it "with link flag" $+ let file = Text.unlines [ "COPY --link source /target" ]+ in assertAst+ file+ [ Copy+ ( CopyArgs [ SourcePath "source" ] ( TargetPath "/target" ) )+ ( CopyFlags NoChown NoChmod Link NoSource [])+ ]+ it "with from flag" $+ let file = Text.unlines ["COPY --from=node foo bar"]+ in assertAst+ file+ [ Copy+ ( CopyArgs [ SourcePath "foo" ] (TargetPath "bar") )+ ( CopyFlags NoChown NoChmod NoLink ( CopySource "node" ) [])+ ]+ it "with all flags" $+ let file =+ Text.unlines+ [ "COPY --from=node --chmod=751 --link --chown=user:group foo bar" ]+ in assertAst+ file+ [ Copy+ ( CopyArgs [ SourcePath "foo" ] (TargetPath "bar") )+ ( CopyFlags+ (Chown "user:group")+ (Chmod "751")+ Link+ (CopySource "node")+ []+ )+ ]+ it "with all flags in different order" $+ let file =+ Text.unlines+ [ "COPY --link --chown=user:group --from=node --chmod=644 foo bar" ]+ in assertAst+ file+ [ Copy+ ( CopyArgs [ SourcePath "foo" ] (TargetPath "bar") )+ ( CopyFlags+ (Chown "user:group")+ (Chmod "644")+ Link+ (CopySource "node")+ []+ )+ ]+ it "with exclude flag" $+ let file = Text.unlines ["COPY --exclude=*.tmp foo bar"]+ in assertAst+ file+ [ Copy+ ( CopyArgs [ SourcePath "foo" ] (TargetPath "bar") )+ ( CopyFlags NoChown NoChmod NoLink NoSource [Exclude "*.tmp"] )+ ]+ it "with multiple exclude flags" $+ let file = Text.unlines ["COPY --exclude=*.tmp --exclude=*.log foo bar"]+ in assertAst+ file+ [ Copy+ ( CopyArgs [ SourcePath "foo" ] (TargetPath "bar") )+ ( CopyFlags NoChown NoChmod NoLink NoSource [Exclude "*.tmp", Exclude "*.log"] )+ ]+ it "with exclude and other flags" $+ let file = Text.unlines ["COPY --chown=root:root --exclude=*.tmp foo bar"]+ in assertAst+ file+ [ Copy+ ( CopyArgs [ SourcePath "foo" ] (TargetPath "bar") )+ ( CopyFlags (Chown "root:root") NoChmod NoLink NoSource [Exclude "*.tmp"] )+ ]++ describe "Copy with Heredocs" $ do+ it "empty heredoc" $+ let file = Text.unlines ["COPY <<EOF /target", "EOF"]+ in assertAst+ file+ [ Copy+ ( CopyArgs [SourcePath "EOF"] (TargetPath "/target") )+ def+ ]+ it "foo heredoc" $+ let file = Text.unlines ["COPY <<FOO /target", "foo", "FOO"]+ in assertAst+ file+ [ Copy+ ( CopyArgs [SourcePath "FOO"] (TargetPath "/target") )+ def+ ]+ it "foo heredoc lowercase +extensions" $+ let file =+ Text.unlines+ [ "COPY <<foo.txt /target", "foo content", "line 2", "foo.txt" ]+ in assertAst+ file+ [ Copy+ ( CopyArgs [SourcePath "foo.txt"] (TargetPath "/target") )+ def+ ]+ it "foo heredoc single quoted marker" $+ let file = Text.unlines ["COPY <<\'FOO\' /target", "foo", "FOO"]+ in assertAst+ file+ [ Copy+ ( CopyArgs [SourcePath "FOO"] (TargetPath "/target") )+ def+ ]+ it "foo heredoc double quoted marker" $+ let file = Text.unlines ["COPY <<\"FOO\" /target", "foo", "FOO"]+ in assertAst+ file+ [ Copy+ ( CopyArgs [SourcePath "FOO"] (TargetPath "/target") )+ def+ ]+ it "foo heredoc +dash" $+ let file = Text.unlines ["COPY <<-FOO /target", "foo", "FOO"]+ in assertAst+ file+ [ Copy+ ( CopyArgs [SourcePath "FOO"] (TargetPath "/target") )+ def+ ]+ it "foo heredoc single quoted marker +dash" $+ let file = Text.unlines ["COPY <<-\'FOO\' /target", "foo", "FOO"]+ in assertAst+ file+ [ Copy+ ( CopyArgs [ SourcePath "FOO" ] ( TargetPath "/target" ) )+ def+ ]+ it "foo heredoc double quoted marker +dash" $+ let file = Text.unlines ["COPY <<-\"FOO\" /target", "foo", "FOO"]+ in assertAst+ file+ [ Copy+ ( CopyArgs [ SourcePath "FOO" ] ( TargetPath "/target" ) )+ def+ ]+ it "multiple heredocs" $+ let file =+ Text.unlines+ [ "COPY <<FOO <<BAR /target", "foo", "FOO", "bar", "BAR" ]+ in assertAst+ file+ [ Copy+ ( CopyArgs+ [ SourcePath "FOO", SourcePath "BAR" ]+ ( TargetPath "/target" )+ )+ def+ ]+ it "multiple heredocs with single/double quotes and dash mixed" $+ let file =+ Text.unlines+ [ "COPY <<FOO <<-\"BAR\" <<\'FIZZ\' <<-BUZZ /target",+ "foo",+ "FOO",+ "bar",+ "BAR",+ "fizz",+ "FIZZ",+ "buss",+ "BUZZ"+ ]+ in assertAst+ file+ [ Copy+ ( CopyArgs+ [ SourcePath "FOO",+ SourcePath "BAR",+ SourcePath "FIZZ",+ SourcePath "BUZZ"+ ]+ ( TargetPath "/target" )+ )+ def+ ]+ it "foo heredoc with line continuations" $+ let file = Text.unlines ["COPY <<FOO /target", "foo \\", "bar", "FOO"]+ in assertAst+ file+ [ Copy+ ( CopyArgs [ SourcePath "FOO" ] ( TargetPath "/target" ) )+ def+ ]
+ test/Language/Docker/ParseExposeSpec.hs view
@@ -0,0 +1,146 @@+module Language.Docker.ParseExposeSpec where++import Language.Docker.Syntax+import TestHelper+import Test.Hspec+++spec :: Spec+spec = do+ describe "parse EXPOSE" $ do++ it "should handle number ports" $+ let content = "EXPOSE 8080"+ in assertAst+ content+ [ Expose+ ( Ports+ [ PortSpec (Port 8080 TCP)+ ]+ )+ ]++ it "should handle many number ports" $+ let content = "EXPOSE 8080 8081"+ in assertAst+ content+ [ Expose+ ( Ports+ [ PortSpec (Port 8080 TCP),+ PortSpec (Port 8081 TCP)+ ]+ )+ ]++ it "should handle ports with protocol" $+ let content = "EXPOSE 8080/TCP 8081/UDP"+ in assertAst+ content+ [ Expose+ ( Ports+ [ PortSpec (Port 8080 TCP),+ PortSpec (Port 8081 UDP)+ ]+ )+ ]++ it "should handle ports with protocol and variables" $+ let content = "EXPOSE $PORT 8080 8081/UDP"+ in assertAst+ content+ [ Expose+ ( Ports+ [ PortSpec (PortStr "$PORT"),+ PortSpec (Port 8080 TCP),+ PortSpec (Port 8081 UDP)+ ]+ )+ ]++ it "should handle port ranges" $+ let content = "EXPOSE 80 81 8080-8085"+ in assertAst+ content+ [ Expose+ ( Ports+ [ PortSpec (Port 80 TCP),+ PortSpec (Port 81 TCP),+ PortRangeSpec+ ( PortRange (Port 8080 TCP) (Port 8085 TCP) )+ ]+ )+ ]++ it "should handle udp port ranges" $+ let content = "EXPOSE 80 81 8080-8085/udp"+ in assertAst+ content+ [ Expose+ ( Ports+ [ PortSpec (Port 80 TCP),+ PortSpec (Port 81 TCP),+ PortRangeSpec+ ( PortRange (Port 8080 UDP) (Port 8085 UDP) )+ ]+ )+ ]++ it "should handle one variable port ranges 1" $+ let content = "EXPOSE 8080-${bar}/udp"+ in assertAst+ content+ [ Expose+ ( Ports+ [ PortRangeSpec+ ( PortRange (Port 8080 UDP) (PortStr "${bar}") )+ ]+ )+ ]++ it "should handle one variable port ranges 2" $+ let content = "EXPOSE ${foo}-8080/udp"+ in assertAst+ content+ [ Expose+ ( Ports+ [ PortRangeSpec+ ( PortRange (PortStr "${foo}") (Port 8080 UDP) )+ ]+ )+ ]++ it "should handle two variables port ranges" $+ let content = "EXPOSE ${foo}-${bar}/udp"+ in assertAst+ content+ [ Expose+ ( Ports+ [ PortRangeSpec+ ( PortRange (PortStr "${foo}") (PortStr "${bar}") )+ ]+ )+ ]++ it "should handle multiline variables" $+ let content =+ "EXPOSE ${PORT} ${PORT_SSL} \\\n\+ \ ${PORT_HTTP} ${PORT_HTTPS} \\\n\+ \ ${PORT_REP} \\\n\+ \ ${PORT_ADMIN} ${PORT_ADMIN_HTTP}"+ in assertAst+ content+ [ Expose+ ( Ports+ [ PortSpec (PortStr "${PORT}"),+ PortSpec (PortStr "${PORT_SSL}"),+ PortSpec (PortStr "${PORT_HTTP}"),+ PortSpec (PortStr "${PORT_HTTPS}"),+ PortSpec (PortStr "${PORT_REP}"),+ PortSpec (PortStr "${PORT_ADMIN}"),+ PortSpec (PortStr "${PORT_ADMIN_HTTP}")+ ]+ )+ ]+ it "should fail with wrong protocol" $+ let content = "EXPOSE 80/ip"+ in expectFail content
+ test/Language/Docker/ParseHealthcheckSpec.hs view
@@ -0,0 +1,89 @@+module Language.Docker.ParseHealthcheckSpec where++import Language.Docker.Syntax+import Test.Hspec+import TestHelper+import qualified Data.Text as Text+++spec :: Spec+spec = do+ describe "parse HEALTHCHECK" $ do+ it "parse healthcheck with interval" $+ assertAst+ "HEALTHCHECK --interval=5m \\\nCMD curl -f http://localhost/"+ [ Healthcheck $+ Check $+ CheckArgs "curl -f http://localhost/" (Just 300) Nothing Nothing Nothing Nothing+ ]+ it "parse healthcheck with retries" $+ assertAst+ "HEALTHCHECK --retries=10 CMD curl -f http://localhost/"+ [ Healthcheck $+ Check $+ CheckArgs "curl -f http://localhost/" Nothing Nothing Nothing Nothing (Just $ Retries 10)+ ]+ it "parse healthcheck with timeout" $+ assertAst+ "HEALTHCHECK --timeout=10s CMD curl -f http://localhost/"+ [ Healthcheck $+ Check $+ CheckArgs "curl -f http://localhost/" Nothing (Just 10) Nothing Nothing Nothing+ ]+ it "parse healthcheck with start-period" $+ assertAst+ "HEALTHCHECK --start-period=2m CMD curl -f http://localhost/"+ [ Healthcheck $+ Check $+ CheckArgs "curl -f http://localhost/" Nothing Nothing (Just 120) Nothing Nothing+ ]+ it "parse healthcheck with start-interval" $+ assertAst+ "HEALTHCHECK --start-interval=4m CMD curl -f http://localhost/"+ [ Healthcheck $+ Check $+ CheckArgs "curl -f http://localhost/" Nothing Nothing Nothing (Just 240) Nothing+ ]+ it "parse healthcheck with all flags" $+ assertAst+ "HEALTHCHECK --start-period=2s --start-interval=10s --timeout=1m --retries=3 --interval=5s CMD curl -f http://localhost/"+ [ Healthcheck $+ Check $+ CheckArgs+ "curl -f http://localhost/"+ (Just 5)+ (Just 60)+ (Just 2)+ (Just 10)+ (Just $ Retries 3)+ ]+ it "parse healthcheck with no flags" $+ assertAst+ "HEALTHCHECK CMD curl -f http://localhost/"+ [ Healthcheck $+ Check $+ CheckArgs "curl -f http://localhost/" Nothing Nothing Nothing Nothing Nothing+ ]++ it "fractional arguments to flags" $+ let file =+ Text.unlines+ [ "HEALTHCHECK \\",+ " --interval=0.5s \\",+ " --timeout=0.1s \\",+ " --start-period=0.2s \\",+ " --start-interval=0.5s \\",+ " CMD curl -f http://localhost"+ ]+ in assertAst+ file+ [ Healthcheck $+ Check $+ CheckArgs+ "curl -f http://localhost"+ ( Just 0.5 )+ ( Just 0.10000000149 )+ ( Just 0.20000000298 )+ ( Just 0.5 )+ Nothing+ ]
+ test/Language/Docker/ParsePragmaSpec.hs view
@@ -0,0 +1,71 @@+module Language.Docker.ParsePragmaSpec where++import qualified Data.Text as Text+import Language.Docker.Syntax+import TestHelper+import Test.Hspec+++spec :: Spec+spec = do+ describe "parse # pragma" $ do+ it "# escape = \\" $+ let dockerfile = Text.unlines ["# escape = \\\\"] -- this need double escaping for some reason.+ in assertAst dockerfile [Pragma (Escape EscapeChar {escape = '\\'})]+ it "#escape=`" $+ let dockerfile = Text.unlines ["#escape=`"]+ in assertAst dockerfile [Pragma (Escape EscapeChar {escape = '`'})]+ it "# escape=`" $+ let dockerfile = Text.unlines ["# escape=`"]+ in assertAst dockerfile [Pragma (Escape EscapeChar {escape = '`'})]+ it "#escape =`" $+ let dockerfile = Text.unlines ["#escape =`"]+ in assertAst dockerfile [Pragma (Escape EscapeChar {escape = '`'})]+ it "#escape= `" $+ let dockerfile = Text.unlines ["#escape= `"]+ in assertAst dockerfile [Pragma (Escape EscapeChar {escape = '`'})]+ it "# escape =`" $+ let dockerfile = Text.unlines ["# escape =`"]+ in assertAst dockerfile [Pragma (Escape EscapeChar {escape = '`'})]+ it "#escape = `" $+ let dockerfile = Text.unlines ["#escape = `"]+ in assertAst dockerfile [Pragma (Escape EscapeChar {escape = '`'})]+ it "# escape = `" $+ let dockerfile = Text.unlines ["# escape = `"]+ in assertAst dockerfile [Pragma (Escape EscapeChar {escape = '`'})]+ it "# Escape = `" $+ let dockerfile = Text.unlines ["# escape = `"]+ in assertAst dockerfile [Pragma (Escape EscapeChar {escape = '`'})]+ it "# ESCAPE = `" $+ let dockerfile = Text.unlines ["# escape = `"]+ in assertAst dockerfile [Pragma (Escape EscapeChar {escape = '`'})]+ it "#syntax=docker/dockerfile:1.0" $+ let dockerfile = Text.unlines ["#syntax=docker/dockerfile:1.0"]+ in assertAst dockerfile [Pragma (Syntax SyntaxImage {syntax = "docker/dockerfile:1.0"})]+ it "# syntax=docker/dockerfile:1.0" $+ let dockerfile = Text.unlines ["# syntax=docker/dockerfile:1.0"]+ in assertAst dockerfile [Pragma (Syntax SyntaxImage {syntax = "docker/dockerfile:1.0"})]+ it "#syntax =docker/dockerfile:1.0" $+ let dockerfile = Text.unlines ["#syntax =docker/dockerfile:1.0"]+ in assertAst dockerfile [Pragma (Syntax SyntaxImage {syntax = "docker/dockerfile:1.0"})]+ it "#syntax= docker/dockerfile:1.0" $+ let dockerfile = Text.unlines ["#syntax= docker/dockerfile:1.0"]+ in assertAst dockerfile [Pragma (Syntax SyntaxImage {syntax = "docker/dockerfile:1.0"})]+ it "# syntax =docker/dockerfile:1.0" $+ let dockerfile = Text.unlines ["# syntax =docker/dockerfile:1.0"]+ in assertAst dockerfile [Pragma (Syntax SyntaxImage {syntax = "docker/dockerfile:1.0"})]+ it "#syntax = docker/dockerfile:1.0" $+ let dockerfile = Text.unlines ["#syntax = docker/dockerfile:1.0"]+ in assertAst dockerfile [Pragma (Syntax SyntaxImage {syntax = "docker/dockerfile:1.0"})]+ it "# syntax= docker/dockerfile:1.0" $+ let dockerfile = Text.unlines ["# syntax= docker/dockerfile:1.0"]+ in assertAst dockerfile [Pragma (Syntax SyntaxImage {syntax = "docker/dockerfile:1.0"})]+ it "# syntax = docker/dockerfile:1.0" $+ let dockerfile = Text.unlines ["# syntax = docker/dockerfile:1.0"]+ in assertAst dockerfile [Pragma (Syntax SyntaxImage {syntax = "docker/dockerfile:1.0"})]+ it "# Syntax = docker/dockerfile:1.0" $+ let dockerfile = Text.unlines ["# syntax = docker/dockerfile:1.0"]+ in assertAst dockerfile [Pragma (Syntax SyntaxImage {syntax = "docker/dockerfile:1.0"})]+ it "# SYNTAX = docker/dockerfile:1.0" $+ let dockerfile = Text.unlines ["# syntax = docker/dockerfile:1.0"]+ in assertAst dockerfile [Pragma (Syntax SyntaxImage {syntax = "docker/dockerfile:1.0"})]
+ test/Language/Docker/ParseRunSpec.hs view
@@ -0,0 +1,736 @@+module Language.Docker.ParseRunSpec where++import Data.Default.Class (def)+import Language.Docker.Syntax+import Test.Hspec+import TestHelper+import qualified Data.Set as Set+import qualified Data.Text as Text+++spec :: Spec+spec = do+ describe "parse RUN instructions" $ do+ it "escaped with space before" $+ let dockerfile = Text.unlines ["RUN yum install -y \\", "imagemagick \\", "mysql"]+ in assertAst dockerfile [Run "yum install -y imagemagick mysql"]+ it "escaped linebreak, indented" $+ let file = Text.unlines [ "RUN foo ; \\", " bar" ]+ in assertAst file [ Run "foo ; bar" ]+ it "does not choke on unmatched brackets" $+ let dockerfile = Text.unlines ["RUN [foo"]+ in assertAst dockerfile [Run "[foo"]+ it "Distinguishes between text and a list" $+ let dockerfile =+ Text.unlines+ [ "RUN echo foo",+ "RUN [\"echo\", \"foo\"]"+ ]+ in assertAst dockerfile [Run $ RunArgs (ArgumentsText "echo foo") def, Run $ RunArgs (ArgumentsList "echo foo") def]+ it "Accepts spaces inside the brackets" $+ let dockerfile =+ Text.unlines+ [ "RUN [ \"echo\", \"foo\" ]"+ ]+ in assertAst dockerfile [Run $ RunArgs (ArgumentsList "echo foo") def]++ describe "RUN with experimental flags" $ do+ it "--mount=type=bind and target" $+ let file = Text.unlines ["RUN --mount=type=bind,target=/foo echo foo"]+ flags = def {mount = Set.singleton $ BindMount (def {bTarget = "/foo"})}+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags+ ]+ it "--mount default to bind" $+ let file = Text.unlines ["RUN --mount=target=/foo echo foo"]+ flags = def {mount = Set.singleton $ BindMount (def {bTarget = "/foo"})}+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags+ ]+ it "--mount=type=bind all modifiers" $+ let file = Text.unlines ["RUN --mount=type=bind,target=/foo,source=/bar,from=ubuntu,ro,relabel=shared echo foo"]+ flags = def {mount = Set.singleton $ BindMount (BindOpts {bTarget = "/foo", bSource = Just "/bar", bFromImage = Just "ubuntu", bReadOnly = Just True, bRelabel = Just RelabelShared})}+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags+ ]+ it "--mount=type=cache with target" $+ let file =+ Text.unlines+ [ "RUN --mount=type=cache,target=/foo echo foo",+ "RUN --mount=type=cache,target=/bar echo foo",+ "RUN --mount=type=cache,target=/baz echo foo"+ ]+ flags1 = def {mount = Set.singleton $ CacheMount (def {cTarget = "/foo"})}+ flags2 = def {mount = Set.singleton $ CacheMount (def {cTarget = "/bar"})}+ flags3 = def {mount = Set.singleton $ CacheMount (def {cTarget = "/baz"})}+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags1,+ Run $ RunArgs (ArgumentsText "echo foo") flags2,+ Run $ RunArgs (ArgumentsText "echo foo") flags3+ ]++ it "--mount=type=cache with id from variable/arg" $+ let file =+ Text.unlines+ [ "ARG debian_version=12",+ "FROM debian:bookworm-slim",+ "RUN --mount=id=debian:${debian_version},type=cache,target=/foo foo bar"+ ]+ flags = def {mount = Set.singleton $ CacheMount (def {cTarget = "/foo", cCacheId = Just "debian:${debian_version}"})}+ in assertAst+ file+ [ Arg "debian_version" (Just "12"),+ From $ BaseImage+ { image = Image+ { registryName = Nothing,+ imageName="debian"+ },+ tag = Just Tag { unTag="bookworm-slim" },+ digest = Nothing,+ alias = Nothing,+ platform = Nothing+ },+ Run $ RunArgs (ArgumentsText "foo bar") flags+ ]++ it "--mount=type=cache,dst=/foo" $+ let file = Text.unlines [ "RUN --mount=type=cache,dst=/foo echo foo" ]+ flags = def {mount = Set.singleton $ CacheMount (def {cTarget = "/foo"})}+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flags ]++ it "--mount=dst=/foo,type=cache" $+ let file = Text.unlines [ "RUN --mount=dst=/foo,type=cache echo foo" ]+ flags = def {mount = Set.singleton $ CacheMount (def {cTarget = "/foo"})}+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flags ]++ it "--mount=type=cache with all modifiers" $+ let file =+ Text.unlines+ [ "RUN --mount=type=cache,target=/foo,sharing=private,id=a,ro,from=ubuntu,source=/bar,mode=0700,uid=0,gid=0 echo foo"+ ]+ flags =+ def+ { mount =+ Set.singleton $+ CacheMount+ ( def+ { cTarget = "/foo",+ cSharing = Just Private,+ cCacheId = Just "a",+ cReadOnly = Just True,+ cFromImage = Just "ubuntu",+ cSource = Just "/bar",+ cMode = Just "0700",+ cUid = Just "0",+ cGid = Just "0"+ }+ )+ }+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags+ ]+ it "--mount=type=cache with all modifiers, different order" $+ let file =+ Text.unlines+ [ "RUN --mount=readonly,sharing=private,id=a,type=cache,from=ubuntu,source=/bar,destination=/foo,mode=0700,uid=0,gid=0 echo foo"+ ]+ flags =+ def+ { mount =+ Set.singleton $+ CacheMount+ ( def+ { cTarget = "/foo",+ cSharing = Just Private,+ cCacheId = Just "a",+ cReadOnly = Just True,+ cFromImage = Just "ubuntu",+ cSource = Just "/bar",+ cMode = Just "0700",+ cUid = Just "0",+ cGid = Just "0"+ }+ )+ }+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags+ ]+ it "--mount=type=tmpfs" $+ let file = Text.unlines ["RUN --mount=type=tmpfs,target=/foo echo foo"]+ flags = def {mount = Set.singleton $ TmpfsMount (def {tTarget = "/foo"})}+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags+ ]+ it "--mount=type=ssh" $+ let file = Text.unlines ["RUN --mount=type=ssh echo foo"]+ flags = def {mount = Set.singleton $ SshMount def}+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags+ ]+ it "--mount=type=ssh,required=false" $+ let file = Text.unlines ["RUN --mount=type=ssh,required=false echo foo"]+ flags = def {mount = Set.singleton $ SshMount def {sIsRequired = Just False}}+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags+ ]+ it "--mount=type=ssh,required=False" $+ let file = Text.unlines ["RUN --mount=type=ssh,required=False echo foo"]+ flags = def {mount = Set.singleton $ SshMount def {sIsRequired = Just False}}+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags+ ]+ it "--mount=type=secret,required=true" $+ let file = Text.unlines ["RUN --mount=type=secret,required=true echo foo"]+ flags = def {mount = Set.singleton $ SecretMount def {sIsRequired = Just True}}+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags+ ]+ it "--mount=type=secret,required=True" $+ let file = Text.unlines ["RUN --mount=type=secret,required=True echo foo"]+ flags = def {mount = Set.singleton $ SecretMount def {sIsRequired = Just True}}+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags+ ]+ it "--mount=type=ssh all modifiers" $+ let file = Text.unlines ["RUN --mount=type=ssh,target=/foo,id=a,required,source=/bar,mode=0700,uid=0,gid=0 echo foo"]+ flags =+ def+ { mount =+ Set.singleton $+ SshMount+ ( def+ { sTarget = Just "/foo",+ sCacheId = Just "a",+ sIsRequired = Just True,+ sSource = Just "/bar",+ sMode = Just "0700",+ sUid = Just "0",+ sGid = Just "0"+ }+ )+ }+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags+ ]+ it "--mount=type=ssh all modifiers, required explicit" $+ let file = Text.unlines ["RUN --mount=type=ssh,target=/foo,id=a,required=true,source=/bar,mode=0700,uid=0,gid=0 echo foo"]+ flags =+ def+ { mount =+ Set.singleton $+ SshMount+ ( def+ { sTarget = Just "/foo",+ sCacheId = Just "a",+ sIsRequired = Just True,+ sSource = Just "/bar",+ sMode = Just "0700",+ sUid = Just "0",+ sGid = Just "0"+ }+ )+ }+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags+ ]+ it "--mount=type=secret all modifiers" $+ let file = Text.unlines ["RUN --mount=type=secret,target=/foo,env=baz,id=a,required,source=/bar,mode=0700,uid=0,gid=0 echo foo"]+ flags =+ def+ { mount =+ Set.singleton $+ SecretMount+ ( def+ { sTarget = Just "/foo",+ sEnv = Just "baz",+ sCacheId = Just "a",+ sIsRequired = Just True,+ sSource = Just "/bar",+ sMode = Just "0700",+ sUid = Just "0",+ sGid = Just "0"+ }+ )+ }+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags+ ]+ it "--mount=type=secret all modifiers, required explicit" $+ let file = Text.unlines ["RUN --mount=type=secret,target=/foo,env=baz,id=a,required=true,source=/bar,mode=0700,uid=0,gid=0 echo foo"]+ flags =+ def+ { mount =+ Set.singleton $+ SecretMount+ ( def+ { sTarget = Just "/foo",+ sEnv = Just "baz",+ sCacheId = Just "a",+ sIsRequired = Just True,+ sSource = Just "/bar",+ sMode = Just "0700",+ sUid = Just "0",+ sGid = Just "0"+ }+ )+ }+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags+ ]+ it "--mount=type=cache uid/gid=$var" $+ let file = Text.unlines ["RUN --mount=type=cache,target=/foo,uid=$VAR_UID,gid=$VAR_GID echo foo"]+ flags =+ def+ { mount =+ Set.singleton $+ CacheMount+ ( def+ { cTarget = TargetPath "/foo",+ cUid = Just "$VAR_UID",+ cGid = Just "$VAR_GID"+ }+ )+ }+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags+ ]+ it "multiple --mount=type=cache flags" $+ let file = Text.unlines+ [ "RUN --mount=type=cache,target=/foo \\",+ " --mount=type=cache,target=/bar \\",+ " echo foo"+ ]+ flags =+ def+ { mount =+ Set.fromList+ [ CacheMount ( def { cTarget = TargetPath "/foo" } ),+ CacheMount ( def { cTarget = TargetPath "/bar" } )+ ]+ }+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags+ ]+ it "multiple different --mount flags" $+ let file = Text.unlines+ [ "RUN --mount=type=cache,target=/foo \\",+ " --mount=type=secret,target=/bar \\",+ " echo foo"+ ]+ flags =+ def+ { mount =+ Set.fromList+ [ CacheMount ( def { cTarget = TargetPath "/foo" } ),+ SecretMount ( def { sTarget = Just "/bar" } )+ ]+ }+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags+ ]+ it "--network=none" $+ let file = Text.unlines ["RUN --network=none echo foo"]+ flags = def {network = Just NetworkNone}+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags+ ]+ it "--network=host" $+ let file = Text.unlines ["RUN --network=host echo foo"]+ flags = def {network = Just NetworkHost}+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags+ ]+ it "--network=default" $+ let file = Text.unlines ["RUN --network=default echo foo"]+ flags = def {network = Just NetworkDefault}+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags+ ]+ it "--security=insecure" $+ let file = Text.unlines ["RUN --security=insecure echo foo"]+ flags = def {security = Just Insecure}+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags+ ]+ it "--security=sandbox" $+ let file = Text.unlines ["RUN --security=sandbox echo foo"]+ flags = def {security = Just Sandbox}+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags+ ]+ it "allows all flags" $+ let file = Text.unlines ["RUN --mount=target=/foo --network=none --security=sandbox echo foo"]+ flags =+ def+ { security = Just Sandbox,+ network = Just NetworkNone,+ mount = Set.singleton $ BindMount $ def {bTarget = "/foo"}+ }+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "echo foo") flags+ ]++ describe "RUN with --mount flag and different ways to write ro/rw" $ do+ let flagsRo = def { mount = Set.singleton $+ BindMount+ ( def+ { bTarget = "/foo",+ bSource = Just "/bar",+ bReadOnly = Just True+ }+ )+ }+ flagsRw = def { mount = Set.singleton $+ BindMount+ ( def+ { bTarget = "/foo",+ bSource = Just "/bar",+ bReadOnly = Just False+ }+ )+ }+ in do+ it "--mount=type=bind,ro" $+ let file = Text.unlines+ [ "RUN --mount=type=bind,target=/foo,source=/bar,ro echo foo" ]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRo ]+ it "--mount=type=bind,RO" $+ let file = Text.unlines+ [ "RUN --mount=type=bind,target=/foo,source=/bar,RO echo foo" ]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRo ]+ it "--mount=type=bind,readonly" $+ let file = Text.unlines+ [ "RUN --mount=type=bind,target=/foo,source=/bar,readonly echo foo" ]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRo ]+ it "--mount=type=bind,Readonly" $+ let file = Text.unlines+ [ "RUN --mount=type=bind,target=/foo,source=/bar,Readonly echo foo" ]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRo ]+ it "--mount=type=bind,ro=true" $+ let file = Text.unlines+ [ "RUN --mount=type=bind,target=/foo,source=/bar,ro=true echo foo" ]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRo ]+ it "--mount=type=bind,RO=TRUE" $+ let file = Text.unlines+ [ "RUN --mount=type=bind,target=/foo,source=/bar,RO=TRUE echo foo" ]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRo ]+ it "--mount=type=bind,readonly=true" $+ let file = Text.unlines+ [ "RUN --mount=type=bind,target=/foo,source=/bar,readonly=true echo foo" ]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRo ]+ it "--mount=type=bind,Readonly=True" $+ let file = Text.unlines+ [ "RUN --mount=type=bind,target=/foo,source=/bar,Readonly=True echo foo" ]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRo ]+ it "--mount=type=bind,ro=True" $+ let file = Text.unlines+ [ "RUN --mount=type=bind,target=/foo,source=/bar,ro=True echo foo" ]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRo ]+ it "--mount=type=bind,readonly=True" $+ let file = Text.unlines+ [ "RUN --mount=type=bind,target=/foo,source=/bar,readonly=True echo foo" ]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRo ]+ it "--mount=type=bind,rw=false" $+ let file = Text.unlines+ [ "RUN --mount=type=bind,target=/foo,source=/bar,rw=false echo foo" ]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRo ]+ it "--mount=type=bind,readwrite=false" $+ let file = Text.unlines+ [ "RUN --mount=type=bind,target=/foo,source=/bar,readwrite=false echo foo" ]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRo ]+ it "--mount=type=bind,rw=False" $+ let file = Text.unlines+ [ "RUN --mount=type=bind,target=/foo,source=/bar,rw=False echo foo" ]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRo ]+ it "--mount=type=bind,readwrite=False" $+ let file = Text.unlines+ [ "RUN --mount=type=bind,target=/foo,source=/bar,readwrite=False echo foo" ]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRo ]++ it "--mount=type=bind,rw" $+ let file = Text.unlines+ [ "RUN --mount=type=bind,target=/foo,source=/bar,rw echo foo" ]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRw ]+ it "--mount=type=bind,readwrite" $+ let file = Text.unlines+ [ "RUN --mount=type=bind,target=/foo,source=/bar,readwrite echo foo" ]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRw ]+ it "--mount=type=bind,rw=true" $+ let file = Text.unlines+ [ "RUN --mount=type=bind,target=/foo,source=/bar,rw=true echo foo" ]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRw ]+ it "--mount=type=bind,readwrite=true" $+ let file = Text.unlines+ [ "RUN --mount=type=bind,target=/foo,source=/bar,readwrite=true echo foo" ]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRw ]+ it "--mount=type=bind,rw=True" $+ let file = Text.unlines+ [ "RUN --mount=type=bind,target=/foo,source=/bar,rw=True echo foo" ]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRw ]+ it "--mount=type=bind,readwrite=True" $+ let file = Text.unlines+ [ "RUN --mount=type=bind,target=/foo,source=/bar,readwrite=True echo foo" ]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRw ]+ it "--mount=type=bind,ro=false" $+ let file = Text.unlines+ [ "RUN --mount=type=bind,target=/foo,source=/bar,ro=false echo foo" ]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRw ]+ it "--mount=type=bind,readonly=false" $+ let file = Text.unlines+ [ "RUN --mount=type=bind,target=/foo,source=/bar,readonly=false echo foo" ]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRw ]+ it "--mount=type=bind,ro=False" $+ let file = Text.unlines+ [ "RUN --mount=type=bind,target=/foo,source=/bar,ro=False echo foo" ]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRw ]+ it "--mount=type=bind,readonly=False" $+ let file = Text.unlines+ [ "RUN --mount=type=bind,target=/foo,source=/bar,readonly=False echo foo" ]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRw ]++ describe "parse RUN in heredocs format" $ do+ it "foo heredoc" $+ let file = Text.unlines [ "RUN <<EOF", "foo", "EOF" ]+ flags = def { security = Nothing }+ in assertAst file [ Run $ RunArgs (ArgumentsText "foo") flags ]+ it "foo heredoc without newline at end" $+ let file = "RUN <<EOF\nfoo\nEOF"+ flags = def { security = Nothing }+ in assertAst file [ Run $ RunArgs (ArgumentsText "foo") flags ]+ it "foo heredoc marker" $+ let file = Text.unlines [ "RUN <<FOO", "foo", "FOO" ]+ flags = def { security = Nothing }+ in assertAst file [ Run $ RunArgs (ArgumentsText "foo") flags ]+ it "foo heredoc quoted marker" $+ let file = Text.unlines [ "RUN <<\"FOO\"", "foo", "FOO" ]+ flags = def { security = Nothing }+ in assertAst file [ Run $ RunArgs (ArgumentsText "foo") flags ]+ it "foo heredoc single quoted marker" $+ let file = Text.unlines [ "RUN <<\'FOO\'", "foo", "FOO" ]+ flags = def { security = Nothing }+ in assertAst file [ Run $ RunArgs (ArgumentsText "foo") flags ]+ it "foo heredoc +dash" $+ let file = Text.unlines [ "RUN <<-FOO", "foo", "FOO" ]+ flags = def { security = Nothing }+ in assertAst file [ Run $ RunArgs (ArgumentsText "foo") flags ]+ it "foo heredoc quoted +dash" $+ let file = Text.unlines [ "RUN <<-\"FOO\"", "foo", "FOO" ]+ flags = def { security = Nothing }+ in assertAst file [ Run $ RunArgs (ArgumentsText "foo") flags ]+ it "foo heredoc single quoted +dash" $+ let file = Text.unlines [ "RUN <<-\'FOO\'", "foo", "FOO" ]+ flags = def { security = Nothing }+ in assertAst file [ Run $ RunArgs (ArgumentsText "foo") flags ]+ it "multiline foo heredoc" $+ let file = Text.unlines [ "RUN <<EOF", "foo", "bar", "", "dodo", "EOF" ]+ flags = def { security = Nothing }+ in assertAst file [ Run $ RunArgs (ArgumentsText "foo\nbar\n\ndodo") flags ]+ it "heredoc with shebang/commend" $+ let file = Text.unlines [ "RUN <<EOF", "#/usr/bin/env python", "", "#print foo", "print(\"foo\")", "EOF" ]+ flags = def { security = Nothing }+ in assertAst file [ Run $ RunArgs (ArgumentsText "#/usr/bin/env python\n\n#print foo\nprint(\"foo\")") flags ]+ it "empty heredoc" $+ let file = Text.unlines [ "RUN <<EOF", "EOF" ]+ flags = def { security = Nothing }+ in assertAst file [ Run $ RunArgs (ArgumentsText "") flags ]+ it "empty heredoc not followed by newline" $+ let file = "RUN <<EOF\nEOF"+ flags = def { security = Nothing }+ in assertAst file [ Run $ RunArgs (ArgumentsText "") flags ]+ it "evil heredoc" $+ let file = Text.unlines [ "RUN <<EOF foo", "bar EOF", "EOF"]+ flags = def { security = Nothing }+ in assertAst file [ Run $ RunArgs (ArgumentsText "foo\nbar EOF") flags ]+ it "heredoc with redirection to file" $+ let file = Text.unlines [ "RUN <<EOF > /file", "foo", "EOF" ]+ flags = def { security = Nothing }+ in assertAst file [ Run $ RunArgs (ArgumentsText "foo") flags ]+ it "heredoc to program stdin" $+ let file = Text.unlines [ "RUN python <<EOF", "print(\"foo\")", "EOF" ]+ flags = def { security = Nothing }+ in assertAst file [ Run $ RunArgs (ArgumentsText "python") flags ]+ it "heredoc to program stdin with redirect to file with '>'" $+ let file = Text.unlines [ "RUN python <<EOF > /file", "print(\"foo\")", "EOF" ]+ flags = def { security = Nothing }+ in assertAst file [ Run $ RunArgs (ArgumentsText "python") flags ]+ it "heredoc to program stdin with redirect to file with '>>'" $+ let file = Text.unlines [ "RUN python <<EOF >> /file", "print(\"foo\")", "EOF" ]+ flags = def { security = Nothing }+ in assertAst file [ Run $ RunArgs (ArgumentsText "python") flags ]+ it "heredoc pipe to program" $+ let file = Text.unlines [ "RUN cat <<EOF | sh", "echo foo", "EOF" ]+ flags = def { security = Nothing }+ in assertAst file [ Run $ RunArgs ( ArgumentsText "cat" ) flags ]+ it "heredoc with line continuation in the heredoc" $+ let file = Text.unlines [ "RUN <<EOF", "apt-get update", "apt-get install foo bar \\", " buzz bar", "EOF" ]+ flags = def {security = Nothing }+ in assertAst+ file+ [ Run $ RunArgs+ ( ArgumentsText+ "apt-get update\napt-get install foo bar \\\n buzz bar"+ )+ flags+ ]+ it "heredoc correct termination" $+ let file = Text.unlines [ "RUN <<EOF", "echo $EOF", "EOF" ]+ flags = def {security = Nothing }+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo $EOF") flags ]+ it "heredoc correct termination without newline" $+ let file = "RUN <<EOF\necho $EOF\nEOF"+ flags = def {security = Nothing }+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo $EOF") flags ]++ -- This one is neccessary to make sure that we can both parse+ -- RUN something <<EOF+ -- bla+ -- EOF+ --+ -- and not overshoot while parsing that `something` and accidentally parse+ -- some docker instructions as well.+ it "heredoc no overzealous parsing" $+ let file =+ Text.unlines+ [ "RUN foo bar",+ "FROM something",+ "COPY <<EOF /foobar.sh",+ "#!/bin/bash",+ "echo foobar",+ "EOF"+ ]+ flags = def+ in assertAst+ file+ [ Run $ RunArgs (ArgumentsText "foo bar") flags,+ From+ ( BaseImage+ { image =+ Image+ { registryName = Nothing,+ imageName = "something"+ },+ tag = Nothing,+ digest = Nothing,+ alias = Nothing,+ platform = Nothing+ }+ ),+ Copy+ ( CopyArgs+ [ SourcePath "EOF" ]+ (TargetPath "/foobar.sh")+ )+ ( def :: CopyFlags )+ ]++ -- See https://github.com/hadolint/hadolint/issues/923 for the following+ -- tests+ it "heredoc in command chain with escaped newlines after redirect" $+ let file =+ Text.unlines+ [ "RUN ls && cat <<EOF >> go.mod && \\",+ " tac go.mod",+ "replace (",+ " github.com/user/repo => ../dir",+ ")",+ "EOF"+ ]+ flags = def++ in assertAst+ file+ [ Run $ RunArgs ( ArgumentsText "ls && cat" ) flags+ ]++ it "heredoc in command chain with escaped newlines before heredoc" $+ let file =+ Text.unlines+ [ "RUN ls && \\",+ " cat <<EOF >> go.mod && tac go.mod",+ "replace (",+ " github.com/user/repo => ../dir",+ ")",+ "EOF"+ ]+ flags = def++ in assertAst+ file+ [ Run $ RunArgs ( ArgumentsText "ls && cat" ) flags+ ]++ it "heredoc in command chain with escaped newlines before and after heredoc" $+ let file =+ Text.unlines+ [ "RUN ls && \\",+ " cat <<EOF >> go.mod && \\",+ " tac go.mod",+ "replace (",+ " github.com/user/repo => ../dir",+ ")",+ "EOF"+ ]+ flags = def++ in assertAst+ file+ [ Run $ RunArgs ( ArgumentsText "ls && cat" ) flags+ ]++ describe "Parse RUN instructions - invalid cases" $ do+ it "fail because of multiple types in --mount" $+ let line = "RUN --mount=type=cache,type=tmpfs,target=/foo foo bar"+ in expectFail line++ describe "Parse RUN instructions - SELinux relabeling" $ do+ let flagsRelabelShared =+ def { mount = Set.singleton $+ BindMount+ ( def+ { bTarget = "/bar",+ bRelabel = Just RelabelShared+ }+ )+ }+ flagsRelabelPrivate =+ def { mount = Set.singleton $+ BindMount+ ( def+ { bTarget = "/bar",+ bRelabel = Just RelabelPrivate+ }+ )+ }+ it "RUN with --relabel=shared" $+ let file = Text.unlines+ ["RUN --mount=type=bind,target=/bar,relabel=shared echo foo"]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRelabelShared ]+ it "RUN with --relabel=private" $+ let file = Text.unlines+ ["RUN --mount=type=bind,target=/bar,relabel=private echo foo"]+ in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flagsRelabelPrivate ]
test/Language/Docker/ParserSpec.hs view
@@ -1,372 +1,247 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE OverloadedLists #-} module Language.Docker.ParserSpec where -import Language.Docker.Normalize-import Language.Docker.Parser+import Data.Default.Class (def)+import qualified Data.Text as Text import Language.Docker.Syntax---import Test.HUnit hiding (Label)+import TestHelper import Test.Hspec-import Text.Megaparsec hiding (Label)-import qualified Data.Text as Text + spec :: Spec spec = do- describe "parse ARG" $ do- it "no default" $- assertAst "ARG FOO" [Arg "FOO" Nothing]- it "with default" $- assertAst "ARG FOO=bar" [Arg "FOO" (Just "bar")]-- describe "parse FROM" $ do- it "parse untagged image" $- assertAst "FROM busybox" [From (UntaggedImage "busybox" Nothing)]- it "parse tagged image" $- assertAst- "FROM busybox:5.12-dev"- [From (TaggedImage "busybox" "5.12-dev" Nothing)]- it "parse digested image" $- assertAst- "FROM ubuntu@sha256:0ef2e08ed3fab"- [From (DigestedImage "ubuntu" "sha256:0ef2e08ed3fab" Nothing)]-- describe "parse aliased FROM" $ do- it "parse untagged image" $- assertAst "FROM busybox as foo" [From (UntaggedImage "busybox" (Just $ ImageAlias "foo"))]- it "parse tagged image" $- assertAst "FROM busybox:5.12-dev AS foo-bar" [From (TaggedImage "busybox" "5.12-dev" (Just $ ImageAlias "foo-bar"))]- it "parse diggested image" $- assertAst "FROM ubuntu@sha256:0ef2e08ed3fab AS foo" [From (DigestedImage "ubuntu" "sha256:0ef2e08ed3fab" (Just $ ImageAlias "foo"))]-- describe "parse FROM with registry" $ do- it "registry without port" $- assertAst "FROM foo.com/node" [From (UntaggedImage (Image (Just "foo.com") "node") Nothing)]- it "parse with port and tag" $- assertAst- "FROM myregistry.com:5000/imagename:5.12-dev"- [From (TaggedImage (Image (Just "myregistry.com:5000") "imagename") "5.12-dev" Nothing)]- it "Not a registry if no TLD" $- assertAst- "FROM myfolder/imagename:5.12-dev"- [From (TaggedImage (Image Nothing "myfolder/imagename") "5.12-dev" Nothing)]-- describe "parse LABEL" $ do- it "parse label" $ assertAst "LABEL foo=bar" [Label[("foo", "bar")]]- it "parse space separated label" $ assertAst "LABEL foo bar baz" [Label[("foo", "bar baz")]]- it "parse quoted labels" $ assertAst "LABEL \"foo bar\"=baz" [Label[("foo bar", "baz")]]- it "parses multiline labels" $- let dockerfile = Text.unlines [ "LABEL foo=bar \\", "hobo=mobo"]- ast = [ Label [("foo", "bar"), ("hobo", "mobo")] ]- in assertAst dockerfile ast-- describe "parse ENV" $ do- it "parses unquoted pair" $ assertAst "ENV foo=bar" [Env [("foo", "bar")]]- it "parse with space between key and value" $- assertAst "ENV foo bar" [Env [("foo", "bar")]]- it "parse with more then one (white)space between key and value" $- let dockerfile = "ENV NODE_VERSION \t v5.7.1"- in assertAst dockerfile [Env[("NODE_VERSION", "v5.7.1")]]- it "parse quoted value pair" $ assertAst "ENV foo=\"bar\"" [Env [("foo", "bar")]]- it "parse multiple unquoted pairs" $- assertAst "ENV foo=bar baz=foo" [Env [("foo", "bar"), ("baz", "foo")]]- it "parse multiple quoted pairs" $- assertAst "ENV foo=\"bar\" baz=\"foo\"" [Env [("foo", "bar"), ("baz", "foo")]]- it "env works before cmd" $- let dockerfile = "ENV PATH=\"/root\"\nCMD [\"hadolint\",\"-i\"]"- ast = [Env [("PATH", "/root")], Cmd ["hadolint", "-i"]]- in assertAst dockerfile ast- it "parse with two spaces between" $- let dockerfile = "ENV NODE_VERSION=v5.7.1 DEBIAN_FRONTEND=noninteractive"- in assertAst dockerfile [Env[("NODE_VERSION", "v5.7.1"), ("DEBIAN_FRONTEND", "noninteractive")]]- it "have envs on multiple lines" $- let dockerfile = Text.unlines [ "FROM busybox"- , "ENV NODE_VERSION=v5.7.1 \\"- , "DEBIAN_FRONTEND=noninteractive"- ]- ast = [ From (UntaggedImage "busybox" Nothing)- , Env[("NODE_VERSION", "v5.7.1"), ("DEBIAN_FRONTEND", "noninteractive")]- ]- in assertAst dockerfile ast- it "parses long env over multiple lines" $- let dockerfile = Text.unlines [ "ENV LD_LIBRARY_PATH=\"/usr/lib/\" \\"- , "APACHE_RUN_USER=\"www-data\" APACHE_RUN_GROUP=\"www-data\""]- ast = [Env [("LD_LIBRARY_PATH", "/usr/lib/")- ,("APACHE_RUN_USER", "www-data")- ,("APACHE_RUN_GROUP", "www-data")- ]- ]- in assertAst dockerfile ast- it "parse single var list" $- assertAst "ENV foo val1 val2 val3 val4" [Env [("foo", "val1 val2 val3 val4")]]- it "parses many env lines with an equal sign in the value" $- let dockerfile = Text.unlines [ "ENV TOMCAT_VERSION 9.0.2"- , "ENV TOMCAT_URL foo.com?q=1"- ]- ast = [ Env [("TOMCAT_VERSION", "9.0.2")]- , Env [("TOMCAT_URL", "foo.com?q=1")]- ]- in assertAst dockerfile ast- it "parses many env lines in mixed style" $- let dockerfile = Text.unlines [ "ENV myName=\"John Doe\" myDog=Rex\\ The\\ Dog \\"- , " myCat=fluffy"- ]- ast = [ Env [("myName", "John Doe")- ,("myDog", "Rex The Dog")- ,("myCat", "fluffy")- ]- ]- in assertAst dockerfile ast- it "parses many env with backslashes" $- let dockerfile = Text.unlines [ "ENV JAVA_HOME=C:\\\\jdk1.8.0_112"- ]- ast = [ Env [("JAVA_HOME", "C:\\\\jdk1.8.0_112")]- ]- in assertAst dockerfile ast-- describe "parse RUN" $ do- it "escaped with space before" $- let dockerfile = Text.unlines ["RUN yum install -y \\", "imagemagick \\", "mysql"]- in assertAst dockerfile [Run "yum install -y imagemagick mysql"]-- it "does not choke on unmatched brackets" $- let dockerfile = Text.unlines ["RUN [foo"]- in assertAst dockerfile [Run "[foo"]-- it "Distinguishes between text and a list" $- let dockerfile = Text.unlines [ "RUN echo foo"- , "RUN [\"echo\", \"foo\"]"- ]- in assertAst dockerfile [Run $ ArgumentsText "echo foo", Run $ ArgumentsList "echo foo"]-- it "Accepts spaces inside the brackets" $- let dockerfile = Text.unlines [ "RUN [ \"echo\", \"foo\" ]"- ]- in assertAst dockerfile [Run $ ArgumentsList "echo foo"]-- describe "parse CMD" $ do- it "one line cmd" $ assertAst "CMD true" [Cmd "true"]- it "cmd over several lines" $- assertAst "CMD true \\\n && true" [Cmd "true && true"]- it "quoted command params" $ assertAst "CMD [\"echo\", \"1\"]" [Cmd ["echo", "1"]]-- describe "parse SHELL" $ do- it "quoted shell params" $- assertAst "SHELL [\"/bin/bash\", \"-c\"]" [Shell ["/bin/bash", "-c"]]-- describe "parse HEALTHCHECK" $ do- it "parse healthcheck with interval" $- assertAst- "HEALTHCHECK --interval=5m \\\nCMD curl -f http://localhost/"- [Healthcheck $- Check $- CheckArgs "curl -f http://localhost/" (Just 300) Nothing Nothing Nothing- ]-- it "parse healthcheck with retries" $- assertAst- "HEALTHCHECK --retries=10 CMD curl -f http://localhost/"- [Healthcheck $- Check $- CheckArgs "curl -f http://localhost/" Nothing Nothing Nothing (Just $ Retries 10)- ]-- it "parse healthcheck with timeout" $- assertAst- "HEALTHCHECK --timeout=10s CMD curl -f http://localhost/"- [Healthcheck $- Check $- CheckArgs "curl -f http://localhost/" Nothing (Just 10) Nothing Nothing- ]-- it "parse healthcheck with start-period" $- assertAst- "HEALTHCHECK --start-period=2m CMD curl -f http://localhost/"- [Healthcheck $- Check $- CheckArgs "curl -f http://localhost/" Nothing Nothing (Just 120) Nothing- ]-- it "parse healthcheck with all flags" $- assertAst- "HEALTHCHECK --start-period=2s --timeout=1m --retries=3 --interval=5s CMD curl -f http://localhost/"- [Healthcheck $- Check $- CheckArgs- "curl -f http://localhost/"- (Just 5)- (Just 60)- (Just 2)- (Just $ Retries 3)+ describe "parse ARG" $ do+ it "no default" $+ assertAst "ARG FOO" [Arg "FOO" Nothing]+ it "no default with =" $+ assertAst "ARG FOO=" [Arg "FOO" Nothing]+ it "with default" $+ assertAst "ARG FOO=bar" [Arg "FOO" (Just "bar")]+ describe "parse FROM" $ do+ it "parse untagged image" $+ assertAst "FROM busybox" [From (untaggedImage "busybox")]+ it "parse tagged image" $+ assertAst+ "FROM busybox:5.12-dev"+ [From (taggedImage "busybox" "5.12-dev")]+ it "parse digested image" $+ assertAst+ "FROM ubuntu@sha256:0ef2e08ed3fab"+ [From (untaggedImage "ubuntu" `withDigest` "sha256:0ef2e08ed3fab")]+ it "parse digested image with tag" $+ assertAst+ "FROM ubuntu:14.04@sha256:0ef2e08ed3fab"+ [From (taggedImage "ubuntu" "14.04" `withDigest` "sha256:0ef2e08ed3fab")]+ it "parse image with spaces at the end" $+ assertAst+ "FROM dockerfile/mariadb "+ [From (untaggedImage "dockerfile/mariadb")]+ describe "parse aliased FROM" $ do+ it "parse untagged image" $+ assertAst "FROM busybox as foo" [From (untaggedImage "busybox" `withAlias` "foo")]+ it "parse tagged image" $+ assertAst+ "FROM busybox:5.12-dev AS foo-bar"+ [ From (taggedImage "busybox" "5.12-dev" `withAlias` "foo-bar")+ ]+ it "parse diggested image" $+ assertAst+ "FROM ubuntu@sha256:0ef2e08ed3fab AS foo"+ [ From (untaggedImage "ubuntu" `withDigest` "sha256:0ef2e08ed3fab" `withAlias` "foo")+ ]+ describe "parse FROM with platform" $ do+ it "parse untagged image with platform" $+ assertAst "FROM --platform=linux busybox" [From (untaggedImage "busybox" `withPlatform` "linux")]+ it "parse tagged image with platform" $+ assertAst "FROM --platform=linux busybox:foo" [From (taggedImage "busybox" "foo" `withPlatform` "linux")]+ describe "parse FROM with registry" $ do+ it "registry without port" $+ assertAst "FROM foo.com/node" [From (untaggedImage (Image (Just "foo.com") "node"))]+ it "parse with port and tag" $+ assertAst+ "FROM myregistry.com:5000/imagename:5.12-dev"+ [From (taggedImage (Image (Just "myregistry.com:5000") "imagename") "5.12-dev")]+ it "Not a registry if no TLD" $+ assertAst+ "FROM myfolder/imagename:5.12-dev"+ [From (taggedImage (Image Nothing "myfolder/imagename") "5.12-dev")]+ describe "parse LABEL" $ do+ it "parse label" $ assertAst "LABEL foo=bar" [Label [("foo", "bar")]]+ it "parse space separated label" $ assertAst "LABEL foo bar baz" [Label [("foo", "bar baz")]]+ it "parse quoted labels" $ assertAst "LABEL \"foo bar\"=baz" [Label [("foo bar", "baz")]]+ it "parses multiline labels" $+ let dockerfile = Text.unlines ["LABEL foo=bar \\", "hobo=mobo"]+ ast = [Label [("foo", "bar"), ("hobo", "mobo")]]+ in assertAst dockerfile ast+ describe "parse ENV" $ do+ it "parses unquoted pair" $ assertAst "ENV foo=bar" [Env [("foo", "bar")]]+ it "parse with space between key and value" $+ assertAst "ENV foo bar" [Env [("foo", "bar")]]+ it "parse with more then one (white)space between key and value" $+ let dockerfile = "ENV NODE_VERSION \t v5.7.1"+ in assertAst dockerfile [Env [("NODE_VERSION", "v5.7.1")]]+ it "parse quoted value pair" $ assertAst "ENV foo=\"bar\"" [Env [("foo", "bar")]]+ it "parse multiple unquoted pairs" $+ assertAst "ENV foo=bar baz=foo" [Env [("foo", "bar"), ("baz", "foo")]]+ it "parse multiple quoted pairs" $+ assertAst "ENV foo=\"bar\" baz=\"foo\"" [Env [("foo", "bar"), ("baz", "foo")]]+ it "env works before cmd" $+ let dockerfile = "ENV PATH=\"/root\"\nCMD [\"hadolint\",\"-i\"]"+ ast = [Env [("PATH", "/root")], Cmd ["hadolint", "-i"]]+ in assertAst dockerfile ast+ it "parse with two spaces between" $+ let dockerfile = "ENV NODE_VERSION=v5.7.1 DEBIAN_FRONTEND=noninteractive"+ in assertAst dockerfile [Env [("NODE_VERSION", "v5.7.1"), ("DEBIAN_FRONTEND", "noninteractive")]]+ it "have envs on multiple lines" $+ let dockerfile =+ Text.unlines+ [ "FROM busybox",+ "ENV NODE_VERSION=v5.7.1 \\",+ "DEBIAN_FRONTEND=noninteractive"+ ]+ ast =+ [ From (untaggedImage "busybox"),+ Env [("NODE_VERSION", "v5.7.1"), ("DEBIAN_FRONTEND", "noninteractive")]+ ]+ in assertAst dockerfile ast+ it "parses long env over multiple lines" $+ let dockerfile =+ Text.unlines+ [ "ENV LD_LIBRARY_PATH=\"/usr/lib/\" \\",+ "APACHE_RUN_USER=\"www-data\" APACHE_RUN_GROUP=\"www-data\""+ ]+ ast =+ [ Env+ [ ("LD_LIBRARY_PATH", "/usr/lib/"),+ ("APACHE_RUN_USER", "www-data"),+ ("APACHE_RUN_GROUP", "www-data") ]-- it "parse healthcheck with no flags" $- assertAst- "HEALTHCHECK CMD curl -f http://localhost/"- [Healthcheck $- Check $- CheckArgs "curl -f http://localhost/" Nothing Nothing Nothing Nothing+ ]+ in assertAst dockerfile ast+ it "parse single var list" $+ assertAst "ENV foo val1 val2 val3 val4" [Env [("foo", "val1 val2 val3 val4")]]+ it "parses many env lines with an equal sign in the value" $+ let dockerfile =+ Text.unlines+ [ "ENV TOMCAT_VERSION 9.0.2",+ "ENV TOMCAT_URL foo.com?q=1"+ ]+ ast =+ [ Env [("TOMCAT_VERSION", "9.0.2")],+ Env [("TOMCAT_URL", "foo.com?q=1")]+ ]+ in assertAst dockerfile ast+ it "parses many env lines in mixed style" $+ let dockerfile =+ Text.unlines+ [ "ENV myName=\"John Doe\" myDog=Rex\\ The\\ Dog \\",+ " myCat=fluffy"+ ]+ ast =+ [ Env+ [ ("myName", "John Doe"),+ ("myDog", "Rex The Dog"),+ ("myCat", "fluffy") ]-- describe "parse MAINTAINER" $ do- it "maintainer of untagged scratch image" $- assertAst- "FROM scratch\nMAINTAINER hudu@mail.com"- [From (UntaggedImage "scratch" Nothing), Maintainer "hudu@mail.com"]- it "maintainer with mail" $- assertAst "MAINTAINER hudu@mail.com" [Maintainer "hudu@mail.com"]- it "maintainer only mail after from" $- let maintainerFromProg = "FROM busybox\nMAINTAINER hudu@mail.com"- maintainerFromAst = [From (UntaggedImage "busybox" Nothing), Maintainer "hudu@mail.com"]- in assertAst maintainerFromProg maintainerFromAst- describe "parse # comment " $ do- it "multiple comments before run" $- let dockerfile = Text.unlines ["# line 1", "# line 2", "RUN apt-get update"]- in assertAst dockerfile [Comment " line 1", Comment " line 2", Run "apt-get update"]- it "multiple comments after run" $- let dockerfile = Text.unlines ["RUN apt-get update", "# line 1", "# line 2"]- in assertAst- dockerfile- [Run "apt-get update", Comment " line 1", Comment " line 2"]-- it "empty comment" $- let dockerfile = Text.unlines ["#", "# Hello"]- in assertAst dockerfile [Comment "", Comment " Hello"]- describe "normalize lines" $ do- it "join multiple ENV" $- let dockerfile = Text.unlines [ "FROM busybox"- , "ENV NODE_VERSION=v5.7.1 \\"- , "DEBIAN_FRONTEND=noninteractive"- ]- normalizedDockerfile = Text.unlines [ "FROM busybox"- , "ENV NODE_VERSION=v5.7.1 DEBIAN_FRONTEND=noninteractive\n"- ]- in normalizeEscapedLines dockerfile `shouldBe` normalizedDockerfile-- it "join escaped lines" $- let dockerfile = Text.unlines ["ENV foo=bar \\", "baz=foz"]- normalizedDockerfile = Text.unlines ["ENV foo=bar baz=foz", ""]- in normalizeEscapedLines dockerfile `shouldBe` normalizedDockerfile-- it "join long CMD" $- let longEscapedCmd =- Text.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 =- Text.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"- ]- in normalizeEscapedLines longEscapedCmd `shouldBe` longEscapedCmdExpected-- it "tolerates spaces after a newline escape" $- let dockerfile = Text.unlines [ "FROM busy\\ "- , "box"- , "RUN echo\\ "- , " hello"- ]- in assertAst dockerfile [ From (UntaggedImage "busybox" Nothing)- , Run "echo hello"- ]- describe "expose" $ do- it "should handle number ports" $- let content = "EXPOSE 8080"- in assertAst content [Expose (Ports [Port 8080 TCP])]- it "should handle many number ports" $- let content = "EXPOSE 8080 8081"- in assertAst content [Expose (Ports [Port 8080 TCP, Port 8081 TCP])]- it "should handle ports with protocol" $- let content = "EXPOSE 8080/TCP 8081/UDP"- in assertAst content [Expose (Ports [Port 8080 TCP, Port 8081 UDP])]- it "should handle ports with protocol and variables" $- let content = "EXPOSE $PORT 8080 8081/UDP"- in assertAst content [Expose (Ports [PortStr "$PORT", Port 8080 TCP, Port 8081 UDP])]- it "should handle port ranges" $- let content = "EXPOSE 80 81 8080-8085"- in assertAst content [Expose (Ports [Port 80 TCP, Port 81 TCP, PortRange 8080 8085 TCP])]- it "should handle udp port ranges" $- let content = "EXPOSE 80 81 8080-8085/udp"- in assertAst content [Expose (Ports [Port 80 TCP, Port 81 TCP, PortRange 8080 8085 UDP])]-- describe "syntax" $ do- it "should handle lowercase instructions (#7 - https://github.com/beijaflor-io/haskell-language-dockerfile/issues/7)" $- let content = "from ubuntu"- in assertAst content [From (UntaggedImage "ubuntu" Nothing)]-- describe "ADD" $ do- it "simple ADD" $- let file = Text.unlines ["ADD . /app", "ADD http://foo.bar/baz ."]- in assertAst file [ Add $ AddArgs [SourcePath "."] (TargetPath "/app") NoChown- , Add $ AddArgs [SourcePath "http://foo.bar/baz"] (TargetPath ".") NoChown- ]- it "multifiles ADD" $- let file = Text.unlines ["ADD foo bar baz /app"]- in assertAst file [ Add $ AddArgs (fmap SourcePath ["foo", "bar", "baz"]) (TargetPath "/app") NoChown- ]-- it "list of quoted files" $- let file = Text.unlines ["ADD [\"foo\", \"bar\", \"baz\", \"/app\"]"]- in assertAst file [ Add $ AddArgs (fmap SourcePath ["foo", "bar", "baz"]) (TargetPath "/app") NoChown- ]-- it "with chown flag" $- let file = Text.unlines ["ADD --chown=root:root foo bar"]- in assertAst file [ Add $ AddArgs (fmap SourcePath ["foo"]) (TargetPath "bar") (Chown "root:root")- ]-- it "list of quoted files and chown" $- let file = Text.unlines ["ADD --chown=user:group [\"foo\", \"bar\", \"baz\", \"/app\"]"]- in assertAst file [ Add $ AddArgs (fmap SourcePath ["foo", "bar", "baz"]) (TargetPath "/app") (Chown "user:group")- ]- describe "COPY" $ do- it "simple COPY" $- let file = Text.unlines ["COPY . /app", "COPY baz /some/long/path"]- in assertAst file [ Copy $ CopyArgs [SourcePath "."] (TargetPath "/app") NoChown NoSource- , Copy $ CopyArgs [SourcePath "baz"] (TargetPath "/some/long/path") NoChown NoSource- ]- it "multifiles COPY" $- let file = Text.unlines ["COPY foo bar baz /app"]- in assertAst file [ Copy $ CopyArgs (fmap SourcePath ["foo", "bar", "baz"]) (TargetPath "/app") NoChown NoSource- ]-- it "list of quoted files" $- let file = Text.unlines ["COPY [\"foo\", \"bar\", \"baz\", \"/app\"]"]- in assertAst file [ Copy $ CopyArgs (fmap SourcePath ["foo", "bar", "baz"]) (TargetPath "/app") NoChown NoSource- ]-- it "with chown flag" $- let file = Text.unlines ["COPY --chown=user:group foo bar"]- in assertAst file [ Copy $ CopyArgs (fmap SourcePath ["foo"]) (TargetPath "bar") (Chown "user:group") NoSource- ]-- it "with from flag" $- let file = Text.unlines ["COPY --from=node foo bar"]- in assertAst file [ Copy $ CopyArgs (fmap SourcePath ["foo"]) (TargetPath "bar") NoChown (CopySource "node")- ]- it "with both flags" $- let file = Text.unlines ["COPY --from=node --chown=user:group foo bar"]- in assertAst file [ Copy $ CopyArgs (fmap SourcePath ["foo"]) (TargetPath "bar") (Chown "user:group") (CopySource "node")- ]- it "with both flags in different order" $- let file = Text.unlines ["COPY --chown=user:group --from=node foo bar"]- in assertAst file [ Copy $ CopyArgs (fmap SourcePath ["foo"]) (TargetPath "bar") (Chown "user:group") (CopySource "node")- ]--assertAst :: HasCallStack => Text.Text -> [Instruction Text.Text] -> Assertion-assertAst s ast =- case parseText s of- Left err -> assertFailure $ parseErrorPretty err- Right dockerfile -> assertEqual "ASTs are not equal" ast $ map instruction dockerfile+ ]+ in assertAst dockerfile ast+ it "parses many env with backslashes" $+ let dockerfile =+ Text.unlines+ [ "ENV JAVA_HOME=C:\\\\jdk1.8.0_112"+ ]+ ast =+ [ Env [("JAVA_HOME", "C:\\\\jdk1.8.0_112")]+ ]+ in assertAst dockerfile ast+ it "parses env with % in them" $+ let dockerfile =+ Text.unlines+ [ "ENV PHP_FPM_ACCESS_FORMAT=\"prefix \\\"quoted\\\" suffix\""+ ]+ ast =+ [ Env [("PHP_FPM_ACCESS_FORMAT", "prefix \"quoted\" suffix")]+ ]+ in assertAst dockerfile ast+ it "parses env with % in them" $+ let dockerfile =+ Text.unlines+ [ "ENV PHP_FPM_ACCESS_FORMAT=\"%R - %u %t \\\"%m %r\\\" %s\""+ ]+ ast =+ [ Env [("PHP_FPM_ACCESS_FORMAT", "%R - %u %t \"%m %r\" %s")]+ ]+ in assertAst dockerfile ast+ describe "parse SHELL" $+ it "quoted shell params" $+ assertAst "SHELL [\"/bin/bash\", \"-c\"]" [Shell ["/bin/bash", "-c"]]+ describe "parse MAINTAINER" $ do+ it "maintainer of untagged scratch image" $+ assertAst+ "FROM scratch\nMAINTAINER hudu@mail.com"+ [From (untaggedImage "scratch"), Maintainer "hudu@mail.com"]+ it "maintainer with mail" $+ assertAst "MAINTAINER hudu@mail.com" [Maintainer "hudu@mail.com"]+ it "maintainer only mail after from" $+ let maintainerFromProg = "FROM busybox\nMAINTAINER hudu@mail.com"+ maintainerFromAst = [From (untaggedImage "busybox"), Maintainer "hudu@mail.com"]+ in assertAst maintainerFromProg maintainerFromAst+ describe "parse # comment " $ do+ it "multiple comments before run" $+ let dockerfile = Text.unlines ["# line 1", "# line 2", "RUN apt-get update"]+ in assertAst dockerfile [Comment " line 1", Comment " line 2", Run "apt-get update"]+ it "multiple comments after run" $+ let dockerfile = Text.unlines ["RUN apt-get update", "# line 1", "# line 2"]+ in assertAst+ dockerfile+ [Run "apt-get update", Comment " line 1", Comment " line 2"]+ it "empty comment" $+ let dockerfile = Text.unlines ["#", "# Hello"]+ in assertAst dockerfile [Comment "", Comment " Hello"]+ it "many escaped lines" $+ let dockerfile =+ Text.unlines+ [ "ENV A=\"a.sh\" \\",+ " # comment a",+ " B=\"b.sh\" \\",+ " c=\"true\"",+ ""+ ]+ in assertAst+ dockerfile+ [ Env [("A", "a.sh"), ("B", "b.sh"), ("c", "true")]+ ]+ it "accepts backslash inside string" $+ let dockerfile = "RUN grep 'foo \\.'"+ in assertAst dockerfile [Run $ RunArgs (ArgumentsText "grep 'foo \\.'") def]+ it "tolerates spaces after a newline escape" $+ let dockerfile =+ Text.unlines+ [ "FROM busy\\ ",+ "box",+ "RUN echo\\ ",+ " hello"+ ]+ in assertAst+ dockerfile+ [ From (untaggedImage "busybox"),+ Run "echo hello"+ ]+ it "Correctly joins blank lines starting with comments" $+ let dockerfile =+ Text.unlines+ [ "FROM busybox",+ "# I forgot to remove the backslash \\",+ "# This is a comment",+ "RUN echo hello"+ ]+ in assertAst+ dockerfile+ [ From (untaggedImage "busybox"),+ Comment " I forgot to remove the backslash \\",+ Comment " This is a comment",+ Run "echo hello"+ ]+ describe "syntax" $ do+ it "should handle lowercase instructions (#7 - https://github.com/beijaflor-io/haskell-language-dockerfile/issues/7)" $+ let content = "from ubuntu"+ in assertAst content [From (untaggedImage "ubuntu")]
+ test/Language/Docker/PrettyPrintSpec.hs view
@@ -0,0 +1,164 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}++module Language.Docker.PrettyPrintSpec where++import Data.Default+import qualified Data.Text as Text+import Prettyprinter+import Prettyprinter.Render.Text+import Language.Docker.Syntax+import Language.Docker.PrettyPrint+import Test.HUnit hiding (Label)+import Test.Hspec+++spec :: Spec+spec = do+ describe "pretty print ADD" $ do+ it "with just copy" $ do+ let add = Add+ ( AddArgs [SourcePath "foo"] (TargetPath "bar") )+ ( def :: AddFlags )+ in assertPretty "ADD foo bar" add+ it "with just checksum" $ do+ let add = Add+ ( AddArgs [SourcePath "http://www.example.com/foo"] (TargetPath "bar") )+ ( AddFlags ( Checksum "sha256:24454f830cdd" ) NoChown NoChmod NoLink [])+ in assertPretty "ADD --checksum=sha256:24454f830cdd http://www.example.com/foo bar" add+ it "with just chown" $ do+ let add = Add+ ( AddArgs [SourcePath "foo"] (TargetPath "bar") )+ ( AddFlags NoChecksum ( Chown "root:root" ) NoChmod NoLink [] )+ in assertPretty "ADD --chown=root:root foo bar" add+ it "with just chmod" $ do+ let add = Add+ ( AddArgs [SourcePath "foo"] (TargetPath "bar") )+ ( AddFlags NoChecksum NoChown ( Chmod "751" ) NoLink [] )+ in assertPretty "ADD --chmod=751 foo bar" add+ it "with just link" $ do+ let add = Add+ ( AddArgs [SourcePath "foo"] (TargetPath "bar") )+ ( AddFlags NoChecksum NoChown NoChmod Link [] )+ in assertPretty "ADD --link foo bar" add+ it "with chown, chmod and link" $ do+ let add = Add+ ( AddArgs [SourcePath "foo"] (TargetPath "bar") )+ ( AddFlags NoChecksum ( Chown "root:root" ) ( Chmod "751" ) Link [] )+ in assertPretty "ADD --chown=root:root --chmod=751 --link foo bar" add+ it "with just exclude" $ do+ let add = Add+ ( AddArgs [SourcePath "foo"] (TargetPath "bar") )+ ( AddFlags NoChecksum NoChown NoChmod NoLink [Exclude "*.tmp"] )+ in assertPretty "ADD --exclude=*.tmp foo bar" add+ it "with multiple exclude flags" $ do+ let add = Add+ ( AddArgs [SourcePath "foo"] (TargetPath "bar") )+ ( AddFlags NoChecksum NoChown NoChmod NoLink [Exclude "*.tmp", Exclude "*.log"] )+ in assertPretty "ADD --exclude=*.tmp --exclude=*.log foo bar" add+ it "with exclude and other flags" $ do+ let add = Add+ ( AddArgs [SourcePath "foo"] (TargetPath "bar") )+ ( AddFlags NoChecksum (Chown "root:root") NoChmod NoLink [Exclude "*.tmp"] )+ in assertPretty "ADD --chown=root:root --exclude=*.tmp foo bar" add++ describe "pretty print COPY" $ do+ it "with just copy" $ do+ let copy = Copy+ ( CopyArgs [SourcePath "foo"] (TargetPath "bar") )+ ( def :: CopyFlags )+ in assertPretty "COPY foo bar" copy+ it "with just chown" $ do+ let copy = Copy+ ( CopyArgs [SourcePath "foo"] (TargetPath "bar") )+ ( CopyFlags ( Chown "root:root" ) NoChmod NoLink NoSource [] )+ in assertPretty "COPY --chown=root:root foo bar" copy+ it "with just chmod" $ do+ let copy = Copy+ ( CopyArgs [SourcePath "foo"] (TargetPath "bar") )+ ( CopyFlags NoChown ( Chmod "751" ) NoLink NoSource [] )+ in assertPretty "COPY --chmod=751 foo bar" copy+ it "with just link" $ do+ let copy = Copy+ ( CopyArgs [SourcePath "foo"] (TargetPath "bar") )+ ( CopyFlags NoChown NoChmod Link NoSource [] )+ in assertPretty "COPY --link foo bar" copy+ it "with source baseimage" $ do+ let copy =+ Copy+ ( CopyArgs [SourcePath "foo"] (TargetPath "bar") )+ ( CopyFlags NoChown NoChmod NoLink ( CopySource "baseimage" ) [])+ in assertPretty "COPY --from=baseimage foo bar" copy+ it "with both chown and chmod" $ do+ let copy =+ Copy+ ( CopyArgs [SourcePath "foo"] (TargetPath "bar") )+ ( CopyFlags+ ( Chown "root:root" ) ( Chmod "751" ) NoLink NoSource []+ )+ in assertPretty "COPY --chown=root:root --chmod=751 foo bar" copy+ it "with all flags" $ do+ let copy =+ Copy+ ( CopyArgs [SourcePath "foo"] (TargetPath "bar") )+ ( CopyFlags+ ( Chown "root:root")+ ( Chmod "751")+ Link+ ( CopySource "baseimage" )+ []+ )+ in assertPretty+ "COPY --chown=root:root --chmod=751 --link --from=baseimage foo bar"+ copy+ it "with just exclude" $ do+ let copy = Copy+ ( CopyArgs [SourcePath "foo"] (TargetPath "bar") )+ ( CopyFlags NoChown NoChmod NoLink NoSource [Exclude "*.tmp"] )+ in assertPretty "COPY --exclude=*.tmp foo bar" copy+ it "with multiple exclude flags" $ do+ let copy = Copy+ ( CopyArgs [SourcePath "foo"] (TargetPath "bar") )+ ( CopyFlags NoChown NoChmod NoLink NoSource [Exclude "*.tmp", Exclude "*.log"] )+ in assertPretty "COPY --exclude=*.tmp --exclude=*.log foo bar" copy+ it "with exclude and other flags" $ do+ let copy = Copy+ ( CopyArgs [SourcePath "foo"] (TargetPath "bar") )+ ( CopyFlags (Chown "root:root") NoChmod NoLink NoSource [Exclude "*.tmp"] )+ in assertPretty "COPY --chown=root:root --exclude=*.tmp foo bar" copy++ describe "pretty print # escape" $ do+ it "# escape = \\" $ do+ let esc = Pragma (Escape (EscapeChar '\\'))+ in assertPretty "# escape = \\" esc+ it "# escape = `" $ do+ let esc = Pragma (Escape (EscapeChar '`'))+ in assertPretty "# escape = `" esc++ describe "pretty print # syntax" $ do+ it "# syntax = docker/dockerfile:1.0" $ do+ let img = Pragma+ ( Syntax+ ( SyntaxImage+ ( Image+ { registryName = Nothing,+ imageName = "docker/dockerfile:1.0"+ }+ )+ )+ )+ in assertPretty "# syntax = docker/dockerfile:1.0" img+++assertPretty :: Text.Text -> Instruction Text.Text -> Assertion+assertPretty expected inst = assertEqual+ "prettyfied instruction not pretty enough"+ expected+ (prettyPrintStrict inst)++prettyPrintStrict :: Instruction Text.Text -> Text.Text+prettyPrintStrict =+ let ?esc = defaultEsc+ in renderStrict+ . layoutPretty (LayoutOptions Unbounded)+ . prettyPrintInstruction
+ test/TestHelper.hs view
@@ -0,0 +1,44 @@+module TestHelper+ ( assertAst,+ expectFail,+ taggedImage,+ withAlias,+ withDigest,+ withPlatform,+ untaggedImage+ )+ where++import qualified Data.Text as Text+import Language.Docker.Parser+import Language.Docker.Syntax+import Test.HUnit hiding (Label)+import Test.Hspec+import Test.Hspec.Megaparsec+import Text.Megaparsec hiding (Label)+++untaggedImage :: Image -> BaseImage+untaggedImage n = BaseImage n Nothing Nothing Nothing Nothing++taggedImage :: Image -> Tag -> BaseImage+taggedImage n t = BaseImage n (Just t) Nothing Nothing Nothing++withDigest :: BaseImage -> Digest -> BaseImage+withDigest i d = i {digest = Just d}++withAlias :: BaseImage -> ImageAlias -> BaseImage+withAlias i a = i {alias = Just a}++withPlatform :: BaseImage -> Platform -> BaseImage+withPlatform i p = i {platform = Just p}++assertAst :: HasCallStack => Text.Text -> [Instruction Text.Text] -> Assertion+assertAst s ast =+ case parseText s of+ Left errmsg -> assertFailure $ errorBundlePretty errmsg+ Right dockerfile -> assertEqual "ASTs are not equal" ast $ map instruction dockerfile++expectFail :: HasCallStack => Text.Text -> Assertion+expectFail input =+ parseText `shouldFailOn` input
+ test/fixtures/1.Dockerfile view
@@ -0,0 +1,17 @@+FROM foo:7-slim++# An extra space after the env value should be no problem+ENV container=false\+ container2=true ++ENV A="a.sh" D="c"\+ B="installDBBinaries.sh" ++ENV X "Y" Z++ENV DOG=Rex\ The\ Dog\ + CAT=Top\ Cat++ENV DOCKER_TLS_CERTDIR=+ENV foo\ a=afoo' bar 'baz"qu\"z"+ENV BASE_PATH /var/spool/apt-mirror
+ test/fixtures/2.Dockerfile view
@@ -0,0 +1,16 @@+FROM scratch++RUN set -ex; \+ apt-get update; \+ if ! which gpg; then \+ apt-get install -y --no-install-recommends gnupg; \+ fi; \+ if ! gpg --version | grep -q '^gpg (GnuPG) 1\.'; then \+# Ubuntu includes "gnupg" (not "gnupg2", but still 2.x), but not dirmngr, and gnupg 2.x requires dirmngr+# so, if we're not running gnupg 1.x, explicitly install dirmngr too+ apt-get install -y --no-install-recommends dirmngr; \+ fi; \+ rm -rf /var/lib/apt/lists/*++ONBUILD ENV RUNNER_CMD_EXEC=${RUNNER_CMD_EXEC:-"java \$JAVA_OPTS -jar /runtime/server.jar \$JAR_OPTS"}+ENV BUNDLE_WITHOUT=${bundle_without:-'development test'}
+ test/fixtures/3.Dockerfile view
@@ -0,0 +1,6 @@+# escape = `++FROM debian:10++RUN first cmd `+ && second cmd
+ test/fixtures/4.Dockerfile view
@@ -0,0 +1,8 @@+#syntax=docker/dockerfile:1.0+# escape = `++FROM debian:10++RUN first cmd `+ && second cmd `+ && command C:\some\windows\style\path
+ test/fixtures/5.Dockerfile view
@@ -0,0 +1,8 @@+# this will prevent the pragma from taking effect+#syntax=docker/dockerfile:1.0+# escape = `++FROM debian:10++RUN first cmd \+ && second cmd
+ test/fixtures/6.Dockerfile view
@@ -0,0 +1,3 @@+# escape = `+RUN cmd a `+ && cmd b
+ test/fixtures/7.Dockerfile view
@@ -0,0 +1,3 @@+FROM debian:buster+RUN cmd a \+ && cmd b
+ test/fixtures/8.Dockerfile view
@@ -0,0 +1,4 @@+FROM debian:buster+# escape = `+RUN cmd a \+ && cmd b
+ test/fixtures/Dockerfile.bom.utf16be view
binary file changed (absent → 76 bytes)
+ test/fixtures/Dockerfile.bom.utf16le view
binary file changed (absent → 76 bytes)
+ test/fixtures/Dockerfile.bom.utf32be view
binary file changed (absent → 152 bytes)
+ test/fixtures/Dockerfile.bom.utf32le view
binary file changed (absent → 152 bytes)
+ test/fixtures/Dockerfile.bom.utf8 view
@@ -0,0 +1,3 @@+FROM debian:11+USER root+RUN foo bar