module Ecluse.Runtime.Server.Middleware (
goingAwayMiddleware,
probeApplication,
jsonResponse,
) where
import Data.Aeson (Value, encode, object, (.=))
import Data.Aeson.Key qualified as Key
import Data.Map.Strict qualified as Map
import Network.HTTP.Types (Status, hConnection, hContentType, status200, status404, status503)
import Network.Wai (Application, Middleware, Response, mapResponseHeaders, modifyResponse, pathInfo, responseLBS)
import Ecluse.Core.Ecosystem (Ecosystem, ecosystemName)
import Ecluse.Core.Server.Readiness (
MountReadiness (MountAwaitingFirstSync, MountReady),
Readiness (AwaitingMounts, Latched, Routable),
routable,
)
import Ecluse.Core.Worker (Liveness (liveHealthy, liveLastPoll))
import Ecluse.Runtime.Server.Drain (DrainSignal, isDraining)
goingAwayMiddleware :: DrainSignal -> Middleware
goingAwayMiddleware :: DrainSignal -> Middleware
goingAwayMiddleware DrainSignal
drain Application
app Request
request Response -> IO ResponseReceived
respond = do
draining <- DrainSignal -> IO Bool
isDraining DrainSignal
drain
if draining
then modifyResponse closeConnection app request respond
else app request respond
closeConnection :: Response -> Response
closeConnection :: Response -> Response
closeConnection = (ResponseHeaders -> ResponseHeaders) -> Response -> Response
mapResponseHeaders ((HeaderName
hConnection, ByteString
"close") Header -> ResponseHeaders -> ResponseHeaders
forall a. a -> [a] -> [a]
:)
probeApplication :: DrainSignal -> IO Readiness -> IO Liveness -> Application
probeApplication :: DrainSignal -> IO Readiness -> IO Liveness -> Application
probeApplication DrainSignal
drain IO Readiness
checkReady IO Liveness
checkLiveness Request
request Response -> IO ResponseReceived
respond =
case Request -> [Text]
pathInfo Request
request of
[Text
"livez"] -> IO Liveness
checkLiveness IO Liveness
-> (Liveness -> IO ResponseReceived) -> IO ResponseReceived
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= Response -> IO ResponseReceived
respond (Response -> IO ResponseReceived)
-> (Liveness -> Response) -> Liveness -> IO ResponseReceived
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Liveness -> Response
livenessResponse
[Text
"readyz"] -> DrainSignal -> IO Readiness -> IO Response
readiness DrainSignal
drain IO Readiness
checkReady IO Response
-> (Response -> IO ResponseReceived) -> IO ResponseReceived
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= Response -> IO ResponseReceived
respond
[Text]
_ -> Response -> IO ResponseReceived
respond Response
notFound
livenessResponse :: Liveness -> Response
livenessResponse :: Liveness -> Response
livenessResponse Liveness
liveness
| Liveness -> Bool
liveHealthy Liveness
liveness = Status -> Text -> Response
body Status
status200 Text
"live"
| Bool
otherwise = Status -> Text -> Response
body Status
status503 Text
"liveness check failed"
where
body :: Status -> Text -> Response
body :: Status -> Text -> Response
body Status
status Text
label =
Status -> ByteString -> Response
jsonResponse Status
status (Value -> ByteString
forall a. ToJSON a => a -> ByteString
encode ([Pair] -> Value
object [Key
"status" Key -> Text -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.= Text
label, Key
"lastPoll" Key -> Maybe UTCTime -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.= Liveness -> Maybe UTCTime
liveLastPoll Liveness
liveness]))
readiness :: DrainSignal -> IO Readiness -> IO Response
readiness :: DrainSignal -> IO Readiness -> IO Response
readiness DrainSignal
drain IO Readiness
checkReady =
DrainSignal -> IO Bool
isDraining DrainSignal
drain IO Bool -> (Bool -> IO Response) -> IO Response
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
Bool
True -> Response -> IO Response
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Status -> Text -> Response
statusOnly Status
status503 Text
"draining")
Bool
False -> Readiness -> Response
readinessResponse (Readiness -> Response) -> IO Readiness -> IO Response
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> IO Readiness
checkReady
readinessResponse :: Readiness -> Response
readinessResponse :: Readiness -> Response
readinessResponse Readiness
verdict = case Readiness
verdict of
Routable Map Ecosystem MountReadiness
mounts -> Text -> Map Ecosystem MountReadiness -> Response
forall {v}.
ToJSON v =>
v -> Map Ecosystem MountReadiness -> Response
withMounts Text
readyLabel Map Ecosystem MountReadiness
mounts
AwaitingMounts Map Ecosystem MountReadiness
mounts -> Text -> Map Ecosystem MountReadiness -> Response
forall {v}.
ToJSON v =>
v -> Map Ecosystem MountReadiness -> Response
withMounts Text
awaitingLabel Map Ecosystem MountReadiness
mounts
Readiness
Latched -> Status -> Text -> Response
statusOnly Status
status Text
"halted"
where
status :: Status
status = Status -> Status -> Bool -> Status
forall a. a -> a -> Bool -> a
bool Status
status503 Status
status200 (Readiness -> Bool
routable Readiness
verdict)
withMounts :: v -> Map Ecosystem MountReadiness -> Response
withMounts v
label Map Ecosystem MountReadiness
mounts =
Status -> ByteString -> Response
jsonResponse Status
status (Value -> ByteString
forall a. ToJSON a => a -> ByteString
encode ([Pair] -> Value
object [Key
"status" Key -> v -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.= v
label, Key
"mounts" Key -> Value -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.= Map Ecosystem MountReadiness -> Value
mountsOf Map Ecosystem MountReadiness
mounts]))
mountsOf :: Map.Map Ecosystem MountReadiness -> Value
mountsOf :: Map Ecosystem MountReadiness -> Value
mountsOf Map Ecosystem MountReadiness
mounts =
[Pair] -> Value
object [Text -> Key
Key.fromText (Ecosystem -> Text
ecosystemName Ecosystem
eco) Key -> Text -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.= MountReadiness -> Text
mountLabel MountReadiness
mount | (Ecosystem
eco, MountReadiness
mount) <- Map Ecosystem MountReadiness -> [(Ecosystem, MountReadiness)]
forall k a. Map k a -> [(k, a)]
Map.toList Map Ecosystem MountReadiness
mounts]
mountLabel :: MountReadiness -> Text
mountLabel :: MountReadiness -> Text
mountLabel = \case
MountReadiness
MountReady -> Text
readyLabel
MountReadiness
MountAwaitingFirstSync -> Text
awaitingLabel
readyLabel, awaitingLabel :: Text
readyLabel :: Text
readyLabel = Text
"ready"
awaitingLabel :: Text
awaitingLabel = Text
"awaiting the advisory database that ecluse pilot publishes"
statusOnly :: Status -> Text -> Response
statusOnly :: Status -> Text -> Response
statusOnly Status
status Text
label = Status -> ByteString -> Response
jsonResponse Status
status (Value -> ByteString
forall a. ToJSON a => a -> ByteString
encode ([Pair] -> Value
object [Key
"status" Key -> Text -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.= Text
label]))
notFound :: Response
notFound :: Response
notFound =
Status -> ResponseHeaders -> ByteString -> Response
responseLBS Status
status404 [(HeaderName
hContentType, ByteString
"text/plain; charset=utf-8")] ByteString
"Not Found\n"
jsonResponse :: Status -> LByteString -> Response
jsonResponse :: Status -> ByteString -> Response
jsonResponse Status
status =
Status -> ResponseHeaders -> ByteString -> Response
responseLBS Status
status [(HeaderName
hContentType, ByteString
"application/json")]