on master help sign in
Squash commits into one.

...leaving the original commit messages below: - Docker.Http compiles - preliminary updates - Using requestHelper for a few more functions - check expected status codes for a given endpoint - split up requestHelper by adding parseResponse. Updated create opts for docker api 1.24 - DockerT instances - outline for inspect container. fixed some warnings - generalize http handlers - monadio instance - switch to statusCodeToError from expectedStatusCode - added tlssettings for client auth - small change - hide constructors for ContainerID and ImageID - preliminary whitelist for ImageID and ContainerID - types for container details and network settings - include HostConfig and NetworkSettings in ContainerDetails - some aeson instances - more aeson instances - Implemented getContainerLogs - some aeson instances - more aeson instances - more aeson instances - Re-export some functionality and hide others - rewrote tests for new api - added supported ciphers for ssl - tests for ssl - ability to set CA - takes care of #10 - use CA in tests Squashing more commits from both myself(Deni) and James into this one: - Get rid of compiler warnings - Mostly just unused imports and such. - Removes old commented out code - Adds defaultCreateOpts and some automatic style changes. - Use local docker daemon in tests (for now without tls...) - send POST bodies - Sets application/json headers expliclty. - Makes tests pass. - Added ExposedPorts data type. Removed some manual to/from json instances in favour of ones generated by generics. - Adds more json instance mostly pertaining to HostConfig - Removes Storage driver options from HostConfig It's unclear from the docs and the go code how this is to be used and what it's for. - Adds a better way to serialize CreateOpts

