Skip to content

Commit

Permalink
Add first integration test (network create + delete)
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasjm committed Jul 29, 2024
1 parent f6778db commit 5fc3e1d
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 141 deletions.
26 changes: 13 additions & 13 deletions integration-tests/app/Main.hs
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
module Main where

import Control.Concurrent
import Control.Monad.IO.Class
import Test.Sandwich
import TestLib.Docker


basic :: TopSpec
basic = describe "Simple tests" $ do
describe "Arithmetic" $ do
it "adds" $ do
(2 + 2) `shouldBe` 4
(2 + 3) `shouldBe` 5
basic = introduceDockerState $ do
describe "Networks" $ do
it "creates and deletes a network" $ do
ds <- getContext dockerState

let name = "test-network"
doesNetworkExist ds name >>= (`shouldBe` False)

it "subtracts" $ do
warn "This might not be right..."
(3 - 2) `shouldBe` 0
networkId <- createNetwork ds name mempty
doesNetworkExist ds name >>= (`shouldBe` True)

describe "Strings" $
it "concatenates" $
("abc" <> "def") `shouldBe` "abcdef"
deleteNetwork ds networkId
doesNetworkExist ds name >>= (`shouldBe` False)

main :: IO ()
main = runSandwichWithCommandLineArgs defaultOptions basic
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ cabal-version: 1.12
--
-- see: https://github.com/sol/hpack

name: docker-engine-integration-tests
name: integration-tests
version: 0.1.0.0
license: BSD3
build-type: Simple
Expand All @@ -13,7 +13,11 @@ library
exposed-modules:
TestLib.Docker
other-modules:
Paths_docker_engine_integration_tests
TestLib.Docker.Core
TestLib.Docker.Networks
TestLib.Docker.Types
TestLib.Docker.Util
Paths_integration_tests
hs-source-dirs:
lib
default-extensions:
Expand All @@ -25,7 +29,7 @@ library
FlexibleContexts
FlexibleInstances
LambdaCase
ghc-options: -threaded -rtsopts -with-rtsopts=-N
ghc-options: -threaded -Wall
build-depends:
aeson
, base
Expand All @@ -51,10 +55,10 @@ library
, unliftio-core
default-language: Haskell2010

executable docker-engine-integration-tests
executable integration-tests
main-is: Main.hs
other-modules:
Paths_docker_engine_integration_tests
Paths_integration_tests
hs-source-dirs:
app
default-extensions:
Expand All @@ -66,8 +70,9 @@ executable docker-engine-integration-tests
FlexibleContexts
FlexibleInstances
LambdaCase
ghc-options: -threaded -rtsopts -with-rtsopts=-N
ghc-options: -threaded -Wall -rtsopts -with-rtsopts=-N
build-depends:
base
, integration-tests
, sandwich
default-language: Haskell2010
130 changes: 12 additions & 118 deletions integration-tests/lib/TestLib/Docker.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,131 +3,25 @@
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE ViewPatterns #-}

module TestLib.Docker where
module TestLib.Docker (
introduceDockerState

, module TestLib.Docker.Core
, module TestLib.Docker.Networks
, module TestLib.Docker.Types
, module TestLib.Docker.Util
) where

import Control.Monad.IO.Unlift
import Control.Monad.Logger
import qualified Data.ByteString.Lazy.Char8 as BL8
import Data.Either
import Data.IP
import qualified Data.Map as M
import Data.String.Interpolate
import Data.Text as T
import DockerEngine.API.Network
import DockerEngine.Client
import DockerEngine.Core
import DockerEngine.MimeTypes
import DockerEngine.Model hiding (Map)
import GHC.Stack
import Network.HTTP.Client as NH
import Network.HTTP.Types.Status as HTTP
import qualified Network.Socket as S
import qualified Network.Socket.ByteString as SBS
import Test.Sandwich
import UnliftIO.Exception


data DockerState = DockerState {
dockerEngineConfig :: DockerEngineConfig
, dockerHttpManager :: Manager
}
import TestLib.Docker.Core
import TestLib.Docker.Networks
import TestLib.Docker.Types
import TestLib.Docker.Util

dockerState :: Label "dockerState" DockerState
dockerState = Label
type HasDockerState context = HasLabel context "dockerState" DockerState

introduceDockerState :: (
HasCallStack, MonadUnliftIO m
) => SpecFree (LabelValue "dockerState" DockerState :> context) m () -> SpecFree context m ()
introduceDockerState = introduce "introduce Docker state" dockerState getDockerState (const $ return ())

