@@ -7,6 +7,7 @@ module Docker.Client.Api (
77 , createContainer
88 , startContainer
99 , stopContainer
10+ , waitContainer
1011 , killContainer
1112 , restartContainer
1213 , pauseContainer
@@ -14,110 +15,131 @@ module Docker.Client.Api (
1415 , deleteContainer
1516 , inspectContainer
1617 , getContainerLogs
18+ , getContainerLogsStream
1719 -- * Images
1820 , listImages
21+ , buildImageFromDockerfile
1922 -- * Other
2023 , getDockerVersion
2124 ) where
2225
23−import Control.Monad.Except (ExceptT (..), runExceptT, throwError)
24−import Control.Monad.Reader (ask, lift)
25−import Data.Aeson (FromJSON, eitherDecode')
26−import qualified Data.ByteString.Lazy as BSL
27−import qualified Data.Text as Text
28−import Network.HTTP.Client (responseBody, responseStatus)
26+import Control.Monad.Catch (MonadMask(..))
27+import Control.Monad.IO.Class
28+import Control.Monad.Reader (ask, lift)
29+import Data.Aeson (FromJSON, eitherDecode')
30+import qualified Data.ByteString as BS
31+import qualified Data.ByteString.Lazy as BSL
32+import Data.Conduit (Sink)
33+import qualified Data.Conduit.Binary as Conduit
34+import qualified Data.Text as Text
35+import Network.HTTP.Client (responseStatus)
2936import Network.HTTP.Types (StdMethod (..))
37+import System.Exit (ExitCode(..))
3038
3139import Docker.Client.Http
3240import Docker.Client.Types
41+import Docker.Client.Utils
3342
34−requestUnit :: (Monad m) => HttpVerb -> Endpoint -> DockerT m (Either DockerError ())
43+requestUnit :: (MonadIO m, MonadMask m) => HttpVerb -> Endpoint -> DockerT m (Either DockerError ())
3544requestUnit verb endpoint = const (Right ()) <$> requestHelper verb endpoint
3645
37−requestHelper :: (Monad m) => HttpVerb -> Endpoint -> DockerT m (Either DockerError Response)
38−requestHelper verb endpoint = runExceptT $ do
39− (opts, httpHandler) <- lift ask
46+requestHelper :: (MonadIO m, MonadMask m) => HttpVerb -> Endpoint -> DockerT m (Either DockerError BSL.ByteString)
47+requestHelper verb endpoint = requestHelper' verb endpoint Conduit.sinkLbs
48+
49+requestHelper' :: (MonadIO m, MonadMask m) => HttpVerb -> Endpoint -> Sink BS.ByteString m a -> DockerT m (Either DockerError a)
50+requestHelper' verb endpoint sink = do
51+ (opts, HttpHandler httpHandler) <- ask
4052 case mkHttpRequest verb endpoint opts of
4153 Nothing ->
42− throwError $ DockerInvalidRequest endpoint
54+ return $ Left $ DockerInvalidRequest endpoint
4355 Just request -> do
44− response <- ExceptT $ lift $ httpHandler request
45−
46− -- Check status code.
47− let status = responseStatus response
48− maybe (return ()) throwError $
49− statusCodeToError endpoint status
50−
51− return response
52−
53−parseResponse :: (FromJSON a, Monad m) => Either DockerError Response -> DockerT m (Either DockerError a)
56+ -- JP: Do we need runResourceT?
57+ -- lift $ NHS.httpSink request $ \response ->
58+ lift $ httpHandler request $ \response ->
59+ -- Check status code.
60+ let status = responseStatus response in
61+ case statusCodeToError endpoint status of
62+ Just err ->
63+ return $ Left err
64+ Nothing ->
65+ fmap Right sink
66+
67+parseResponse :: (FromJSON a, Monad m) => Either DockerError BSL.ByteString -> DockerT m (Either DockerError a)
5468parseResponse (Left err) =
5569 return $ Left err
5670parseResponse (Right response) =
5771 -- Parse request body.
58− case eitherDecode' $ responseBody response of
72+ case eitherDecode' response of
5973 Left err ->
6074 return $ Left $ DockerClientDecodeError $ Text.pack err
6175 Right r ->
6276 return $ Right r
6377
6478-- | Gets the version of the docker engine remote API.
65−getDockerVersion :: forall m. Monad m => DockerT m (Either DockerError DockerVersion)
79+getDockerVersion :: forall m. (MonadIO m, MonadMask m) => DockerT m (Either DockerError DockerVersion)
6680getDockerVersion = requestHelper GET VersionEndpoint >>= parseResponse
6781
6882-- | Lists all running docker containers. Pass in @'defaultListOpts' {all
6983-- = True}@ to get a list of stopped containers as well.
70−listContainers :: forall m. Monad m => ListOpts -> DockerT m (Either DockerError [Container])
84+listContainers :: forall m. (MonadIO m, MonadMask m) => ListOpts -> DockerT m (Either DockerError [Container])
7185listContainers opts = requestHelper GET (ListContainersEndpoint opts) >>= parseResponse
7286
7387-- | Lists all docker images.
74−listImages :: forall m. Monad m => ListOpts -> DockerT m (Either DockerError [Image])
88+listImages :: forall m. (MonadIO m, MonadMask m) => ListOpts -> DockerT m (Either DockerError [Image])
7589listImages opts = requestHelper GET (ListImagesEndpoint opts) >>= parseResponse
7690
7791-- | Creates a docker container but does __not__ start it. See
7892-- 'CreateOpts' for a list of options and you can use 'defaultCreateOpts'
7993-- for some sane defaults.
80−createContainer :: forall m. Monad m => CreateOpts -> Maybe ContainerName -> DockerT m (Either DockerError ContainerID)
94+createContainer :: forall m. (MonadIO m, MonadMask m) => CreateOpts -> Maybe ContainerName -> DockerT m (Either DockerError ContainerID)
8195createContainer opts cn = requestHelper POST (CreateContainerEndpoint opts cn) >>= parseResponse
8296
8397-- | Start a container from a given 'ContainerID' that we get from
8498-- 'createContainer'. See 'StartOpts' for a list of configuration options
8599-- for starting a container. Use 'defaultStartOpts' for sane defaults.
86−startContainer :: forall m. Monad m => StartOpts -> ContainerID -> DockerT m (Either DockerError ())
100+startContainer :: forall m. (MonadIO m, MonadMask m) => StartOpts -> ContainerID -> DockerT m (Either DockerError ())
87101startContainer sopts cid = requestUnit POST $ StartContainerEndpoint sopts cid
88102
89103-- | Attempts to stop a container with the given 'ContainerID' gracefully
90104-- (SIGTERM).
91105-- The docker daemon will wait for the given 'Timeout' and then send
92106-- a SIGKILL killing the container.
93−stopContainer :: forall m. Monad m => Timeout -> ContainerID -> DockerT m (Either DockerError ())
107+stopContainer :: forall m. (MonadIO m, MonadMask m) => Timeout -> ContainerID -> DockerT m (Either DockerError ())
94108stopContainer t cid = requestUnit POST $ StopContainerEndpoint t cid
95109
110+-- | Blocks until a container with the given 'ContainerID' stops,
111+-- then returns the exit code
112+waitContainer :: forall m. (MonadIO m, MonadMask m) => ContainerID -> DockerT m (Either DockerError ExitCode)
113+waitContainer cid = fmap (fmap statusCodeToExitCode) (requestHelper POST (WaitContainerEndpoint cid) >>= parseResponse)
114+ where
115+ statusCodeToExitCode (StatusCode 0) = ExitSuccess
116+ statusCodeToExitCode (StatusCode x) = ExitFailure x
117+
96118-- | Sends a 'Signal' to the container with the given 'ContainerID'. Same
97119-- as 'stopContainer' but you choose the signal directly.
98−killContainer :: forall m. Monad m => Signal -> ContainerID -> DockerT m (Either DockerError ())
120+killContainer :: forall m. (MonadIO m, MonadMask m) => Signal -> ContainerID -> DockerT m (Either DockerError ())
99121killContainer s cid = requestUnit POST $ KillContainerEndpoint s cid
100122
101123-- | Restarts a container with the given 'ContainerID'.
102−restartContainer :: forall m. Monad m => Timeout -> ContainerID -> DockerT m (Either DockerError ())
124+restartContainer :: forall m. (MonadIO m, MonadMask m) => Timeout -> ContainerID -> DockerT m (Either DockerError ())
103125restartContainer t cid = requestUnit POST $ RestartContainerEndpoint t cid
104126
105127-- | Pauses a container with the given 'ContainerID'.
106−pauseContainer :: forall m. Monad m => ContainerID -> DockerT m (Either DockerError ())
128+pauseContainer :: forall m. (MonadIO m, MonadMask m) => ContainerID -> DockerT m (Either DockerError ())
107129pauseContainer cid = requestUnit POST $ PauseContainerEndpoint cid
108130
109131-- | Unpauses a container with the given 'ContainerID'.
110−unpauseContainer :: forall m. Monad m => ContainerID -> DockerT m (Either DockerError ())
132+unpauseContainer :: forall m. (MonadIO m, MonadMask m) => ContainerID -> DockerT m (Either DockerError ())
111133unpauseContainer cid = requestUnit GET $ UnpauseContainerEndpoint cid
112134
113135-- | Deletes a container with the given 'ContainerID'.
114136-- See "DeleteOpts" for options and use 'defaultDeleteOpts' for sane
115137-- defaults.
116−deleteContainer :: forall m. Monad m => DeleteOpts -> ContainerID -> DockerT m (Either DockerError ())
138+deleteContainer :: forall m. (MonadIO m, MonadMask m) => DeleteOpts -> ContainerID -> DockerT m (Either DockerError ())
117139deleteContainer dopts cid = requestUnit DELETE $ DeleteContainerEndpoint dopts cid
118140
119141-- | Gets 'ContainerDetails' for a given 'ContainerID'.
120−inspectContainer :: forall m . Monad m => ContainerID -> DockerT m (Either DockerError ContainerDetails)
142+inspectContainer :: forall m . (MonadIO m, MonadMask m) => ContainerID -> DockerT m (Either DockerError ContainerDetails)
121143inspectContainer cid = requestHelper GET (InspectContainerEndpoint cid) >>= parseResponse
122144
123145-- | Get's container's logs for a given 'ContainerID'.
@@ -135,12 +157,19 @@ inspectContainer cid = requestHelper GET (InspectContainerEndpoint cid) >>= pars
135157--
136158-- The recommended method is to use one of the other 'LogDriverType's available (like
137159-- syslog) for creating your containers.
138−getContainerLogs :: forall m. Monad m => LogOpts -> ContainerID -> DockerT m (Either DockerError BSL.ByteString)
139−getContainerLogs logopts cid = fmap responseBody <$> requestHelper GET (ContainerLogsEndpoint logopts False cid)
140−
141−-- TODO: Use http-conduit to output to a sink.
142−-- getContainerLogsStream :: forall m. Monad m => Sink BSL.ByteString m b -> LogOpts -> ContainerID -> DockerT m (Either DockerError b)
143−-- getContainerLogsStream sink logopts cid = runResourceT $ do
144−-- response <- http request manager
145−-- responseBody response C.$$+- sink
146−
160+getContainerLogs :: forall m. (MonadIO m, MonadMask m) => LogOpts -> ContainerID -> DockerT m (Either DockerError BSL.ByteString)
161+getContainerLogs logopts cid = requestHelper GET (ContainerLogsEndpoint logopts False cid)
162+
163+getContainerLogsStream :: forall m b . (MonadIO m, MonadMask m) => LogOpts -> ContainerID -> Sink BS.ByteString m b -> DockerT m (Either DockerError b)
164+getContainerLogsStream logopts cid sink = requestHelper' GET (ContainerLogsEndpoint logopts True cid) sink
165+-- JP: Should the second (follow) argument be True? XXX
166+
167+-- TODO: Add X-Registry-Config
168+-- TODO: Add support for remote URLs to a Dockerfile
169+-- TODO: Clean up temp tar.gz file after the image is built
170+buildImageFromDockerfile :: forall m. (MonadIO m, MonadMask m) => BuildOpts -> FilePath -> DockerT m (Either DockerError ())
171+buildImageFromDockerfile opts base = do
172+ ctx <- makeBuildContext $ BuildContextRootDir base
173+ case ctx of
174+ Left e -> return $ Left e
175+ Right c -> requestUnit POST (BuildImageEndpoint opts c)