commit79d00d9c538e100921cdd1253b359739103eeb21 authorJames Parker <dev@jamesparker.me> dateThu 7 Jul 2016, 16:27 (10 years ago) parent9c61413
7 files changed+1225−342
docker.cabal+25−9 src/Docker.hs+108−88 src/Docker/Http.hs+163−41 src/Docker/Internal.hs+42−27 src/Docker/Types.hs+835−152 stack.yaml+1−1 tests/tests.hs+51−24
docker.cabal+25−9
@@ -15,35 +15,51 @@ cabal-version: >=1.10
1515stability: experimental
1616
1717library
18− default-extensions: DeriveGeneric, OverloadedStrings, ScopedTypeVariables, ExplicitForAll, MultiParamTypeClasses, FlexibleContexts, GeneralizedNewtypeDeriving, RankNTypes, DeriveFunctor
18+ default-extensions: DeriveGeneric, OverloadedStrings, ScopedTypeVariables, ExplicitForAll, MultiParamTypeClasses, FlexibleContexts, GeneralizedNewtypeDeriving, RankNTypes, DeriveFunctor, FlexibleInstances
1919 hs-source-dirs: src
2020 exposed-modules: Docker, Docker.Types, Docker.Internal, Docker.Http
2121 -- other-modules: Docker.Internal
2222 build-depends: base >= 4.7 && < 5
23− , lifted-base
24− , lens
2523 , aeson
26− , lens-aeson
24+ , blaze-builder
2725 , bytestring
26+ , containers
27+ , connection
28+ , data-default-class
2829 , exceptions
29− , text
3030 , http-client
3131 , http-types
32− , blaze-builder
33− , containers
32+ , lens
33+ , lens-aeson
34+ , lifted-base
35+ , mtl
3436 , network >= 2.6
3537 , network-uri
36− , mtl
38+ , text
39+ , time
40+ , tls >= 1.3.7
3741 , transformers
42+ , unordered-containers
43+ , x509
44+ , x509-store
45+ , x509-system
3846 default-language: Haskell2010
47+ ghc-options: -Wall -fno-warn-name-shadowing
3948
4049test-suite docker-hs-tests
4150 build-depends: base >= 4.7 && < 5
51+ , bytestring
52+ , connection
4253 , docker
54+ , http-client
55+ , http-client-tls
56+ , http-types
4357 , tasty
58+ , tasty-hunit
4459 , tasty-quickcheck
60+ , text
61+ , transformers
4562 , QuickCheck
46− , tasty-hunit
4763 , process
4864 type: exitcode-stdio-1.0
4965 main-is: tests.hs
src/Docker.hs+108−88
@@ -1,98 +1,118 @@
11{-# LANGUAGE ExplicitForAll #-}
22{-# LANGUAGE ScopedTypeVariables #-}
33
4−module Docker where
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
530
31+import Control.Monad.Except (ExceptT (..), runExceptT, throwError)
632import Control.Monad.Reader (ask, lift)
7−import Data.Aeson (decode)
8−import Data.Maybe (fromJust)
9−import Network.HTTP.Client (responseBody)
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)
1037import Network.HTTP.Types (StdMethod (..))
1138
1239import Docker.Http
13−import Docker.Internal
1440import Docker.Types
1541
16−getDockerVersion :: forall m. Monad m => DockerT m DockerVersion
17−getDockerVersion = do
18− (opts, httpHandler) <- ask
19− let request = fromJust $ mkHttpRequest GET VersionEndpoint opts
20− response <- lift $ httpHandler request
21− -- let res = decode $ responseBody response
22− -- return $ fromJust res
23− case response of
24− Right r -> do
25− let res = decode $ responseBody r
26− fromJust res
27− Left err -> err
28−
29−listContainers :: forall m. Monad m => ListOpts -> DockerT m [Container]
30−listContainers lopts = do
31− (opts, httpHandler) <- ask
32− let request = fromJust $ mkHttpRequest GET (ListContainersEndpoint lopts) opts
33− response <- lift . lift $ (httpHandler request :: m Response)
34− let res = decode $ responseBody response
35− return $ fromJust res
36−
37−listImages :: forall m. Monad m => ListOpts -> DockerT m [Image]
38−listImages lopts = do
39− (opts, httpHandler) <- ask
40− let request = fromJust $ mkHttpRequest GET (ListImagesEndpoint lopts) opts
41− response <- lift . lift $ (httpHandler request :: m Response)
42− let res = decode $ responseBody response
43− return $ fromJust res
44−
45−createContainer :: forall m. Monad m => CreateOpts -> DockerT m ContainerID
46−createContainer = undefined
47−
48−startContainer :: forall m. Monad m => StartOpts -> ContainerID -> DockerT m ContainerID
49−startContainer sopts cid = do
50− (opts, httpHandler) <- ask
51− let request = fromJust $ mkHttpRequest POST (StartContainerEndpoint sopts cid) opts
52− _ <- lift . lift $ (httpHandler request :: m Response)
53− return cid
54−
55−stopContainer :: forall m. Monad m => Timeout -> ContainerID ->DockerT m ContainerID
56−stopContainer t cid = do
57− (opts, httpHandler) <- ask
58− let request = fromJust $ mkHttpRequest POST (StopContainerEndpoint t cid) opts
59− _ <- lift . lift $ (httpHandler request :: m Response)
60− return cid
61−
62−killContainer :: forall m. Monad m => Signal -> ContainerID -> DockerT m ContainerID
63−killContainer s cid = do
64− (opts, httpHandler) <- ask
65− let request = fromJust $ mkHttpRequest POST (KillContainerEndpoint s cid) opts
66− _ <- lift . lift $ (httpHandler request :: m Response)
67− return cid
68−
69−restartContainer :: forall m. Monad m => Timeout -> ContainerID -> DockerT m ContainerID
70−restartContainer t cid = do
71− (opts, httpHandler) <- ask
72− let request = fromJust $ mkHttpRequest POST (RestartContainerEndpoint t cid) opts
73− _ <- lift . lift $ (httpHandler request :: m Response)
74− return cid
75−
76−pauseContainer :: forall m. Monad m => ContainerID -> DockerT m ContainerID
77−pauseContainer cid = do
78− (opts, httpHandler) <- ask
79− let request = fromJust $ mkHttpRequest POST (PauseContainerEndpoint cid) opts
80− _ <- lift . lift $ (httpHandler request :: m Response)
81− return cid
82−
83−unpauseContainer :: forall m. Monad m => ContainerID -> DockerT m ContainerID
84−unpauseContainer cid = do
85− (opts, httpHandler) <- ask
86− let request = fromJust $ mkHttpRequest GET (UnpauseContainerEndpoint cid) opts
87− _ <- lift . lift $ (httpHandler request :: m Response)
88− return cid
89−
90−deleteContainer :: forall m. Monad m => DeleteOpts -> ContainerID -> DockerT m ContainerID
91−deleteContainer dopts cid = do
92− (opts, httpHandler) <- ask
93− let request = fromJust $ mkHttpRequest DELETE (DeleteContainerEndpoint dopts cid) opts
94− _ <- lift . lift $ (httpHandler request :: m Response)
95− return cid
96−
97−getContainerLogs :: forall m. Monad m => LogOpts -> ContainerID -> DockerT m ()
98−getContainerLogs logopts cid = undefined
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.hs+163−41
@@ -3,29 +3,42 @@
33
44module Docker.Http where
55
6−import Control.Applicative
7−import Control.Exception (catch, try)
8−import Control.Monad.Except
9−import Control.Monad.IO.Class (liftIO)
10−import Control.Monad.Reader
11−import Control.Monad.Reader.Class
12−import Control.Monad.Trans.Except
6+import Control.Monad.Reader (ReaderT, runReaderT)
7+import qualified Data.ByteString.Char8 as BSC
138import qualified Data.ByteString.Lazy as BL
14−import Data.Text as T
9+import Data.Default.Class (def)
10+import Data.Monoid ((<>))
1511import Data.Text.Encoding (encodeUtf8)
16−import Data.Typeable (Typeable)
12+import Data.X509 (CertificateChain (..))
13+import Data.X509.CertificateStore (makeCertificateStore)
14+import Data.X509.File (readKeyFile, readSignedObject)
1715import Network.HTTP.Client (defaultManagerSettings, httpLbs,
1816 managerRawConnection, method,
19− newManager, parseUrl)
17+ newManager, parseUrl,
18+ requestBody, requestHeaders)
2019import qualified Network.HTTP.Client as HTTP
2120import Network.HTTP.Client.Internal (makeConnection)
22−import Network.HTTP.Types (StdMethod)
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
2336import qualified Network.Socket as S
2437import qualified Network.Socket.ByteString as SBS
25−import System.IO.Error (catchIOError, ioError)
2638
27−import Docker.Internal (getEndpoint)
28−import Docker.Types (DockerClientOpts, Endpoint,
39+import Docker.Internal (getEndpoint,
40+ getEndpointRequestBody)
41+import Docker.Types (DockerClientOpts, Endpoint (..),
2942 baseUrl)
3043
3144type Request = HTTP.Request
@@ -34,21 +47,33 @@ type HttpVerb = StdMethod
3447type HttpHandler m = Request -> m (Either DockerError Response)
3548
3649data 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.
3753 | GenericDockerError Text deriving (Eq, Show, Typeable)
3854
3955newtype DockerT m a = DockerT {
4056 unDockerT :: Monad m => ReaderT (DockerClientOpts, HttpHandler m) m a
4157 } deriving (Functor) -- Applicative, Monad, MonadReader, MonadError, MonadTrans
4258
59+instance Applicative m => Applicative (DockerT m) where
60+ pure a = DockerT $ pure a
61+ (<*>) (DockerT f) (DockerT v) = DockerT $ f <*> v
4362
44−-- instance Applicative m => Applicative (DockerT m) where
45−-- pure a = DockerT $ pure a
46−-- (<*>) (DockerT f) (DockerT v) = DockerT $ f <*> v
63+instance Monad m => Monad (DockerT m) where
64+ (DockerT m) >>= f = DockerT $ m >>= unDockerT . f
65+ return = pure
4766
67+instance Monad m => MonadReader (DockerClientOpts, HttpHandler m) (DockerT m) where
68+ ask = DockerT ask
69+ local f (DockerT m) = DockerT $ local f m
4870
49−-- type DockerT m a = ReaderT (DockerClientOpts, HttpHandler m) (ExceptT DockerError m) a
71+instance MonadTrans DockerT where
72+ lift m = DockerT $ lift m
73+
74+instance MonadIO m => MonadIO (DockerT m) where
75+ liftIO = lift . liftIO
5076
51−-- runDockerT :: forall m a. Monad m => (DockerClientOpts, HttpHandler m) -> DockerT m a -> m (Either DockerError a)
5277runDockerT :: Monad m => (DockerClientOpts, HttpHandler m) -> DockerT m a -> m a
5378runDockerT (opts, h) r = runReaderT (unDockerT r) (opts, h)
5479
@@ -60,22 +85,22 @@ mkHttpRequest :: HttpVerb -> Endpoint -> DockerClientOpts -> Maybe Request
6085mkHttpRequest verb e opts = request
6186 where fullE = T.unpack (baseUrl opts) ++ T.unpack (getEndpoint e)
6287 initialR = parseUrl fullE
63− request = case initialR of
64− Just ir -> return $ ir { method = (encodeUtf8 . T.pack $ show verb) } -- , requestBody = RequestBodyLBS $ encode requestObject }
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")]}
6592 Nothing -> Nothing
66− -- TODO: manager = newManager defaultManagerSettings -- We likely need
67− -- this for TLS.
68−
69−-- mapHttpToDocker :: HTTP.HttpException -> DockerError
70−-- mapHttpToDocker e = case e of
71−-- HTTP.FailedConnectionException{} -> DockerConnectionError
72−-- HTTP.FailedConnectionException2{} -> DockerConnectionError
73−-- otherwise -> GenericDockerError "dinamo"
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?
7496
75−defaultHttpHandler :: HttpHandler IO
97+defaultHttpHandler :: MonadIO m => HttpHandler m
7698defaultHttpHandler request = do
77− manager <- newManager defaultManagerSettings
78− -- either mapHttpToDocker id <$> try httpLbs request manager
99+ manager <- liftIO $ newManager defaultManagerSettings
100+ httpHandler manager request
101+
102+httpHandler :: MonadIO m => HTTP.Manager -> HttpHandler m
103+httpHandler manager request = liftIO $ do
79104 try (httpLbs request manager) >>= \res -> case res of
80105 Right res -> return $ Right res
81106 Left HTTP.FailedConnectionException{} -> return $ Left DockerConnectionError
@@ -87,17 +112,13 @@ defaultHttpHandler request = do
87112--
88113-- Docker seems to ignore the hostname in requests sent over unix domain
89114-- sockets (and the port obviously doesn't matter either)
90−unixHttpHandler :: FilePath -- ^ The socket to connect to
91− -> HttpHandler IO
92−unixHttpHandler fp request= do
115+unixHttpHandler :: MonadIO m => FilePath -- ^ The socket to connect to
116+ -> HttpHandler m
117+unixHttpHandler fp request = do
93118 let mSettings = defaultManagerSettings
94119 { managerRawConnection = return $ openUnixSocket fp}
95− manager <- newManager mSettings
96− try (httpLbs request manager) >>= \res -> case res of
97− Right res -> return $ Right res
98− Left HTTP.FailedConnectionException{} -> return $ Left DockerConnectionError
99− Left HTTP.FailedConnectionException2{} -> return $ Left DockerConnectionError
100− Left e -> return $ Left $ GenericDockerError (T.pack $ show e)
120+ manager <- liftIO $ newManager mSettings
121+ httpHandler manager request
101122
102123 where
103124 openUnixSocket filePath _ _ _ = do
@@ -107,4 +128,105 @@ unixHttpHandler fp request= do
107128 (SBS.sendAll s)
108129 (S.sClose s)
109130
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+
110232
src/Docker/Internal.hs+42−27
@@ -1,56 +1,71 @@
11module Docker.Internal where
22
33
4−import Blaze.ByteString.Builder (toByteString)
5−import Data.ByteString (ByteString)
6−import Data.Maybe
7−import Data.Text as T
8−import Data.Text (Text)
9−import Data.Text.Encoding (decodeUtf8, encodeUtf8)
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)
1010import Docker.Types
11−import Network.HTTP.Client.Internal (makeConnection)
12−import Network.HTTP.Types (Query, encodePath,
13− encodePathSegments)
14−import qualified Network.Socket as S
15−import qualified Network.Socket.ByteString as SBS
11+import Network.HTTP.Types (Query, encodePath,
12+ encodePathSegments)
1613
17−encodeURL :: [Text] -> Text
14+encodeURL :: [T.Text] -> T.Text
1815encodeURL ps = decodeUtf8 $ toByteString $ encodePathSegments ps
1916
20−encodeURLWithQuery :: [Text] -> Query -> Text
17+encodeURLWithQuery :: [T.Text] -> Query -> T.Text
2118encodeURLWithQuery ps q = decodeUtf8 $ toByteString $ encodePath ps q
2219
2320encodeQ :: String -> ByteString
2421encodeQ = encodeUtf8 . T.pack
2522
26−getEndpoint :: Endpoint -> Text
23+getEndpoint :: Endpoint -> T.Text
2724getEndpoint VersionEndpoint = encodeURL ["version"]
28−getEndpoint (ListContainersEndpoint lsOpts) = encodeURL ["containers", "json"]
29−getEndpoint (ListImagesEndpoint lsOpts) = encodeURL ["images", "json"]
30−getEndpoint CreateContainerEndpoint = encodeURL ["containers", "create"]
31−getEndpoint (StartContainerEndpoint startOpts cid) = encodeURLWithQuery ["containers", cid, "start"] query
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
3229 where query = case (detachKeys startOpts) of
3330 WithCtrl c -> [("detachKeys", Just (encodeQ $ ctrl ++ [c]))]
3431 WithoutCtrl c -> [("detachKeys", Just (encodeQ [c]))]
3532 DefaultDetachKey -> []
3633 ctrl = ['c', 't', 'r', 'l', '-']
37−getEndpoint (StopContainerEndpoint t cid) = encodeURLWithQuery ["containers", cid, "stop"] query
34+getEndpoint (StopContainerEndpoint t cid) = encodeURLWithQuery ["containers", fromContainerID cid, "stop"] query
3835 where query = case t of
3936 Timeout x -> [("t", Just (encodeQ $ show x))]
4037 DefaultTimeout -> []
41−getEndpoint (KillContainerEndpoint s cid) = encodeURLWithQuery ["containers", cid, "kill"] query
38+getEndpoint (KillContainerEndpoint s cid) = encodeURLWithQuery ["containers", fromContainerID cid, "kill"] query
4239 where query = case s of
4340 SIG x -> [("signal", Just (encodeQ $ show x))]
44− otherwise -> [("signal", Just (encodeQ $ show s))]
45−getEndpoint (RestartContainerEndpoint t cid) = encodeURLWithQuery ["containers", cid, "restart"] query
41+ _ -> [("signal", Just (encodeQ $ show s))]
42+getEndpoint (RestartContainerEndpoint t cid) = encodeURLWithQuery ["containers", fromContainerID cid, "restart"] query
4643 where query = case t of
4744 Timeout x -> [("t", Just (encodeQ $ show x))]
4845 DefaultTimeout -> []
49−getEndpoint (PauseContainerEndpoint cid) = encodeURL ["containers", cid, "pause"]
50−getEndpoint (UnpauseContainerEndpoint cid) = encodeURL ["containers", cid, "unpause"]
51−getEndpoint (ContainerLogsEndpoint (LogOpts follow stdout stderr since timestamps tail) cid) =
52− encodeURLWithQuery ["containers", cid, "logs"] query
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
5351 where query = [("stdout", Just (encodeQ $ show stdout)), ("stderr", Just (encodeQ $ show stderr)), ("follow", Just (encodeQ $ show follow))]
5452getEndpoint (DeleteContainerEndpoint (DeleteOpts removeVolumes force) cid) =
55− encodeURLWithQuery ["containers", cid] query
53+ encodeURLWithQuery ["containers", fromContainerID cid] query
5654 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.hs+835−152
@@ -1,35 +1,137 @@
1−module Docker.Types where
2−
3−import Data.Aeson (FromJSON, ToJSON, decode, encode,
4− genericParseJSON, genericToJSON, object,
5− parseJSON, toJSON, (.:), (.:?), (.=))
6−import qualified Data.Aeson as JSON
7−import Data.Aeson.Types (defaultOptions, fieldLabelModifier)
8−import Data.Char (toUpper)
9−import qualified Data.Map as M
10−import Data.Text (Text)
11−import qualified Data.Text as T
12−import GHC.Generics (Generic)
13−import Prelude hiding (all, tail)
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+ , cResources
33+ , hConfig
34+ , defaultCreateOpts
35+ , DetachKeys(..)
36+ , StartOpts(..)
37+ , defaultStartOpts
38+ , DeleteOpts(..)
39+ , Timestamp
40+ , TailLogOpt(..)
41+ , LogOpts(..)
42+ , defaultLogOpts
43+ , VolumePermission(..)
44+ , Volume(..)
45+ , Device(..)
46+ , ContainerName
47+ , VolumeFrom(..)
48+ , Link(..)
49+ , LogDriverType(..)
50+ , LogDriverOptions(..)
51+ , LogDriverConfig(..)
52+ , NetworkMode(..)
53+ , PortType(..)
54+ , NetworkInterface(..)
55+ , Networks(..)
56+ , NetworkSettings(..)
57+ , NetworkOptions(..)
58+ , Mount(..)
59+ , PortBinding(..)
60+ , PortBindings(..)
61+ , HostPort(..)
62+ , RetryCount
63+ , RestartPolicy(..)
64+ , Isolation(..)
65+ , UTSMode(..)
66+ , HostConfig(..)
67+ , Ulimit(..)
68+ , ContainerResources(..)
69+ , Port
70+ , Name
71+ , Value
72+ , EnvVar(..)
73+ , ContainerConfig(..)
74+ ) where
75+
76+import Data.Aeson (FromJSON, ToJSON, genericParseJSON,
77+ genericToJSON, object, parseJSON, toJSON,
78+ (.!=), (.:), (.:?), (.=))
79+import qualified Data.Aeson as JSON
80+import Data.Aeson.Types (defaultOptions, fieldLabelModifier)
81+import Data.Char (isAlphaNum, toUpper)
82+import qualified Data.HashMap.Strict as HM
83+import qualified Data.Map as M
84+import Data.Monoid ((<>))
85+import Data.Text (Text)
86+import qualified Data.Text as T
87+import Data.Time.Clock (UTCTime)
88+import GHC.Generics (Generic)
89+import Prelude hiding (all, tail)
90+import Text.Read (readMaybe)
1491
1592data Endpoint =
1693 VersionEndpoint
1794 | ListContainersEndpoint ListOpts
1895 | ListImagesEndpoint ListOpts
19− | CreateContainerEndpoint
96+ | CreateContainerEndpoint CreateOpts
2097 | StartContainerEndpoint StartOpts ContainerID
2198 | StopContainerEndpoint Timeout ContainerID
2299 | KillContainerEndpoint Signal ContainerID
23100 | RestartContainerEndpoint Timeout ContainerID
24101 | PauseContainerEndpoint ContainerID
25102 | UnpauseContainerEndpoint ContainerID
26− | ContainerLogsEndpoint LogOpts ContainerID
103+ | ContainerLogsEndpoint LogOpts Bool ContainerID -- Second argument (Bool) is whether to follow.
27104 | DeleteContainerEndpoint DeleteOpts ContainerID
105+ | InspectContainerEndpoint ContainerID
106+ deriving (Eq, Show)
28107
29108type URL = Text
30109type ApiVersion = Text
31−type ContainerID = Text
32−type ImageID = Text
110+newtype ContainerID = ContainerID Text
111+ deriving (Eq, Show)
112+
113+fromContainerID :: ContainerID -> Text
114+fromContainerID (ContainerID t) = t
115+
116+toContainerID :: Text -> Maybe ContainerID
117+toContainerID t =
118+ if T.all (\c -> isAlphaNum c || c == ':') t then -- Note: Can we improve this whitelist?
119+ Just $ ContainerID t
120+ else
121+ Nothing
122+
123+newtype ImageID = ImageID Text
124+ deriving (Eq, Show)
125+
126+fromImageID :: ImageID -> Text
127+fromImageID (ImageID t) = t
128+
129+toImageID :: Text -> Maybe ImageID
130+toImageID t =
131+ if T.all (\c -> isAlphaNum c || c == ':') t then -- Note: Can we improve this whitelist?
132+ Just $ ImageID t
133+ else
134+ Nothing
33135
34136data Timeout = Timeout Integer | DefaultTimeout deriving (Eq, Show)
35137
@@ -43,21 +145,121 @@ data Signal = SIGHUP
43145 | SIG Integer
44146 | SIGKILL deriving (Eq, Show)
45147
148+instance FromJSON Signal where
149+ parseJSON (JSON.String "SIGTERM") = return SIGTERM
150+ parseJSON (JSON.String "SIGHUP") = return SIGHUP -- Note: Guessing on the string values for these.
151+ parseJSON (JSON.String "SIGINT") = return SIGINT
152+ parseJSON (JSON.String "SIGQUIT") = return SIGQUIT
153+ parseJSON (JSON.String "SIGSTOP") = return SIGSTOP
154+ parseJSON (JSON.String "SIGUSR1") = return SIGUSR1
155+ parseJSON _ = fail "Unknown Signal"
156+
157+instance ToJSON Signal where
158+ toJSON SIGHUP = "SIGHUP"
159+ toJSON SIGINT = "SIGINT"
160+ toJSON SIGQUIT = "SIGQUIT"
161+ toJSON SIGSTOP = "SIGSTOP"
162+ toJSON SIGTERM = "SIGTERM"
163+ toJSON SIGUSR1 = "SIGUSR1"
164+ toJSON (SIG i) = toJSON i
165+ toJSON SIGKILL = "SIGKILL"
166+
167+data ContainerDetails = ContainerDetails {
168+ appArmorProfile :: Text
169+ , args :: [Text]
170+ , containerDetailsConfig :: ContainerConfig
171+ , created :: UTCTime
172+ , driver :: Text
173+ -- , execIDs -- Not sure what this is in 1.24 spec.
174+ , containerDetailsHostConfig :: HostConfig
175+ , hostnamePath :: FilePath
176+ , hostsPath :: FilePath
177+ , logPath :: FilePath
178+ , containerDetailsId :: ContainerID
179+ , containerDetailsImage :: ImageID
180+ , mountLabel :: Text
181+ , name :: Text
182+ , networkSettings :: NetworkSettings
183+ , path :: FilePath
184+ , processLabel :: Text
185+ , resolveConfPath :: FilePath
186+ , restartCount :: Int
187+ , state :: ContainerState
188+ , mounts :: [Mount]
189+ }
190+ deriving (Eq, Show, Generic)
191+
192+data Mount = Mount {
193+ mountName :: Text
194+ , mountSource :: FilePath
195+ , mountDestination :: FilePath
196+ , mountDriver :: Text
197+ , mountMode :: Text -- Note: Improve this?
198+ , mountRW :: Bool
199+ , mountPropogation :: Text
200+ }
201+ deriving (Eq, Show, Generic)
202+
203+instance FromJSON Mount where
204+ parseJSON (JSON.Object o) = do
205+ name <- o .: "Name"
206+ src <- o .: "Source"
207+ dest <- o .: "Destination"
208+ driver <- o .: "Driver"
209+ mode <- o .: "Mode"
210+ rw <- o .: "RW"
211+ prop <- o .: "Propagation"
212+ return $ Mount name src dest driver mode rw prop
213+ parseJSON _ = fail "Mount is not an object"
214+
215+data ContainerState = ContainerState {
216+ containerError :: Text
217+ , exitCode :: Int
218+ , finishedAt :: Maybe UTCTime -- Note: Is this a maybe?
219+ , oomKilled :: Bool
220+ , dead :: Bool
221+ , paused :: Bool
222+ , pid :: Int
223+ , restarting :: Bool
224+ , running :: Bool
225+ , startedAt :: UTCTime
226+ , status :: Status
227+ }
228+ deriving (Eq, Show, Generic)
229+
230+instance FromJSON ContainerState where
231+ parseJSON (JSON.Object o) = do
232+ err <- o .: "Error"
233+ exit <- o .: "ExitCode"
234+ finished <- o .:? "FinishedAt"
235+ oomKilled <- o .: "OOMKilled"
236+ dead <- o .: "Dead"
237+ paused <- o .: "Paused"
238+ pid <- o .: "Pid"
239+ restarting <- o .: "Restarting"
240+ running <- o .: "Running"
241+ started <- o .: "StartedAt"
242+ st <- o .: "Status"
243+ return $ ContainerState err exit finished oomKilled dead paused pid restarting running started st
244+ parseJSON _ = fail "ContainerState is not an object"
245+
46246data DockerClientOpts = DockerClientOpts {
47247 apiVer :: ApiVersion
48248 , baseUrl :: URL
49249 }
50−
51−data ListOpts = ListOpts { all :: Bool } deriving (Eq, Show)
52−
53−defaultListOpts = ListOpts { all=False }
250+ deriving (Eq, Show)
54251
55252defaultClientOpts :: DockerClientOpts
56253defaultClientOpts = DockerClientOpts {
57− apiVer = "v1.22"
254+ apiVer = "v1.23"
58255 , baseUrl = "http://127.0.0.1:2375"
59256 }
60257
258+data ListOpts = ListOpts { all :: Bool } deriving (Eq, Show)
259+
260+defaultListOpts :: ListOpts
261+defaultListOpts = ListOpts { all=False }
262+
61263data DockerVersion = DockerVersion {
62264 version :: Text
63265 , apiVersion :: ApiVersion
@@ -78,6 +280,57 @@ instance FromJSON DockerVersion where
78280 parseJSON = genericParseJSON defaultOptions {
79281 fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
80282
283+-- instance ToJSON ContainerDetails where
284+-- toJSON = error "TODO"
285+
286+instance FromJSON ContainerDetails where
287+ parseJSON (JSON.Object o) = do
288+ appArmor <- o .: "AppArmorProfile"
289+ args <- o .: "Args"
290+ config <- o .: "Config"
291+ created <- o .: "Created"
292+ driver <- o .: "Driver"
293+ hostConfig <- o .: "HostConfig"
294+ hostnamePath <- o .: "HostnamePath"
295+ hostsPath <- o .: "HostsPath"
296+ logPath <- o .: "LogPath"
297+ id <- o .: "Id"
298+ image <- o .: "Image"
299+ mountLabel <- o .: "MountLabel"
300+ name <- o .: "Name"
301+ networkSettings <- o .: "NetworkSettings"
302+ path <- o .: "Path"
303+ processLabel <- o .: "ProcessLabel"
304+ resolveConfPath <- o .: "ResolvConfPath"
305+ restartCount <- o .: "RestartCount"
306+ state <- o .: "State"
307+ mounts <- o .: "Mounts"
308+ return $ ContainerDetails appArmor args config created driver hostConfig hostnamePath hostsPath logPath id image mountLabel name networkSettings path processLabel resolveConfPath restartCount state mounts
309+ parseJSON _ = fail "ContainerDetails is not an object"
310+
311+instance ToJSON ContainerID where
312+ toJSON (ContainerID cid) = object ["Id" .= cid]
313+
314+instance FromJSON ContainerID where
315+ parseJSON (JSON.Object o) = do
316+ cid <- o .: "Id"
317+ case toContainerID cid of
318+ Nothing ->
319+ fail "Invalid ContainerID"
320+ Just cid ->
321+ return cid
322+ parseJSON _ = fail "ContainerID is not an object."
323+
324+instance ToJSON ImageID where
325+ toJSON (ImageID iid) = JSON.String iid
326+
327+instance FromJSON ImageID where
328+ parseJSON (JSON.String t) = case toImageID t of
329+ Nothing ->
330+ fail "Invalid ImageID"
331+ Just iid ->
332+ return iid
333+ parseJSON _ = fail "ImageID is not an object."
81334
82335data ContainerPortInfo = ContainerPortInfo {
83336 ipAddressInfo :: Maybe Text
@@ -92,50 +345,100 @@ instance FromJSON ContainerPortInfo where
92345 <*> (v .: "PrivatePort")
93346 <*> (v .:? "PublicPort")
94347 <*> (v .:? "Type")
95−
96−-- data NetworkOptions = NetworkOptions {
97−-- ipamConfig :: Maybe Text
98−-- , links :: Maybe Text
99−-- , aliases :: Maybe Text
100−-- , networkId :: Text
101−-- , endpointId :: Text
102−-- , gateway :: Text
103−-- , ipAddress :: Text
104−-- , ipPrefixLen :: Text
105−-- , ipV6Gateway :: Maybe Text
106−-- , globalIPv6Address :: Maybe Text
107−-- , globalIPv6PrefixLen :: Maybe Text
108−-- , macAddress :: Text
109−-- } deriving (Eq, Show)
110−
111−-- data Network = Network (M.Map NetworkMode NetworkOptions) deriving (Eq, Show)
112−
113−-- data NetworkSettings = NetworkSettings {
114−-- bridge
115−-- , sandboxId
116−-- , hairpinMode
117−-- , linkLocalIPv6Address
118−-- , linkLocalIPv6PrefixLen
119−-- , ports
120−-- , sandboxKey
121−-- , secondaryIPAddresses
122−-- , secondaryIPv6Addresses
123−-- , endpointID
124−-- , gateway
125−-- , globalIPv6Address
126−-- , globalIPv6PrefixLen
127−-- , ipAddress
128−-- , ipPrefixLen
129−-- , ipv6Gateway
130−-- , macAddress
131−-- , networks :: [Network]
132−-- }
133−
134−-- data NetworkSettings = NetworkSettings (M.Map Text Text) deriving (Eq, Show, Generic)
135−
136−-- instance FromJSON NetworkSettings
137−
138−-- TODO: Add NetworkSettings
348+ parseJSON _ = fail "ContainerPortInfo: Not a JSON object."
349+
350+-- For inspecting container details.
351+data NetworkOptions = NetworkOptions {
352+-- ipamConfig :: Maybe Text -- Don't see in 1.24
353+-- , links :: Maybe Text -- Don't see in 1.24
354+-- , aliases :: Maybe Text -- Don't see in 1.24
355+ networkOptionsId :: Text
356+ , networkOptionsEndpointId :: Text
357+ , networkOptionsGateway :: Text
358+ , networkOptionsIpAddress :: Text -- Note: Parse this?
359+ , networkOptionsIpPrefixLen :: Int
360+ , networkOptionsIpV6Gateway :: Maybe Text
361+ , networkOptionsGlobalIPv6Address :: Maybe Text
362+ , networkOptionsGlobalIPv6PrefixLen :: Maybe Int
363+ , networkOptionsMacAddress :: Text
364+ } deriving (Eq, Show)
365+
366+instance FromJSON NetworkOptions where
367+ parseJSON (JSON.Object o) = do
368+ networkId <- o .: "NetworkID"
369+ endpointId <- o .: "EndpointID"
370+ gateway <- o .: "Gateway"
371+ ip <- o .: "IPAddress"
372+ ipLen <- o .: "IPPrefixLen"
373+ ip6Gateway <- o .:? "IPv6Gateway"
374+ globalIP6 <- o .:? "GlobalIPv6Address"
375+ globalIP6Len <- o .:? "GlobalIPv6PrefixLen"
376+ mac <- o .: "MacAddress"
377+ return $ NetworkOptions networkId endpointId gateway ip ipLen ip6Gateway globalIP6 globalIP6Len mac
378+ parseJSON _ = fail "NetworkOptions is not an object"
379+
380+newtype Networks = Networks (M.Map NetworkMode NetworkOptions) -- Note: Is it ever possible that there will be duplicates of network modes?
381+ deriving (Eq, Show)
382+
383+instance FromJSON Networks where
384+ parseJSON (JSON.Object o) = do
385+ Networks <$> HM.foldlWithKey' f (return M.empty) o
386+
387+ where
388+ f accM k' v' = do
389+ acc <- accM
390+ k <- parseJSON $ JSON.String k'
391+ v <- parseJSON v'
392+ return $ M.insert k v acc
393+
394+ parseJSON _ = fail "Networks is not an object"
395+
396+data NetworkSettings = NetworkSettings {
397+ networkSettingsBridge :: Text
398+ , networkSettingsSandboxId :: Text
399+ , networkSettingsHairpinMode :: Bool
400+ , networkSettingsLinkLocalIPv6Address :: Text
401+ , networkSettingsLinkLocalIPv6PrefixLen :: Int
402+ , networkSettingsPorts :: [Int] -- TODO: 1.24 spec is unclear
403+ , networkSettingsSandboxKey :: Text
404+ , networkSettingsSecondaryIPAddresses :: Maybe [Text] -- TODO: 1.24 spec is unclear
405+ , networkSettingsSecondaryIPv6Addresses :: Maybe [Text] -- TODO: 1.24 spec is unclear
406+ , networkSettingsEndpointID :: Text
407+ , networkSettingsGateway :: Text
408+ , networkSettingsGlobalIPv6Address :: Text
409+ , networkSettingsGlobalIPv6PrefixLen :: Int
410+ , networkSettingsIpAddress :: Text
411+ , networkSettingsIpPrefixLen :: Int
412+ , networkSettingsIpv6Gateway
413+ , networkSettingsMacAddress
414+ , networkSettingsNetworks :: Networks
415+ }
416+ deriving (Eq, Show)
417+
418+instance FromJSON NetworkSettings where
419+ parseJSON (JSON.Object o) = do
420+ bridge <- o .: "Bridge"
421+ sandbox <- o .: "SandboxID"
422+ hairpin <- o .: "HairpinMode"
423+ localIP6 <- o .: "LinkLocalIPv6Address"
424+ localIP6Len <- o .: "LinkLocalIPv6PrefixLen"
425+ ports <- o .:? "Ports" .!= []
426+ sandboxKey <- o .: "SandboxKey"
427+ secondaryIP <- o .: "SecondaryIPAddresses"
428+ secondayIP6 <- o .: "SecondaryIPv6Addresses"
429+ endpointID <- o .: "EndpointID"
430+ gateway <- o .: "Gateway"
431+ globalIP6 <- o .: "GlobalIPv6Address"
432+ globalIP6Len <- o .: "GlobalIPv6PrefixLen"
433+ ip <- o .: "IPAddress"
434+ ipLen <- o .: "IPPrefixLen"
435+ ip6Gateway <- o .: "IPv6Gateway"
436+ mac <- o .: "MacAddress"
437+ networks <- o .: "Networks"
438+ return $ NetworkSettings bridge sandbox hairpin localIP6 localIP6Len ports sandboxKey secondaryIP secondayIP6 endpointID gateway globalIP6 globalIP6Len ip ipLen ip6Gateway mac networks
439+ parseJSON _ = fail "NetworkSettings is not an Object"
440+
441+-- TODO: Add NetworkSettings, Mounts
139442data Container = Container
140443 { containerId :: ContainerID
141444 , containerNames :: [Text]
@@ -143,10 +446,11 @@ data Container = Container
143446 , containerImageId :: ImageID
144447 , containerCommand :: Text
145448 , containerCreatedAt :: Int
146− , containerStatus :: Text
449+ , containerStatus :: Status
147450 , containerPorts :: [ContainerPortInfo]
148451 , containerLabels :: Labels
149− -- , containerNetworkSettings :: NetworkSettings
452+ , containerNetworks :: Networks
453+ , containerMounts :: [Mount]
150454 } deriving (Show, Eq)
151455
152456instance FromJSON Container where
@@ -160,14 +464,29 @@ instance FromJSON Container where
160464 <*> (v .: "Status")
161465 <*> (v .: "Ports")
162466 <*> (v .: "Labels")
163− -- <*> (v .: "NetworkSettings")
164−
165−data Status = Created | Restarting | Running | Paused | Exited | Dead deriving (Eq, Show, Generic)
166−
167−instance FromJSON Status
168−instance ToJSON Status
467+ <*> (v .: "NetworkSettings" >>= parseNetworks)
468+ <*> (v .: "Mounts")
469+ where
470+ parseNetworks (JSON.Object v) =
471+ (v .: "Networks") >>= parseJSON
472+ parseNetworks _ = fail "Container NetworkSettings: Not a JSON object."
473+
474+ parseJSON _ = fail "Container: Not a JSON object."
475+
476+data Status = Created | Restarting | Running | Paused | Exited | Dead
477+ deriving (Eq, Show, Generic)
478+
479+instance FromJSON Status where
480+ parseJSON (JSON.String "running") = return Running
481+ parseJSON (JSON.String "created") = return Created -- Note: Guessing on the string values of these.
482+ parseJSON (JSON.String "restarting") = return Restarting
483+ parseJSON (JSON.String "paused") = return Paused
484+ parseJSON (JSON.String "exited") = return Exited
485+ parseJSON (JSON.String "dead") = return Dead
486+ parseJSON _ = fail "Unknown Status"
169487
170488type Digest = Text
489+
171490newtype Labels = Labels (M.Map Name Value) deriving (Eq, Show)
172491
173492instance FromJSON Labels where
@@ -183,12 +502,13 @@ data Image = DockerImage {
183502 , imageCreated :: Integer
184503 , imageParentId :: Maybe ImageID
185504 , imageRepoTags :: [Tag]
186− , imageRepoDigests :: [Digest]
505+ , imageRepoDigests :: Maybe [Digest]
187506 , imageSize :: Integer
188507 , imageVirtualSize :: Integer
189− , imageLabels :: Labels
508+ , imageLabels :: Maybe Labels
190509 } deriving (Show, Eq, Generic)
191510
511+dropImagePrefix :: [a] -> [a]
192512dropImagePrefix = drop 5
193513
194514instance FromJSON Image where
@@ -196,17 +516,102 @@ instance FromJSON Image where
196516 fieldLabelModifier = dropImagePrefix}
197517
198518data CreateOpts = CreateOpts {
199− containerConfig :: ContainerConfig
200− , hostConfig :: HostConfig
201− , containerResources :: ContainerResources
519+ containerConfig :: ContainerConfig
520+ , hostConfig :: HostConfig
202521 } deriving (Eq, Show)
203522
523+instance ToJSON CreateOpts where
524+ toJSON (CreateOpts cc hc) = do
525+ let ccJSON = toJSON cc
526+ let hcJSON = toJSON hc
527+ case ccJSON of
528+ JSON.Object (o :: HM.HashMap T.Text JSON.Value) -> do
529+ JSON.Object $ HM.insert "HostConfig" hcJSON o
530+ _ -> error "ContainerConfig is not an object." -- This should never happen.
531+
532+cConfig :: Text -> ContainerConfig
533+cConfig imageName = ContainerConfig {
534+ hostname=Nothing
535+ , domainname=Nothing
536+ , user=Nothing
537+ , attachStdin=False
538+ , attachStdout=False
539+ , image=imageName
540+ , attachStderr=False
541+ , exposedPorts=ExposedPorts M.empty
542+ , tty=False
543+ , openStdin=False
544+ , stdinOnce=False
545+ , env=[]
546+ , cmd=[]
547+ , volumes=Nothing
548+ , workingDir=Nothing
549+ , entrypoint=Nothing
550+ , networkDisabled=False
551+ , macAddress=Nothing
552+ , labels=Nothing
553+ , stopSignal=SIGTERM
554+ }
555+
556+hConfig :: HostConfig
557+hConfig = HostConfig {
558+ binds=[]
559+ , containerIDFile=Nothing
560+ , logConfig=LogDriverConfig JsonFile Nothing
561+ , networkMode=Bridge
562+ , portBindings=PortBindings []
563+ , restartPolicy=RestartOff
564+ , volumeDriver=Nothing
565+ , volumesFrom=[]
566+ , capAdd=[]
567+ , capDrop=[]
568+ , dns=[]
569+ , dnsOptions=[]
570+ , dnsSearch=[]
571+ , extraHosts=[]
572+ , ipcMode=Nothing
573+ , links=[]
574+ , oomScoreAdj=Nothing
575+ , privileged=False
576+ , publishAllPorts=False
577+ , readOnlyRootfs=False
578+ , securityOpt=[]
579+ , shmSize=Nothing
580+ , resources=cResources
581+ }
582+
583+cResources :: ContainerResources
584+cResources = ContainerResources {
585+ cpuShares=Nothing
586+ , blkioWeight=Nothing
587+ , blkioWeightDevice=Nothing
588+ , blkioDeviceReadBps=Nothing
589+ , blkioDeviceWriteBps=Nothing
590+ , blkioDeviceReadIOps=Nothing
591+ , blkioDeviceWriteIOps=Nothing
592+ , cpuPeriod=Nothing
593+ , cpusetCpus=Nothing
594+ , cpusetMems=Nothing
595+ , devices=[]
596+ , kernelMemory=Nothing
597+ , memory=Nothing
598+ , memoryReservation=Nothing
599+ , memorySwap=Nothing
600+ , oomKillDisable=False
601+ , ulimits=[]
602+ }
603+
604+
605+defaultCreateOpts :: T.Text -> CreateOpts
606+defaultCreateOpts imageName = CreateOpts { containerConfig = cConfig imageName, hostConfig = hConfig }
607+
204608-- detachKeys – Override the key sequence for detaching a container.
205609-- Format is a single character [a-Z] or ctrl-<value> where <value> is one of: a-z, @, ^, [, , or _.
206610data DetachKeys = WithCtrl Char | WithoutCtrl Char | DefaultDetachKey deriving (Eq, Show)
207611
208612data StartOpts = StartOpts { detachKeys :: DetachKeys } deriving (Eq, Show)
209613
614+defaultStartOpts :: StartOpts
210615defaultStartOpts = StartOpts { detachKeys = DefaultDetachKey }
211616
212617data DeleteOpts = DeleteOpts {
@@ -218,16 +623,15 @@ type Timestamp = Integer
218623data TailLogOpt = Tail Integer | All deriving (Eq, Show)
219624
220625data LogOpts = LogOpts {
221− follow :: Bool
222− , stdout :: Bool
626+ stdout :: Bool
223627 , stderr :: Bool
224628 , since :: Maybe Timestamp
225629 , timestamps :: Bool
226630 , tail :: TailLogOpt
227631 } deriving (Eq, Show)
228632
229−defaultLogOpts = LogOpts { follow = False
230− , stdout = True
633+defaultLogOpts :: LogOpts
634+defaultLogOpts = LogOpts { stdout = True
231635 , stderr = True
232636 , since = Nothing
233637 , timestamps = True
@@ -236,14 +640,36 @@ defaultLogOpts = LogOpts { follow = False
236640
237641data VolumePermission = Rw | Ro deriving (Eq, Show, Generic)
238642
239−instance ToJSON VolumePermission
240−instance FromJSON VolumePermission
643+-- instance ToJSON VolumePermission where
644+-- toJSON = error "TODO"
645+-- instance FromJSON VolumePermission where
646+-- parseJSON = error "TODO"
647+
648+-- Note: Note really sure what this should be (1.24).
649+-- This is not the same as [Volume] in HostConfig.
650+newtype Volumes = Volumes [Text] -- [Volume]
651+ deriving (Eq, Show)
652+
653+instance FromJSON Volumes where
654+ parseJSON (JSON.Object o) = return $ Volumes $ HM.keys o
655+ parseJSON _ = fail "Volumes is not an object"
656+
657+instance ToJSON Volumes where
658+ toJSON (Volumes vs) = toJSON vs
241659
242660data Volume = Volume { hostSrc :: Text
243661 , containerDest :: Text
244662 , volumePermission :: Maybe VolumePermission
245663 } deriving (Eq, Show)
246664
665+instance FromJSON Volume where
666+ parseJSON (JSON.String t) = case T.split (== ':') t of
667+ [src, dest] -> return $ Volume src dest Nothing
668+ [src, dest, "rw"] -> return $ Volume src dest $ Just Rw
669+ [src, dest, "ro"] -> return $ Volume src dest $ Just Ro
670+ _ -> fail "Could not parse Volume"
671+ parseJSON _ = fail "Volume is not a string"
672+
247673data Device = Device {
248674 pathOnHost :: FilePath
249675 , pathInContainer :: FilePath
@@ -260,7 +686,20 @@ instance FromJSON Device where
260686
261687type ContainerName = Text
262688
263−data VolumeFrom = VolumeFrom ContainerName VolumePermission deriving (Eq, Show)
689+data VolumeFrom = VolumeFrom ContainerName (Maybe VolumePermission) deriving (Eq, Show)
690+
691+instance FromJSON VolumeFrom where
692+ parseJSON (JSON.String t) = case T.split (== ':') t of
693+ [vol] -> return $ VolumeFrom vol Nothing
694+ [vol, "rw"] -> return $ VolumeFrom vol $ Just Rw
695+ [vol, "ro"] -> return $ VolumeFrom vol $ Just Ro
696+ _ -> fail "Could not parse VolumeFrom"
697+ parseJSON _ = fail "VolumeFrom is not a string"
698+
699+instance ToJSON VolumeFrom where
700+ toJSON (VolumeFrom n p) = case p of
701+ Nothing -> toJSON $ n <> ":" <> "rw"
702+ Just per -> toJSON $ n <> ":" <> (T.pack $ show per)
264703
265704instance ToJSON Volume where
266705 toJSON (Volume src dest mode) = toJSON $ case mode of
@@ -269,47 +708,206 @@ instance ToJSON Volume where
269708
270709data Link = Link Text (Maybe Text) deriving (Eq, Show)
271710
711+instance FromJSON Link where
712+ parseJSON (JSON.String t) = case T.split (== ':') t of
713+ [f] -> return $ Link f Nothing
714+ [f,s] -> return $ Link f $ Just s
715+ _ -> fail "Could not parse Link"
716+ parseJSON _ = fail "Link is not a string"
717+
272718instance ToJSON Link where
273719 toJSON (Link n1 n2) = toJSON $ case n2 of
274720 Nothing -> T.concat[n1, ":", n1] -- use same name in container
275721 Just n -> T.concat[n1, ":", n] -- used specified name in container
276722
277723-- { "Type": "<driver_name>", "Config": {"key1": "val1"} }
278−data LogDriverType = JsonFile | Syslog | Journald | Gelf | AwsLogs | Splunk | LoggingDisabled deriving (Eq, Show)
279−
280−data LogDriverOptions = Map Text Text deriving (Eq, Show)
724+data LogDriverType = JsonFile | Syslog | Journald | Gelf | Fluentd | AwsLogs | Splunk | Etwlogs | LoggingDisabled deriving (Eq, Show)
725+
726+instance FromJSON LogDriverType where
727+ parseJSON (JSON.String "json-file") = return JsonFile
728+ parseJSON (JSON.String "syslog") = return Syslog
729+ parseJSON (JSON.String "journald") = return Journald
730+ parseJSON (JSON.String "gelf") = return Gelf
731+ parseJSON (JSON.String "fluentd") = return Fluentd
732+ parseJSON (JSON.String "awslogs") = return AwsLogs
733+ parseJSON (JSON.String "splunk") = return Splunk
734+ parseJSON (JSON.String "etwlogs") = return Etwlogs
735+ parseJSON (JSON.String "none") = return LoggingDisabled
736+ parseJSON _ = fail "Unknown LogDriverType"
737+
738+instance ToJSON LogDriverType where
739+ toJSON JsonFile = JSON.String "json-file"
740+ toJSON Syslog = JSON.String "syslog"
741+ toJSON Journald = JSON.String "journald"
742+ toJSON Gelf = JSON.String "gelf"
743+ toJSON Fluentd = JSON.String "fluentd"
744+ toJSON AwsLogs = JSON.String "awslogs"
745+ toJSON Splunk = JSON.String "splunk"
746+ toJSON Etwlogs = JSON.String "etwlogs"
747+ toJSON LoggingDisabled = JSON.String "none"
748+
749+newtype LogDriverOptions = LogDriverOptions (M.Map Text Text) deriving (Eq, Show)
750+
751+instance FromJSON LogDriverOptions where
752+ parseJSON (JSON.Object o) = do
753+ LogDriverOptions <$> HM.foldlWithKey' f (return M.empty) o
754+
755+ where
756+ f accM k (JSON.String v) = do
757+ acc <- accM
758+ return $ M.insert k v acc
759+ f _ _ _ = fail "Value of LogDriverOptions is not a string"
760+
761+ parseJSON JSON.Null = return $ LogDriverOptions M.empty
762+ parseJSON _ = fail "LogDriverOptions is not an object"
763+
764+instance ToJSON LogDriverOptions where
765+ toJSON (LogDriverOptions mp) = toJSON mp
281766
282767data LogDriverConfig = LogDriverConfig LogDriverType (Maybe LogDriverOptions) deriving (Eq, Show)
283768
769+instance ToJSON LogDriverConfig where
770+ toJSON (LogDriverConfig driverType driverOptions) = object ["Type" .= driverType, "Config" .= driverOptions]
771+
772+instance FromJSON LogDriverConfig where
773+ parseJSON (JSON.Object o) = do
774+ typ <- o .: "Type"
775+ opts <- o .: "Config"
776+ return $ LogDriverConfig typ opts
777+ parseJSON _ = fail "LogDriverConfig is not an object"
778+
284779-- TODO: Add container:<name|id> mode
285−data NetworkMode = Bridge | Host | NetworkDisabled deriving (Eq, Show)
780+data NetworkMode = Bridge | Host | NetworkDisabled
781+ deriving (Eq, Show, Ord)
782+
783+instance FromJSON NetworkMode where
784+ parseJSON (JSON.String "bridge") = return Bridge
785+ parseJSON (JSON.String "host") = return Host -- Note: Guessing on these.
786+ parseJSON (JSON.String "none") = return NetworkDisabled
787+ parseJSON _ = fail "Unknown NetworkMode"
788+
789+instance ToJSON NetworkMode where
790+ toJSON Bridge = JSON.String "bridge"
791+ toJSON Host = JSON.String "host"
792+ toJSON NetworkDisabled = JSON.String "none"
286793
287−data PortType = TCP | UDP deriving (Eq, Show, Generic)
794+data PortType = TCP | UDP deriving (Eq, Show, Generic, Read, Ord)
288795
289796instance ToJSON PortType where
290797 toJSON TCP = "tcp"
291798 toJSON UDP = "udp"
292799
293800instance FromJSON PortType where
294− parseJSON val = return $ case val of
295− "tcp" -> TCP
296− "udp" -> UDP
801+ parseJSON val = case val of
802+ "tcp" -> return TCP
803+ "udp" -> return UDP
804+ _ -> fail "PortType: Invalid port type."
297805
298806newtype NetworkInterface = NetworkInterface Text deriving (Eq, Show)
299807
808+instance FromJSON NetworkInterface where
809+ parseJSON (JSON.String v) = return $ NetworkInterface v
810+ parseJSON _ = fail "Network interface is not a string."
811+
812+instance ToJSON NetworkInterface where
813+ toJSON (NetworkInterface i) = JSON.String i
814+
815+-- | This datastructure models mapping a Port from the container onto the
816+-- host system s that the service running in the container can be accessed from
817+-- the outside world. We either map a port onto all interfaces (default) or onto a specific
818+-- interface like `127.0.0.1`.
819+newtype PortBindings = PortBindings [PortBinding]
820+ deriving (Eq, Show)
821+
300822-- { <port>/<protocol>: [{ "HostPort": "<port>" }] }
301823data PortBinding = PortBinding {
302824 containerPort :: Port
303825 , portType :: PortType
304− , hostPort :: [HostPort]
826+ , hostPorts :: HostPorts
305827 } deriving (Eq, Show)
306828
307−data HostPort = HostPort NetworkInterface Port deriving (Eq, Show)
829+
830+portAndType2Text :: Port -> PortType -> Text
831+portAndType2Text p t = (T.pack $ show p) <> "/" <> (T.pack $ show t)
832+
833+instance ToJSON PortBinding where
834+ toJSON (PortBinding {..}) = object [portAndType2Text containerPort portType .= hostPorts]
835+
836+instance FromJSON PortBindings where
837+ parseJSON (JSON.Object o) = do
838+ PortBindings <$> HM.foldlWithKey' f (return []) o
839+
840+ where
841+ f accM k v = case T.split (== '/') k of
842+ [port', portType'] -> do
843+ port <- parseIntegerText port'
844+ portType <- parseJSON $ JSON.String portType'
845+ acc <- accM
846+ hps <- parseJSON v
847+ return $ (PortBinding port portType hps):acc
848+ _ ->
849+ fail "Could not parse PortBindings"
850+ parseJSON _ = fail "PortBindings is not an object"
851+
852+instance ToJSON PortBindings where
853+ toJSON (PortBindings []) = JSON.Object HM.empty
854+ toJSON (PortBindings (p:ps)) = toJSON (p:ps)
855+
856+data HostPort = HostPort NetworkInterface Port
857+ deriving (Eq, Show)
858+
859+instance ToJSON HostPort where
860+ toJSON (HostPort i p) = object ["HostPort" .= p, "HostIP" .= i]
861+
862+instance FromJSON HostPort where
863+ parseJSON (JSON.Object o) = do
864+ p <- o .: "HostPort"
865+ i <- o .: "HostIP"
866+ return $ HostPort i p
867+ parseJSON _ = fail "HostPort is not an object."
868+
869+newtype HostPorts = HostPorts [HostPort]
870+ deriving (Eq, Show)
871+
872+
873+instance ToJSON HostPorts where
874+ toJSON (HostPorts hps) = toJSON hps
875+
876+instance FromJSON HostPorts where
877+ parseJSON (JSON.Object o) = do
878+ HostPorts <$> HM.foldlWithKey' f (return []) o
879+
880+ where
881+ f accM k v = do
882+ acc <- accM
883+ p' <- parseJSON v
884+ p <- parseIntegerText p'
885+ return $ (HostPort (NetworkInterface k) p):acc
886+ parseJSON _ = fail "HostPorts is not an object"
308887
309888-- { "Name": "on-failure" , "MaximumRetryCount": 2}
310889type RetryCount = Integer
311890data RestartPolicy = RestartAlways | RestartUnlessStopped | RestartOnFailure RetryCount | RestartOff deriving (Eq, Show)
312891
892+instance FromJSON RestartPolicy where
893+ parseJSON (JSON.Object o) = do
894+ (name :: Text) <- o .: "Name"
895+ case name of
896+ "always" -> return RestartAlways
897+ "unless-stopped" -> return RestartUnlessStopped
898+ "on-failure" -> do
899+ retry <- o .: "MaximumRetryCount"
900+ return $ RestartOnFailure retry
901+ "" -> return RestartOff -- Note: Is this correct? 1.24 spec is unclear.
902+ _ -> fail "Could not parse RestartPolicy"
903+ parseJSON _ = fail "RestartPolicy is not an object"
904+
905+instance ToJSON RestartPolicy where
906+ toJSON RestartAlways = object ["Name" .= JSON.String "always"]
907+ toJSON RestartUnlessStopped = object ["Name" .= JSON.String "unless-stopped"]
908+ toJSON (RestartOnFailure c) = object ["Name" .= JSON.String "on-failure", "MaximumRetryCount" .= c]
909+ toJSON RestartOff = object ["Name" .= JSON.String "off"]
910+
313911data Isolation = Default | Process | Hyperv deriving (Eq, Show)
314912
315913newtype UTSMode = UTSMode Text deriving (Eq, Show)
@@ -319,12 +917,12 @@ newtype UTSMode = UTSMode Text deriving (Eq, Show)
319917-- TODO: Sysctls map[string]string `json:",omitempty"` // List of Namespaced sysctls used for the container
320918data HostConfig = HostConfig
321919 { binds :: [Volume]
322− , containerIDFile :: FilePath
920+ , containerIDFile :: Maybe FilePath -- 1.24: Only in responses, not create
323921 , logConfig :: LogDriverConfig
324922 , networkMode :: NetworkMode
325− , portBindings :: [PortBinding]
923+ , portBindings :: PortBindings
326924 , restartPolicy :: RestartPolicy
327− , volumeDriver :: Text
925+ , volumeDriver :: Maybe Text
328926 , volumesFrom :: [VolumeFrom]
329927 , capAdd :: [Text]
330928 , capDrop :: [Text]
@@ -332,93 +930,178 @@ data HostConfig = HostConfig
332930 , dnsOptions :: [Text]
333931 , dnsSearch :: [Text]
334932 , extraHosts :: [Text]
335− , groupAdd :: [Integer]
336− , ipcMode :: Text
933+ -- , groupAdd :: [Integer] -- 1.24: Missing from inspecting container details... Going to omit for now.
934+ , ipcMode :: Maybe Text -- 1.24: Only in inspect, not create
337935 , links :: [Link]
338− , oomScoreAdj :: Integer
339− , pidMode :: Text
936+ , oomScoreAdj :: Maybe Integer
937+ -- , pidMode :: Text -- 1.24: Don't see pidMode, just pidsLimit
340938 , privileged :: Bool
341939 , publishAllPorts :: Bool
342940 , readOnlyRootfs :: Bool
343941 , securityOpt :: [Text]
344− , storageOpt :: [Text]
345− , utsMode :: UTSMode
346− , shmSize :: Integer
347− , consoleSize :: Integer
348− , isolation :: Isolation
942+ -- , utsMode :: UTSMode -- 1.24: Don't see this
943+ , shmSize :: Maybe Integer
944+ -- , consoleSize :: Integer -- 1.24: Don't see this
945+ -- , isolation :: Isolation -- 1.24: Don't see this
349946 , resources :: ContainerResources
350947 } deriving (Eq, Show, Generic)
351948
949+instance FromJSON HostConfig where
950+ parseJSON = genericParseJSON defaultOptions {
951+ fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
952+
953+instance ToJSON HostConfig where
954+ toJSON = genericToJSON defaultOptions {
955+ fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
956+
957+
352958-- { "Name": <name>, "Soft": <soft limit>, "Hard": <hard limit> }
353959-- { "Name": "nofile", "Soft": 1024, "Hard": 2048 }
354960data Ulimit = Ulimit {
355961 ulimitName :: Text
356962 , ulimitSoft :: Integer
357963 , ulimitHard :: Integer
358− } deriving (Eq, Show)
964+ } deriving (Eq, Show, Generic)
965+
966+instance FromJSON Ulimit where
967+ parseJSON = genericParseJSON defaultOptions {
968+ fieldLabelModifier = drop 5}
969+
970+instance ToJSON Ulimit where
971+ toJSON = genericToJSON defaultOptions {
972+ fieldLabelModifier = drop 5}
359973
360974data ContainerResources = ContainerResources {
361− cpuShares :: Integer
362− , cgroupParent :: Text
363− , blkioWeight :: Text
364− , blkioWeightDevice :: Text
365− , blkioDeviceReadBps :: Text
366− , blkioDeviceWriteBps :: Text
367− , blkioDeviceReadIOps :: Text
368− , blkioDeviceWriteIOps :: Text
369− , cpuPeriod :: Integer
370− , cpuQuota :: Integer
371− , cpusetCpus :: Integer
372− , cpusetMems :: Text
975+ cpuShares :: Maybe Integer
976+ -- , cgroupParent :: Text -- 1.24: Missing from inspecting container details... Going to omit for now.
977+ , blkioWeight :: Maybe Text
978+ , blkioWeightDevice :: Maybe Text
979+ , blkioDeviceReadBps :: Maybe Text
980+ , blkioDeviceWriteBps :: Maybe Text
981+ , blkioDeviceReadIOps :: Maybe Text
982+ , blkioDeviceWriteIOps :: Maybe Text
983+ , cpuPeriod :: Maybe Integer
984+ -- , cpuQuota :: Integer -- 1.24: Missing from inspecting container details... Going to omit for now.
985+ , cpusetCpus :: Maybe Integer
986+ , cpusetMems :: Maybe Text
373987 , devices :: [Device]
374− , diskQuota :: Integer
375− , kernelMemory :: Integer
376− , memory :: Integer
377− , memoryReservation :: Integer
378− , memorySwap :: Integer
379− , memorySwappiness :: Integer
988+ -- , diskQuota :: Integer -- Don't see this ins 1.24.
989+ , kernelMemory :: Maybe Integer
990+ , memory :: Maybe Integer
991+ , memoryReservation :: Maybe Integer
992+ , memorySwap :: Maybe Integer
993+ -- , memorySwappiness :: Integer -- 1.24: Missing from inspecting container details... Going to omit for now.
380994 , oomKillDisable :: Bool
381− , pidsLimit :: Integer
995+ -- , pidsLimit :: Integer -- 1.24: Missing from inspecting container details... Going to omit for now.
382996 , ulimits :: [Ulimit]
383− } deriving (Eq, Show)
997+ -- TODO: Missing from 1.24
998+ -- StorageOpt :: [(Text, Text)]
999+ -- VolumeDriver :: ??
1000+ -- EndpointsConfig :: ??
1001+ -- TODO: Only in inspect container in 1.24
1002+ -- CpuPercent :: Int +
1003+ -- MaximumIOps :: Int +
1004+ -- MaximumIOBps :: Int +
1005+ -- LxcConf :: [??] +
1006+ } deriving (Eq, Show, Generic)
1007+
1008+instance ToJSON ContainerResources where
1009+ toJSON = genericToJSON defaultOptions {
1010+ fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
1011+
1012+instance FromJSON ContainerResources where
1013+ parseJSON = genericParseJSON defaultOptions {
1014+ fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
3841015
3851016type Port = Integer
3861017
3871018type Name = Text
3881019type Value = Text
3891020
390−data EnvVar = EnvVar Name Value deriving (Eq, Show)
1021+data EnvVar = EnvVar Name Value
1022+ deriving (Eq, Show)
1023+
1024+instance FromJSON EnvVar where
1025+ parseJSON (JSON.String env) =
1026+ let (n, v') = T.break (== '=') env in
1027+ let v = T.drop 1 v' in
1028+ return $ EnvVar n v
1029+ parseJSON _ = fail "EnvVar is not a string"
1030+
1031+instance ToJSON EnvVar where
1032+ toJSON (EnvVar n v) = object [n .= v]
1033+
1034+-- | ExposedPorts represent a enumeraton of all the ports (and their type)
1035+-- that a container should expose to other containers or the host system.
1036+-- `NOTE`: This does not automatically expose the ports onto the host
1037+-- system but rathet it just tags them. It's best to be used with with
1038+-- the PublishAllPorts flag. It is also useful for
1039+-- the deamon to know wich Environment variables to
1040+-- inject into a container linking to our container.
1041+-- Example linking a Postgres container named db would inject the following
1042+-- environment variables automatically if we set the corresponding
1043+-- ExposedPorts:
1044+-- DB_PORT_5432_TCP_PORT="5432"
1045+-- DB_PORT_5432_TCP_PROTO="tcp"
1046+-- DB_PORT_5432_TCP="tcp://172.17.0.1:5432"
1047+newtype ExposedPorts = ExposedPorts (M.Map PortType Port) deriving (Eq, Show)
1048+
1049+instance FromJSON ExposedPorts where
1050+ parseJSON (JSON.Object o) = do
1051+ ExposedPorts <$> HM.foldlWithKey' f (return M.empty) o
1052+
1053+ where
1054+ f accM k _ = case T.split (== '/') k of
1055+ [port', portType'] -> do
1056+ port <- parseIntegerText port'
1057+ portType <- parseJSON $ JSON.String portType'
1058+ acc <- accM
1059+ return $ M.insert portType port acc
1060+ _ ->
1061+ fail "Could not parse ExposedPorts"
1062+ parseJSON _ = fail "ExposedPorts is not an object."
1063+
1064+instance ToJSON ExposedPorts where
1065+ toJSON (ExposedPorts kvs) = object [((T.pack $ show p) <> "/" <> (T.pack $ show t)) .= JSON.Object HM.empty | (t,p) <- (M.toList kvs)]
1066+
3911067
3921068data ContainerConfig = ContainerConfig {
393− hostname :: Text
394− , domainname :: Text
395− , user :: Text
1069+ hostname :: Maybe Text
1070+ , domainname :: Maybe Text
1071+ , user :: Maybe Text
3961072 , attachStdin :: Bool
3971073 , attachStdout :: Bool
3981074 , attachStderr :: Bool
399− -- ExposedPorts are used for the PublishAllPorts flag
400− -- to know which ports to publish. Also it's used for
401− -- the deamon to know wich Environment variables to
402− -- inject into a container linking to our container.
403− -- Example linking a Postgres container named db:
404− -- DB_PORT_5432_TCP_PORT="5432"
405− -- DB_PORT_5432_TCP_PROTO="tcp"
406− -- DB_PORT_5432_TCP="tcp://172.17.0.1:5432"
407− , exposedPorts :: [Port]
408− , publishService :: Text
1075+ , exposedPorts :: ExposedPorts
1076+ -- , publishService :: Text -- Don't see this in 1.24
4091077 , tty :: Bool
4101078 , openStdin :: Bool
4111079 , stdinOnce :: Bool
4121080 , env :: [EnvVar]
413− , cmd :: Text
414− , argsEscaped :: Bool
1081+ , cmd :: [Text]
1082+ -- , argsEscaped :: Bool -- Don't see this in 1.24
4151083 , image :: Text
416− , volumes :: [Volume]
417− , workingDir :: FilePath
418− , entrypoint :: Text
1084+ , volumes :: Maybe Volumes
1085+ , workingDir :: Maybe FilePath
1086+ , entrypoint :: Maybe Text -- Can be null?
4191087 , networkDisabled :: Bool
420− , macAddress :: Text
421− , onBuild :: Text
422− , labels :: Labels
1088+ , macAddress :: Maybe Text
1089+ -- , onBuild :: Text -- For 1.24, only see this in the inspect response.
1090+ , labels :: Maybe Labels
4231091 , stopSignal :: Signal
424− } deriving (Eq, Show)
1092+ } deriving (Eq, Show, Generic)
1093+
1094+instance ToJSON ContainerConfig where
1095+ toJSON = genericToJSON defaultOptions {
1096+ fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
1097+
1098+instance FromJSON ContainerConfig where
1099+ parseJSON = genericParseJSON defaultOptions {
1100+ fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
1101+
1102+parseIntegerText :: (Monad m) =>Text -> m Integer
1103+parseIntegerText t = case readMaybe $ T.unpack t of
1104+ Nothing ->
1105+ fail "Could not parse Integer"
1106+ Just i ->
1107+ return i
stack.yaml+1−1
@@ -1,7 +1,7 @@
11# For more information, see: https://github.com/commercialhaskell/stack/blob/release/doc/yaml_configuration.md
22
33# 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-4.1
4+resolver: lts-5.18
55
66# Local packages, usually specified by relative directory name
77packages:
tests/tests.hs+51−24
@@ -1,3 +1,5 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
13module Main where
24
35import qualified Test.QuickCheck.Monadic as QCM
@@ -6,46 +8,62 @@ import Test.Tasty.HUnit
68import qualified Test.Tasty.QuickCheck as QC
79
810import Control.Concurrent (threadDelay)
11+import Control.Monad.IO.Class
12+import Control.Monad.Trans.Class (MonadTrans, lift)
913import qualified Data.ByteString as B
1014import qualified Data.ByteString.Char8 as C
1115import qualified Data.ByteString.Lazy as BL
1216import Data.Maybe (fromJust, isJust)
17+import Data.Monoid
1318import Data.Text (unpack)
19+import Network.Connection (TLSSettings (..))
20+import Network.HTTP.Client (newManager)
21+import Network.HTTP.Client.TLS
1422import Network.HTTP.Types.Status
1523import System.Process (system)
1624
17−import Network.Docker
18−import Network.Docker.Types
25+import Docker
26+import Docker.Types
1927
2028
21−opts = defaultClientOpts
29+-- opts = defaultClientOpts
2230
23−test_image_name = "docker-hs-test"
31+testImageName = "docker-hs-test"
2432
2533toStrict1 = B.concat . BL.toChunks
2634
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+
2743checkDockerVersion :: IO ()
28−checkDockerVersion = do v <- getDockerVersion opts
29− assert $ isJust v
44+checkDockerVersion = runDocker $ do
45+ v <- getDockerVersion
46+ lift $ assert $ isRight v
3047
3148findTestImage :: IO ()
32−findTestImage = do images <- listImages opts
33− let x = fmap (filter ((== [test_image_name++":latest"]) . _repoTags)) images
34− 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
3553
3654runAndReadLog :: IO ()
37−runAndReadLog = do containerId <- createContainer opts (defaultCreateOpts {_image = test_image_name++":latest"})
38− assert $ isJust containerId
39− let c = unpack $ fromJust containerId
40− status1 <- startContainer opts c defaultStartOpts
41− threadDelay 300000 -- give 300ms for the application to finish
42− assert $ status1 == status204
43− status2 <- killContainer opts c
44− logs <- getContainerLogs opts c
45− assert $ status2 == status204
46− assert $ (C.pack "123") `C.isInfixOf` (toStrict1 logs)
47− status3 <- deleteContainer opts c
48− 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 ()
4967
5068
5169tests :: TestTree
@@ -54,10 +72,19 @@ tests = testGroup "Metrics tests" [
5472 testCase "Find image by name" findTestImage,
5573 testCase "Run a dummy container and read its log" runAndReadLog]
5674
57−setup :: IO()
58−setup = system ("docker build -t "++test_image_name++" tests") >> return ()
75+setup :: IO ()
76+setup = system ("docker build -t "++unpack testImageName++" tests") >> return ()
5977
60−main :: IO()
78+main :: IO ()
6179main = do
6280 setup
6381 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