on 926a5cb help sign in
10 files changed+303−40
since splitsnapshots
unifiedsplit
.travis.yml−4 CHANGELOG.md−6 docker.cabal+4−4 src/Docker/Client/Api.hs+14 src/Docker/Client/Http.hs+15 src/Docker/Client/Internal.hs+19−12 src/Docker/Client/Types.hs+219−8 stack-7.10.yaml−1 stack-8.8.2.yaml−5 tests/tests.hs+32
.travis.yml−4
@@ -43,10 +43,6 @@ env:
4343 - DOCKER_VERSION="1.13.1-0~ubuntu-trusty" GHCVER=8.6.4
4444 - DOCKER_VERSION="17.05.0~ce-0~ubuntu-trusty" GHCVER=8.6.4
4545
46− - DOCKER_VERSION="1.12.3-0~trusty" GHCVER=8.8.2
47− - DOCKER_VERSION="1.13.1-0~ubuntu-trusty" GHCVER=8.8.2
48− - DOCKER_VERSION="17.05.0~ce-0~ubuntu-trusty" GHCVER=8.8.2
49−
5046addons:
5147 apt:
5248 packages:
CHANGELOG.md−6
@@ -1,11 +1,5 @@
11# CHANGELOG
22
3−## 0.6.0.3 (2020-02-17)
4−
5−- Support for ghc 8.8.2.
6−- Bump dependency version.
7−- Use MonadFail for later versions.
8−
93## 0.6.0.1 (2019-05-23)
104
115- Support ghc 8.6.4.
docker.cabal+4−4
@@ -1,5 +1,5 @@
11name: docker
2−version: 0.6.0.3
2+version: 0.6.0.2
33synopsis: An API client for docker written in Haskell
44description: See API documentation below.
55homepage: https://github.com/denibertovic/docker-hs
@@ -30,7 +30,7 @@ library
3030 , bytestring >= 0.10.0 && < 0.11.0
3131 , containers >= 0.5.0 && < 0.7.0
3232 , data-default-class >= 0.0.1 && < 0.2.0
33− , http-client >= 0.4.0 && < 0.7.0
33+ , http-client >= 0.4.0 && < 0.6.0
3434 , http-types >= 0.9 && < 0.13
3535 , vector
3636 , conduit
@@ -51,9 +51,9 @@ library
5151 , mtl >= 2.0.0 && < 3.0.0
5252 , network >= 2.6.0
5353 , text >= 1.0.0 && < 2.0.0
54− , time >= 1.5.0 && < 2.0.0
54+ , time >= 1.5.0 && < 1.9.0
5555 , scientific >= 0.3.0 && < 0.4.0
56− , tls >= 1.3.7 && < 1.6.0
56+ , tls >= 1.3.7 && < 1.5.0
5757 , unordered-containers >= 0.2.0 && < 0.3.0
5858 , x509 >= 1.6.0 && < 1.8.0
5959 , x509-store >= 1.6.0 && < 1.8.0
src/Docker/Client/Api.hs+14
@@ -24,6 +24,9 @@ module Docker.Client.Api (
2424 -- * Network
2525 , createNetwork
2626 , removeNetwork
27+ , listNetworks
28+ , inspectNetwork
29+ , connectNetwork
2730 -- * Other
2831 , getDockerVersion
2932 ) where
@@ -225,3 +228,14 @@ createNetwork opts = requestHelper POST (CreateNetworkEndpoint opts) >>= parseR
225228removeNetwork :: forall m. (MonadIO m, MonadMask m) => NetworkID -> DockerT m (Either DockerError ())
226229removeNetwork nid = requestUnit DELETE $ RemoveNetworkEndpoint nid
227230
231+-- | Lists networks optionally matching a list of 'NetworkFilter's.
232+listNetworks :: forall m . (MonadIO m, MonadMask m) => [NetworkFilter] -> DockerT m (Either DockerError [NetworkDetails])
233+listNetworks nfs = requestHelper GET (ListNetworksEndpoint nfs) >>= parseResponse
234+
235+-- | Gets 'NetworkDetails' for a network, given its name or id.
236+inspectNetwork :: forall m . (MonadIO m, MonadMask m) => NetworkID -> DockerT m (Either DockerError NetworkDetails)
237+inspectNetwork nid = requestHelper GET (InspectNetworkEndpoint nid) >>= parseResponse
238+
239+-- | Connects a container to a network.
240+connectNetwork :: forall m . (MonadIO m, MonadMask m) => NetworkID -> ConnectConfig -> DockerT m (Either DockerError ())
241+connectNetwork nid cfg = requestUnit POST $ ConnectNetworkEndpoint nid cfg
src/Docker/Client/Http.hs+15
@@ -288,3 +288,18 @@ statusCodeToError (RemoveNetworkEndpoint _) st =
288288 Nothing
289289 else
290290 Just $ DockerInvalidStatusCode st
291+statusCodeToError (ListNetworksEndpoint _) st =
292+ if st == status200 then
293+ Nothing
294+ else
295+ Just $ DockerInvalidStatusCode st
296+statusCodeToError (InspectNetworkEndpoint _) st =
297+ if st == status200 then
298+ Nothing
299+ else
300+ Just $ DockerInvalidStatusCode st
301+statusCodeToError (ConnectNetworkEndpoint _ _) st =
302+ if st == status200 then
303+ Nothing
304+ else
305+ Just $ DockerInvalidStatusCode st
src/Docker/Client/Internal.hs+19−12
@@ -1,17 +1,18 @@
11module Docker.Client.Internal where
22
3−import Blaze.ByteString.Builder (toByteString)
4−import qualified Data.Aeson as JSON
5−import Data.ByteString (ByteString)
6−import qualified Data.ByteString.Char8 as BSC
7−import qualified Data.Conduit.Binary as CB
8−import qualified Data.Text as T
9−import Data.Text.Encoding (decodeUtf8, encodeUtf8)
10−import qualified Network.HTTP.Client as HTTP
11−import Network.HTTP.Conduit (requestBodySourceChunked)
12−import Network.HTTP.Types (Query, encodePath,
13− encodePathSegments)
14−import Prelude hiding (all)
3+import Blaze.ByteString.Builder (toByteString)
4+import qualified Data.Aeson as JSON
5+import Data.ByteString (ByteString)
6+import qualified Data.ByteString.Char8 as BSC
7+import qualified Data.ByteString.Lazy.Char8 as BSLC
8+import qualified Data.Conduit.Binary as CB
9+import qualified Data.Text as T
10+import Data.Text.Encoding (decodeUtf8, encodeUtf8)
11+import qualified Network.HTTP.Client as HTTP
12+import Network.HTTP.Conduit (requestBodySourceChunked)
13+import Network.HTTP.Types (Query, encodePath,
14+ encodePathSegments)
15+import Prelude hiding (all)
1516
1617import Docker.Client.Types
1718
@@ -78,6 +79,9 @@ getEndpoint v (CreateImageEndpoint name tag _) = encodeURLWithQuery [v, "images"
7879getEndpoint v (DeleteImageEndpoint _ cid) = encodeURL [v, "images", fromImageID cid]
7980getEndpoint v (CreateNetworkEndpoint _) = encodeURL [v, "networks", "create"]
8081getEndpoint v (RemoveNetworkEndpoint nid) = encodeURL [v, "networks", fromNetworkID nid]
82+getEndpoint v (ListNetworksEndpoint nfs) = encodeURLWithQuery [v, "networks"] [("filters", Just . BSLC.toStrict $ JSON.encode nfs)]
83+getEndpoint v (InspectNetworkEndpoint nid) = encodeURL [v, "networks", fromNetworkID nid]
84+getEndpoint v (ConnectNetworkEndpoint nid _) = encodeURL [v, "networks", fromNetworkID nid, "connect"]
8185
8286getEndpointRequestBody :: Endpoint -> Maybe HTTP.RequestBody
8387getEndpointRequestBody VersionEndpoint = Nothing
@@ -101,6 +105,9 @@ getEndpointRequestBody (DeleteImageEndpoint _ _) = Nothing
101105
102106getEndpointRequestBody (CreateNetworkEndpoint opts) = Just $ HTTP.RequestBodyLBS (JSON.encode opts)
103107getEndpointRequestBody (RemoveNetworkEndpoint _) = Nothing
108+getEndpointRequestBody (ListNetworksEndpoint _) = Nothing
109+getEndpointRequestBody (InspectNetworkEndpoint _) = Nothing
110+getEndpointRequestBody (ConnectNetworkEndpoint _ cfg) = Just $ HTTP.RequestBodyLBS (JSON.encode cfg)
104111
105112getEndpointContentType :: Endpoint -> BSC.ByteString
106113getEndpointContentType (BuildImageEndpoint _ _) = BSC.pack "application/tar"
src/Docker/Client/Types.hs+219−8
@@ -76,6 +76,22 @@ module Docker.Client.Types (
7676 , HostConfig(..)
7777 , defaultHostConfig
7878 , NetworkingConfig(..)
79+ , NetworkScope(..)
80+ , CIDR(..)
81+ , IPAMDriver(..)
82+ , IPAMConfig(..)
83+ , IPAM(..)
84+ , NetworkContainer(..)
85+ , NetworkDetails(..)
86+ , NetworkType(..)
87+ , NetworkName
88+ , NetworkFilter(..)
89+ , ConnectConfig(..)
90+ , defaultConnectConfig
91+ , EndpointSettings(..)
92+ , defaultEndpointSettings
93+ , IPAMSettings(..)
94+ , defaultIPAMSettings
7995 , EndpointConfig(..)
8096 , Ulimit(..)
8197 , ContainerResources(..)
@@ -114,6 +130,7 @@ import qualified Data.Text as T
114130import Data.Time.Clock (UTCTime)
115131import qualified Data.Vector as V
116132import GHC.Generics (Generic)
133+import Numeric (readDec)
117134import Prelude hiding (all, tail)
118135import Text.Read (readMaybe)
119136
@@ -139,6 +156,9 @@ data Endpoint =
139156 | DeleteImageEndpoint ImageDeleteOpts ImageID
140157 | CreateNetworkEndpoint CreateNetworkOpts
141158 | RemoveNetworkEndpoint NetworkID
159+ | ListNetworksEndpoint [NetworkFilter]
160+ | InspectNetworkEndpoint NetworkID
161+ | ConnectNetworkEndpoint NetworkID ConnectConfig
142162 deriving (Eq, Show)
143163
144164-- | We should newtype this
@@ -858,6 +878,198 @@ instance ToJSON CreateNetworkOpts where
858878 , "EnableIPv6" .= createNetworkEnableIPv6 opts
859879 ]
860880
881+data NetworkScope = LocalScope | GlobalScope | SwarmScope deriving (Eq, Show)
882+
883+fromNetworkScope :: NetworkScope -> Text
884+fromNetworkScope LocalScope = "local"
885+fromNetworkScope GlobalScope = "global"
886+fromNetworkScope SwarmScope = "swarm"
887+
888+instance ToJSON NetworkScope where
889+ toJSON = JSON.String . fromNetworkScope
890+
891+instance FromJSON NetworkScope where
892+ parseJSON "local" = return LocalScope
893+ parseJSON "global" = return GlobalScope
894+ parseJSON "swarm" = return SwarmScope
895+ parseJSON _ = fail "Failed to parse NetworkScope"
896+
897+data CIDR = CIDR Text Int deriving (Eq, Show)
898+
899+instance FromJSON CIDR where
900+ parseJSON (JSON.String t) = case T.splitOn "/" t of
901+ [a,p] -> CIDR a <$> parsePrefixLen p
902+ _ -> fail "Failed to parse CIDR"
903+ where
904+ parsePrefixLen p = case readDec (T.unpack p) of
905+ (n,""):_ -> return n
906+ _ -> fail "Failed to parse CIDR prefix length"
907+ parseJSON _ = fail "Failed to parse CIDR"
908+
909+data IPAMDriver = DefaultIPAMDriver | NamedIPAMDriver Text deriving (Eq, Show)
910+
911+instance FromJSON IPAMDriver where
912+ parseJSON (JSON.String "default") = return DefaultIPAMDriver
913+ parseJSON (JSON.String t) = return $ NamedIPAMDriver t
914+ parseJSON _ = fail "IPAMDriver is not a string"
915+
916+data IPAMConfig = IPAMConfig
917+ { ipamConfigSubnet :: CIDR
918+ , ipamConfigIPRange :: Maybe CIDR
919+ , ipamSubnetGateway :: Maybe Text
920+ , ipamConfigAuxAddress :: Maybe Text
921+ } deriving (Eq, Show)
922+
923+instance FromJSON IPAMConfig where
924+ parseJSON (JSON.Object o) = IPAMConfig
925+ <$> o .: "Subnet"
926+ <*> o .:? "IPRange"
927+ <*> o .:? "Gateway"
928+ <*> o .:? "AuxAddress"
929+ parseJSON _ = fail "IPAMConfig is not an object"
930+
931+data IPAM = IPAM
932+ { ipamDriver :: IPAMDriver
933+ , ipamConfig :: [IPAMConfig]
934+ , ipamOptions :: HM.HashMap Text Text
935+ } deriving (Eq, Show)
936+
937+instance FromJSON IPAM where
938+ parseJSON (JSON.Object o) = IPAM
939+ <$> o .:? "Driver" .!= DefaultIPAMDriver
940+ <*> o .:? "Config" .!= []
941+ <*> o .:? "Options" .!= HM.empty
942+ parseJSON _ = fail "IPAM is not an object"
943+
944+data NetworkContainer = NetworkContainer
945+ { networkContainerName :: ContainerName
946+ , networkContainerEndpointID :: Text
947+ , networkContainerMacAddress :: Text
948+ , networkContainerIPv4Address :: Text
949+ , networkContainerIPv6Address :: Text
950+ } deriving (Eq, Show)
951+
952+instance FromJSON NetworkContainer where
953+ parseJSON (JSON.Object o) = NetworkContainer
954+ <$> o .: "Name"
955+ <*> o .: "EndpointID"
956+ <*> o .: "MacAddress"
957+ <*> o .: "IPv4Address"
958+ <*> o .: "IPv6Address"
959+ parseJSON _ = fail "NetworkContainer is not an object"
960+
961+data NetworkDetails = NetworkDetails
962+ { networkDetailsName :: Text
963+ , networkDetailsID :: NetworkID
964+ , networkDetailsCreated :: UTCTime
965+ , networkDetailsScope :: NetworkScope
966+ , networkDetailsDriver :: NetworkMode
967+ , networkDetailsEnableIPv6 :: Bool
968+ , networkDetailsInternal :: Bool
969+ , networkDetailsAttachable :: Bool
970+ , networkDetailsIngress :: Bool
971+ , networkDetailsIPAM :: IPAM
972+ , networkDetailsOptions :: HM.HashMap Text Text
973+ , networkDetailsLabels :: HM.HashMap Text Text
974+ , networkDetailsContainers :: HM.HashMap Text NetworkContainer
975+ } deriving (Eq, Show)
976+
977+instance FromJSON NetworkDetails where
978+ parseJSON v@(JSON.Object o) = NetworkDetails
979+ <$> o .: "Name"
980+ <*> parseJSON v
981+ <*> o .: "Created"
982+ <*> o .: "Scope"
983+ <*> o .: "Driver"
984+ <*> o .: "EnableIPv6"
985+ <*> o .: "Internal"
986+ <*> o .: "Attachable"
987+ <*> o .: "Ingress"
988+ <*> o .: "IPAM"
989+ <*> o .:? "Options" .!= HM.empty
990+ <*> o .:? "Labels" .!= HM.empty
991+ <*> o .:? "Containers" .!= HM.empty
992+ parseJSON _ = fail "NetworkDetails is not an object"
993+
994+data NetworkType = BuiltinNetwork | CustomNetwork deriving (Eq, Show)
995+
996+fromNetworkType :: NetworkType -> Text
997+fromNetworkType BuiltinNetwork = "builtin"
998+fromNetworkType CustomNetwork = "custom"
999+
1000+type NetworkName = Text
1001+
1002+instance ToJSON NetworkType where
1003+ toJSON = JSON.String . fromNetworkType
1004+
1005+instance FromJSON NetworkType where
1006+ parseJSON (JSON.String "builtin") = return BuiltinNetwork
1007+ parseJSON (JSON.String "custom") = return CustomNetwork
1008+ parseJSON _ = fail "Failed to parse NetworkType"
1009+
1010+data NetworkFilter
1011+ = NetworkFilterName Text
1012+ | NetworkFilterID NetworkID
1013+ | NetworkFilterLabel Text
1014+ | NetworkFilterDriver NetworkMode
1015+ | NetworkFilterScope NetworkScope
1016+ | NetworkFilterType NetworkType
1017+ deriving (Eq, Show)
1018+
1019+instance {-# OVERLAPPING #-} ToJSON [NetworkFilter] where
1020+ toJSON = object . fmap toKV
1021+ where
1022+ toKV (NetworkFilterName n) = "name" .= [n]
1023+ toKV (NetworkFilterID i) = "id" .= [fromNetworkID i]
1024+ toKV (NetworkFilterLabel l) = "label" .= [l]
1025+ toKV (NetworkFilterDriver d) = "driver" .= [fromNetworkMode d]
1026+ toKV (NetworkFilterScope s) = "scope" .= [fromNetworkScope s]
1027+ toKV (NetworkFilterType t) = "type" .= [fromNetworkType t]
1028+
1029+data ConnectConfig = ConnectConfig
1030+ { connectContainer :: Text
1031+ , connectEndpointConfig :: EndpointSettings
1032+ } deriving (Eq, Show, Generic)
1033+
1034+instance ToJSON ConnectConfig where
1035+ toJSON = genericToJSON defaultOptions { fieldLabelModifier = drop 7 }
1036+
1037+defaultConnectConfig :: Text -> ConnectConfig
1038+defaultConnectConfig = flip ConnectConfig defaultEndpointSettings
1039+
1040+data EndpointSettings = EndpointSettings
1041+ { endpointIPAMConfig :: IPAMSettings
1042+ , endpointLinks :: [Text]
1043+ , endpointAliases :: [Text]
1044+ , endpointNetworkID :: Maybe Text
1045+ , endpointEndpointID :: Maybe Text
1046+ , endpointGateway :: Maybe Text
1047+ , endpointIPAddress :: Maybe Text
1048+ , endpointIPPrefixLen :: Maybe Int
1049+ , endpointIPv6Gateway :: Maybe Text
1050+ , endpointGlobalIPv6Address :: Maybe Text
1051+ , endpointGlobalIPv6PrefixLen :: Maybe Int
1052+ , endpointMacAddress :: Maybe Text
1053+ } deriving (Eq, Show, Generic)
1054+
1055+instance ToJSON EndpointSettings where
1056+ toJSON = genericToJSON defaultOptions { fieldLabelModifier = drop 8 }
1057+
1058+defaultEndpointSettings :: EndpointSettings
1059+defaultEndpointSettings = EndpointSettings defaultIPAMSettings [] [] Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
1060+
1061+data IPAMSettings = IPAMSettings
1062+ { ipamIPv4Address :: Maybe Text
1063+ , ipamIPv6Address :: Maybe Text
1064+ , ipamLinkLocalIPs :: [Text]
1065+ } deriving (Eq, Show, Generic)
1066+
1067+instance ToJSON IPAMSettings where
1068+ toJSON = genericToJSON defaultOptions { fieldLabelModifier = drop 4 }
1069+
1070+defaultIPAMSettings :: IPAMSettings
1071+defaultIPAMSettings = IPAMSettings Nothing Nothing []
1072+
8611073-- TOOD: Add support for SELinux Volume labels (eg. "ro,z" or "ro/Z")
8621074-- | Set permissions on volumes that you mount in the container.
8631075data VolumePermission = ReadWrite | ReadOnly deriving (Eq, Show, Generic)
@@ -1027,11 +1239,14 @@ instance FromJSON NetworkMode where
10271239 parseJSON (JSON.String n) = return $ NetworkNamed n
10281240 parseJSON _ = fail "Unknown NetworkMode"
10291241
1242+fromNetworkMode :: NetworkMode -> Text
1243+fromNetworkMode NetworkBridge = "bridge"
1244+fromNetworkMode NetworkHost = "host"
1245+fromNetworkMode NetworkDisabled = "none"
1246+fromNetworkMode (NetworkNamed n) = n
1247+
10301248instance ToJSON NetworkMode where
1031− toJSON NetworkBridge = JSON.String "bridge"
1032− toJSON NetworkHost = JSON.String "host"
1033− toJSON NetworkDisabled = JSON.String "none"
1034− toJSON (NetworkNamed n) = JSON.String n
1249+ toJSON = JSON.String . fromNetworkMode
10351250
10361251newtype NetworkID = NetworkID Text
10371252 deriving (Eq, Show)
@@ -1564,11 +1779,7 @@ instance FromJSON ContainerConfig where
15641779 macAddress labels stopSignal
15651780 parseJSON _ = fail "NetworkSettings is not an object."
15661781
1567−#if MIN_VERSION_base(4,13,0)
1568−parseIntegerText :: (MonadFail m) => Text -> m Integer
1569−#else
15701782parseIntegerText :: (Monad m) => Text -> m Integer
1571−#endif
15721783parseIntegerText t = case readMaybe $ T.unpack t of
15731784 Nothing ->
15741785 fail "Could not parse Integer"
stack-7.10.yaml−1
@@ -14,7 +14,6 @@ flags: {}
1414extra-deps:
1515- directory-1.2.7.0
1616- unliftio-core-0.1.1.0
17−- process-1.6.5.1
1817
1918# Control whether we use the GHC we find on the path
2019# system-ghc: true
stack-8.8.2.yamldeleted−5
@@ -1,5 +0,0 @@
1−resolver: lts-15.0
2−packages:
3−- '.'
4−extra-deps: []
5−
tests/tests.hs+32
@@ -144,6 +144,35 @@ testCreateRemoveNetwork = do
144144 removeStatus <- removeNetwork nid
145145 lift $ assertBool ("removing a network, unexpected status: " ++ show removeStatus) $ isRight removeStatus
146146
147+testListNetworks :: IO ()
148+testListNetworks =
149+ runDocker $ do
150+ res <- listNetworks [NetworkFilterName "bridge"]
151+ lift $ case res of
152+ Left _ -> assertFailure $ "listing networks, unexpected status: " ++ show res
153+ Right (d:_) -> assertBool "listing networks, bridge network missing" $ networkDetailsName d == "bridge"
154+
155+testInspectNetwork :: IO ()
156+testInspectNetwork =
157+ runDocker $ do
158+ res <- inspectNetwork . fromJust $ toNetworkID "bridge"
159+ lift $ assertBool ("inspecting networks, unexpected status: " ++ show res) $ isRight res
160+
161+testConnectNetwork :: IO ()
162+testConnectNetwork =
163+ runDocker $ do
164+ containerId <- fromRight =<< createContainer (defaultCreateOpts (testImageName <> ":latest")) Nothing
165+ networkId <- fromRight =<< createNetwork (defaultCreateNetworkOpts "mynetwork")
166+ res <- connectNetwork networkId . defaultConnectConfig $ fromContainerID containerId
167+ lift $ assertBool ("connecting network, unexpected status: " ++ show res) $ isRight res
168+ details <- fromRight =<< inspectContainer containerId
169+ _ <- deleteContainer defaultContainerDeleteOpts containerId
170+ _ <- removeNetwork networkId
171+ let networks = networkMode <$> (networkSettingsNetworks . networkSettings) details
172+ lift $ assertBool "connecting network failed" $ NetworkNamed "mynetwork" `elem` networks
173+ where
174+ networkMode (Network mode _) = mode
175+
147176testLogDriverOptionsJson :: TestTree
148177testLogDriverOptionsJson = testGroup "Testing LogDriverOptions JSON" [test1, test2, test3]
149178 where
@@ -249,6 +278,9 @@ integrationTests =
249278 , testCase "Run a dummy container with networking and read its log" testRunAndReadLogWithNetworking
250279 , testCase "Try to stop a container that doesn't exist" testStopNonexisting
251280 , testCase "Create and remove a network" testCreateRemoveNetwork
281+ , testCase "List networks" testListNetworks
282+ , testCase "Inspect a network" testInspectNetwork
283+ , testCase "Connect a container to a network" testConnectNetwork
252284 ]
253285
254286jsonTests :: TestTree