{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE LambdaCase #-} -- | A Simple DSL for describing and generating Dockerfiles in Haskell -- -- Compatible w/ Docker v18.03 -- -- @ -- main :: IO () -- main = putStrLn $ -- dockerfile $ do -- from "debian:stable" -- maintainer "creichert " -- run "apt-get -y update" -- run "apt-get -y upgrade" -- cmd [ "echo", "hello world"] -- @ module Data.Docker ( -- * Types Docker , CopyOpt(..) , dockerfile , dockerfileWrite -- * Docker Instructions , from , fromas , run , cmd , label , maintainer , expose , env , add , addchown , copy , copyfrom , copychown , entrypoint , volume , user , workdir , arg , onbuild , stopsignal , healthcheck , shell ) where import Control.Monad.Writer -- | A 'Dockerfile' is represented as a list of instructions. type DockerFile = [Instruction] -- | A 'Docker' writer monad for creating 'Dockerfile's type Docker a = Writer DockerFile a -- | Render a list of 'Docker' instructions to a 'String'. dockerfile :: Docker a -> String dockerfile = unlines . map prettyCmd . execWriter -- | Render a list of 'Docker' instructions to a 'FilePath'. dockerfileWrite :: FilePath -> Docker a -> IO () dockerfileWrite fp docker = do let content = "# this file was generated by the `dockerfile` haskell library" : "" : fmap prettyCmd (execWriter docker) writeFile fp (unlines content) type Script = String type ScriptFile = FilePath type Param = String type ImageName = String type As = String -- | Dockerfile instruction set -- -- This data type is not exposed. All Docker commands/instructions are -- exposed through combinator functions intended to be run from w/in -- `dockerfile` and similar functions. data Instruction = From ImageName (Maybe As) -- ^ The @FROM@ instruction initializes a new build stage and sets -- the Base Image for subsequent instructions. As such, a valid -- Dockerfile must start with a @FROM@ instruction. The image can be -- any valid image – it is especially easy to start by pulling an -- image from the Public Repositories. -- -- @ARG@ is the only instruction that may precede @FROM@ in the -- Dockerfile. See Understand how @ARG@ and @FROM@ interact. -- -- @FROM@ can appear multiple times within a single Dockerfile to -- create multiple images or use one build stage as a dependency for -- another. Simply make a note of the last image ID output by the -- commit before each new FROM instruction. Each @FROM@ instruction -- clears any state created by previous instructions. -- -- Optionally a name can be given to a new build stage by adding @AS -- name@ to the @FROM@ instruction. The name can be used in -- subsequent @FROM@ and @COPY --from=@ instructions to -- refer to the image built in this stage. -- -- The @tag@ or @digest@ values are optional. If you omit either of -- them, the builder assumes a @latest@ tag by default. The builder -- returns an error if it cannot find the tag value. | Run Script -- File [ScriptParam] -- ^ RUN has 2 forms: -- -- - RUN (shell form, the command is run in a shell, which -- by default is /bin/sh -c on Linux or cmd /S /C on Windows) -- -- - RUN ["executable", "param1", "param2"] (exec form) -- -- The RUN instruction will execute any commands in a new layer on -- top of the current image and commit the results. The resulting -- committed image will be used for the next step in the Dockerfile. -- -- Layering RUN instructions and generating commits conforms to the -- core concepts of Docker where commits are cheap and containers can -- be created from any point in an image’s history, much like source -- control. -- -- The exec form makes it possible to avoid shell string munging, and -- to RUN commands using a base image that does not contain the -- specified shell executable. -- -- The default shell for the shell form can be changed using the -- SHELL command. | Cmd [ ScriptFile ] -- ^ The CMD instruction has three forms: -- -- Syntax: -- -- @ -- CMD ["executable","param1","param2"] (exec form, this is the preferred form) -- CMD ["param1","param2"] (as default parameters to ENTRYPOINT) -- CMD command param1 param2 (shell form) -- @ -- -- There can only be one CMD instruction in a Dockerfile. If you list -- more than one CMD then only the last CMD will take effect. -- @ -- -- If the CMD instruction does not specify an executable, an -- ENTRYPOINT instruction must be present. | Label [(String, String)] -- ^ The LABEL instruction adds metadata to an image. A LABEL is a -- key-value pair. To include spaces within a LABEL value, use quotes and -- blackslashes as you would in command-line parsing. -- -- Syntax: -- -- @ -- LABEL com.example.label-without-value -- LABEL com.example.label-with-value="foo" -- LABEL version="1.0" -- LABEL description="This text illustrates \ -- that label-values can span multiple lines." -- @ | Maintainer String -- ^ The MAINTAINER instruction sets the Author field of the -- generated images. The LABEL instruction is a much more flexible -- version of this and you should use it instead, as it enables -- setting any metadata you require, and can be viewed easily, for -- example with docker inspect. To set a label corresponding to the -- MAINTAINER field you could use: -- -- LABEL maintainer="SvenDowideit@home.org.au" -- -- This will then be visible from docker inspect with the other -- labels. | Expose Int -- ^ EXPOSE [...] | Env String String -- ^ -- -- The ENV instruction sets the environment variable to the -- value . This value will be in the environment of all -- "descendent" Dockerfile commands and can be replaced inline in many as -- well. -- -- -- Syntax: -- -- @ -- ENV -- ENV = ... -- @ -- -- The second form allows multiple key value pairs to be specified -- -- @ -- ENV myName="John Doe" myDog=Rex\ The\ Dog \ -- myCat=fluffy -- and -- -- ENV myName John Doe -- ENV myDog Rex The Dog -- ENV myCat fluffy -- @ | Add [FilePath] FilePath [AddOpt] -- ^ ADD has two forms: -- -- - @ADD [--chown=:] ... @ -- -- - @ADD [--chown=:] ["",... ""]@ (this form -- is required for paths containing whitespace) -- -- > Note: The @--chown@ feature is only supported on Dockerfiles -- > used to build Linux containers, and will not work on Windows -- > containers. Since user and group ownership concepts do not -- > translate between Linux and Windows, the use of @/etc/passwd@ -- > and @/etc/group@ for translating user and group names to IDs -- > restricts this feature to only be viable for Linux OS-based -- > containers. -- -- The ADD instruction copies new files, directories or remote file -- URLs from and adds them to the filesystem of the image at -- the path . -- -- Multiple resources may be specified but if they are files or -- directories, their paths are interpreted as relative to the source -- of the context of the build. -- -- ADD obeys the following rules: -- -- - The path must be inside the context of the build; you -- cannot ADD ../something /something, because the first step of a -- docker build is to send the context directory (and -- subdirectories) to the docker daemon. -- -- - If is a URL and does not end with a trailing slash, -- then a file is downloaded from the URL and copied to . -- -- - If is a URL and does end with a trailing slash, -- then the filename is inferred from the URL and the file is -- downloaded to /. For instance, ADD -- http://example.com/foobar / would create the file /foobar. The -- URL must have a nontrivial path so that an appropriate filename -- can be discovered in this case (http://example.com will not -- work). -- -- - If is a directory, the entire contents of the directory -- are copied, including filesystem metadata. -- -- - Note: The directory itself is not copied, just its contents. -- -- - If is a local tar archive in a recognized compression -- format (identity, gzip, bzip2 or xz) then it is unpacked as a -- directory. Resources from remote URLs are not decompressed. When -- a directory is copied or unpacked, it has the same behavior as -- tar -x, the result is the union of: -- -- - Whatever existed at the destination path and The contents of the -- source tree, with conflicts resolved in favor of “2.” on a -- file-by-file basis. Note: Whether a file is identified as a -- recognized compression format or not is done solely based on the -- contents of the file, not the name of the file. For example, if -- an empty file happens to end with .tar.gz this will not be -- recognized as a compressed file and will not generate any kind -- of decompression error message, rather the file will simply be -- copied to the destination. -- -- - If is any other kind of file, it is copied individually -- along with its metadata. In this case, if ends with a -- trailing slash /, it will be considered a directory and the -- contents of will be written at /base(). -- -- - If multiple resources are specified, either directly or -- due to the use of a wildcard, then must be a directory, -- and it must end with a slash /. -- -- - If does not end with a trailing slash, it will be -- considered a regular file and the contents of will be -- written at . -- -- - If doesn’t exist, it is created along with all missing -- directories in its path. | Copy [FilePath] FilePath [CopyOpt] -- ^ -- COPY has two forms: -- -- COPY ... -- COPY [""... ""] (this form is required for paths containing whitespace) -- -- The COPY instruction copies new files or directories from -- and adds them to the filesystem of the container at the path . | Entrypoint String [Param] -- ^ An ENTRYPOINT allows you to configure a container that will run as -- an executable. -- -- @ -- ENTRYPOINT ["executable", "param1", "param2"] (the preferred exec form) -- ENTRYPOINT command param1 param2 (shell form) -- @ | Volume [FilePath] -- ^ @ VOLUME ["/data"] @ -- -- The VOLUME instruction creates a mount point with the specified -- name and marks it as holding externally mounted volumes from native -- host or other containers. | User String -- ^ USER daemon -- -- The USER instruction sets the user name or UID to use when running the -- image and for any RUN, CMD and ENTRYPOINT instructions that follow it -- in the Dockerfile. | WorkDir FilePath -- ^ The WORKDIR instruction sets the working directory for any RUN, CMD, -- ENTRYPOINT, COPY and ADD instructions that follow it in the -- Dockerfile. -- -- @ WORKDIR /path/to/workdir @ | Arg String (Maybe String) -- ^ The ARG instruction defines a variable that users can pass at -- build-time to the builder with the docker build command using the -- @--build-arg = flag@. If a user specifies a build -- argument that was not defined in the Dockerfile, the build outputs -- a warning. -- -- = Default values -- -- An ARG instruction can optionally include a default value: -- -- @ -- FROM busybox -- ARG user1=someuser -- ARG buildno=1 -- @ -- -- If an ARG instruction has a default value and if there is no value -- passed at build-time, the builder uses the default. | OnBuild Instruction -- ^ The ONBUILD instruction adds to the image a trigger -- instruction to be executed at a later time, when the image is used as -- the base for another build. The trigger will be executed in the -- context of the downstream build, as if it had been inserted -- immediately after the FROM instruction in the downstream Dockerfile. -- -- @ -- ^ -- [...] -- ONBUILD ADD . /app/src -- ONBUILD RUN /usr/local/bin/python-build --dir /app/src -- [...] -- @ | StopSignal String -- ^ The STOPSIGNAL instruction sets the system call signal that will -- be sent to the container to exit. This signal can be a valid -- unsigned number that matches a position in the kernel’s syscall -- table, for instance 9, or a signal name in the format SIGNAME, for -- instance SIGKILL. | HealthCheck (Maybe ([String], String)) -- ^ The HEALTHCHECK instruction has two forms: -- -- - HEALTHCHECK [OPTIONS] CMD command (check container health by -- - running a command inside the container) -- -- - HEALTHCHECK NONE (disable any healthcheck inherited from the -- - base image) -- -- The HEALTHCHECK instruction tells Docker how to test a container -- to check that it is still working. This can detect cases such as a -- web server that is stuck in an infinite loop and unable to handle -- new connections, even though the server process is still running. -- -- When a container has a healthcheck specified, it has a health -- status in addition to its normal status. This status is initially -- starting. Whenever a health check passes, it becomes healthy -- (whatever state it was previously in). After a certain number of -- consecutive failures, it becomes unhealthy. -- -- The options that can appear before CMD are: -- -- @ -- --interval=DURATION (default: 30s) -- --timeout=DURATION (default: 30s) -- --start-period=DURATION (default: 0s) -- --retries=N (default: 3) -- @ -- -- The health check will first run interval seconds after the -- container is started, and then again interval seconds after each -- previous check completes. -- -- If a single run of the check takes longer than timeout seconds -- then the check is considered to have failed. -- -- It takes retries consecutive failures of the health check for the -- container to be considered unhealthy. -- -- Start period provides initialization time for containers that need -- time to bootstrap. Probe failure during that period will not be -- counted towards the maximum number of retries. However, if a -- health check succeeds during the start period, the container is -- considered started and all consecutive failures will be counted -- towards the maximum number of retries. -- -- There can only be one HEALTHCHECK instruction in a Dockerfile. If -- you list more than one then only the last HEALTHCHECK will take -- effect. -- -- The command after the CMD keyword can be either a shell command -- (e.g. HEALTHCHECK CMD /bin/check-running) or an exec array (as -- with other Dockerfile commands; see e.g. ENTRYPOINT for details). -- -- The command’s exit status indicates the health status of the -- container. The possible values are: -- -- - 0: success - the container is healthy and ready for use -- -- - 1: unhealthy - the container is not working correctly -- -- - 2: reserved - do not use this exit code -- -- - For example, to check every five minutes or so that a web-server -- - is able to serve the site’s main page within three seconds: -- -- HEALTHCHECK --interval=5m --timeout=3s \ -- CMD curl -f http://localhost/ || exit 1 -- -- To help debug failing probes, any output text (UTF-8 encoded) that -- the command writes on stdout or stderr will be stored in the -- health status and can be queried with docker inspect. Such output -- should be kept short (only the first 4096 bytes are stored -- currently). -- -- When the health status of a container changes, a health_status -- event is generated with the new status. -- -- The HEALTHCHECK feature was added in Docker 1.12. | Shell deriving Show prettyCmd :: Instruction -> String prettyCmd = \case From f mas -> "FROM " ++ f ++ maybe "" (" AS " ++) mas Run scr -> "RUN " ++ scr Cmd cmds -> "CMD " ++ show cmds Label kvs -> "LABEL " ++ unwords (fmap (\(k,v) -> show k ++ "=" ++ show v) kvs) Maintainer m -> "MAINTAINER " ++ m Expose p -> "EXPOSE " ++ show p Env k v -> "ENV " ++ k ++ " " ++ v Add s d opts -> "ADD " ++ (if null opts then "" else renderOpts opts ++ " ") ++ unwords s ++ " " ++ d Copy s d opts -> "COPY " ++ (if null opts then "" else renderOpts opts ++ " ") ++ unwords s ++ " " ++ d Entrypoint e ps -> "ENTRYPOINT " ++ show (e:ps) Volume vs -> "VOLUME " ++ show vs User u -> "USER " ++ u WorkDir cwd -> "WORKDIR " ++ cwd Arg name mval -> "ARG " ++ name ++ maybe "" ("=" ++) mval OnBuild _instr -> error "ONBUILD instruction is not currently supported." StopSignal sig -> "STOPSIGNAL " ++ sig HealthCheck (Just (opts, c)) -> "HEALTHCHECK " ++ unwords opts ++ " CMD " ++ c HealthCheck Nothing -> "HEALTHCHECK NONE" Shell -> error "SHELL instruction is not currently supported" class DockerOpt a where renderDockerOpt :: a -> String data CopyOpt = CopyOptFrom String | CopyOptChown [String] deriving Show instance DockerOpt CopyOpt where renderDockerOpt = \case CopyOptFrom n -> "--from=" ++ n CopyOptChown chowns -> unwords (fmap ("--chown=" ++) chowns) data AddOpt = AddOptFrom String | AddOptChown [String] deriving Show instance DockerOpt AddOpt where renderDockerOpt = \case AddOptFrom n -> "--from=" ++ n AddOptChown chowns -> unwords (fmap ("--chown=" ++) chowns) renderOpts :: DockerOpt a => [a] -> String renderOpts = unwords . fmap renderDockerOpt -- * Instructions from :: String -> Docker () from f = tell [ From f Nothing ] fromas :: String -> As -> Docker () fromas f as = tell [ From f (Just as) ] run :: Script -> Docker () run scr = tell [ Run scr ] cmd :: [ScriptFile] -> Docker () cmd cs = tell [ Cmd cs ] label :: [(String, String)] -> Docker () label kvs = tell [ Label kvs ] maintainer :: String -> Docker () maintainer m = tell [ Maintainer m ] expose :: Int -> Docker () expose p = tell [ Expose p ] env :: String -> String -> Docker () env k v = tell [ Env k v ] add :: [FilePath] -> FilePath -> Docker () add k v = tell [ Add k v [] ] addchown :: [String] -> [FilePath] -> FilePath -> Docker () addchown chowns k v = tell [ Add k v [AddOptChown chowns] ] copy :: [FilePath] -> FilePath -> Docker () copy s d = tell [ Copy s d [] ] copyfrom :: String -> [FilePath] -> FilePath -> Docker () copyfrom frm s d = tell [ Copy s d [CopyOptFrom frm] ] copychown :: [String] -> [FilePath] -> FilePath -> Docker () copychown chowns s d = tell [ Copy s d [CopyOptChown chowns] ] entrypoint :: FilePath -> [Param] -> Docker () entrypoint e ps = tell [ Entrypoint e ps ] volume :: [FilePath] -> Docker () volume vs = tell [ Volume vs ] user :: String -> Docker () user u = tell [ User u ] workdir :: FilePath -> Docker () workdir cwd = tell [ WorkDir cwd ] arg :: String -> Maybe String -> Docker () arg name val = tell [ Arg name val ] onbuild :: Instruction -> Docker () onbuild _ = error "ONBUILD instruction is not yet supported" stopsignal :: String -> Docker () stopsignal c = tell [StopSignal c] healthcheck :: Maybe ([String], String) -> Docker () healthcheck = \case Just (opts, cmd') -> tell [HealthCheck (Just (opts, cmd'))] Nothing -> tell [HealthCheck Nothing] shell :: String -> Docker () shell = error "SHELL instruction is not yet supported"