on master help sign in
Merge pull request #11 from denibertovic/dev

Merge refactored library changes into master

commit2bf90f6caae9da8d4133adf0a5fcb28b5981594f authorDeni Bertovic <deni@denibertovic.com> dateSun 31 Jul 2016, 14:39 (10 years ago) parentd731cba5241d7a
13 files changed+1666−515
Makefile+4−1 Network/Docker.hs−144 Network/Docker/Options.hs−12 Network/Docker/Types.hs−263 Network/Docker/Utils.hs−6 docker.cabal+48−46 src/Docker.hs+118 src/Docker/Http.hs+232 src/Docker/Internal.hs+71 src/Docker/Types.hs+1109 stack.yaml+30−12 tests/Dockerfile+1−6 tests/tests.hs+53−25
Makefile+4−1
@@ -1,10 +1,13 @@
1−.PHONY: test build upload dist configure
1+.PHONY: test build upload dist repl sdist
22
33VERSION ?= $(shell grep "^version:" docker.cabal | cut -d " " -f14)
44
55build:
66 @stack build
77
8+repl:
9+ @stack repl
10+
811test:
912 @stack test
1013
Network/Docker.hsdeleted−144
@@ -1,144 +0,0 @@
1−{-# LANGUAGE OverloadedStrings #-}
2−
3−module Network.Docker where
4−
5−import Control.Applicative ((<$>), (<*>))
6−import Control.Lens
7−import Data.Aeson (FromJSON, ToJSON, decode,
8− eitherDecode, toJSON)
9−import Data.Aeson.Lens (key, _String)
10−import Data.Aeson.TH
11−import qualified Data.ByteString.Lazy as L
12−import Data.Char
13−import qualified Data.Text as T
14−import Network.Docker.Options
15−import Network.Docker.Types
16−import Network.HTTP.Client.OpenSSL
17−import Network.Wreq
18−import OpenSSL (withOpenSSL)
19−import OpenSSL.Session (SSLContext, context)
20−import qualified OpenSSL.Session as SSL
21−import Pipes
22−import qualified Pipes.ByteString as PB
23−import qualified Pipes.HTTP as PH
24−import Text.Printf (printf)
25−
26−
27−defaultClientOpts :: DockerClientOpts
28−defaultClientOpts = DockerClientOpts
29− { apiVersion = "v1.12"
30− , baseUrl = "http://127.0.0.1:3128/"
31− , ssl = NoSSL
32− }
33−
34−constructUrl :: URL -> ApiVersion -> Endpoint -> URL
35−constructUrl url apiVersion endpoint = printf "%s%s%s" url apiVersion endpoint
36−
37−constructRelativeUrl url = url :: String
38−
39−decodeResponse r = decode <$> (^. responseBody) <$> r
40−
41−getOutOfResponse k r = (^? responseBody . key k . _String) r
42−
43−getResponseStatusCode r = (^. responseStatus) r
44−
45−fullUrl :: DockerClientOpts -> Endpoint -> URL
46−fullUrl clientOpts endpoint = constructUrl (baseUrl clientOpts) (apiVersion clientOpts) endpoint
47−
48−setupSSLCtx :: SSLOptions -> IO SSLContext
49−setupSSLCtx (SSLOptions key cert) = do
50− ctx <- SSL.context
51− SSL.contextSetPrivateKeyFile ctx key
52− SSL.contextSetCertificateFile ctx cert
53− SSL.contextAddOption ctx SSL.SSL_OP_NO_SSLv3
54− SSL.contextAddOption ctx SSL.SSL_OP_NO_SSLv2
55− return ctx
56−
57−
58−mkOpts c = defaults & manager .~ Left (opensslManagerSettings c)
59−
60−getSSL
61− :: SSLOptions
62− -> String
63− -> IO (Response L.ByteString)
64−getSSL sopts url = withOpenSSL $ getWith (mkOpts $ setupSSLCtx sopts) url
65−
66−postSSL
67− :: ToJSON a
68− => SSLOptions
69− -> String
70− -> a
71− -> IO (Response L.ByteString)
72−postSSL sopts url = withOpenSSL . postWith (mkOpts $ setupSSLCtx sopts) url . toJSON
73−
74−_dockerGetQuery :: Endpoint -> DockerClientOpts -> IO(Response L.ByteString)
75−_dockerGetQuery endpoint clientOpts@DockerClientOpts{ssl = NoSSL} =
76− get (fullUrl clientOpts endpoint)
77−_dockerGetQuery endpoint clientOpts@DockerClientOpts{ssl = SSL sslOpts} =
78− getSSL sslOpts (fullUrl clientOpts endpoint)
79−
80−_dockerPostQuery :: ToJSON a => Endpoint -> DockerClientOpts -> a -> IO (Response L.ByteString)
81−_dockerPostQuery endpoint clientOpts@DockerClientOpts{ssl = NoSSL} postObject =
82− post (fullUrl clientOpts endpoint) (toJSON postObject)
83−_dockerPostQuery endpoint clientOpts@DockerClientOpts{ssl = SSL sslOpts} postObject =
84− postSSL sslOpts (fullUrl clientOpts endpoint) postObject
85−
86−emptyPost = "" :: String
87−_dockerEmptyPostQuery endpoint clientOpts = post (fullUrl clientOpts endpoint) (toJSON emptyPost)
88−
89−_dockerEmptyDeleteQuery endpoint clientOpts = delete (fullUrl clientOpts endpoint)
90−
91−getDockerVersion :: DockerClientOpts -> IO (Maybe DockerVersion)
92−getDockerVersion = decodeResponse . _dockerGetQuery "/version"
93−
94−getDockerContainers :: DockerClientOpts -> IO (Maybe [DockerContainer])
95−getDockerContainers = decodeResponse . _dockerGetQuery "/containers/json"
96−
97−getAllDockerContainers :: DockerClientOpts -> IO (Maybe [DockerContainer])
98−getAllDockerContainers = decodeResponse . _dockerGetQuery "/containers/json?all=true"
99−
100−getDockerImages :: DockerClientOpts -> IO (Maybe [DockerImage])
101−getDockerImages = decodeResponse . _dockerGetQuery "/images/json"
102−
103−getAllDockerImages :: DockerClientOpts -> IO (Maybe [DockerImage])
104−getAllDockerImages = decodeResponse . _dockerGetQuery "/images/json?all=true"
105−
106−createContainer :: DockerClientOpts -> CreateContainerOpts -> IO(Maybe T.Text)
107−createContainer clientOpts createOpts = getOutOfResponse "Id" <$> (_dockerPostQuery "/containers/create" clientOpts createOpts)
108−
109−startContainer :: DockerClientOpts -> String -> StartContainerOpts -> IO(Status)
110−startContainer clientOpts containerId startOpts = (^. responseStatus) <$> _dockerPostQuery (printf "/containers/%s/start" containerId) clientOpts startOpts
111−
112−stopContainer :: DockerClientOpts -> String -> IO (Status)
113−stopContainer clientOpts containerId = (^. responseStatus) <$> _dockerEmptyPostQuery (printf "/containers/%s/stop" containerId) clientOpts
114−
115−killContainer :: DockerClientOpts -> String -> IO (Status)
116−killContainer clientOpts containerId = (^. responseStatus) <$> _dockerEmptyPostQuery (printf "/containers/%s/kill" containerId) clientOpts
117−
118−restartContainer :: DockerClientOpts -> String -> IO (Status)
119−restartContainer clientOpts containerId = (^. responseStatus) <$> _dockerEmptyPostQuery (printf "/containers/%s/restart" containerId) clientOpts
120−
121−pauseContainer :: DockerClientOpts -> String -> IO (Status)
122−pauseContainer clientOpts containerId = (^. responseStatus) <$> _dockerEmptyPostQuery (printf "/containers/%s/pause" containerId) clientOpts
123−
124−unpauseContainer :: DockerClientOpts -> String -> IO (Status)
125−unpauseContainer clientOpts containerId = (^. responseStatus) <$> _dockerEmptyPostQuery (printf "/containers/%s/unpause" containerId) clientOpts
126−
127−deleteContainer :: DockerClientOpts -> String -> IO (Status)
128−deleteContainer = deleteContainerWithOpts defaultDeleteOpts
129−
130−deleteContainerWithOpts :: DeleteOpts -> DockerClientOpts -> String -> IO (Status)
131−deleteContainerWithOpts (DeleteOpts removeVolumes force) clientOpts containerId = (^. responseStatus) <$> _dockerEmptyDeleteQuery req clientOpts
132− where req = printf "/containers/%s?v=%s;force=%s" containerId (show removeVolumes) (show force)
133−
134−getContainerLogsStream :: DockerClientOpts -> String -> IO ()
135−getContainerLogsStream clientOpts containerId = do
136− req <- PH.parseUrl (fullUrl clientOpts url)
137− let req' = req {PH.method = "GET"}
138− PH.withManager PH.defaultManagerSettings $ \m -> PH.withHTTP req' m $ \resp -> runEffect $ PH.responseBody resp >-> PB.stdout
139− where url = (printf "/containers/%s/logs?stdout=1&stderr=1&follow=1" containerId)
140−
141−getContainerLogs :: DockerClientOpts -> String -> IO (L.ByteString)
142−getContainerLogs clientOpts containerId = (^. responseBody) <$> _dockerGetQuery url clientOpts
143− where url = (printf "/containers/%s/logs?stdout=1&stderr=1" containerId)
144−
Network/Docker/Options.hsdeleted−12
@@ -1,12 +0,0 @@
1−module Network.Docker.Options where
2−
3−import Data.Aeson.TH
4−import Network.Docker.Utils
5−
6−dopts :: Options
7−dopts = defaultOptions {
8− fieldLabelModifier = strip_underscore
9− }
10−
11−
12−
Network/Docker/Types.hsdeleted−263
@@ -1,263 +0,0 @@
1−{-# LANGUAGE DeriveFunctor #-}
2−{-# LANGUAGE FlexibleContexts #-}
3−{-# LANGUAGE OverloadedStrings #-}
4−{-# LANGUAGE RecordWildCards #-}
5−{-# LANGUAGE TemplateHaskell #-}
6−
7−module Network.Docker.Types where
8−
9−import Control.Applicative
10−import Control.Lens.TH
11−import Data.Aeson
12−import Data.Aeson.TH
13−import Data.Bool
14−import qualified Data.ByteString.Lazy as BS
15−import Data.Default
16−import qualified Data.Map as Map
17−import qualified Data.Text as T
18−import GHC.Generics
19−import Network.Docker.Options
20−import Network.Wreq.Types (Postable)
21−import OpenSSL.Session (SSLContext)
22−import Prelude hiding (id)
23−
24−type URL = String
25−type ApiVersion = String
26−type Endpoint = String
27−
28−type Tag = String
29−type IP = String
30−type Port = Int
31−type PortType = String
32−
33−data SSL = NoSSL | SSL SSLOptions deriving Show
34−
35−data DockerClientOpts = DockerClientOpts {
36− apiVersion :: ApiVersion
37− , baseUrl :: URL
38− , ssl :: SSL
39− } deriving (Show)
40−
41−
42−data SSLOptions = SSLOptions {
43− optionsKey :: FilePath
44− , optionsCert :: FilePath
45− } deriving Show
46−
47−
48−data ResourceId = ResourceId { _id :: String } deriving (Show, Eq)
49−
50−
51−data DockerImage = DockerImage
52− { _imageId :: ResourceId
53− , _imageCreatedAt :: Int
54− , _parentId :: Maybe String
55− , _repoTags :: [Tag]
56− , _size :: Int
57− , _virtualSize :: Int
58− } deriving (Show, Eq)
59−
60−
61−data DockerVersion = DockerVersion
62− { _Version :: String
63− , _GitCommit :: String
64− , _GoVersion :: String
65− , _Arch :: String
66− , _KernelVersion :: String
67− } deriving (Show, Eq)
68−
69−
70−-- The JSON looks likes this:
71−-- "Ports":[{"IP":"0.0.0.0","PrivatePort":55555,"PublicPort":55555,"Type":"tcp"}]
72−
73−data PortMap = PortMap
74− { _ip :: IP
75− , _privatePort :: Port
76− , _publicPort :: Port
77− , _type :: PortType
78− } deriving (Show, Eq)
79−
80−data DeleteOpts = DeleteOpts
81− { removeVolumes :: Bool
82− , force :: Bool
83− }
84−
85−defaultDeleteOpts = DeleteOpts False False
86−
87−
88−data DockerContainer = DockerContainer
89− { _containerId :: ResourceId
90− , _containerImageId :: ResourceId
91− , _command :: String
92− , _containerCreatedAt :: Int
93− , _names :: [String]
94− , _status :: String
95− , _ports :: Maybe [PortMap]
96− } deriving (Show, Eq)
97−
98−
99−data CreateContainerOpts = CreateContainerOpts
100− { _hostname :: String
101− , _user :: String
102− , _memory :: Int
103− , _memorySwap :: Int
104− , _attachStdin :: Bool
105− , _attachStdout :: Bool
106− , _attachStderr :: Bool
107− , _portSpecs :: Maybe Object
108− , _tty :: Bool
109− , _openStdin :: Bool
110− , _stdinOnce :: Bool
111− , _env :: Maybe Object
112− , _cmd :: [String]
113− , _image :: String
114− , _volumes :: Maybe Object
115− , _volumesFrom :: Maybe Object
116− , _workingDir :: String
117− , _disableNetwork :: Bool
118− , _exposedPorts :: Maybe Object
119− } deriving (Show)
120−
121−defaultCreateOpts = CreateContainerOpts {
122− _hostname = ""
123− , _user = ""
124− , _memory = 0
125− , _memorySwap = 0
126− , _attachStdin = False
127− , _attachStdout = False
128− , _attachStderr = False
129− , _portSpecs = Nothing
130− , _tty = False
131− , _openStdin = False
132− , _stdinOnce = False
133− , _env = Nothing
134− , _cmd = []
135− , _image = "debian"
136− , _volumes = Nothing
137− , _volumesFrom = Nothing
138− , _workingDir = ""
139− , _disableNetwork = False
140− , _exposedPorts = Nothing
141− }
142−
143−instance ToJSON CreateContainerOpts where
144− toJSON (CreateContainerOpts {..}) = object
145− [ "Hostname" .= _hostname
146− , "User" .= _user
147− , "Memory" .= _memory
148− , "MemorySwap" .= _memorySwap
149− , "AttachStdin" .= _attachStdin
150− , "AttachStdout" .= _attachStdout
151− , "AttachStderr" .= _attachStderr
152− , "PortSpecs" .= _portSpecs
153− , "Tty" .= _tty
154− , "OpenStdin" .= _openStdin
155− , "StdinOnce" .= _stdinOnce
156− , "Env" .= _env
157− , "Cmd" .= _cmd
158− , "Image" .= _image
159− , "Volumes" .= _volumes
160− , "VolumesFrom" .= _volumesFrom
161− , "WrokingDir" .= _workingDir
162− , "DisableNetwork" .= _disableNetwork
163− , "ExposedPorts" .= _exposedPorts
164− ]
165−
166−-- data CreateContainerResponse = CreateContainerResponse
167−-- { _createdContainerId :: String
168−-- , _warnings :: Maybe [T.Text]
169−-- } deriving (Show)
170−
171−data StartContainerOpts = StartContainerOpts
172− { _Binds :: [T.Text]
173− , _Links :: [T.Text]
174− , _LxcConf :: [(T.Text, T.Text)]
175− , _PortBindings :: [((Int,T.Text),Int)]
176− , _PublishAllPorts :: Bool
177− , _Privileged :: Bool
178− , _Dns :: [T.Text]
179− , _VolumesFrom :: [T.Text]
180− , _RestartPolicy :: RestartPolicy
181− } deriving (Show)
182−
183−defaultStartOpts = StartContainerOpts
184− { _Binds = []
185− , _Links = []
186− , _LxcConf = []
187− , _PortBindings = []
188− , _PublishAllPorts = False
189− , _Privileged = False
190− , _Dns = []
191− , _VolumesFrom = []
192− , _RestartPolicy = RestartNever
193− }
194−
195−instance ToJSON StartContainerOpts where
196− toJSON (StartContainerOpts {..}) = object
197− [ "Binds" .= _Binds
198− , "Links" .= _Links
199− , "LxcConf" .= _LxcConf
200− , "PortBindings" .= object (map (\((h,p),c)->T.concat [T.pack (show h),"/",p] .= [object ["HostPort" .= show c]]) _PortBindings)
201− , "PublishAllPorts" .= _PublishAllPorts
202− , "Privileged" .= _Privileged
203− , "Dns" .= _Dns
204− , "VolumesFrom" .= _VolumesFrom
205− , "RestartPolicy" .= _RestartPolicy
206− ]
207−
208−data RestartPolicy = RestartNever
209− | RestartAlways
210− | RestartOnFailure Int
211− deriving (Show)
212−
213−instance ToJSON RestartPolicy where
214− toJSON RestartNever = object ["Name" .= (""::String), "MaximumRetryCount" .= (0::Int) ]
215− toJSON RestartAlways = object ["Name" .= ("always"::String), "MaximumRetryCount" .= (0::Int) ]
216− toJSON (RestartOnFailure n) = object ["Name" .= ("on-failure"::String), "MaximumRetryCount" .= n ]
217−
218−makeClassy ''ResourceId
219−
220−-- makeLenses ''CreateContainerResponse
221−makeLenses ''DockerImage
222−makeLenses ''DockerContainer
223−makeLenses ''CreateContainerOpts
224−
225−instance HasResourceId DockerImage where
226− resourceId = imageId
227−
228−instance FromJSON DockerImage where
229− parseJSON (Object v) =
230− DockerImage <$> ResourceId <$> (v .: "Id")
231− <*> (v .: "Created")
232− <*> (v .:? "ParentId")
233− <*> (v .: "RepoTags")
234− <*> (v .: "Size")
235− <*> (v .: "VirtualSize")
236−
237−instance FromJSON PortMap where
238− parseJSON (Object v) =
239− PortMap <$> (v .: "IP")
240− <*> (v .: "PrivatePort")
241− <*> (v .: "PublicPort")
242− <*> (v .: "Type")
243−
244−instance HasResourceId DockerContainer where
245− resourceId = containerId
246−
247−instance FromJSON DockerContainer where
248− parseJSON (Object v) =
249− DockerContainer <$> (ResourceId <$> (v .: "Id"))
250− <*> (ResourceId <$> (v .: "Id"))
251− <*> (v .: "Command")
252− <*> (v .: "Created")
253− <*> (v .: "Names")
254− <*> (v .: "Status")
255− <*> (v .:? "Ports")
256−
257−-- instance FromJSON CreateContainerResponse where
258−-- parseJSON (Object v) =
259−-- CreateContainerResponse <$> (v .: "Id")
260−-- <*> (v .:? "warnings")
261−
262−$(deriveJSON dopts ''DockerVersion)
263−
Network/Docker/Utils.hsdeleted−6
@@ -1,6 +0,0 @@
1−module Network.Docker.Utils where
2−
3−import Data.Char
4−
5−strip_underscore :: String -> String
6−strip_underscore (_:xs) = xs
docker.cabal+48−46
@@ -1,16 +1,13 @@
1−-- Initial docker.cabal generated by cabal init. For further
2−-- documentation, see http://haskell.org/cabal/users-guide/
3−
41name: docker
5−version: 0.2.0.4
2+version: 0.3.0.0
63synopsis: Haskell wrapper for Docker Remote API
7−-- description:
4+description: See README.md
85homepage: https://github.com/denibertovic/docker-hs
96license: BSD3
107license-file: LICENSE
118author: Deni Bertovic
129maintainer: deni@denibertovic.com
13−-- copyright:
10+copyright: BSD3
1411category: Network
1512build-type: Simple
1613extra-source-files: README.md
@@ -18,52 +15,57 @@ cabal-version: >=1.10
1815stability: experimental
1916
2017library
21− exposed-modules: Network.Docker, Network.Docker.Options, Network.Docker.Types, Network.Docker.Utils
22− -- other-modules:
23− other-extensions: OverloadedStrings, DeriveFunctor, FlexibleContexts, TemplateHaskell
24− build-depends: base >= 4.7 && < 4.9
25− , lens >= 4.2
26− , aeson >= 0.7
18+ default-extensions: DeriveGeneric, OverloadedStrings, ScopedTypeVariables, ExplicitForAll, MultiParamTypeClasses, FlexibleContexts, GeneralizedNewtypeDeriving, RankNTypes, DeriveFunctor, FlexibleInstances
19+ hs-source-dirs: src
20+ exposed-modules: Docker, Docker.Types, Docker.Internal, Docker.Http
21+ -- other-modules: Docker.Internal
22+ build-depends: base >= 4.7 && < 5
23+ , aeson
24+ , blaze-builder
25+ , bytestring
26+ , containers
27+ , connection
28+ , data-default-class
29+ , exceptions
30+ , http-client
31+ , http-types
32+ , lens
2733 , lens-aeson
28− , bytestring >= 0.10
29− , text >= 1.1
30− , wreq >= 0.3
31− , containers >= 0.5
32− , data-default >= 0.5.3
33− , network-uri >= 2.6.0.1
34− , pipes-http >= 1.0.0
35− , pipes >= 4.1.2
36− , pipes-text >= 0.0.0.12
37− , pipes-bytestring >= 2.1.0
38− , HsOpenSSL
39− , http-client-openssl
40− -- hs-source-dirs:
34+ , lifted-base
35+ , mtl
36+ , network >= 2.6
37+ , network-uri
38+ , text
39+ , time
40+ , tls >= 1.3.7
41+ , transformers
42+ , unordered-containers
43+ , x509
44+ , x509-store
45+ , x509-system
4146 default-language: Haskell2010
47+ ghc-options: -Wall -fno-warn-name-shadowing
4248
43−test-suite tests
44− build-depends: base >= 4.7 && < 4.9
49+test-suite docker-hs-tests
50+ build-depends: base >= 4.7 && < 5
51+ , bytestring
52+ , connection
4553 , docker
46− , lens >= 4.2
47− , aeson >= 0.7
48− , lens-aeson
49− , bytestring >= 0.10
50− , text >= 1.1
51− , wreq >= 0.3
52− , containers >= 0.5
53− , data-default >= 0.5.3
54− , pipes >= 4.1.2
55− , pipes-http >= 1.0.0
56− , pipes-text >= 0.0.0.12
57− , pipes-bytestring >= 2.1.0
58− , tasty >= 0.10.1
59− , tasty-quickcheck >= 0.8.2
60− , QuickCheck >= 2.7
61− , tasty-hunit >= 0.8
62− , process
54+ , http-client
55+ , http-client-tls
6356 , http-types
64− , HsOpenSSL
65− , http-client-openssl
57+ , tasty
58+ , tasty-hunit
59+ , tasty-quickcheck
60+ , text
61+ , transformers
62+ , QuickCheck
63+ , process
6664 type: exitcode-stdio-1.0
6765 main-is: tests.hs
6866 hs-source-dirs: tests
6967 default-language: Haskell2010
68+
69+source-repository head
70+ type: git
71+ location: https://github.com/denibertovic/docker-hs
src/Docker.hsnew file+118
@@ -0,0 +1,118 @@
1+{-# LANGUAGE ExplicitForAll #-}
2+{-# LANGUAGE ScopedTypeVariables #-}
3+
4+module Docker (
5+ runDockerT
6+ , DockerT
7+ , DockerError(..)
8+ , DockerClientOpts(..)
9+ , defaultClientOpts
10+ , HttpHandler
11+ , defaultHttpHandler
12+ , httpHandler
13+ , unixHttpHandler
14+ , clientParamsWithClientAuthentication
15+ , clientParamsSetCA
16+ , getDockerVersion
17+ , listContainers
18+ , listImages
19+ , createContainer
20+ , startContainer
21+ , stopContainer
22+ , killContainer
23+ , restartContainer
24+ , pauseContainer
25+ , unpauseContainer
26+ , deleteContainer
27+ , inspectContainer
28+ , getContainerLogs
29+ ) where
30+
31+import Control.Monad.Except (ExceptT (..), runExceptT, throwError)
32+import Control.Monad.Reader (ask, lift)
33+import Data.Aeson (FromJSON, eitherDecode')
34+import qualified Data.ByteString.Lazy as BSL
35+import qualified Data.Text as Text
36+import Network.HTTP.Client (responseBody, responseStatus)
37+import Network.HTTP.Types (StdMethod (..))
38+
39+import Docker.Http
40+import Docker.Types
41+
42+requestUnit :: (Monad m) => HttpVerb -> Endpoint -> DockerT m (Either DockerError ())
43+requestUnit verb endpoint = const (Right ()) <$> requestHelper verb endpoint
44+
45+requestHelper :: (Monad m) => HttpVerb -> Endpoint -> DockerT m (Either DockerError Response)
46+requestHelper verb endpoint = runExceptT $ do
47+ (opts, httpHandler) <- lift ask
48+ case mkHttpRequest verb endpoint opts of
49+ Nothing ->
50+ throwError $ DockerInvalidRequest endpoint
51+ Just request -> do
52+ response <- ExceptT $ lift $ httpHandler request
53+
54+ -- Check status code.
55+ let status = responseStatus response
56+ maybe (return ()) throwError $
57+ statusCodeToError endpoint status
58+
59+ return response
60+
61+parseResponse :: (FromJSON a, Monad m) => Either DockerError Response -> DockerT m (Either DockerError a)
62+parseResponse (Left err) =
63+ return $ Left err
64+parseResponse (Right response) =
65+ -- Parse request body.
66+ case eitherDecode' $ responseBody response of
67+ Left err ->
68+ return $ Left $ DockerClientDecodeError $ Text.pack err
69+ Right r ->
70+ return $ Right r
71+
72+getDockerVersion :: forall m. Monad m => DockerT m (Either DockerError DockerVersion)
73+getDockerVersion = requestHelper GET VersionEndpoint >>= parseResponse
74+
75+listContainers :: forall m. Monad m => ListOpts -> DockerT m (Either DockerError [Container])
76+listContainers opts = requestHelper GET (ListContainersEndpoint opts) >>= parseResponse
77+
78+listImages :: forall m. Monad m => ListOpts -> DockerT m (Either DockerError [Image])
79+listImages opts = requestHelper GET (ListImagesEndpoint opts) >>= parseResponse
80+
81+createContainer :: forall m. Monad m => CreateOpts -> DockerT m (Either DockerError ContainerID)
82+createContainer opts = requestHelper POST (CreateContainerEndpoint opts) >>= parseResponse
83+
84+-- TODO: Is this the correct return type? XXX
85+-- convert status code to unit or error
86+startContainer :: forall m. Monad m => StartOpts -> ContainerID -> DockerT m (Either DockerError ())
87+startContainer sopts cid = requestUnit POST $ StartContainerEndpoint sopts cid
88+
89+stopContainer :: forall m. Monad m => Timeout -> ContainerID -> DockerT m (Either DockerError ())
90+stopContainer t cid = requestUnit POST $ StopContainerEndpoint t cid
91+
92+killContainer :: forall m. Monad m => Signal -> ContainerID -> DockerT m (Either DockerError ())
93+killContainer s cid = requestUnit POST $ KillContainerEndpoint s cid
94+
95+restartContainer :: forall m. Monad m => Timeout -> ContainerID -> DockerT m (Either DockerError ())
96+restartContainer t cid = requestUnit POST $ RestartContainerEndpoint t cid
97+
98+pauseContainer :: forall m. Monad m => ContainerID -> DockerT m (Either DockerError ())
99+pauseContainer cid = requestUnit POST $ PauseContainerEndpoint cid
100+
101+unpauseContainer :: forall m. Monad m => ContainerID -> DockerT m (Either DockerError ())
102+unpauseContainer cid = requestUnit GET $ UnpauseContainerEndpoint cid
103+
104+deleteContainer :: forall m. Monad m => DeleteOpts -> ContainerID -> DockerT m (Either DockerError ())
105+deleteContainer dopts cid = requestUnit DELETE $ DeleteContainerEndpoint dopts cid
106+
107+inspectContainer :: forall m . Monad m => ContainerID -> DockerT m (Either DockerError ContainerDetails)
108+inspectContainer cid = requestHelper GET (InspectContainerEndpoint cid) >>= parseResponse
109+
110+getContainerLogs :: forall m. Monad m => LogOpts -> ContainerID -> DockerT m (Either DockerError BSL.ByteString)
111+getContainerLogs logopts cid = fmap responseBody <$> requestHelper GET (ContainerLogsEndpoint logopts False cid)
112+
113+-- TODO: Use http-conduit to output to a sink.
114+-- getContainerLogsStream :: forall m. Monad m => Sink BSL.ByteString m b -> LogOpts -> ContainerID -> DockerT m (Either DockerError b)
115+-- getContainerLogsStream sink logopts cid = runResourceT $ do
116+-- response <- http request manager
117+-- responseBody response C.$$+- sink
118+
src/Docker/Http.hsnew file+232
@@ -0,0 +1,232 @@
1+{-# LANGUAGE RankNTypes #-}
2+{-# LANGUAGE ScopedTypeVariables #-}
3+
4+module Docker.Http where
5+
6+import Control.Monad.Reader (ReaderT, runReaderT)
7+import qualified Data.ByteString.Char8 as BSC
8+import qualified Data.ByteString.Lazy as BL
9+import Data.Default.Class (def)
10+import Data.Monoid ((<>))
11+import Data.Text.Encoding (encodeUtf8)
12+import Data.X509 (CertificateChain (..))
13+import Data.X509.CertificateStore (makeCertificateStore)
14+import Data.X509.File (readKeyFile, readSignedObject)
15+import Network.HTTP.Client (defaultManagerSettings, httpLbs,
16+ managerRawConnection, method,
17+ newManager, parseUrl,
18+ requestBody, requestHeaders)
19+import qualified Network.HTTP.Client as HTTP
20+import Network.HTTP.Client.Internal (makeConnection)
21+import Network.HTTP.Types (StdMethod, status101, status200,
22+ status201, status204)
23+import Network.TLS (ClientHooks (..),
24+ ClientParams (..), Shared (..),
25+ Supported (..),
26+ defaultParamsClient)
27+import Network.TLS.Extra (ciphersuite_strong)
28+import System.X509 (getSystemCertificateStore)
29+
30+import Control.Exception (try)
31+import Control.Monad.Except
32+import Control.Monad.Reader.Class
33+import Data.Text as T
34+import Data.Typeable (Typeable)
35+import qualified Network.HTTP.Types as HTTP
36+import qualified Network.Socket as S
37+import qualified Network.Socket.ByteString as SBS
38+
39+import Docker.Internal (getEndpoint,
40+ getEndpointRequestBody)
41+import Docker.Types (DockerClientOpts, Endpoint (..),
42+ baseUrl)
43+
44+type Request = HTTP.Request
45+type Response = HTTP.Response BL.ByteString
46+type HttpVerb = StdMethod
47+type HttpHandler m = Request -> m (Either DockerError Response)
48+
49+data DockerError = DockerConnectionError
50+ | DockerInvalidRequest Endpoint
51+ | DockerClientDecodeError Text -- ^ Could not parse the response from the Docker endpoint.
52+ | DockerInvalidStatusCode HTTP.Status -- ^ Invalid exit code received from Docker endpoint.
53+ | GenericDockerError Text deriving (Eq, Show, Typeable)
54+
55+newtype DockerT m a = DockerT {
56+ unDockerT :: Monad m => ReaderT (DockerClientOpts, HttpHandler m) m a
57+ } deriving (Functor) -- Applicative, Monad, MonadReader, MonadError, MonadTrans
58+
59+instance Applicative m => Applicative (DockerT m) where
60+ pure a = DockerT $ pure a
61+ (<*>) (DockerT f) (DockerT v) = DockerT $ f <*> v
62+
63+instance Monad m => Monad (DockerT m) where
64+ (DockerT m) >>= f = DockerT $ m >>= unDockerT . f
65+ return = pure
66+
67+instance Monad m => MonadReader (DockerClientOpts, HttpHandler m) (DockerT m) where
68+ ask = DockerT ask
69+ local f (DockerT m) = DockerT $ local f m
70+
71+instance MonadTrans DockerT where
72+ lift m = DockerT $ lift m
73+
74+instance MonadIO m => MonadIO (DockerT m) where
75+ liftIO = lift . liftIO
76+
77+runDockerT :: Monad m => (DockerClientOpts, HttpHandler m) -> DockerT m a -> m a
78+runDockerT (opts, h) r = runReaderT (unDockerT r) (opts, h)
79+
80+-- The reason we return Maybe Request is because the parseURL function
81+-- might find out parameters are invalid and will fail to build a Request
82+-- Since we are the ones building the Requests this shouldn't happen, but would
83+-- benefit from testing that on all of our Endpoints
84+mkHttpRequest :: HttpVerb -> Endpoint -> DockerClientOpts -> Maybe Request
85+mkHttpRequest verb e opts = request
86+ where fullE = T.unpack (baseUrl opts) ++ T.unpack (getEndpoint e)
87+ initialR = parseUrl fullE
88+ request' = case initialR of
89+ Just ir ->
90+ return $ ir {method = (encodeUtf8 . T.pack $ show verb),
91+ requestHeaders = [("Content-Type", "application/json; charset=utf-8")]}
92+ Nothing -> Nothing
93+ request = (\r -> maybe r (\body -> r {requestBody = HTTP.RequestBodyLBS body,
94+ requestHeaders = [("Content-Type", "application/json; charset=utf-8")]}) $ getEndpointRequestBody e) <$> request'
95+ -- Note: Do we need to set length header?
96+
97+defaultHttpHandler :: MonadIO m => HttpHandler m
98+defaultHttpHandler request = do
99+ manager <- liftIO $ newManager defaultManagerSettings
100+ httpHandler manager request
101+
102+httpHandler :: MonadIO m => HTTP.Manager -> HttpHandler m
103+httpHandler manager request = liftIO $ do
104+ try (httpLbs request manager) >>= \res -> case res of
105+ Right res -> return $ Right res
106+ Left HTTP.FailedConnectionException{} -> return $ Left DockerConnectionError
107+ Left HTTP.FailedConnectionException2{} -> return $ Left DockerConnectionError
108+ Left e -> return $ Left $ GenericDockerError (T.pack $ show e)
109+
110+-- | Connect to a unix domain socket (the default docker socket is
111+-- at \/var\/run\/docker.sock)
112+--
113+-- Docker seems to ignore the hostname in requests sent over unix domain
114+-- sockets (and the port obviously doesn't matter either)
115+unixHttpHandler :: MonadIO m => FilePath -- ^ The socket to connect to
116+ -> HttpHandler m
117+unixHttpHandler fp request = do
118+ let mSettings = defaultManagerSettings
119+ { managerRawConnection = return $ openUnixSocket fp}
120+ manager <- liftIO $ newManager mSettings
121+ httpHandler manager request
122+
123+ where
124+ openUnixSocket filePath _ _ _ = do
125+ s <- S.socket S.AF_UNIX S.Stream S.defaultProtocol
126+ S.connect s (S.SockAddrUnix filePath)
127+ makeConnection (SBS.recv s 8096)
128+ (SBS.sendAll s)
129+ (S.sClose s)
130+
131+-- TODO:
132+-- Move this to http-client-tls or network?
133+-- Add CA.
134+-- Maybe change this to: HostName -> PortNumber -> ClientParams -> IO (Either String TLSSettings)
135+clientParamsWithClientAuthentication :: S.HostName -> S.PortNumber -> FilePath -> FilePath -> IO (Either String ClientParams)
136+clientParamsWithClientAuthentication host port keyFile certificateFile = do
137+ keys <- readKeyFile keyFile
138+ cert <- readSignedObject certificateFile
139+ case keys of
140+ [key] ->
141+ -- TODO: load keys/path from file
142+ let params = (defaultParamsClient host $ BSC.pack $ show port) {
143+ clientHooks = def
144+ { onCertificateRequest = \_ -> return (Just (CertificateChain cert, key))}
145+ , clientSupported = def
146+ { supportedCiphers = ciphersuite_strong}
147+ }
148+ in
149+ return $ Right params
150+ _ ->
151+ return $ Left $ "Could not read key file: " ++ keyFile
152+
153+clientParamsSetCA :: ClientParams -> FilePath -> IO ClientParams
154+clientParamsSetCA params path = do
155+ userStore <- makeCertificateStore <$> readSignedObject path
156+ systemStore <- getSystemCertificateStore
157+ let store = userStore <> systemStore
158+ let oldShared = clientShared params
159+ return $ params { clientShared = oldShared
160+ { sharedCAStore = store }
161+ }
162+
163+
164+-- If the status is an error, returns a Just DockerError. Otherwise, returns Nothing.
165+statusCodeToError :: Endpoint -> HTTP.Status -> Maybe DockerError
166+statusCodeToError VersionEndpoint st =
167+ if st == status200 then
168+ Nothing
169+ else
170+ Just $ DockerInvalidStatusCode st
171+statusCodeToError (ListContainersEndpoint _) st =
172+ if st == status200 then
173+ Nothing
174+ else
175+ Just $ DockerInvalidStatusCode st
176+statusCodeToError (ListImagesEndpoint _) st =
177+ if st == status200 then
178+ Nothing
179+ else
180+ Just $ DockerInvalidStatusCode st
181+statusCodeToError (CreateContainerEndpoint _) st =
182+ if st == status201 then
183+ Nothing
184+ else
185+ Just $ DockerInvalidStatusCode st
186+statusCodeToError (StartContainerEndpoint _ _) st =
187+ if st == status204 then
188+ Nothing
189+ else
190+ Just $ DockerInvalidStatusCode st
191+statusCodeToError (StopContainerEndpoint _ _) st =
192+ if st == status204 then
193+ Nothing
194+ else
195+ Just $ DockerInvalidStatusCode st
196+statusCodeToError (KillContainerEndpoint _ _) st =
197+ if st == status204 then
198+ Nothing
199+ else
200+ Just $ DockerInvalidStatusCode st
201+statusCodeToError (RestartContainerEndpoint _ _) st =
202+ if st == status204 then
203+ Nothing
204+ else
205+ Just $ DockerInvalidStatusCode st
206+statusCodeToError (PauseContainerEndpoint _) st =
207+ if st == status204 then
208+ Nothing
209+ else
210+ Just $ DockerInvalidStatusCode st
211+statusCodeToError (UnpauseContainerEndpoint _) st =
212+ if st == status204 then
213+ Nothing
214+ else
215+ Just $ DockerInvalidStatusCode st
216+statusCodeToError (ContainerLogsEndpoint _ _ _) st =
217+ if st == status200 || st == status101 then
218+ Nothing
219+ else
220+ Just $ DockerInvalidStatusCode st
221+statusCodeToError (DeleteContainerEndpoint _ _) st =
222+ if st == status204 then
223+ Nothing
224+ else
225+ Just $ DockerInvalidStatusCode st
226+statusCodeToError (InspectContainerEndpoint _) st =
227+ if st == status200 then
228+ Nothing
229+ else
230+ Just $ DockerInvalidStatusCode st
231+
232+
src/Docker/Internal.hsnew file+71
@@ -0,0 +1,71 @@
1+module Docker.Internal where
2+
3+
4+import Blaze.ByteString.Builder (toByteString)
5+import qualified Data.Aeson as JSON
6+import Data.ByteString (ByteString)
7+import qualified Data.ByteString.Lazy as BSL
8+import Data.Text as T
9+import Data.Text.Encoding (decodeUtf8, encodeUtf8)
10+import Docker.Types
11+import Network.HTTP.Types (Query, encodePath,
12+ encodePathSegments)
13+
14+encodeURL :: [T.Text] -> T.Text
15+encodeURL ps = decodeUtf8 $ toByteString $ encodePathSegments ps
16+
17+encodeURLWithQuery :: [T.Text] -> Query -> T.Text
18+encodeURLWithQuery ps q = decodeUtf8 $ toByteString $ encodePath ps q
19+
20+encodeQ :: String -> ByteString
21+encodeQ = encodeUtf8 . T.pack
22+
23+getEndpoint :: Endpoint -> T.Text
24+getEndpoint VersionEndpoint = encodeURL ["version"]
25+getEndpoint (ListContainersEndpoint _) = encodeURL ["containers", "json"] -- Make use of lsOpts here
26+getEndpoint (ListImagesEndpoint _) = encodeURL ["images", "json"] -- Make use of lsOpts here
27+getEndpoint (CreateContainerEndpoint _) = encodeURL ["containers", "create"]
28+getEndpoint (StartContainerEndpoint startOpts cid) = encodeURLWithQuery ["containers", fromContainerID cid, "start"] query
29+ where query = case (detachKeys startOpts) of
30+ WithCtrl c -> [("detachKeys", Just (encodeQ $ ctrl ++ [c]))]
31+ WithoutCtrl c -> [("detachKeys", Just (encodeQ [c]))]
32+ DefaultDetachKey -> []
33+ ctrl = ['c', 't', 'r', 'l', '-']
34+getEndpoint (StopContainerEndpoint t cid) = encodeURLWithQuery ["containers", fromContainerID cid, "stop"] query
35+ where query = case t of
36+ Timeout x -> [("t", Just (encodeQ $ show x))]
37+ DefaultTimeout -> []
38+getEndpoint (KillContainerEndpoint s cid) = encodeURLWithQuery ["containers", fromContainerID cid, "kill"] query
39+ where query = case s of
40+ SIG x -> [("signal", Just (encodeQ $ show x))]
41+ _ -> [("signal", Just (encodeQ $ show s))]
42+getEndpoint (RestartContainerEndpoint t cid) = encodeURLWithQuery ["containers", fromContainerID cid, "restart"] query
43+ where query = case t of
44+ Timeout x -> [("t", Just (encodeQ $ show x))]
45+ DefaultTimeout -> []
46+getEndpoint (PauseContainerEndpoint cid) = encodeURL ["containers", fromContainerID cid, "pause"]
47+getEndpoint (UnpauseContainerEndpoint cid) = encodeURL ["containers", fromContainerID cid, "unpause"]
48+-- Make use of since/timestamps/tail logopts here instead of ignoreing them
49+getEndpoint (ContainerLogsEndpoint (LogOpts stdout stderr _ _ _) follow cid) =
50+ encodeURLWithQuery ["containers", fromContainerID cid, "logs"] query
51+ where query = [("stdout", Just (encodeQ $ show stdout)), ("stderr", Just (encodeQ $ show stderr)), ("follow", Just (encodeQ $ show follow))]
52+getEndpoint (DeleteContainerEndpoint (DeleteOpts removeVolumes force) cid) =
53+ encodeURLWithQuery ["containers", fromContainerID cid] query
54+ where query = [("v", Just (encodeQ $ show removeVolumes)), ("force", Just (encodeQ $ show force))]
55+getEndpoint (InspectContainerEndpoint cid) =
56+ encodeURLWithQuery ["containers", fromContainerID cid, "json"] []
57+
58+getEndpointRequestBody :: Endpoint -> Maybe BSL.ByteString
59+getEndpointRequestBody VersionEndpoint = Nothing
60+getEndpointRequestBody (ListContainersEndpoint _) = Nothing
61+getEndpointRequestBody (ListImagesEndpoint _) = Nothing
62+getEndpointRequestBody (CreateContainerEndpoint opts) = Just $ JSON.encode opts
63+getEndpointRequestBody (StartContainerEndpoint _ _) = Nothing
64+getEndpointRequestBody (StopContainerEndpoint _ _) = Nothing
65+getEndpointRequestBody (KillContainerEndpoint _ _) = Nothing
66+getEndpointRequestBody (RestartContainerEndpoint _ _) = Nothing
67+getEndpointRequestBody (PauseContainerEndpoint _) = Nothing
68+getEndpointRequestBody (UnpauseContainerEndpoint _) = Nothing
69+getEndpointRequestBody (ContainerLogsEndpoint _ _ _) = Nothing
70+getEndpointRequestBody (DeleteContainerEndpoint _ _) = Nothing
71+getEndpointRequestBody (InspectContainerEndpoint _) = Nothing
src/Docker/Types.hsnew file+1109
@@ -0,0 +1,1109 @@
1+{-# LANGUAGE RecordWildCards #-}
2+{-# LANGUAGE ScopedTypeVariables #-}
3+
4+module Docker.Types (
5+ Endpoint(..)
6+ , URL
7+ , ApiVersion
8+ , ContainerID
9+ , fromContainerID
10+ , toContainerID
11+ , ImageID
12+ , fromImageID
13+ , toImageID
14+ , Timeout(..)
15+ , Signal(..)
16+ , ContainerDetails(..)
17+ , DockerClientOpts(..)
18+ , defaultClientOpts
19+ , ListOpts(..)
20+ , defaultListOpts
21+ , DockerVersion(..)
22+ , ContainerPortInfo(..)
23+ , Container(..)
24+ , ContainerState(..)
25+ , Status(..)
26+ , Digest
27+ , Labels(..)
28+ , Tag
29+ , Image(..)
30+ , dropImagePrefix
31+ , CreateOpts(..)
32+ , defaultCreateOpts
33+ , DetachKeys(..)
34+ , StartOpts(..)
35+ , defaultStartOpts
36+ , DeleteOpts(..)
37+ , Timestamp
38+ , TailLogOpt(..)
39+ , LogOpts(..)
40+ , defaultLogOpts
41+ , VolumePermission(..)
42+ , Volume(..)
43+ , Device(..)
44+ , ContainerName
45+ , VolumeFrom(..)
46+ , Link(..)
47+ , LogDriverType(..)
48+ , LogDriverOptions(..)
49+ , LogDriverConfig(..)
50+ , NetworkMode(..)
51+ , PortType(..)
52+ , NetworkInterface(..)
53+ , Networks(..)
54+ , NetworkSettings(..)
55+ , NetworkOptions(..)
56+ , Mount(..)
57+ , PortBinding(..)
58+ , PortBindings(..)
59+ , HostPort(..)
60+ , RetryCount
61+ , RestartPolicy(..)
62+ , Isolation(..)
63+ , UTSMode(..)
64+ , HostConfig(..)
65+ , defaultHostConfig
66+ , Ulimit(..)
67+ , ContainerResources(..)
68+ , defaultContainerResources
69+ , Port
70+ , Name
71+ , Value
72+ , EnvVar(..)
73+ , ContainerConfig(..)
74+ , defaultContainerConfig
75+ , ExposedPorts(..)
76+ ) where
77+
78+import Data.Aeson (FromJSON, ToJSON, genericParseJSON,
79+ genericToJSON, object, parseJSON, toJSON,
80+ (.!=), (.:), (.:?), (.=))
81+import qualified Data.Aeson as JSON
82+import Data.Aeson.Types (defaultOptions, fieldLabelModifier)
83+import Data.Char (isAlphaNum, toUpper)
84+import qualified Data.HashMap.Strict as HM
85+import qualified Data.Map as M
86+import Data.Monoid ((<>))
87+import Data.Text (Text)
88+import qualified Data.Text as T
89+import Data.Time.Clock (UTCTime)
90+import GHC.Generics (Generic)
91+import Prelude hiding (all, tail)
92+import Text.Read (readMaybe)
93+
94+data Endpoint =
95+ VersionEndpoint
96+ | ListContainersEndpoint ListOpts
97+ | ListImagesEndpoint ListOpts
98+ | CreateContainerEndpoint CreateOpts
99+ | StartContainerEndpoint StartOpts ContainerID
100+ | StopContainerEndpoint Timeout ContainerID
101+ | KillContainerEndpoint Signal ContainerID
102+ | RestartContainerEndpoint Timeout ContainerID
103+ | PauseContainerEndpoint ContainerID
104+ | UnpauseContainerEndpoint ContainerID
105+ | ContainerLogsEndpoint LogOpts Bool ContainerID -- Second argument (Bool) is whether to follow.
106+ | DeleteContainerEndpoint DeleteOpts ContainerID
107+ | InspectContainerEndpoint ContainerID
108+ deriving (Eq, Show)
109+
110+type URL = Text
111+type ApiVersion = Text
112+newtype ContainerID = ContainerID Text
113+ deriving (Eq, Show)
114+
115+fromContainerID :: ContainerID -> Text
116+fromContainerID (ContainerID t) = t
117+
118+toContainerID :: Text -> Maybe ContainerID
119+toContainerID t =
120+ if T.all (\c -> isAlphaNum c || c == ':') t then -- Note: Can we improve this whitelist?
121+ Just $ ContainerID t
122+ else
123+ Nothing
124+
125+newtype ImageID = ImageID Text
126+ deriving (Eq, Show)
127+
128+fromImageID :: ImageID -> Text
129+fromImageID (ImageID t) = t
130+
131+toImageID :: Text -> Maybe ImageID
132+toImageID t =
133+ if T.all (\c -> isAlphaNum c || c == ':') t then -- Note: Can we improve this whitelist?
134+ Just $ ImageID t
135+ else
136+ Nothing
137+
138+data Timeout = Timeout Integer | DefaultTimeout deriving (Eq, Show)
139+
140+-- TODO: Add more Signals or use an existing lib
141+data Signal = SIGHUP
142+ | SIGINT
143+ | SIGQUIT
144+ | SIGSTOP
145+ | SIGTERM
146+ | SIGUSR1
147+ | SIG Integer
148+ | SIGKILL deriving (Eq, Show)
149+
150+instance FromJSON Signal where
151+ parseJSON (JSON.String "SIGTERM") = return SIGTERM
152+ parseJSON (JSON.String "SIGHUP") = return SIGHUP -- Note: Guessing on the string values for these.
153+ parseJSON (JSON.String "SIGINT") = return SIGINT
154+ parseJSON (JSON.String "SIGQUIT") = return SIGQUIT
155+ parseJSON (JSON.String "SIGSTOP") = return SIGSTOP
156+ parseJSON (JSON.String "SIGUSR1") = return SIGUSR1
157+ parseJSON _ = fail "Unknown Signal"
158+
159+instance ToJSON Signal where
160+ toJSON SIGHUP = "SIGHUP"
161+ toJSON SIGINT = "SIGINT"
162+ toJSON SIGQUIT = "SIGQUIT"
163+ toJSON SIGSTOP = "SIGSTOP"
164+ toJSON SIGTERM = "SIGTERM"
165+ toJSON SIGUSR1 = "SIGUSR1"
166+ toJSON (SIG i) = toJSON i
167+ toJSON SIGKILL = "SIGKILL"
168+
169+data ContainerDetails = ContainerDetails {
170+ appArmorProfile :: Text
171+ , args :: [Text]
172+ , containerDetailsConfig :: ContainerConfig
173+ , created :: UTCTime
174+ , driver :: Text
175+ -- , execIDs -- Not sure what this is in 1.24 spec.
176+ , containerDetailsHostConfig :: HostConfig
177+ , hostnamePath :: FilePath
178+ , hostsPath :: FilePath
179+ , logPath :: FilePath
180+ , containerDetailsId :: ContainerID
181+ , containerDetailsImage :: ImageID
182+ , mountLabel :: Text
183+ , name :: Text
184+ , networkSettings :: NetworkSettings
185+ , path :: FilePath
186+ , processLabel :: Text
187+ , resolveConfPath :: FilePath
188+ , restartCount :: Int
189+ , state :: ContainerState
190+ , mounts :: [Mount]
191+ }
192+ deriving (Eq, Show, Generic)
193+
194+data Mount = Mount {
195+ mountName :: Text
196+ , mountSource :: FilePath
197+ , mountDestination :: FilePath
198+ , mountDriver :: Text
199+ , mountMode :: Text -- Note: Improve this?
200+ , mountRW :: Bool
201+ , mountPropogation :: Text
202+ }
203+ deriving (Eq, Show, Generic)
204+
205+instance FromJSON Mount where
206+ parseJSON (JSON.Object o) = do
207+ name <- o .: "Name"
208+ src <- o .: "Source"
209+ dest <- o .: "Destination"
210+ driver <- o .: "Driver"
211+ mode <- o .: "Mode"
212+ rw <- o .: "RW"
213+ prop <- o .: "Propagation"
214+ return $ Mount name src dest driver mode rw prop
215+ parseJSON _ = fail "Mount is not an object"
216+
217+data ContainerState = ContainerState {
218+ containerError :: Text
219+ , exitCode :: Int
220+ , finishedAt :: Maybe UTCTime -- Note: Is this a maybe?
221+ , oomKilled :: Bool
222+ , dead :: Bool
223+ , paused :: Bool
224+ , pid :: Int
225+ , restarting :: Bool
226+ , running :: Bool
227+ , startedAt :: UTCTime
228+ , status :: Status
229+ }
230+ deriving (Eq, Show, Generic)
231+
232+instance FromJSON ContainerState where
233+ parseJSON (JSON.Object o) = do
234+ err <- o .: "Error"
235+ exit <- o .: "ExitCode"
236+ finished <- o .:? "FinishedAt"
237+ oomKilled <- o .: "OOMKilled"
238+ dead <- o .: "Dead"
239+ paused <- o .: "Paused"
240+ pid <- o .: "Pid"
241+ restarting <- o .: "Restarting"
242+ running <- o .: "Running"
243+ started <- o .: "StartedAt"
244+ st <- o .: "Status"
245+ return $ ContainerState err exit finished oomKilled dead paused pid restarting running started st
246+ parseJSON _ = fail "ContainerState is not an object"
247+
248+data DockerClientOpts = DockerClientOpts {
249+ apiVer :: ApiVersion
250+ , baseUrl :: URL
251+ }
252+ deriving (Eq, Show)
253+
254+defaultClientOpts :: DockerClientOpts
255+defaultClientOpts = DockerClientOpts {
256+ apiVer = "v1.23"
257+ , baseUrl = "http://127.0.0.1:2375"
258+ }
259+
260+data ListOpts = ListOpts { all :: Bool } deriving (Eq, Show)
261+
262+defaultListOpts :: ListOpts
263+defaultListOpts = ListOpts { all=False }
264+
265+data DockerVersion = DockerVersion {
266+ version :: Text
267+ , apiVersion :: ApiVersion
268+ , gitCommit :: Text
269+ , goVersion :: Text
270+ , os :: Text
271+ , arch :: Text
272+ , kernelVersion :: Text
273+ , buildTime :: Text
274+ } deriving (Show, Eq, Generic)
275+
276+
277+instance ToJSON DockerVersion where
278+ toJSON = genericToJSON defaultOptions {
279+ fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
280+
281+instance FromJSON DockerVersion where
282+ parseJSON = genericParseJSON defaultOptions {
283+ fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
284+
285+-- instance ToJSON ContainerDetails where
286+-- toJSON = error "TODO"
287+
288+instance FromJSON ContainerDetails where
289+ parseJSON (JSON.Object o) = do
290+ appArmor <- o .: "AppArmorProfile"
291+ args <- o .: "Args"
292+ config <- o .: "Config"
293+ created <- o .: "Created"
294+ driver <- o .: "Driver"
295+ hostConfig <- o .: "HostConfig"
296+ hostnamePath <- o .: "HostnamePath"
297+ hostsPath <- o .: "HostsPath"
298+ logPath <- o .: "LogPath"
299+ id <- o .: "Id"
300+ image <- o .: "Image"
301+ mountLabel <- o .: "MountLabel"
302+ name <- o .: "Name"
303+ networkSettings <- o .: "NetworkSettings"
304+ path <- o .: "Path"
305+ processLabel <- o .: "ProcessLabel"
306+ resolveConfPath <- o .: "ResolvConfPath"
307+ restartCount <- o .: "RestartCount"
308+ state <- o .: "State"
309+ mounts <- o .: "Mounts"
310+ return $ ContainerDetails appArmor args config created driver hostConfig hostnamePath hostsPath logPath id image mountLabel name networkSettings path processLabel resolveConfPath restartCount state mounts
311+ parseJSON _ = fail "ContainerDetails is not an object"
312+
313+instance ToJSON ContainerID where
314+ toJSON (ContainerID cid) = object ["Id" .= cid]
315+
316+instance FromJSON ContainerID where
317+ parseJSON (JSON.Object o) = do
318+ cid <- o .: "Id"
319+ case toContainerID cid of
320+ Nothing ->
321+ fail "Invalid ContainerID"
322+ Just cid ->
323+ return cid
324+ parseJSON _ = fail "ContainerID is not an object."
325+
326+instance ToJSON ImageID where
327+ toJSON (ImageID iid) = JSON.String iid
328+
329+instance FromJSON ImageID where
330+ parseJSON (JSON.String t) = case toImageID t of
331+ Nothing ->
332+ fail "Invalid ImageID"
333+ Just iid ->
334+ return iid
335+ parseJSON _ = fail "ImageID is not an object."
336+
337+data ContainerPortInfo = ContainerPortInfo {
338+ ipAddressInfo :: Maybe Text
339+ , privatePortInfo :: Port
340+ , publicPortInfo :: Maybe Port
341+ , portTypeInfo :: Maybe PortType
342+ } deriving (Eq, Show)
343+
344+instance FromJSON ContainerPortInfo where
345+ parseJSON (JSON.Object v) =
346+ ContainerPortInfo <$> (v .:? "IP")
347+ <*> (v .: "PrivatePort")
348+ <*> (v .:? "PublicPort")
349+ <*> (v .:? "Type")
350+ parseJSON _ = fail "ContainerPortInfo: Not a JSON object."
351+
352+-- For inspecting container details.
353+data NetworkOptions = NetworkOptions {
354+-- ipamConfig :: Maybe Text -- Don't see in 1.24
355+-- , links :: Maybe Text -- Don't see in 1.24
356+-- , aliases :: Maybe Text -- Don't see in 1.24
357+ networkOptionsId :: Text
358+ , networkOptionsEndpointId :: Text
359+ , networkOptionsGateway :: Text
360+ , networkOptionsIpAddress :: Text -- Note: Parse this?
361+ , networkOptionsIpPrefixLen :: Int
362+ , networkOptionsIpV6Gateway :: Maybe Text
363+ , networkOptionsGlobalIPv6Address :: Maybe Text
364+ , networkOptionsGlobalIPv6PrefixLen :: Maybe Int
365+ , networkOptionsMacAddress :: Text
366+ } deriving (Eq, Show)
367+
368+instance FromJSON NetworkOptions where
369+ parseJSON (JSON.Object o) = do
370+ networkId <- o .: "NetworkID"
371+ endpointId <- o .: "EndpointID"
372+ gateway <- o .: "Gateway"
373+ ip <- o .: "IPAddress"
374+ ipLen <- o .: "IPPrefixLen"
375+ ip6Gateway <- o .:? "IPv6Gateway"
376+ globalIP6 <- o .:? "GlobalIPv6Address"
377+ globalIP6Len <- o .:? "GlobalIPv6PrefixLen"
378+ mac <- o .: "MacAddress"
379+ return $ NetworkOptions networkId endpointId gateway ip ipLen ip6Gateway globalIP6 globalIP6Len mac
380+ parseJSON _ = fail "NetworkOptions is not an object"
381+
382+newtype Networks = Networks (M.Map NetworkMode NetworkOptions) -- Note: Is it ever possible that there will be duplicates of network modes?
383+ deriving (Eq, Show)
384+
385+instance FromJSON Networks where
386+ parseJSON (JSON.Object o) = do
387+ Networks <$> HM.foldlWithKey' f (return M.empty) o
388+
389+ where
390+ f accM k' v' = do
391+ acc <- accM
392+ k <- parseJSON $ JSON.String k'
393+ v <- parseJSON v'
394+ return $ M.insert k v acc
395+
396+ parseJSON _ = fail "Networks is not an object"
397+
398+data NetworkSettings = NetworkSettings {
399+ networkSettingsBridge :: Text
400+ , networkSettingsSandboxId :: Text
401+ , networkSettingsHairpinMode :: Bool
402+ , networkSettingsLinkLocalIPv6Address :: Text
403+ , networkSettingsLinkLocalIPv6PrefixLen :: Int
404+ , networkSettingsPorts :: [Port] -- TODO: 1.24 spec is unclear
405+ , networkSettingsSandboxKey :: Text
406+ , networkSettingsSecondaryIPAddresses :: Maybe [Text] -- TODO: 1.24 spec is unclear
407+ , networkSettingsSecondaryIPv6Addresses :: Maybe [Text] -- TODO: 1.24 spec is unclear
408+ , networkSettingsEndpointID :: Text
409+ , networkSettingsGateway :: Text
410+ , networkSettingsGlobalIPv6Address :: Text
411+ , networkSettingsGlobalIPv6PrefixLen :: Int
412+ , networkSettingsIpAddress :: Text
413+ , networkSettingsIpPrefixLen :: Int
414+ , networkSettingsIpv6Gateway
415+ , networkSettingsMacAddress
416+ , networkSettingsNetworks :: Networks
417+ }
418+ deriving (Eq, Show)
419+
420+instance FromJSON NetworkSettings where
421+ parseJSON (JSON.Object o) = do
422+ bridge <- o .: "Bridge"
423+ sandbox <- o .: "SandboxID"
424+ hairpin <- o .: "HairpinMode"
425+ localIP6 <- o .: "LinkLocalIPv6Address"
426+ localIP6Len <- o .: "LinkLocalIPv6PrefixLen"
427+ ports <- o .:? "Ports" .!= []
428+ sandboxKey <- o .: "SandboxKey"
429+ secondaryIP <- o .: "SecondaryIPAddresses"
430+ secondayIP6 <- o .: "SecondaryIPv6Addresses"
431+ endpointID <- o .: "EndpointID"
432+ gateway <- o .: "Gateway"
433+ globalIP6 <- o .: "GlobalIPv6Address"
434+ globalIP6Len <- o .: "GlobalIPv6PrefixLen"
435+ ip <- o .: "IPAddress"
436+ ipLen <- o .: "IPPrefixLen"
437+ ip6Gateway <- o .: "IPv6Gateway"
438+ mac <- o .: "MacAddress"
439+ networks <- o .: "Networks"
440+ return $ NetworkSettings bridge sandbox hairpin localIP6 localIP6Len ports sandboxKey secondaryIP secondayIP6 endpointID gateway globalIP6 globalIP6Len ip ipLen ip6Gateway mac networks
441+ parseJSON _ = fail "NetworkSettings is not an Object"
442+
443+data Container = Container
444+ { containerId :: ContainerID
445+ , containerNames :: [Text]
446+ , containerImageName :: Text
447+ , containerImageId :: ImageID
448+ , containerCommand :: Text
449+ , containerCreatedAt :: Int
450+ , containerStatus :: Status
451+ , containerPorts :: [ContainerPortInfo]
452+ , containerLabels :: Labels
453+ , containerNetworks :: Networks
454+ , containerMounts :: [Mount]
455+ } deriving (Show, Eq)
456+
457+instance FromJSON Container where
458+ parseJSON (JSON.Object v) =
459+ Container <$> (v .: "Id")
460+ <*> (v .: "Names")
461+ <*> (v .: "Image")
462+ <*> (v .: "ImageID")
463+ <*> (v .: "Command")
464+ <*> (v .: "Created")
465+ <*> (v .: "Status")
466+ <*> (v .: "Ports")
467+ <*> (v .: "Labels")
468+ <*> (v .: "NetworkSettings" >>= parseNetworks)
469+ <*> (v .: "Mounts")
470+ where
471+ parseNetworks (JSON.Object v) =
472+ (v .: "Networks") >>= parseJSON
473+ parseNetworks _ = fail "Container NetworkSettings: Not a JSON object."
474+
475+ parseJSON _ = fail "Container: Not a JSON object."
476+
477+data Status = Created | Restarting | Running | Paused | Exited | Dead
478+ deriving (Eq, Show, Generic)
479+
480+instance FromJSON Status where
481+ parseJSON (JSON.String "running") = return Running
482+ parseJSON (JSON.String "created") = return Created -- Note: Guessing on the string values of these.
483+ parseJSON (JSON.String "restarting") = return Restarting
484+ parseJSON (JSON.String "paused") = return Paused
485+ parseJSON (JSON.String "exited") = return Exited
486+ parseJSON (JSON.String "dead") = return Dead
487+ parseJSON _ = fail "Unknown Status"
488+
489+type Digest = Text
490+
491+newtype Labels = Labels (M.Map Name Value) deriving (Eq, Show)
492+
493+instance FromJSON Labels where
494+ parseJSON val = Labels <$> parseJSON val
495+
496+instance ToJSON Labels where
497+ toJSON (Labels kvs) = object [k .= v | (k,v) <- (M.toList kvs)]
498+
499+type Tag = Text
500+
501+data Image = DockerImage {
502+ imageId :: ImageID
503+ , imageCreated :: Integer
504+ , imageParentId :: Maybe ImageID
505+ , imageRepoTags :: [Tag]
506+ , imageRepoDigests :: Maybe [Digest]
507+ , imageSize :: Integer
508+ , imageVirtualSize :: Integer
509+ , imageLabels :: Maybe Labels
510+ } deriving (Show, Eq, Generic)
511+
512+dropImagePrefix :: [a] -> [a]
513+dropImagePrefix = drop 5
514+
515+instance FromJSON Image where
516+ parseJSON = genericParseJSON defaultOptions {
517+ fieldLabelModifier = dropImagePrefix}
518+
519+data CreateOpts = CreateOpts {
520+ containerConfig :: ContainerConfig
521+ , hostConfig :: HostConfig
522+ } deriving (Eq, Show)
523+
524+instance ToJSON CreateOpts where
525+ toJSON (CreateOpts cc hc) = do
526+ let ccJSON = toJSON cc
527+ let hcJSON = toJSON hc
528+ case ccJSON of
529+ JSON.Object (o :: HM.HashMap T.Text JSON.Value) -> do
530+ JSON.Object $ HM.insert "HostConfig" hcJSON o
531+ _ -> error "ContainerConfig is not an object." -- This should never happen.
532+
533+defaultContainerConfig :: Text -> ContainerConfig
534+defaultContainerConfig imageName = ContainerConfig {
535+ hostname=Nothing
536+ , domainname=Nothing
537+ , user=Nothing
538+ , attachStdin=False
539+ , attachStdout=False
540+ , image=imageName
541+ , attachStderr=False
542+ , exposedPorts=ExposedPorts M.empty
543+ , tty=False
544+ , openStdin=False
545+ , stdinOnce=False
546+ , env=[]
547+ , cmd=[]
548+ , volumes=Nothing
549+ , workingDir=Nothing
550+ , entrypoint=Nothing
551+ , networkDisabled=False
552+ , macAddress=Nothing
553+ , labels=Nothing
554+ , stopSignal=SIGTERM
555+ }
556+
557+defaultHostConfig :: HostConfig
558+defaultHostConfig = HostConfig {
559+ binds=[]
560+ , containerIDFile=Nothing
561+ , logConfig=LogDriverConfig JsonFile Nothing
562+ , networkMode=Bridge
563+ , portBindings=PortBindings []
564+ , restartPolicy=RestartOff
565+ , volumeDriver=Nothing
566+ , volumesFrom=[]
567+ , capAdd=[]
568+ , capDrop=[]
569+ , dns=[]
570+ , dnsOptions=[]
571+ , dnsSearch=[]
572+ , extraHosts=[]
573+ , ipcMode=Nothing
574+ , links=[]
575+ , oomScoreAdj=Nothing
576+ , privileged=False
577+ , publishAllPorts=False
578+ , readOnlyRootfs=False
579+ , securityOpt=[]
580+ , shmSize=Nothing
581+ , resources=defaultContainerResources
582+ }
583+
584+defaultContainerResources :: ContainerResources
585+defaultContainerResources = ContainerResources {
586+ cpuShares=Nothing
587+ , blkioWeight=Nothing
588+ , blkioWeightDevice=Nothing
589+ , blkioDeviceReadBps=Nothing
590+ , blkioDeviceWriteBps=Nothing
591+ , blkioDeviceReadIOps=Nothing
592+ , blkioDeviceWriteIOps=Nothing
593+ , cpuPeriod=Nothing
594+ , cpusetCpus=Nothing
595+ , cpusetMems=Nothing
596+ , devices=[]
597+ , kernelMemory=Nothing
598+ , memory=Nothing
599+ , memoryReservation=Nothing
600+ , memorySwap=Nothing
601+ , oomKillDisable=False
602+ , ulimits=[]
603+ }
604+
605+
606+defaultCreateOpts :: T.Text -> CreateOpts
607+defaultCreateOpts imageName = CreateOpts { containerConfig = defaultContainerConfig imageName, hostConfig = defaultHostConfig }
608+
609+-- detachKeys – Override the key sequence for detaching a container.
610+-- Format is a single character [a-Z] or ctrl-<value> where <value> is one of: a-z, @, ^, [, , or _.
611+data DetachKeys = WithCtrl Char | WithoutCtrl Char | DefaultDetachKey deriving (Eq, Show)
612+
613+data StartOpts = StartOpts { detachKeys :: DetachKeys } deriving (Eq, Show)
614+
615+defaultStartOpts :: StartOpts
616+defaultStartOpts = StartOpts { detachKeys = DefaultDetachKey }
617+
618+data DeleteOpts = DeleteOpts {
619+ deleteVolumes :: Bool
620+ , force :: Bool
621+ } deriving (Eq, Show)
622+
623+type Timestamp = Integer
624+data TailLogOpt = Tail Integer | All deriving (Eq, Show)
625+
626+data LogOpts = LogOpts {
627+ stdout :: Bool
628+ , stderr :: Bool
629+ , since :: Maybe Timestamp
630+ , timestamps :: Bool
631+ , tail :: TailLogOpt
632+ } deriving (Eq, Show)
633+
634+defaultLogOpts :: LogOpts
635+defaultLogOpts = LogOpts { stdout = True
636+ , stderr = True
637+ , since = Nothing
638+ , timestamps = True
639+ , tail = All
640+ }
641+
642+data VolumePermission = Rw | Ro deriving (Eq, Show, Generic)
643+
644+-- instance ToJSON VolumePermission where
645+-- toJSON = error "TODO"
646+-- instance FromJSON VolumePermission where
647+-- parseJSON = error "TODO"
648+
649+-- Note: Note really sure what this should be (1.24).
650+-- This is not the same as [Volume] in HostConfig.
651+newtype Volumes = Volumes [Text] -- [Volume]
652+ deriving (Eq, Show)
653+
654+instance FromJSON Volumes where
655+ parseJSON (JSON.Object o) = return $ Volumes $ HM.keys o
656+ parseJSON _ = fail "Volumes is not an object"
657+
658+instance ToJSON Volumes where
659+ toJSON (Volumes vs) = toJSON vs
660+
661+data Volume = Volume { hostSrc :: Text
662+ , containerDest :: Text
663+ , volumePermission :: Maybe VolumePermission
664+ } deriving (Eq, Show)
665+
666+instance FromJSON Volume where
667+ parseJSON (JSON.String t) = case T.split (== ':') t of
668+ [src, dest] -> return $ Volume src dest Nothing
669+ [src, dest, "rw"] -> return $ Volume src dest $ Just Rw
670+ [src, dest, "ro"] -> return $ Volume src dest $ Just Ro
671+ _ -> fail "Could not parse Volume"
672+ parseJSON _ = fail "Volume is not a string"
673+
674+data Device = Device {
675+ pathOnHost :: FilePath
676+ , pathInContainer :: FilePath
677+ , cgroupPermissions :: Text
678+ } deriving (Eq, Show, Generic)
679+
680+instance ToJSON Device where
681+ toJSON = genericToJSON defaultOptions {
682+ fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
683+
684+instance FromJSON Device where
685+ parseJSON = genericParseJSON defaultOptions {
686+ fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
687+
688+type ContainerName = Text
689+
690+data VolumeFrom = VolumeFrom ContainerName (Maybe VolumePermission) deriving (Eq, Show)
691+
692+instance FromJSON VolumeFrom where
693+ parseJSON (JSON.String t) = case T.split (== ':') t of
694+ [vol] -> return $ VolumeFrom vol Nothing
695+ [vol, "rw"] -> return $ VolumeFrom vol $ Just Rw
696+ [vol, "ro"] -> return $ VolumeFrom vol $ Just Ro
697+ _ -> fail "Could not parse VolumeFrom"
698+ parseJSON _ = fail "VolumeFrom is not a string"
699+
700+instance ToJSON VolumeFrom where
701+ toJSON (VolumeFrom n p) = case p of
702+ Nothing -> toJSON $ n <> ":" <> "rw"
703+ Just per -> toJSON $ n <> ":" <> (T.pack $ show per)
704+
705+instance ToJSON Volume where
706+ toJSON (Volume src dest mode) = toJSON $ case mode of
707+ Nothing -> T.concat[src, ":", dest]
708+ Just m -> T.concat[src, ":", dest, ":", (T.pack $ show m)]
709+
710+data Link = Link Text (Maybe Text) deriving (Eq, Show)
711+
712+instance FromJSON Link where
713+ parseJSON (JSON.String t) = case T.split (== ':') t of
714+ [f] -> return $ Link f Nothing
715+ [f,s] -> return $ Link f $ Just s
716+ _ -> fail "Could not parse Link"
717+ parseJSON _ = fail "Link is not a string"
718+
719+instance ToJSON Link where
720+ toJSON (Link n1 n2) = toJSON $ case n2 of
721+ Nothing -> T.concat[n1, ":", n1] -- use same name in container
722+ Just n -> T.concat[n1, ":", n] -- used specified name in container
723+
724+-- { "Type": "<driver_name>", "Config": {"key1": "val1"} }
725+data LogDriverType = JsonFile | Syslog | Journald | Gelf | Fluentd | AwsLogs | Splunk | Etwlogs | LoggingDisabled deriving (Eq, Show)
726+
727+instance FromJSON LogDriverType where
728+ parseJSON (JSON.String "json-file") = return JsonFile
729+ parseJSON (JSON.String "syslog") = return Syslog
730+ parseJSON (JSON.String "journald") = return Journald
731+ parseJSON (JSON.String "gelf") = return Gelf
732+ parseJSON (JSON.String "fluentd") = return Fluentd
733+ parseJSON (JSON.String "awslogs") = return AwsLogs
734+ parseJSON (JSON.String "splunk") = return Splunk
735+ parseJSON (JSON.String "etwlogs") = return Etwlogs
736+ parseJSON (JSON.String "none") = return LoggingDisabled
737+ parseJSON _ = fail "Unknown LogDriverType"
738+
739+instance ToJSON LogDriverType where
740+ toJSON JsonFile = JSON.String "json-file"
741+ toJSON Syslog = JSON.String "syslog"
742+ toJSON Journald = JSON.String "journald"
743+ toJSON Gelf = JSON.String "gelf"
744+ toJSON Fluentd = JSON.String "fluentd"
745+ toJSON AwsLogs = JSON.String "awslogs"
746+ toJSON Splunk = JSON.String "splunk"
747+ toJSON Etwlogs = JSON.String "etwlogs"
748+ toJSON LoggingDisabled = JSON.String "none"
749+
750+newtype LogDriverOptions = LogDriverOptions (M.Map Text Text) deriving (Eq, Show)
751+
752+instance FromJSON LogDriverOptions where
753+ parseJSON (JSON.Object o) = do
754+ LogDriverOptions <$> HM.foldlWithKey' f (return M.empty) o
755+
756+ where
757+ f accM k (JSON.String v) = do
758+ acc <- accM
759+ return $ M.insert k v acc
760+ f _ _ _ = fail "Value of LogDriverOptions is not a string"
761+
762+ parseJSON JSON.Null = return $ LogDriverOptions M.empty
763+ parseJSON _ = fail "LogDriverOptions is not an object"
764+
765+instance ToJSON LogDriverOptions where
766+ toJSON (LogDriverOptions mp) = toJSON mp
767+
768+data LogDriverConfig = LogDriverConfig LogDriverType (Maybe LogDriverOptions) deriving (Eq, Show)
769+
770+instance ToJSON LogDriverConfig where
771+ toJSON (LogDriverConfig driverType driverOptions) = object ["Type" .= driverType, "Config" .= driverOptions]
772+
773+instance FromJSON LogDriverConfig where
774+ parseJSON (JSON.Object o) = do
775+ typ <- o .: "Type"
776+ opts <- o .: "Config"
777+ return $ LogDriverConfig typ opts
778+ parseJSON _ = fail "LogDriverConfig is not an object"
779+
780+-- TODO: Add container:<name|id> mode
781+data NetworkMode = Bridge | Host | NetworkDisabled
782+ deriving (Eq, Show, Ord)
783+
784+instance FromJSON NetworkMode where
785+ parseJSON (JSON.String "bridge") = return Bridge
786+ parseJSON (JSON.String "host") = return Host -- Note: Guessing on these.
787+ parseJSON (JSON.String "none") = return NetworkDisabled
788+ parseJSON _ = fail "Unknown NetworkMode"
789+
790+instance ToJSON NetworkMode where
791+ toJSON Bridge = JSON.String "bridge"
792+ toJSON Host = JSON.String "host"
793+ toJSON NetworkDisabled = JSON.String "none"
794+
795+data PortType = TCP | UDP deriving (Eq, Show, Generic, Read, Ord)
796+
797+instance ToJSON PortType where
798+ toJSON TCP = "tcp"
799+ toJSON UDP = "udp"
800+
801+instance FromJSON PortType where
802+ parseJSON val = case val of
803+ "tcp" -> return TCP
804+ "udp" -> return UDP
805+ _ -> fail "PortType: Invalid port type."
806+
807+newtype NetworkInterface = NetworkInterface Text deriving (Eq, Show)
808+
809+instance FromJSON NetworkInterface where
810+ parseJSON (JSON.String v) = return $ NetworkInterface v
811+ parseJSON _ = fail "Network interface is not a string."
812+
813+instance ToJSON NetworkInterface where
814+ toJSON (NetworkInterface i) = JSON.String i
815+
816+-- | This datastructure models mapping a Port from the container onto the
817+-- host system s that the service running in the container can be accessed from
818+-- the outside world. We either map a port onto all interfaces (default) or onto a specific
819+-- interface like `127.0.0.1`.
820+newtype PortBindings = PortBindings [PortBinding]
821+ deriving (Eq, Show)
822+
823+-- { <port>/<protocol>: [{ "HostPort": "<port>" }] }
824+data PortBinding = PortBinding {
825+ containerPort :: Port
826+ , portType :: PortType
827+ , hostPorts :: HostPorts
828+ } deriving (Eq, Show)
829+
830+
831+portAndType2Text :: Port -> PortType -> Text
832+portAndType2Text p t = (T.pack $ show p) <> "/" <> (T.pack $ show t)
833+
834+instance ToJSON PortBinding where
835+ toJSON (PortBinding {..}) = object [portAndType2Text containerPort portType .= hostPorts]
836+
837+instance FromJSON PortBindings where
838+ parseJSON (JSON.Object o) = do
839+ PortBindings <$> HM.foldlWithKey' f (return []) o
840+
841+ where
842+ f accM k v = case T.split (== '/') k of
843+ [port', portType'] -> do
844+ port <- parseIntegerText port'
845+ portType <- parseJSON $ JSON.String portType'
846+ acc <- accM
847+ hps <- parseJSON v
848+ return $ (PortBinding port portType hps):acc
849+ _ ->
850+ fail "Could not parse PortBindings"
851+ parseJSON _ = fail "PortBindings is not an object"
852+
853+instance ToJSON PortBindings where
854+ toJSON (PortBindings []) = JSON.Object HM.empty
855+ toJSON (PortBindings (p:ps)) = toJSON (p:ps)
856+
857+data HostPort = HostPort NetworkInterface Port
858+ deriving (Eq, Show)
859+
860+instance ToJSON HostPort where
861+ toJSON (HostPort i p) = object ["HostPort" .= p, "HostIP" .= i]
862+
863+instance FromJSON HostPort where
864+ parseJSON (JSON.Object o) = do
865+ p <- o .: "HostPort"
866+ i <- o .: "HostIP"
867+ return $ HostPort i p
868+ parseJSON _ = fail "HostPort is not an object."
869+
870+newtype HostPorts = HostPorts [HostPort]
871+ deriving (Eq, Show)
872+
873+
874+instance ToJSON HostPorts where
875+ toJSON (HostPorts hps) = toJSON hps
876+
877+instance FromJSON HostPorts where
878+ parseJSON (JSON.Object o) = do
879+ HostPorts <$> HM.foldlWithKey' f (return []) o
880+
881+ where
882+ f accM k v = do
883+ acc <- accM
884+ p' <- parseJSON v
885+ p <- parseIntegerText p'
886+ return $ (HostPort (NetworkInterface k) p):acc
887+ parseJSON _ = fail "HostPorts is not an object"
888+
889+-- { "Name": "on-failure" , "MaximumRetryCount": 2}
890+type RetryCount = Integer
891+data RestartPolicy = RestartAlways | RestartUnlessStopped | RestartOnFailure RetryCount | RestartOff deriving (Eq, Show)
892+
893+instance FromJSON RestartPolicy where
894+ parseJSON (JSON.Object o) = do
895+ (name :: Text) <- o .: "Name"
896+ case name of
897+ "always" -> return RestartAlways
898+ "unless-stopped" -> return RestartUnlessStopped
899+ "on-failure" -> do
900+ retry <- o .: "MaximumRetryCount"
901+ return $ RestartOnFailure retry
902+ "" -> return RestartOff -- Note: Is this correct? 1.24 spec is unclear.
903+ _ -> fail "Could not parse RestartPolicy"
904+ parseJSON _ = fail "RestartPolicy is not an object"
905+
906+instance ToJSON RestartPolicy where
907+ toJSON RestartAlways = object ["Name" .= JSON.String "always"]
908+ toJSON RestartUnlessStopped = object ["Name" .= JSON.String "unless-stopped"]
909+ toJSON (RestartOnFailure c) = object ["Name" .= JSON.String "on-failure", "MaximumRetryCount" .= c]
910+ toJSON RestartOff = object ["Name" .= JSON.String "off"]
911+
912+data Isolation = Default | Process | Hyperv deriving (Eq, Show)
913+
914+newtype UTSMode = UTSMode Text deriving (Eq, Show)
915+
916+-- TODO: Add Tmpfs : List of tmpfs (mounts) used for the container
917+-- TODO: Add UTSMode : UTS namespace to use for the container
918+-- TODO: Sysctls map[string]string `json:",omitempty"` // List of Namespaced sysctls used for the container
919+data HostConfig = HostConfig
920+ { binds :: [Volume]
921+ , containerIDFile :: Maybe FilePath -- 1.24: Only in responses, not create
922+ , logConfig :: LogDriverConfig
923+ , networkMode :: NetworkMode
924+ , portBindings :: PortBindings
925+ , restartPolicy :: RestartPolicy
926+ , volumeDriver :: Maybe Text
927+ , volumesFrom :: [VolumeFrom]
928+ , capAdd :: [Text]
929+ , capDrop :: [Text]
930+ , dns :: [Text]
931+ , dnsOptions :: [Text]
932+ , dnsSearch :: [Text]
933+ , extraHosts :: [Text]
934+ -- , groupAdd :: [Integer] -- 1.24: Missing from inspecting container details... Going to omit for now.
935+ , ipcMode :: Maybe Text -- 1.24: Only in inspect, not create
936+ , links :: [Link]
937+ , oomScoreAdj :: Maybe Integer
938+ -- , pidMode :: Text -- 1.24: Don't see pidMode, just pidsLimit
939+ , privileged :: Bool
940+ , publishAllPorts :: Bool
941+ , readOnlyRootfs :: Bool
942+ , securityOpt :: [Text]
943+ -- , utsMode :: UTSMode -- 1.24: Don't see this
944+ , shmSize :: Maybe Integer
945+ -- , consoleSize :: Integer -- 1.24: Don't see this
946+ -- , isolation :: Isolation -- 1.24: Don't see this
947+ , resources :: ContainerResources
948+ } deriving (Eq, Show, Generic)
949+
950+instance FromJSON HostConfig where
951+ parseJSON = genericParseJSON defaultOptions {
952+ fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
953+
954+instance ToJSON HostConfig where
955+ toJSON = genericToJSON defaultOptions {
956+ fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
957+
958+
959+-- { "Name": <name>, "Soft": <soft limit>, "Hard": <hard limit> }
960+-- { "Name": "nofile", "Soft": 1024, "Hard": 2048 }
961+data Ulimit = Ulimit {
962+ ulimitName :: Text
963+ , ulimitSoft :: Integer
964+ , ulimitHard :: Integer
965+ } deriving (Eq, Show, Generic)
966+
967+instance FromJSON Ulimit where
968+ parseJSON = genericParseJSON defaultOptions {
969+ fieldLabelModifier = drop 5}
970+
971+instance ToJSON Ulimit where
972+ toJSON = genericToJSON defaultOptions {
973+ fieldLabelModifier = drop 5}
974+
975+data ContainerResources = ContainerResources {
976+ cpuShares :: Maybe Integer
977+ -- , cgroupParent :: Text -- 1.24: Missing from inspecting container details... Going to omit for now.
978+ , blkioWeight :: Maybe Text
979+ , blkioWeightDevice :: Maybe Text
980+ , blkioDeviceReadBps :: Maybe Text
981+ , blkioDeviceWriteBps :: Maybe Text
982+ , blkioDeviceReadIOps :: Maybe Text
983+ , blkioDeviceWriteIOps :: Maybe Text
984+ , cpuPeriod :: Maybe Integer
985+ -- , cpuQuota :: Integer -- 1.24: Missing from inspecting container details... Going to omit for now.
986+ , cpusetCpus :: Maybe Integer
987+ , cpusetMems :: Maybe Text
988+ , devices :: [Device]
989+ -- , diskQuota :: Integer -- Don't see this ins 1.24.
990+ , kernelMemory :: Maybe Integer
991+ , memory :: Maybe Integer
992+ , memoryReservation :: Maybe Integer
993+ , memorySwap :: Maybe Integer
994+ -- , memorySwappiness :: Integer -- 1.24: Missing from inspecting container details... Going to omit for now.
995+ , oomKillDisable :: Bool
996+ -- , pidsLimit :: Integer -- 1.24: Missing from inspecting container details... Going to omit for now.
997+ , ulimits :: [Ulimit]
998+ -- TODO: Missing from 1.24
999+ -- StorageOpt :: [(Text, Text)]
1000+ -- VolumeDriver :: ??
1001+ -- EndpointsConfig :: ??
1002+ -- TODO: Only in inspect container in 1.24
1003+ -- CpuPercent :: Int +
1004+ -- MaximumIOps :: Int +
1005+ -- MaximumIOBps :: Int +
1006+ -- LxcConf :: [??] +
1007+ } deriving (Eq, Show, Generic)
1008+
1009+instance ToJSON ContainerResources where
1010+ toJSON = genericToJSON defaultOptions {
1011+ fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
1012+
1013+instance FromJSON ContainerResources where
1014+ parseJSON = genericParseJSON defaultOptions {
1015+ fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
1016+
1017+type Port = Integer
1018+
1019+type Name = Text
1020+type Value = Text
1021+
1022+data EnvVar = EnvVar Name Value
1023+ deriving (Eq, Show)
1024+
1025+instance FromJSON EnvVar where
1026+ parseJSON (JSON.String env) =
1027+ let (n, v') = T.break (== '=') env in
1028+ let v = T.drop 1 v' in
1029+ return $ EnvVar n v
1030+ parseJSON _ = fail "EnvVar is not a string"
1031+
1032+instance ToJSON EnvVar where
1033+ toJSON (EnvVar n v) = object [n .= v]
1034+
1035+-- | ExposedPorts represent a enumeraton of all the ports (and their type)
1036+-- that a container should expose to other containers or the host system.
1037+-- `NOTE`: This does not automatically expose the ports onto the host
1038+-- system but rather it just tags them. It's best to be used with
1039+-- the PublishAllPorts flag. It is also useful for
1040+-- the daemon to know which Environment variables to
1041+-- inject into a container linking to our container.
1042+-- Example linking a Postgres container named db would inject the following
1043+-- environment variables automatically if we set the corresponding
1044+-- ExposedPorts:
1045+-- DB_PORT_5432_TCP_PORT="5432"
1046+-- DB_PORT_5432_TCP_PROTO="tcp"
1047+-- DB_PORT_5432_TCP="tcp://172.17.0.1:5432"
1048+newtype ExposedPorts = ExposedPorts (M.Map Port PortType) deriving (Eq, Show)
1049+-- JP: Should this be ExposedPorts [(Port, PortType)]?
1050+
1051+instance FromJSON ExposedPorts where
1052+ parseJSON (JSON.Object o) = do
1053+ ExposedPorts <$> HM.foldlWithKey' f (return M.empty) o
1054+
1055+ where
1056+ f accM k _ = case T.split (== '/') k of
1057+ [port', portType'] -> do
1058+ port <- parseIntegerText port'
1059+ portType <- parseJSON $ JSON.String portType'
1060+ acc <- accM
1061+ return $ M.insert port portType acc
1062+ _ ->
1063+ fail "Could not parse ExposedPorts"
1064+ parseJSON _ = fail "ExposedPorts is not an object."
1065+
1066+instance ToJSON ExposedPorts where
1067+ toJSON (ExposedPorts kvs) = object [((T.pack $ show p) <> "/" <> (T.pack $ show t)) .= JSON.Object HM.empty | (p,t) <- (M.toList kvs)]
1068+
1069+
1070+data ContainerConfig = ContainerConfig {
1071+ hostname :: Maybe Text
1072+ , domainname :: Maybe Text
1073+ , user :: Maybe Text
1074+ , attachStdin :: Bool
1075+ , attachStdout :: Bool
1076+ , attachStderr :: Bool
1077+ , exposedPorts :: ExposedPorts
1078+ -- , publishService :: Text -- Don't see this in 1.24
1079+ , tty :: Bool
1080+ , openStdin :: Bool
1081+ , stdinOnce :: Bool
1082+ , env :: [EnvVar]
1083+ , cmd :: [Text]
1084+ -- , argsEscaped :: Bool -- Don't see this in 1.24
1085+ , image :: Text
1086+ , volumes :: Maybe Volumes
1087+ , workingDir :: Maybe FilePath
1088+ , entrypoint :: Maybe Text -- Can be null?
1089+ , networkDisabled :: Bool
1090+ , macAddress :: Maybe Text
1091+ -- , onBuild :: Text -- For 1.24, only see this in the inspect response.
1092+ , labels :: Maybe Labels
1093+ , stopSignal :: Signal
1094+ } deriving (Eq, Show, Generic)
1095+
1096+instance ToJSON ContainerConfig where
1097+ toJSON = genericToJSON defaultOptions {
1098+ fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
1099+
1100+instance FromJSON ContainerConfig where
1101+ parseJSON = genericParseJSON defaultOptions {
1102+ fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
1103+
1104+parseIntegerText :: (Monad m) =>Text -> m Integer
1105+parseIntegerText t = case readMaybe $ T.unpack t of
1106+ Nothing ->
1107+ fail "Could not parse Integer"
1108+ Just i ->
1109+ return i
stack.yaml+30−12
@@ -1,14 +1,32 @@
1−flags: {}
1+# For more information, see: https://github.com/commercialhaskell/stack/blob/release/doc/yaml_configuration.md
2+
3+# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2)
4+resolver: lts-5.18
5+
6+# Local packages, usually specified by relative directory name
27packages:
38- '.'
4−extra-deps:
5−- wreq-0.4.0.0
6−- pipes-http-1.0.2
7−- pipes-group-1.0.2
8−- pipes-safe-2.2.2
9−- pipes-text-0.0.0.16
10−- pipes-bytestring-2.1.1
11−- psqueues-0.2.0.2
12−- HsOpenSSL-0.11.1.1
13−- http-client-openssl-0.2.0.1
14−resolver: lts-1.15
9+
10+# Packages to be pulled from upstream that are not in the resolver (e.g., acme-missiles-0.3)
11+extra-deps: []
12+
13+# Override default flag values for local packages and extra-deps
14+flags: {}
15+
16+# Extra package databases containing global packages
17+extra-package-dbs: []
18+
19+# Control whether we use the GHC we find on the path
20+# system-ghc: true
21+
22+# Require a specific version of stack, using version ranges
23+# require-stack-version: -any # Default
24+# require-stack-version: >= 0.1.4.0
25+
26+# Override the architecture used by stack, especially useful on Windows
27+# arch: i386
28+# arch: x86_64
29+
30+# Extra directories used by stack for building
31+# extra-include-dirs: [/path/to/dir]
32+# extra-lib-dirs: [/path/to/dir]
tests/Dockerfile+1−6
@@ -1,9 +1,4 @@
11FROM debian:jessie
22
3−RUN apt-get update && apt-get install -y --no-install-recommends \
4− python
5−
6−RUN echo "#!/usr/bin/python\nprint '123'" > /entrypoint.py
7−RUN chmod +x /entrypoint.py
8−CMD ["python", "entrypoint.py"]
3+CMD ["/bin/bash", "-c", "echo 123"]
94
tests/tests.hs+53−25
@@ -1,7 +1,6 @@
1−module Main where
1+{-# LANGUAGE OverloadedStrings #-}
22
3−import Network.Docker
4−import Network.Docker.Types
3+module Main where
54
65import qualified Test.QuickCheck.Monadic as QCM
76import Test.Tasty
@@ -9,42 +8,62 @@ import Test.Tasty.HUnit
98import qualified Test.Tasty.QuickCheck as QC
109
1110import Control.Concurrent (threadDelay)
11+import Control.Monad.IO.Class
12+import Control.Monad.Trans.Class (MonadTrans, lift)
1213import qualified Data.ByteString as B
1314import qualified Data.ByteString.Char8 as C
1415import qualified Data.ByteString.Lazy as BL
1516import Data.Maybe (fromJust, isJust)
17+import Data.Monoid
1618import Data.Text (unpack)
19+import Network.Connection (TLSSettings (..))
20+import Network.HTTP.Client (newManager)
21+import Network.HTTP.Client.TLS
1722import Network.HTTP.Types.Status
1823import System.Process (system)
1924
20−opts = defaultClientOpts { baseUrl = "http://127.0.0.1:2375/" }
25+import Docker
26+import Docker.Types
27+
28+
29+-- opts = defaultClientOpts
2130
22−test_image_name = "docker-hs-test"
31+testImageName = "docker-hs-test"
2332
2433toStrict1 = B.concat . BL.toChunks
2534
35+runDocker f = do
36+ -- let opts = defaultClientOpts {baseUrl = "https://127.0.0.1:2376/"}
37+ -- params' <- clientParamsWithClientAuthentication "127.0.0.1" 2376 "~/.docker/test-client-key.pem" "~/.docker/test-client-cert.pem" >>= fromRight
38+ -- params <- clientParamsSetCA params' "~/.docker/test-ca.pem"
39+ -- let settings = mkManagerSettings (TLSSettings params) Nothing
40+ -- mgr <- newManager settings
41+ runDockerT (defaultClientOpts, defaultHttpHandler) f
42+
2643checkDockerVersion :: IO ()
27−checkDockerVersion = do v <- getDockerVersion opts
28− assert $ isJust v
44+checkDockerVersion = runDocker $ do
45+ v <- getDockerVersion
46+ lift $ assert $ isRight v
2947
3048findTestImage :: IO ()
31−findTestImage = do images <- getDockerImages opts
32− let x = fmap (filter ((== [test_image_name++":latest"]) . _repoTags)) images
33− assert $ fmap length x == Just 1
49+findTestImage = runDocker $ do
50+ images <- listImages defaultListOpts >>= fromRight
51+ let x = filter ((== [testImageName<>":latest"]) . imageRepoTags) images
52+ lift $ assert $ length x == 1
3453
3554runAndReadLog :: IO ()
36−runAndReadLog = do containerId <- createContainer opts (defaultCreateOpts {_image = test_image_name++":latest"})
37− assert $ isJust containerId
38− let c = unpack $ fromJust containerId
39− status1 <- startContainer opts c defaultStartOpts
40− threadDelay 300000 -- give 300ms for the application to finish
41− assert $ status1 == status204
42− status2 <- killContainer opts c
43− logs <- getContainerLogs opts c
44− assert $ status2 == status204
45− assert $ (C.pack "123") `C.isInfixOf` (toStrict1 logs)
46− status3 <- deleteContainer opts c
47− assert $ status3 == status204
55+runAndReadLog = runDocker $ do
56+ containerId <- createContainer (defaultCreateOpts (testImageName <> ":latest"))
57+ c <- fromRight containerId
58+ status1 <- startContainer defaultStartOpts c
59+ lift $ threadDelay 300000 -- give 300ms for the application to finish
60+ lift $ assert $ status1 == Right ()
61+ status2 <- killContainer SIGTERM c
62+ logs <- getContainerLogs defaultLogOpts c >>= fromRight
63+ lift $ assert $ status2 == Right ()
64+ lift $ assert $ (C.pack "123") `C.isInfixOf` (toStrict1 logs)
65+ status3 <- deleteContainer (DeleteOpts True True) c
66+ lift $ assert $ status3 == Right ()
4867
4968
5069tests :: TestTree
@@ -53,10 +72,19 @@ tests = testGroup "Metrics tests" [
5372 testCase "Find image by name" findTestImage,
5473 testCase "Run a dummy container and read its log" runAndReadLog]
5574
56−setup :: IO()
57−setup = system ("docker build -t "++test_image_name++" tests") >> return ()
75+setup :: IO ()
76+setup = system ("docker build -t "++unpack testImageName++" tests") >> return ()
5877
59−main :: IO()
78+main :: IO ()
6079main = do
6180 setup
6281 defaultMain tests
82+
83+isRight (Left _) = False
84+isRight (Right _) = True
85+
86+fromRight :: (MonadIO m, Show l) => Either l r -> m r
87+fromRight (Left l) = do
88+ liftIO $ assertFailure $ "Left: " ++ show l
89+ undefined
90+fromRight (Right r) = return r