dockercook-0.5.0.0: src/lib/Cook/DirectDocker.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Cook.DirectDocker
( DockerHostId(..), dockerHostIdAsText
, dockerInfo, DockerInfo(..)
, dockerImageId, dockerInspectImage, DockerImageInfo(..)
, dockerImages, DockerImageListInfo(..)
, newDockerImagesCache, doesImageExist, DockerImagesCache(..)
, DockerTag(..), DockerTagVersion(..), parseDockerTag
)
where
import Cook.Types
import Cook.Util
import Control.Applicative
import Control.Concurrent.STM
import Control.Exception
import Control.Lens ((^?))
import Control.Monad
import Data.Aeson
import Data.Char (isDigit)
import Data.List (foldl')
import Data.Monoid
import Network.HTTP.Client (HttpException(..))
import Network.Wreq
import System.Environment
import qualified Data.Set as S
import qualified Data.Text as T
newtype DockerBaseUrl
= DockerBaseUrl { _unDockerBaseUrl :: T.Text }
deriving (Show, Eq)
newtype DockerHostId
= DockerHostId { unDockerHostId :: T.Text }
deriving (Show, Eq, FromJSON, ToJSON)
dockerHostIdAsText :: DockerHostId -> T.Text
dockerHostIdAsText = unDockerHostId
data DockerTag
= DockerTag
{ dt_name :: !T.Text
, dt_version :: !DockerTagVersion
} deriving (Show, Eq, Ord)
data DockerTagVersion
= DockerTagVersionLatest
| DockerTagVersionNone
| DockerTagVersionOther !T.Text
deriving (Show, Eq, Ord)
parseDockerTag :: T.Text -> DockerTag
parseDockerTag rawTag =
DockerTag
{ dt_name = host
, dt_version =
case tag of
"" -> DockerTagVersionLatest
"latest" -> DockerTagVersionLatest
"<none>" -> DockerTagVersionNone
xs -> DockerTagVersionOther xs
}
where
(host, tag) =
if T.isInfixOf "/" tag' && T.length (T.takeWhile isDigit tag') >= 2
then (host' <> ":" <> tag', "")
else (host', tag')
(host', tag') =
if T.isInfixOf ":" rawTag
then let (a, b) = T.breakOn ":" (T.reverse rawTag)
in (T.take (T.length b - 1) $ T.reverse b, T.reverse a)
else (rawTag, "")
instance FromJSON DockerTag where
parseJSON =
withText "DockerTag" $ \str ->
pure $ parseDockerTag str
data DockerInfo
= DockerInfo
{ di_id :: !DockerHostId
, di_name :: !T.Text
} deriving (Show, Eq)
instance FromJSON DockerInfo where
parseJSON =
withObject "DockerInfo" $ \obj ->
DockerInfo
<$> obj .: "ID"
<*> obj .: "Name"
data DockerImageInfo
= DockerImageInfo
{ dii_id :: !DockerImageId
, dii_size :: !Int
, dii_virtualSize :: !Int
} deriving (Show, Eq)
instance FromJSON DockerImageInfo where
parseJSON =
withObject "DockerImageInfo" $ \obj ->
DockerImageInfo
<$> obj .: "Id"
<*> obj .: "Size"
<*> obj .: "VirtualSize"
data DockerImageListInfo
= DockerImageListInfo
{ dili_id :: !DockerImageId
, dili_parentId :: !DockerImageId
, dili_size :: !Int
, dili_virtualSize :: !Int
, dili_repoTags :: [DockerTag]
} deriving (Show, Eq)
instance FromJSON DockerImageListInfo where
parseJSON =
withObject "DockerImageListInfo" $ \obj ->
DockerImageListInfo
<$> obj .: "Id"
<*> obj .: "ParentId"
<*> obj .: "Size"
<*> obj .: "VirtualSize"
<*> obj .: "RepoTags"
withDockerBaseUrl :: (DockerBaseUrl -> IO a) -> IO a
withDockerBaseUrl action =
do host <- getEnv "DOCKER_HOST"
action $ DockerBaseUrl $ T.replace "tcp://" "http://" (T.pack host) <> "/v1.19/"
-- | Retrieve information about the remote docker host
dockerInfo :: IO (Maybe DockerInfo)
dockerInfo =
withDockerBaseUrl $ \(DockerBaseUrl url) ->
do r <- asJSON =<< get (T.unpack url <> "info")
return (r ^? responseBody)
-- | Get the image id providing an image tag
dockerImageId :: DockerImage -> IO (Maybe DockerImageId)
dockerImageId di = liftM (fmap dii_id) $ dockerInspectImage di
-- | Looking information about an image provided an image tag
dockerInspectImage :: DockerImage -> IO (Maybe DockerImageInfo)
dockerInspectImage (DockerImage name) =
action `catch` \(_ :: HttpException) -> return Nothing
where
action =
withDockerBaseUrl $ \(DockerBaseUrl url) ->
do r <- asJSON =<< get (T.unpack url <> "images/" <> T.unpack name <> "/json")
return (r ^? responseBody)
-- | List docker images on remote docker host
dockerImages :: IO (Maybe [DockerImageListInfo])
dockerImages =
withDockerBaseUrl $ \(DockerBaseUrl url) ->
do r <- asJSON =<< get (T.unpack url <> "images/json?all=0")
return (r ^? responseBody)
newtype DockerImagesCache
= DockerImagesCache { unDockerImagesCache :: TVar (Maybe (S.Set DockerTag, S.Set DockerImageId)) }
-- | Create new cache for 'doesImageExist'
newDockerImagesCache :: IO DockerImagesCache
newDockerImagesCache =
DockerImagesCache <$> newTVarIO Nothing
-- | Check if an image exists on remote docker host
doesImageExist :: DockerImagesCache -> Either DockerImage DockerImageId -> IO Bool
doesImageExist (DockerImagesCache cacheVar) eImage =
do cacheData <-
do cd <- atomically $ readTVar cacheVar
case cd of
Nothing ->
do logDebug "No image cache available, hitting server to get a list"
res <- dockerImages
case res of
Nothing ->
error "Docker images failed!"
Just imageList ->
do let cacheState =
foldl' (\(tags, ids) img ->
( S.fromList (dili_repoTags img) `S.union` tags
, S.insert (dili_id img) ids
)
) (S.empty, S.empty) imageList
atomically $ writeTVar cacheVar (Just cacheState)
return cacheState
Just d -> return d
return $ doLookup cacheData
where
doLookup (tagSet, imageIdSet) =
case eImage of
Left (DockerImage n) ->
S.member (parseDockerTag n) tagSet
Right n -> S.member n imageIdSet