{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NoGeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
module Network.Consul where
import Boots
import Boots.Web
import Data.Aeson
import Data.ByteString (ByteString)
import qualified Data.HashMap.Strict as HM
import Data.Text (Text, toLower)
import Data.Word
import GHC.Generics
import Network.HTTP.Client (newManager)
import Network.HTTP.Client.Internal hiding (Proxy)
import Salak
import Servant.API
import Servant.Client
data ServiceKind
= KindNil
| KindConnectProxy
| KindMeshGateway
deriving (Eq, Show, Generic, ToSchema, FromJSON, ToJSON)
newtype ServiceConnect = ServiceConnect
{ enabled :: Bool
} deriving (Eq, Show, Generic, ToSchema, FromJSON, ToJSON)
data ServiceCheck = ServiceCheck
{ cname :: !Text
, cid :: !Text
, cinternal :: !String
, cdcsa :: !String
, chttp :: !String
} deriving (Eq, Show, Generic, ToSchema)
instance ToJSON ServiceCheck where
toJSON ServiceCheck{..} = object
[ "ID" .= cid
, "Name" .= cname
, "Interval" .= cinternal
, "DeregisterCriticalServiceAfter" .= cdcsa
, "HTTP" .= chttp
]
instance FromJSON ServiceCheck where
parseJSON = withObject "ServiceCheck" $ \v -> ServiceCheck
<$> v .: "Name"
<*> v .: "ID"
<*> v .: "Interval"
<*> v .: "DeregisterCriticalServiceAfter"
<*> v .:? "HTTP" .!= ""
data ServiceWeight = ServiceWeight
{
} deriving (Eq, Show, Generic, ToSchema, FromJSON, ToJSON)
data ServiceDef = ServiceDef
{ sname :: !Text
, sid :: !Text
, stags :: ![Text]
, saddr :: !(Maybe String)
, saddrmap :: !(HM.HashMap String String)
, smeta :: !(HM.HashMap Text Text)
, sport :: !(Maybe Word16)
, skind :: !ServiceKind
, sconnect :: !(Maybe ServiceConnect)
, scheck :: !(Maybe ServiceCheck)
, schecks :: ![ServiceCheck]
, stagoverride :: !Bool
, sweights :: !(Maybe ServiceWeight)
} deriving (Eq, Show, Generic, ToSchema)
data HttpServer = HttpServer
{ sname :: !Text
, sid :: !Text
, saddr :: !(Maybe String)
, sport :: !(Maybe Word16)
, stags :: ![Text]
, smeta :: !(HM.HashMap Text Text)
, chk :: !ServiceCheck
}
newServer :: HttpServer -> ServiceDef
newServer HttpServer{..} = ServiceDef
{ saddrmap = HM.empty
, skind = KindNil
, sconnect = Nothing
, scheck = Just chk
, schecks = []
, sweights = Nothing
, stagoverride = False
,..}
instance ToJSON ServiceDef where
toJSON ServiceDef{..} = object
[ "ID" .= sid
, "Name" .= sname
, "Address" .= saddr
, "Port" .= sport
, "Tags" .= toJSON stags
, "Meta" .= toJSON smeta
, "Check" .= toJSON scheck
]
instance FromJSON ServiceDef where
parseJSON = withObject "ServiceDef" $ \v -> ServiceDef
<$> v .: "Name"
<*> v .:? "ID" .!= ""
<*> v .:? "Tags" .!= []
<*> v .: "Address"
<*> return HM.empty
<*> v .:? "Meta" .!= HM.empty
<*> v .: "Port"
<*> return KindNil
<*> return Nothing
<*> return Nothing
<*> return []
<*> v .:? "EnableTagOverride" .!= True
<*> return Nothing
type ConsulEndpoint = "v1" :> "agent" :> AgentEndpoint
type AgentEndpoint
= "services" :> Get '[JSON] (HM.HashMap Text ServiceDef)
:<|> "service" :> Capture "serviceId" Text :> Get '[JSON] ServiceDef
:<|> "service" :> "register" :> ReqBody '[JSON] ServiceDef :> Put '[JSON] NoContent
:<|> "service" :> "deregister" :> Capture "serviceId" Text :> Put '[JSON] NoContent
:<|> "service" :> "maintenance" :> Capture "serviceId" Text :> Put '[JSON] NoContent
:<|> "health" :> "service" :> HealthEndpoint
type HealthEndpoint
= "name" :> Capture "serviceName" Text :> Get '[JSON] ServiceDef
:<|> "id" :> Capture "serviceId" Text :> Get '[JSON] ServiceDef
data ConsulApi m = ConsulApi
{ getServices :: m (HM.HashMap Text ServiceDef)
, getService :: Text -> m ServiceDef
, registerService :: ServiceDef -> m NoContent
, deregisterService :: Text -> m NoContent
, maintenanceService :: Text -> m NoContent
, checkHealthByName :: Text -> m ServiceDef
, checkHealthById :: Text -> m ServiceDef
}
consulApi
:: (MonadThrow m, MonadIO m)
=> ConsulConfig -> ManagerSettings -> ConsulApi m
consulApi cc hc =
let getServices
:<|> getService
:<|> registerService
:<|> deregisterService
:<|> maintenanceService
:<|> checkHealthByName
:<|> checkHealthById = hoistClient api (runConsul cc hc) (client api)
in ConsulApi{..}
api :: Proxy ConsulEndpoint
api = Proxy
runConsul
:: (MonadThrow m, MonadIO m)
=> ConsulConfig -> ManagerSettings -> ClientM a -> m a
runConsul ConsulConfig{..} mg cma = do
let mgn = case token of
Just t -> mg { managerModifyRequest = \req -> return req { requestHeaders = ("X-Consul-Token", t) : requestHeaders req }}
_ -> mg
liftIO $ do
m <- newManager mgn
v <- runClientM cma (ClientEnv m url Nothing)
case v of
Left e -> throwM e
Right a -> return a
data ConsulConfig = ConsulConfig
{ meta :: HM.HashMap Text Text
, tags :: [Text]
, token :: Maybe ByteString
, interval :: String
, dcsa :: String
, url :: BaseUrl
}
instance FromProp m ConsulConfig where
fromProp = ConsulConfig
<$> "meta"
<*> "tags"
<*> "token"
<*> "interval" .?= "10s"
<*> "deregister-critical-service-after" .?= "30m"
<*> (BaseUrl
<$> "schema" .?= Http
<*> "host" .?= "127.0.0.1"
<*> "port" .?= 8500
<*> "path" .?= "")
instance FromProp m Scheme where
fromProp = readEnum (go.toLower)
where
{-# INLINE go #-}
go "http" = Right Http
go "https" = Right Https
go _ = Left "unkown schema"