getDockerState :: MonadLoggerIO m => m DockerState
getDockerState = do
config <- liftIO (newConfig >>= return)
manager <- liftIO $ newUnixDomainSocketManager "/var/run/docker.sock"
return $ DockerState config manager
where
newUnixDomainSocketManager :: FilePath -> IO Manager
newUnixDomainSocketManager path = do
newManager $ defaultManagerSettings { managerRawConnection = return $ openUnixSocket path }
where
openUnixSocket filePath _ _ _ = do
s <- S.socket S.AF_UNIX S.Stream S.defaultProtocol
S.connect s (S.SockAddrUnix filePath)
makeConnection (SBS.recv s 8096)
(SBS.sendAll s)
(S.close s)

runDockerEngineLBS :: (HasCallStack, Produces req accept, MimeType contentType, MonadLoggerIO m)
=> DockerState -> DockerEngineRequest req contentType res accept -> m (NH.Response BL8.ByteString)
runDockerEngineLBS ds req = do
runDockerEngineLBS' ds req

runDockerEngineLBS' :: (HasCallStack, Produces req accept, MimeType contentType, MonadLoggerIO m)
=> DockerState -> DockerEngineRequest req contentType res accept -> m (NH.Response BL8.ByteString)
runDockerEngineLBS' (DockerState config manager) req = do
debug [i|---> #{req}|]
liftIO $ dispatchLbs manager config req



-- * Networks

doesNetworkExist :: (
HasCallStack, MonadLoggerIO m, MonadUnliftIO m
) => DockerState -> Text -> m Bool
doesNetworkExist ds networkName = isRight <$> inspectNetwork ds (Id networkName)

inspectNetwork :: (
HasCallStack, MonadLoggerIO m, MonadUnliftIO m
) => DockerState -> Id -> m (Either Text Network)
inspectNetwork (DockerState config manager) ident = leftOnException $ do
let req = networkInspect ident
debug [i|---> #{req}|]
liftIO (dispatchMime manager config req) >>= \case
MimeResult (Left err) _ -> return $ Left [i|(#{ident}) inspectNetwork failed: '#{err}'|]
MimeResult (Right result) _ -> return $ Right result

createNetwork :: (HasCallStack, MonadUnliftIO m, MonadLoggerIO m) => DockerState -> Text -> M.Map Text Text -> m (Either Text ())
createNetwork ds networkName labels = leftOnException $ do
let networkConfig = (mkNetworkCreateRequest networkName) {
networkCreateRequestLabels = Just $ M.mapKeys T.unpack labels
}
let req = networkCreate networkConfig
runDockerEngineLBS ds req >>= \case
(is2xx -> True) -> return $ Right ()
x@(is403 -> True) -> return $ Left [i|Failed to create network '#{networkName}'. operation not supported for pre-defined networks: '#{x}'|]
x@(is404 -> True) -> return $ Left [i|Failed to create network '#{networkName}'. Plugin not found: '#{x}'|]
x@(is5xx -> True) -> return $ Left [i|Server error in createNetwork for '#{networkName}': '#{x}'|]
x -> return $ Left [i|Unexpected response in createNetwork for '#{networkName}': '#{x}'|]


-- * HTTP

is2xx :: NH.Response a -> Bool
is2xx (responseStatus -> (HTTP.Status code _)) = code >= 200 && code < 300

-- is304 :: NH.Response a -> Bool
-- is304 (responseStatus -> (HTTP.Status code _)) = code == 304

-- is400 :: NH.Response a -> Bool
-- is400 (responseStatus -> (HTTP.Status code _)) = code == 400

is403 :: NH.Response a -> Bool
is403 (responseStatus -> (HTTP.Status code _)) = code == 403

is404 :: NH.Response a -> Bool
is404 (responseStatus -> (HTTP.Status code _)) = code == 404

-- is409 :: NH.Response a -> Bool
-- is409 (responseStatus -> (HTTP.Status code _)) = code == 409

is5xx :: NH.Response a -> Bool
is5xx (responseStatus -> (HTTP.Status code _)) = code >= 500 && code < 600

-- * Util

leftOnException :: (MonadUnliftIO m) => m (Either Text a) -> m (Either Text a)
leftOnException = handleAny $ \e -> return $ Left $ T.pack $ case fromException e of
Just (Reason _ msg) -> msg
_ -> show e
66 changes: 66 additions & 0 deletions integration-tests/lib/TestLib/Docker/Core.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@

module TestLib.Docker.Core where

import Control.Monad.Catch (MonadThrow)
import Control.Monad.IO.Unlift
import Control.Monad.Logger
import Data.String.Interpolate
import DockerEngine.Client
import DockerEngine.Core
import DockerEngine.MimeTypes
import GHC.Stack
import Network.HTTP.Client as NH
import qualified Network.Socket as S
import qualified Network.Socket.ByteString as SBS
import Test.Sandwich
import TestLib.Docker.Types


getDockerState :: MonadLoggerIO m => m DockerState
getDockerState = do
config <- liftIO (newConfig >>= return)
manager <- liftIO $ newUnixDomainSocketManager "/var/run/docker.sock"
return $ DockerState config manager
where
newUnixDomainSocketManager :: FilePath -> IO Manager
newUnixDomainSocketManager path = do
newManager $ defaultManagerSettings { managerRawConnection = return $ openUnixSocket path }
where
openUnixSocket filePath _ _ _ = do
s <- S.socket S.AF_UNIX S.Stream S.defaultProtocol
S.connect s (S.SockAddrUnix filePath)
makeConnection (SBS.recv s 8096)
(SBS.sendAll s)
(S.close s)

runDocker :: (
HasCallStack, Produces req accept, MimeType contentType, MimeUnrender accept res, MonadLoggerIO m
)
=> DockerState
-> DockerEngineRequest req contentType res accept
-> m (MimeResult res)
runDocker (DockerState config manager) req = do
debug [i|---> #{req}|]
liftIO $ dispatchMime manager config req

runDocker' :: (
HasCallStack, Produces req accept, MimeType contentType, MimeUnrender accept res, MonadLoggerIO m
)
=> DockerState
-> DockerEngineRequest req contentType res accept
-> m (Either MimeError res)
runDocker' (DockerState config manager) req = do
debug [i|---> #{req}|]
liftIO $ dispatchMime' manager config req

runDockerException :: (
HasCallStack, Produces req accept, MimeType contentType, MimeUnrender accept res, MonadLoggerIO m, MonadThrow m
)
=> DockerState
-> DockerEngineRequest req contentType res accept
-> m res
runDockerException (DockerState config manager) req = do
debug [i|---> #{req}|]
liftIO (dispatchMime' manager config req) >>= \case
Left mimeError -> expectationFailure [i|Got MimeError: #{mimeError}|]
Right x -> return x
50 changes: 50 additions & 0 deletions integration-tests/lib/TestLib/Docker/Networks.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{-# LANGUAGE ViewPatterns #-}

module TestLib.Docker.Networks where

import Control.Monad.Catch
import Control.Monad.IO.Unlift
import Control.Monad.Logger
import Data.Either
import qualified Data.Map as M
import Data.String.Interpolate
import Data.Text as T
import DockerEngine.API.Network
import DockerEngine.Client
import DockerEngine.Model hiding (Map)
import GHC.Stack
import Test.Sandwich
import TestLib.Docker.Core
import TestLib.Docker.Types
import TestLib.Docker.Util


doesNetworkExist :: (
HasCallStack, MonadLoggerIO m, MonadUnliftIO m
) => DockerState -> Text -> m Bool
doesNetworkExist ds networkName = isRight <$> inspectNetwork ds (Id networkName)

inspectNetwork :: (
HasCallStack, MonadLoggerIO m, MonadUnliftIO m
) => DockerState -> Id -> m (Either Text Network)
inspectNetwork (DockerState config manager) ident = leftOnException $ do
let req = networkInspect ident
debug [i|---> #{req}|]
liftIO (dispatchMime manager config req) >>= \case
MimeResult (Left err) _ -> return $ Left [i|(#{ident}) inspectNetwork failed: '#{err}'|]
MimeResult (Right result) _ -> return $ Right result

createNetwork :: (
HasCallStack, MonadUnliftIO m, MonadLoggerIO m, MonadFail m, MonadThrow m
) => DockerState -> Text -> M.Map Text Text -> m Id
createNetwork ds networkName labels = do
let networkConfig = (mkNetworkCreateRequest networkName) {
networkCreateRequestLabels = Just $ M.mapKeys T.unpack labels
}
NetworkCreateResponse {networkCreateResponseId=(Just x)} <- runDockerException ds (networkCreate networkConfig)
return (Id x)

deleteNetwork :: (HasCallStack, MonadUnliftIO m, MonadLoggerIO m, MonadThrow m) => DockerState -> Id -> m ()
deleteNetwork ds networkId = do
_ <- runDockerException ds (networkDelete networkId)
return ()
19 changes: 19 additions & 0 deletions integration-tests/lib/TestLib/Docker/Types.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}

module TestLib.Docker.Types where


import DockerEngine.Core
import Network.HTTP.Client as NH
import Test.Sandwich


data DockerState = DockerState {
dockerEngineConfig :: DockerEngineConfig
, dockerHttpManager :: Manager
}

dockerState :: Label "dockerState" DockerState
dockerState = Label
type HasDockerState context = HasLabel context "dockerState" DockerState
Loading

0 comments on commit 5fc3e1d

Please sign in to comment.