diff --git a/integration-tests/app/Main.hs b/integration-tests/app/Main.hs index b825e34..a63d425 100644 --- a/integration-tests/app/Main.hs +++ b/integration-tests/app/Main.hs @@ -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 diff --git a/integration-tests/docker-engine-integration-tests.cabal b/integration-tests/integration-tests.cabal similarity index 78% rename from integration-tests/docker-engine-integration-tests.cabal rename to integration-tests/integration-tests.cabal index 21eb3be..7ccd031 100644 --- a/integration-tests/docker-engine-integration-tests.cabal +++ b/integration-tests/integration-tests.cabal @@ -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 @@ -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: @@ -25,7 +29,7 @@ library FlexibleContexts FlexibleInstances LambdaCase - ghc-options: -threaded -rtsopts -with-rtsopts=-N + ghc-options: -threaded -Wall build-depends: aeson , base @@ -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: @@ -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 diff --git a/integration-tests/lib/TestLib/Docker.hs b/integration-tests/lib/TestLib/Docker.hs index 9e9d92e..396540e 100644 --- a/integration-tests/lib/TestLib/Docker.hs +++ b/integration-tests/lib/TestLib/Docker.hs @@ -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 diff --git a/integration-tests/lib/TestLib/Docker/Core.hs b/integration-tests/lib/TestLib/Docker/Core.hs new file mode 100644 index 0000000..5ca1df7 --- /dev/null +++ b/integration-tests/lib/TestLib/Docker/Core.hs @@ -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 diff --git a/integration-tests/lib/TestLib/Docker/Networks.hs b/integration-tests/lib/TestLib/Docker/Networks.hs new file mode 100644 index 0000000..b530d9a --- /dev/null +++ b/integration-tests/lib/TestLib/Docker/Networks.hs @@ -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 () diff --git a/integration-tests/lib/TestLib/Docker/Types.hs b/integration-tests/lib/TestLib/Docker/Types.hs new file mode 100644 index 0000000..5436769 --- /dev/null +++ b/integration-tests/lib/TestLib/Docker/Types.hs @@ -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 diff --git a/integration-tests/lib/TestLib/Docker/Util.hs b/integration-tests/lib/TestLib/Docker/Util.hs new file mode 100644 index 0000000..54b693c --- /dev/null +++ b/integration-tests/lib/TestLib/Docker/Util.hs @@ -0,0 +1,41 @@ +{-# LANGUAGE ViewPatterns #-} + +module TestLib.Docker.Util where + +import Control.Monad.IO.Unlift +import Data.Text as T +import Network.HTTP.Client as NH +import Network.HTTP.Types.Status as HTTP +import Test.Sandwich +import UnliftIO.Exception + + +-- * 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 diff --git a/integration-tests/package.yaml b/integration-tests/package.yaml index 7cfe9ec..d93cb56 100644 --- a/integration-tests/package.yaml +++ b/integration-tests/package.yaml @@ -1,4 +1,4 @@ -name: docker-engine-integration-tests +name: integration-tests version: 0.1.0.0 license: BSD3 @@ -18,11 +18,12 @@ default-extensions: ghc-options: - -threaded -- -rtsopts -- -with-rtsopts=-N +- -Wall library: source-dirs: lib + exposed-modules: + - TestLib.Docker dependencies: - aeson - bytestring @@ -46,6 +47,11 @@ library: - unliftio-core executables: - docker-engine-integration-tests: + integration-tests: main: Main.hs source-dirs: app + ghc-options: + - -rtsopts + - -with-rtsopts=-N + dependencies: + - integration-tests