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

Merge latest changes from dev

commit2f728a792ab81ccde8c7840fb89d703d3320fbe0 authorDeni Bertovic <deni@denibertovic.com> dateThu 11 Aug 2016, 23:32 (10 years ago) parent57a6d3445d7f43
11 files changed+332−149
.travis.yml+16−3 LICENSE+1−1 Makefile+4−5 README.md+1−1 docker.cabal+9−11 src/Docker/Client.hs+13 src/Docker.hs → src/Docker/Client/Api.hs+42−17 src/Docker/Http.hs → src/Docker/Client/Http.hs+4−3 src/Docker/Internal.hs → src/Docker/Client/Internal.hs+4−2 src/Docker/Types.hs → src/Docker/Client/Types.hs+198−84 tests/tests.hs+40−22
.travis.yml+16−3
@@ -1,9 +1,17 @@
1−sudo:
2− false
1+sudo: required
2+dist: trusty
33
4−language:
4+# Choose a lightweight base image; we provide our own build tools.
5+language:
56 c
67
8+# Caching so the next build will be fast too.
9+cache:
10+ directories:
11+ - $HOME/.ghc
12+ - $HOME/.cabal
13+ - $HOME/.stack
14+
715services:
816 - docker
917
@@ -16,6 +24,11 @@ addons:
1624 - libgmp-dev
1725
1826before_install:
27+ # Upgrade docker to 1.12
28+ - sudo sh -c 'echo "deb https://apt.dockerproject.org/repo ubuntu-trusty main" > /etc/apt/sources.list.d/docker.list'
29+ - sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D
30+ - sudo apt-get update
31+ - sudo apt-get -qqy -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" install docker-engine=1.12.0-0~trusty
1932 - mkdir -p "$HOME"/.local/bin
2033 - export PATH=$HOME/.local/bin:$PATH
2134
LICENSE+1−1
@@ -1,4 +1,4 @@
1−Copyright (c) 2014, Deni Bertovic
1+Copyright (c) 2016, Deni Bertovic
22
33All rights reserved.
44
Makefile+4−5
@@ -1,4 +1,4 @@
1−.PHONY: test build upload dist repl sdist
1+.PHONY: test build repl release
22
33VERSION ?= $(shell grep "^version:" docker.cabal | cut -d " " -f14)
44
@@ -11,9 +11,8 @@ repl:
1111test:
1212 @stack test
1313
14−sdist:
14+release:
15+ @stack build --haddock
1516 @stack sdist
16−
17−upload: dist
18− @stack upload .stack-work/dist/x86_64-linux/Cabal-1.18.1.5/docker-${VERSION}.tar.gz
17+ @echo "\n\nDone. Now run: \n\nstack upload <path_to_tarball>\n"
1918
README.md+1−1
@@ -1,4 +1,4 @@
1−# Docker Remote API wrapper for Haskell
1+# An API client for docker written in Haskell
22
33Travis CI : [![Build Status](https://travis-ci.org/denibertovic/docker-hs.svg?branch=master)](https://travis-ci.org/denibertovic/docker-hs)
44
docker.cabal+9−11
@@ -1,12 +1,12 @@
11name: docker
22version: 0.3.0.0
3−synopsis: Haskell wrapper for Docker Remote API
3+synopsis: An API client for docker written in Haskell
44description: See README.md
55homepage: https://github.com/denibertovic/docker-hs
66license: BSD3
77license-file: LICENSE
8−author: Deni Bertovic
9−maintainer: deni@denibertovic.com
8+author: Deni Bertovic <deni@denibertovic.com>, James Parker <jp@jamesparker.me>
9+maintainer: Deni Bertovic <deni@denibertovic.com>
1010copyright: BSD3
1111category: Network
1212build-type: Simple
@@ -17,28 +17,21 @@ stability: experimental
1717library
1818 default-extensions: DeriveGeneric, OverloadedStrings, ScopedTypeVariables, ExplicitForAll, MultiParamTypeClasses, FlexibleContexts, GeneralizedNewtypeDeriving, RankNTypes, DeriveFunctor, FlexibleInstances
1919 hs-source-dirs: src
20− exposed-modules: Docker, Docker.Types, Docker.Internal, Docker.Http
20+ exposed-modules: Docker.Client, Docker.Client.Api, Docker.Client.Types, Docker.Client.Internal, Docker.Client.Http
2121 -- other-modules: Docker.Internal
2222 build-depends: base >= 4.7 && < 5
2323 , aeson
2424 , blaze-builder
2525 , bytestring
2626 , containers
27− , connection
2827 , data-default-class
29− , exceptions
3028 , http-client
3129 , http-types
32− , lens
33− , lens-aeson
34− , lifted-base
3530 , mtl
3631 , network >= 2.6
37− , network-uri
3832 , text
3933 , time
4034 , tls >= 1.3.7
41− , transformers
4235 , unordered-containers
4336 , x509
4437 , x509-store
@@ -51,6 +44,9 @@ test-suite docker-hs-tests
5144 , bytestring
5245 , connection
5346 , docker
47+ , aeson
48+ , containers
49+ , unordered-containers
5450 , http-client
5551 , http-client-tls
5652 , http-types
@@ -58,6 +54,8 @@ test-suite docker-hs-tests
5854 , tasty-hunit
5955 , tasty-quickcheck
6056 , text
57+ , lens
58+ , lens-aeson
6159 , transformers
6260 , QuickCheck
6361 , process
src/Docker/Client.hsnew file+13
@@ -0,0 +1,13 @@
1+{-# LANGUAGE ExplicitForAll #-}
2+{-# LANGUAGE ScopedTypeVariables #-}
3+
4+module Docker.Client (
5+ module Docker.Client.Api
6+ , module Docker.Client.Types
7+ , module Docker.Client.Http
8+ ) where
9+
10+import Docker.Client.Api
11+import Docker.Client.Http
12+import Docker.Client.Types
13+
src/Docker.hs → src/Docker/Client/Api.hsrenamed+42−17
@@ -1,19 +1,8 @@
11{-# LANGUAGE ExplicitForAll #-}
22{-# LANGUAGE ScopedTypeVariables #-}
33
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
4+module Docker.Client.Api (
5+ getDockerVersion
176 , listContainers
187 , listImages
198 , createContainer
@@ -36,8 +25,8 @@ import qualified Data.Text as Text
3625import Network.HTTP.Client (responseBody, responseStatus)
3726import Network.HTTP.Types (StdMethod (..))
3827
39−import Docker.Http
40−import Docker.Types
28+import Docker.Client.Http
29+import Docker.Client.Types
4130
4231requestUnit :: (Monad m) => HttpVerb -> Endpoint -> DockerT m (Either DockerError ())
4332requestUnit verb endpoint = const (Right ()) <$> requestHelper verb endpoint
@@ -69,44 +58,80 @@ parseResponse (Right response) =
6958 Right r ->
7059 return $ Right r
7160
61+-- | Gets the version of the docker engine remote API.
7262getDockerVersion :: forall m. Monad m => DockerT m (Either DockerError DockerVersion)
7363getDockerVersion = requestHelper GET VersionEndpoint >>= parseResponse
7464
65+-- | Lists all running docker containers. Pass in 'defaultListOpts' {all
66+-- = True} to get a list of stopped containers as well.
7567listContainers :: forall m. Monad m => ListOpts -> DockerT m (Either DockerError [Container])
7668listContainers opts = requestHelper GET (ListContainersEndpoint opts) >>= parseResponse
7769
70+-- | Lists all docker images.
7871listImages :: forall m. Monad m => ListOpts -> DockerT m (Either DockerError [Image])
7972listImages opts = requestHelper GET (ListImagesEndpoint opts) >>= parseResponse
8073
74+-- | Creates a docker container but does *not* start it. See
75+-- "CreateOpts" for a list of options and you can use 'defaultCreateOpts'
76+-- for some sane defaults.
8177createContainer :: forall m. Monad m => CreateOpts -> DockerT m (Either DockerError ContainerID)
8278createContainer opts = requestHelper POST (CreateContainerEndpoint opts) >>= parseResponse
8379
84−-- TODO: Is this the correct return type? XXX
85−-- convert status code to unit or error
80+-- | Start a container from a given "ContainerID" that we get from
81+-- 'createContainer'. See "StartOpts" for a list of configuration options
82+-- for starting a container. Use 'defaultStartOpts' for sane defaults.
8683startContainer :: forall m. Monad m => StartOpts -> ContainerID -> DockerT m (Either DockerError ())
8784startContainer sopts cid = requestUnit POST $ StartContainerEndpoint sopts cid
8885
86+-- | Attempts to stop a container with the given "ContainerID" gracefully
87+-- (SIGTERM).
88+-- The docker daemon will wait for the given "Tiemout" and then send
89+-- a SIGKILL killing the container.
8990stopContainer :: forall m. Monad m => Timeout -> ContainerID -> DockerT m (Either DockerError ())
9091stopContainer t cid = requestUnit POST $ StopContainerEndpoint t cid
9192
93+-- | Sends a "Signal" to the container with the given "ContainerID". Same
94+-- as "stopContainer" but you choose the signal directly.
9295killContainer :: forall m. Monad m => Signal -> ContainerID -> DockerT m (Either DockerError ())
9396killContainer s cid = requestUnit POST $ KillContainerEndpoint s cid
9497
98+-- | Restarts a container with the given "ContainerID".
9599restartContainer :: forall m. Monad m => Timeout -> ContainerID -> DockerT m (Either DockerError ())
96100restartContainer t cid = requestUnit POST $ RestartContainerEndpoint t cid
97101
102+-- | Pauses a container with the given "ContainerID"
98103pauseContainer :: forall m. Monad m => ContainerID -> DockerT m (Either DockerError ())
99104pauseContainer cid = requestUnit POST $ PauseContainerEndpoint cid
100105
106+-- | Unpauses a container with the given "ContainerID".
101107unpauseContainer :: forall m. Monad m => ContainerID -> DockerT m (Either DockerError ())
102108unpauseContainer cid = requestUnit GET $ UnpauseContainerEndpoint cid
103109
110+-- | Deletes a container with the given "ContainerID".
111+-- See "DeleteOpts" for options and use 'defaultDeleteOpts' for sane
112+-- defaults.
104113deleteContainer :: forall m. Monad m => DeleteOpts -> ContainerID -> DockerT m (Either DockerError ())
105114deleteContainer dopts cid = requestUnit DELETE $ DeleteContainerEndpoint dopts cid
106115
116+-- | Gets "ContainerDetails" for a given "ContainerID".
107117inspectContainer :: forall m . Monad m => ContainerID -> DockerT m (Either DockerError ContainerDetails)
108118inspectContainer cid = requestHelper GET (InspectContainerEndpoint cid) >>= parseResponse
109119
120+-- | Get's container's logs for a given "ContainerID".
121+-- This will only work with the "JsonFile" "LogDriverType" as the other driver types disable
122+-- this endpoint and it will return a "DockerError".
123+--
124+-- See "LogOpts" for options that you can pass and
125+-- 'defaultLogOpts' for sane defaults.
126+--
127+-- __NOTE__: Currently streaming logs is
128+-- not supported and this function will fetch the entire log that the
129+-- container produced in the json-file on disk. Depending on the logging
130+-- setup of the process in your container this can be a significant amount
131+-- which might block your application...so use with caution.
132+--
133+-- The recommended method is to use one of the other "LogDriverType"'s available (like
134+-- syslog) for creating your containers.
110135getContainerLogs :: forall m. Monad m => LogOpts -> ContainerID -> DockerT m (Either DockerError BSL.ByteString)
111136getContainerLogs logopts cid = fmap responseBody <$> requestHelper GET (ContainerLogsEndpoint logopts False cid)
112137
src/Docker/Http.hs → src/Docker/Client/Http.hsrenamed+4−3
@@ -1,7 +1,7 @@
11{-# LANGUAGE RankNTypes #-}
22{-# LANGUAGE ScopedTypeVariables #-}
33
4−module Docker.Http where
4+module Docker.Client.Http where
55
66import Control.Monad.Reader (ReaderT, runReaderT)
77import qualified Data.ByteString.Char8 as BSC
@@ -36,9 +36,10 @@ import qualified Network.HTTP.Types as HTTP
3636import qualified Network.Socket as S
3737import qualified Network.Socket.ByteString as SBS
3838
39−import Docker.Internal (getEndpoint,
39+
40+import Docker.Client.Internal (getEndpoint,
4041 getEndpointRequestBody)
41−import Docker.Types (DockerClientOpts, Endpoint (..),
42+import Docker.Client.Types (DockerClientOpts, Endpoint (..),
4243 baseUrl)
4344
4445type Request = HTTP.Request
src/Docker/Internal.hs → src/Docker/Client/Internal.hsrenamed+4−2
@@ -1,4 +1,4 @@
1−module Docker.Internal where
1+module Docker.Client.Internal where
22
33
44import Blaze.ByteString.Builder (toByteString)
@@ -7,10 +7,12 @@ import Data.ByteString (ByteString)
77import qualified Data.ByteString.Lazy as BSL
88import Data.Text as T
99import Data.Text.Encoding (decodeUtf8, encodeUtf8)
10−import Docker.Types
1110import Network.HTTP.Types (Query, encodePath,
1211 encodePathSegments)
1312
13+import Docker.Client.Types
14+
15+
1416encodeURL :: [T.Text] -> T.Text
1517encodeURL ps = decodeUtf8 $ toByteString $ encodePathSegments ps
1618
src/Docker/Types.hs → src/Docker/Client/Types.hsrenamed+198−84
@@ -1,7 +1,7 @@
11{-# LANGUAGE RecordWildCards #-}
22{-# LANGUAGE ScopedTypeVariables #-}
33
4−module Docker.Types (
4+module Docker.Client.Types (
55 Endpoint(..)
66 , URL
77 , ApiVersion
@@ -34,6 +34,7 @@ module Docker.Types (
3434 , StartOpts(..)
3535 , defaultStartOpts
3636 , DeleteOpts(..)
37+ , defaultDeleteOpts
3738 , Timestamp
3839 , TailLogOpt(..)
3940 , LogOpts(..)
@@ -49,7 +50,7 @@ module Docker.Types (
4950 , LogDriverConfig(..)
5051 , NetworkMode(..)
5152 , PortType(..)
52− , NetworkInterface(..)
53+-- , NetworkInterface(..)
5354 , Networks(..)
5455 , NetworkSettings(..)
5556 , NetworkOptions(..)
@@ -73,6 +74,8 @@ module Docker.Types (
7374 , ContainerConfig(..)
7475 , defaultContainerConfig
7576 , ExposedPorts(..)
77+ , DeviceWeight(..)
78+ , DeviceRate(..)
7679 ) where
7780
7881import Data.Aeson (FromJSON, ToJSON, genericParseJSON,
@@ -102,7 +105,8 @@ data Endpoint =
102105 | RestartContainerEndpoint Timeout ContainerID
103106 | PauseContainerEndpoint ContainerID
104107 | UnpauseContainerEndpoint ContainerID
105− | ContainerLogsEndpoint LogOpts Bool ContainerID -- Second argument (Bool) is whether to follow.
108+ | ContainerLogsEndpoint LogOpts Bool ContainerID -- ^ Second argument (Bool) is whether to follow which is currently hardcoded to False.
109+ -- See note in 'Docker.Client.Api.getContainerLogs' for explanation why.
106110 | DeleteContainerEndpoint DeleteOpts ContainerID
107111 | InspectContainerEndpoint ContainerID
108112 deriving (Eq, Show)
@@ -253,7 +257,7 @@ data DockerClientOpts = DockerClientOpts {
253257
254258defaultClientOpts :: DockerClientOpts
255259defaultClientOpts = DockerClientOpts {
256− apiVer = "v1.23"
260+ apiVer = "v1.24"
257261 , baseUrl = "http://127.0.0.1:2375"
258262 }
259263
@@ -282,11 +286,8 @@ instance FromJSON DockerVersion where
282286 parseJSON = genericParseJSON defaultOptions {
283287 fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
284288
285−-- instance ToJSON ContainerDetails where
286−-- toJSON = error "TODO"
287−
288289instance FromJSON ContainerDetails where
289− parseJSON (JSON.Object o) = do
290+ parseJSON v@(JSON.Object o) = do
290291 appArmor <- o .: "AppArmorProfile"
291292 args <- o .: "Args"
292293 config <- o .: "Config"
@@ -296,7 +297,7 @@ instance FromJSON ContainerDetails where
296297 hostnamePath <- o .: "HostnamePath"
297298 hostsPath <- o .: "HostsPath"
298299 logPath <- o .: "LogPath"
299− id <- o .: "Id"
300+ id <- parseJSON v
300301 image <- o .: "Image"
301302 mountLabel <- o .: "MountLabel"
302303 name <- o .: "Name"
@@ -396,24 +397,24 @@ instance FromJSON Networks where
396397 parseJSON _ = fail "Networks is not an object"
397398
398399data NetworkSettings = NetworkSettings {
399− networkSettingsBridge :: Text
400− , networkSettingsSandboxId :: Text
401− , networkSettingsHairpinMode :: Bool
402− , networkSettingsLinkLocalIPv6Address :: Text
400+ networkSettingsBridge :: Text
401+ , networkSettingsSandboxId :: Text
402+ , networkSettingsHairpinMode :: Bool
403+ , networkSettingsLinkLocalIPv6Address :: Text
403404 , networkSettingsLinkLocalIPv6PrefixLen :: Int
404− , networkSettingsPorts :: [Port] -- TODO: 1.24 spec is unclear
405− , networkSettingsSandboxKey :: Text
406− , networkSettingsSecondaryIPAddresses :: Maybe [Text] -- TODO: 1.24 spec is unclear
405+ , networkSettingsPorts :: PortBindings
406+ , networkSettingsSandboxKey :: Text
407+ , networkSettingsSecondaryIPAddresses :: Maybe [Text] -- TODO: 1.24 spec is unclear
407408 , 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
409+ , networkSettingsEndpointID :: Text
410+ , networkSettingsGateway :: Text
411+ , networkSettingsGlobalIPv6Address :: Text
412+ , networkSettingsGlobalIPv6PrefixLen :: Int
413+ , networkSettingsIpAddress :: Text
414+ , networkSettingsIpPrefixLen :: Int
415+ , networkSettingsIpv6Gateway :: Text
416+ , networkSettingsMacAddress :: Text
417+ , networkSettingsNetworks :: Networks
417418 }
418419 deriving (Eq, Show)
419420
@@ -424,7 +425,7 @@ instance FromJSON NetworkSettings where
424425 hairpin <- o .: "HairpinMode"
425426 localIP6 <- o .: "LinkLocalIPv6Address"
426427 localIP6Len <- o .: "LinkLocalIPv6PrefixLen"
427− ports <- o .:? "Ports" .!= []
428+ ports <- o .: "Ports" -- .!= []
428429 sandboxKey <- o .: "SandboxKey"
429430 secondaryIP <- o .: "SecondaryIPAddresses"
430431 secondayIP6 <- o .: "SecondaryIPv6Addresses"
@@ -438,7 +439,7 @@ instance FromJSON NetworkSettings where
438439 mac <- o .: "MacAddress"
439440 networks <- o .: "Networks"
440441 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+ parseJSON _ = fail "NetworkSettings is not an object."
442443
443444data Container = Container
444445 { containerId :: ContainerID
@@ -539,7 +540,7 @@ defaultContainerConfig imageName = ContainerConfig {
539540 , attachStdout=False
540541 , image=imageName
541542 , attachStderr=False
542− , exposedPorts=ExposedPorts M.empty
543+ , exposedPorts=Nothing -- ExposedPorts M.empty
543544 , tty=False
544545 , openStdin=False
545546 , stdinOnce=False
@@ -548,7 +549,7 @@ defaultContainerConfig imageName = ContainerConfig {
548549 , volumes=Nothing
549550 , workingDir=Nothing
550551 , entrypoint=Nothing
551− , networkDisabled=False
552+ , networkDisabled=Nothing
552553 , macAddress=Nothing
553554 , labels=Nothing
554555 , stopSignal=SIGTERM
@@ -575,7 +576,7 @@ defaultHostConfig = HostConfig {
575576 , oomScoreAdj=Nothing
576577 , privileged=False
577578 , publishAllPorts=False
578− , readOnlyRootfs=False
579+ , readonlyRootfs=False
579580 , securityOpt=[]
580581 , shmSize=Nothing
581582 , resources=defaultContainerResources
@@ -616,10 +617,13 @@ defaultStartOpts :: StartOpts
616617defaultStartOpts = StartOpts { detachKeys = DefaultDetachKey }
617618
618619data DeleteOpts = DeleteOpts {
619− deleteVolumes :: Bool
620− , force :: Bool
620+ deleteVolumes :: Bool -- ^ Automatically cleanup volumes that the container created as well.
621+ , force :: Bool -- ^ If the container is still running force deletion anyway.
621622 } deriving (Eq, Show)
622623
624+defaultDeleteOpts :: DeleteOpts
625+defaultDeleteOpts = DeleteOpts { deleteVolumes = False, force = False }
626+
623627type Timestamp = Integer
624628data TailLogOpt = Tail Integer | All deriving (Eq, Show)
625629
@@ -804,14 +808,14 @@ instance FromJSON PortType where
804808 "udp" -> return UDP
805809 _ -> fail "PortType: Invalid port type."
806810
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
811+-- newtype NetworkInterface = NetworkInterface Text deriving (Eq, Show)
812+--
813+-- instance FromJSON NetworkInterface where
814+-- parseJSON (JSON.String v) = return $ NetworkInterface v
815+-- parseJSON _ = fail "Network interface is not a string."
816+--
817+-- instance ToJSON NetworkInterface where
818+-- toJSON (NetworkInterface i) = JSON.String i
815819
816820-- | This datastructure models mapping a Port from the container onto the
817821-- host system s that the service running in the container can be accessed from
@@ -824,7 +828,7 @@ newtype PortBindings = PortBindings [PortBinding]
824828data PortBinding = PortBinding {
825829 containerPort :: Port
826830 , portType :: PortType
827− , hostPorts :: HostPorts
831+ , hostPorts :: [HostPort]
828832 } deriving (Eq, Show)
829833
830834
@@ -854,37 +858,39 @@ instance ToJSON PortBindings where
854858 toJSON (PortBindings []) = JSON.Object HM.empty
855859 toJSON (PortBindings (p:ps)) = toJSON (p:ps)
856860
857−data HostPort = HostPort NetworkInterface Port
861+data HostPort = HostPort {
862+ hostIp :: Text
863+ , hostPost :: Port
864+ }
858865 deriving (Eq, Show)
859866
860867instance ToJSON HostPort where
861− toJSON (HostPort i p) = object ["HostPort" .= p, "HostIP" .= i]
868+ toJSON (HostPort i p) = object ["HostPort" .= show p, "HostIp" .= i]
862869
863870instance FromJSON HostPort where
864871 parseJSON (JSON.Object o) = do
865− p <- o .: "HostPort"
866− i <- o .: "HostIP"
872+ p <- o .: "HostPort" >>= parseIntegerText
873+ i <- o .: "HostIp"
867874 return $ HostPort i p
868875 parseJSON _ = fail "HostPort is not an object."
869876
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"
877+-- newtype HostPorts = HostPorts [HostPort]
878+-- deriving (Eq, Show)
879+--
880+-- instance ToJSON HostPorts where
881+-- toJSON (HostPorts hps) = toJSON hps
882+--
883+-- instance FromJSON HostPorts where
884+-- parseJSON (JSON.Object o) = do
885+-- HostPorts <$> HM.foldlWithKey' f (return []) o
886+--
887+-- where
888+-- f accM k v = do
889+-- acc <- accM
890+-- p' <- parseJSON v
891+-- p <- parseIntegerText p'
892+-- return $ (HostPort (NetworkInterface k) p):acc
893+-- parseJSON _ = fail $ "HostPorts is not an object"
888894
889895-- { "Name": "on-failure" , "MaximumRetryCount": 2}
890896type RetryCount = Integer
@@ -899,7 +905,7 @@ instance FromJSON RestartPolicy where
899905 "on-failure" -> do
900906 retry <- o .: "MaximumRetryCount"
901907 return $ RestartOnFailure retry
902− "" -> return RestartOff -- Note: Is this correct? 1.24 spec is unclear.
908+ "off" -> return RestartOff
903909 _ -> fail "Could not parse RestartPolicy"
904910 parseJSON _ = fail "RestartPolicy is not an object"
905911
@@ -938,7 +944,7 @@ data HostConfig = HostConfig
938944 -- , pidMode :: Text -- 1.24: Don't see pidMode, just pidsLimit
939945 , privileged :: Bool
940946 , publishAllPorts :: Bool
941− , readOnlyRootfs :: Bool
947+ , readonlyRootfs :: Bool
942948 , securityOpt :: [Text]
943949 -- , utsMode :: UTSMode -- 1.24: Don't see this
944950 , shmSize :: Maybe Integer
@@ -948,12 +954,84 @@ data HostConfig = HostConfig
948954 } deriving (Eq, Show, Generic)
949955
950956instance FromJSON HostConfig where
951− parseJSON = genericParseJSON defaultOptions {
952− fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
957+ parseJSON v@(JSON.Object o) = HostConfig
958+ <$> o .: "Binds"
959+ <*> o .: "ContainerIDFile"
960+ <*> o .: "LogConfig"
961+ <*> o .: "NetworkMode"
962+ <*> o .: "PortBindings"
963+ <*> o .: "RestartPolicy"
964+ <*> o .: "VolumeDriver"
965+ <*> o .: "VolumesFrom"
966+ <*> o .: "CapAdd"
967+ <*> o .: "CapDrop"
968+ <*> o .: "Dns"
969+ <*> o .: "DnsOptions"
970+ <*> o .: "DnsSearch"
971+ <*> o .: "ExtraHosts"
972+ <*> o .: "IpcMode"
973+ <*> o .:? "Links" .!= []
974+ <*> o .: "OomScoreAdj"
975+ <*> o .: "Privileged"
976+ <*> o .: "PublishAllPorts"
977+ <*> o .: "ReadonlyRootfs"
978+ <*> o .: "SecurityOpt"
979+ <*> o .: "ShmSize"
980+ <*> parseJSON v
981+ parseJSON _ = fail "HostConfig is not an object."
953982
954983instance ToJSON HostConfig where
955− toJSON = genericToJSON defaultOptions {
956− fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
984+ toJSON HostConfig{..} =
985+ let arr = [
986+ "Binds" .= binds
987+ , "ContainerIDFile" .= containerIDFile
988+ , "LogConfig" .= logConfig
989+ , "NetworkMode" .= networkMode
990+ , "PortBindings" .= portBindings
991+ , "RestartPolicy" .= restartPolicy
992+ , "VolumeDriver" .= volumeDriver
993+ , "VolumesFrom" .= volumesFrom
994+ , "CapAdd" .= capAdd
995+ , "CapDrop" .= capDrop
996+ , "Dns" .= dns
997+ , "DnsOptions" .= dnsOptions
998+ , "DnsSearch" .= dnsSearch
999+ , "ExtraHosts" .= extraHosts
1000+ , "IpcMode" .= ipcMode
1001+ , "Links" .= links
1002+ , "OomScoreAdj" .= oomScoreAdj
1003+ , "Privileged" .= privileged
1004+ , "PublishAllPorts" .= publishAllPorts
1005+ , "ReadonlyRootfs" .= readonlyRootfs
1006+ , "SecurityOpt" .= securityOpt
1007+ , "ShmSize" .= shmSize
1008+ ]
1009+ in
1010+ object $ arr <> ( resourcesArr resources)
1011+
1012+ where
1013+ -- JP: Not sure if this is better than a separate ToJSON instance with a bunch of `HM.insert`s.
1014+ resourcesArr ContainerResources{..} = [
1015+ "CpuShares" .= cpuShares
1016+ , "BlkioWeight" .= blkioWeight
1017+ , "BlkioWeightDevice" .= blkioWeightDevice
1018+ , "BlkioDeviceReadBps" .= blkioDeviceReadBps
1019+ , "BlkioDeviceWriteBps" .= blkioDeviceWriteBps
1020+ , "BlkioDeviceReadIOps" .= blkioDeviceReadIOps
1021+ , "BlkioDeviceWriteIOps" .= blkioDeviceWriteIOps
1022+ , "CpuPeriod" .= cpuPeriod
1023+ , "CpusetCpus" .= cpusetCpus
1024+ , "CpusetMems" .= cpusetMems
1025+ , "Devices" .= devices
1026+ , "KernelMemory" .= kernelMemory
1027+ , "Memory" .= memory
1028+ , "MemoryReservation" .= memoryReservation
1029+ , "MemorySwap" .= memorySwap
1030+ , "OomKillDisable" .= oomKillDisable
1031+ , "Ulimits" .= ulimits
1032+ ]
1033+
1034+
9571035
9581036
9591037-- { "Name": <name>, "Soft": <soft limit>, "Hard": <hard limit> }
@@ -972,18 +1050,54 @@ instance ToJSON Ulimit where
9721050 toJSON = genericToJSON defaultOptions {
9731051 fieldLabelModifier = drop 5}
9741052
1053+data DeviceWeight = DeviceWeight {
1054+ deviceWeightPath :: FilePath
1055+ , deviceWeightWeight :: Text
1056+ }
1057+ deriving (Show, Eq)
1058+
1059+instance FromJSON DeviceWeight where
1060+ parseJSON (JSON.Object o) = DeviceWeight
1061+ <$> o .: "Path"
1062+ <*> o .: "Weight"
1063+ parseJSON _ = fail "DeviceWeight is not an object."
1064+
1065+instance ToJSON DeviceWeight where
1066+ toJSON (DeviceWeight p w) = object [
1067+ "Path" .= p
1068+ , "Weight" .= w
1069+ ]
1070+
1071+data DeviceRate = DeviceRate {
1072+ deviceRatePath :: FilePath
1073+ , deviceRateRate :: Text
1074+ }
1075+ deriving (Show, Eq)
1076+
1077+instance FromJSON DeviceRate where
1078+ parseJSON (JSON.Object o) = DeviceRate
1079+ <$> o .: "Path"
1080+ <*> o .: "Rate"
1081+ parseJSON _ = fail "DeviceRate is not an object."
1082+
1083+instance ToJSON DeviceRate where
1084+ toJSON (DeviceRate p r) = object [
1085+ "Path" .= p
1086+ , "Rate" .= r
1087+ ]
1088+
9751089data ContainerResources = ContainerResources {
9761090 cpuShares :: Maybe Integer
9771091 -- , 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
1092+ , blkioWeight :: Maybe Integer
1093+ , blkioWeightDevice :: Maybe [DeviceWeight]
1094+ , blkioDeviceReadBps :: Maybe [DeviceRate] -- TODO: Not Text
1095+ , blkioDeviceWriteBps :: Maybe [DeviceRate] -- TODO: Not Text
1096+ , blkioDeviceReadIOps :: Maybe [DeviceRate] -- TODO: Not Text
1097+ , blkioDeviceWriteIOps :: Maybe [DeviceRate] -- TODO: Not Text
9841098 , cpuPeriod :: Maybe Integer
9851099 -- , cpuQuota :: Integer -- 1.24: Missing from inspecting container details... Going to omit for now.
986− , cpusetCpus :: Maybe Integer
1100+ , cpusetCpus :: Maybe Text
9871101 , cpusetMems :: Maybe Text
9881102 , devices :: [Device]
9891103 -- , diskQuota :: Integer -- Don't see this ins 1.24.
@@ -1006,13 +1120,13 @@ data ContainerResources = ContainerResources {
10061120 -- LxcConf :: [??] +
10071121 } deriving (Eq, Show, Generic)
10081122
1009−instance ToJSON ContainerResources where
1010− toJSON = genericToJSON defaultOptions {
1011− fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
1123+-- instance ToJSON ContainerResources where
1124+-- toJSON = genericToJSON defaultOptions {
1125+-- fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
10121126
10131127instance FromJSON ContainerResources where
10141128 parseJSON = genericParseJSON defaultOptions {
1015− fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
1129+ fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
10161130
10171131type Port = Integer
10181132
@@ -1074,7 +1188,7 @@ data ContainerConfig = ContainerConfig {
10741188 , attachStdin :: Bool
10751189 , attachStdout :: Bool
10761190 , attachStderr :: Bool
1077− , exposedPorts :: ExposedPorts
1191+ , exposedPorts :: Maybe ExposedPorts -- Note: Should we expand the JSON instance and take away the Maybe?
10781192 -- , publishService :: Text -- Don't see this in 1.24
10791193 , tty :: Bool
10801194 , openStdin :: Bool
@@ -1086,9 +1200,9 @@ data ContainerConfig = ContainerConfig {
10861200 , volumes :: Maybe Volumes
10871201 , workingDir :: Maybe FilePath
10881202 , entrypoint :: Maybe Text -- Can be null?
1089− , networkDisabled :: Bool
1203+ , networkDisabled :: Maybe Bool -- Note: Should we expand the JSON instance and take away the Maybe? Null is False?
10901204 , macAddress :: Maybe Text
1091− -- , onBuild :: Text -- For 1.24, only see this in the inspect response.
1205+ -- , onBuild :: Maybe Text -- For 1.24, only see this in the inspect response.
10921206 , labels :: Maybe Labels
10931207 , stopSignal :: Signal
10941208 } deriving (Eq, Show, Generic)
@@ -1101,7 +1215,7 @@ instance FromJSON ContainerConfig where
11011215 parseJSON = genericParseJSON defaultOptions {
11021216 fieldLabelModifier = (\(x:xs) -> toUpper x : xs)}
11031217
1104−parseIntegerText :: (Monad m) =>Text -> m Integer
1218+parseIntegerText :: (Monad m) => Text -> m Integer
11051219parseIntegerText t = case readMaybe $ T.unpack t of
11061220 Nothing ->
11071221 fail "Could not parse Integer"
tests/tests.hs+40−22
@@ -5,26 +5,30 @@ module Main where
55import qualified Test.QuickCheck.Monadic as QCM
66import Test.Tasty
77import Test.Tasty.HUnit
8−import qualified Test.Tasty.QuickCheck as QC
8+import Test.Tasty.QuickCheck (testProperty)
99
1010import Control.Concurrent (threadDelay)
11+import Control.Lens ((^.), (^?))
1112import Control.Monad.IO.Class
12−import Control.Monad.Trans.Class (MonadTrans, lift)
13+import Control.Monad.Trans.Class (lift)
14+import qualified Data.Aeson as JSON
15+import Data.Aeson.Lens (key, _Object, _String, _Value)
1316import qualified Data.ByteString as B
1417import qualified Data.ByteString.Char8 as C
1518import qualified Data.ByteString.Lazy as BL
19+import qualified Data.HashMap.Strict as HM
20+import Data.Int (Int)
21+import qualified Data.Map as M
1622import Data.Maybe (fromJust, isJust)
1723import Data.Monoid
18−import Data.Text (unpack)
24+import Data.Text (Text, unpack)
1925import Network.Connection (TLSSettings (..))
2026import Network.HTTP.Client (newManager)
2127import Network.HTTP.Client.TLS
2228import Network.HTTP.Types.Status
2329import System.Process (system)
2430
25−import Docker
26−import Docker.Types
27−
31+import Docker.Client
2832
2933-- opts = defaultClientOpts
3034
@@ -40,22 +44,23 @@ runDocker f = do
4044 -- mgr <- newManager settings
4145 runDockerT (defaultClientOpts, defaultHttpHandler) f
4246
43−checkDockerVersion :: IO ()
44−checkDockerVersion = runDocker $ do
47+testDockerVersion :: IO ()
48+testDockerVersion = runDocker $ do
4549 v <- getDockerVersion
4650 lift $ assert $ isRight v
4751
48−findTestImage :: IO ()
49−findTestImage = runDocker $ do
52+testFindImage :: IO ()
53+testFindImage = runDocker $ do
5054 images <- listImages defaultListOpts >>= fromRight
5155 let x = filter ((== [testImageName<>":latest"]) . imageRepoTags) images
5256 lift $ assert $ length x == 1
5357
54−runAndReadLog :: IO ()
55−runAndReadLog = runDocker $ do
58+testRunAndReadLog :: IO ()
59+testRunAndReadLog = runDocker $ do
5660 containerId <- createContainer (defaultCreateOpts (testImageName <> ":latest"))
5761 c <- fromRight containerId
5862 status1 <- startContainer defaultStartOpts c
63+ _ <- inspectContainer c >>= fromRight
5964 lift $ threadDelay 300000 -- give 300ms for the application to finish
6065 lift $ assert $ status1 == Right ()
6166 status2 <- killContainer SIGTERM c
@@ -66,20 +71,27 @@ runAndReadLog = runDocker $ do
6671 lift $ assert $ status3 == Right ()
6772
6873
69−tests :: TestTree
70−tests = testGroup "Metrics tests" [
71− testCase "Get docker version" checkDockerVersion,
72− testCase "Find image by name" findTestImage,
73− testCase "Run a dummy container and read its log" runAndReadLog]
74+sampleEP = ExposedPorts $ M.fromList [(80, TCP), (1337, UDP)]
75+
76+testExposedPorts :: TestTree
77+testExposedPorts = testGroup "Testing ExposedPorts JSON" [ testTCP, testUDP ]
78+ where
79+ testTCP = testCase "tcp port" $ assert $ JSON.toJSON sampleEP ^. key "80/tcp" . _Object == HM.empty
80+ testUDP = testCase "udp port" $ assert $ JSON.toJSON sampleEP ^. key "1337/tcp" . _Object == HM.empty
81+
82+
83+integrationTests :: TestTree
84+integrationTests = testGroup "Integration Tests" [
85+ testCase "Get docker version" testDockerVersion,
86+ testCase "Find image by name" testFindImage,
87+ testCase "Run a dummy container and read its log" testRunAndReadLog]
88+
89+jsonTests :: TestTree
90+jsonTests = testGroup "JSON Tests" [testExposedPorts]
7491
7592setup :: IO ()
7693setup = system ("docker build -t "++unpack testImageName++" tests") >> return ()
7794
78−main :: IO ()
79−main = do
80− setup
81− defaultMain tests
82−
8395isRight (Left _) = False
8496isRight (Right _) = True
8597
@@ -88,3 +100,9 @@ fromRight (Left l) = do
88100 liftIO $ assertFailure $ "Left: " ++ show l
89101 undefined
90102fromRight (Right r) = return r
103+
104+main :: IO ()
105+main = do
106+ setup
107+ defaultMain $ testGroup "Tests" [jsonTests, integrationTests]
108+