module Ecluse.Core.Rules (
RuleDeps (..),
inertRuleDeps,
evalRule,
PreparedRule (..),
Resilience (..),
prepare,
bootOrder,
renderBootOrder,
evalRules,
renderDecision,
renderDuration,
runEffectfulRule,
EffectfulConfig (..),
defaultEffectfulConfig,
backoffPolicy,
Breaker (..),
newBreaker,
BreakerReporter (..),
noBreakerReporter,
) where
import Control.Retry (
RetryPolicyM (RetryPolicyM),
RetryStatus (rsIterNumber),
retrying,
)
import Data.Text qualified as T
import Data.Time (NominalDiffTime, UTCTime, diffUTCTime)
import UnliftIO (timeout, tryAny)
import UnliftIO.Async (Async, async, cancel, uninterruptibleCancel, wait)
import UnliftIO.Exception (bracket)
import Ecluse.Core.Breaker (
Breaker (..),
BreakerReporter (..),
admit,
initialBreaker,
noBreakerReporter,
recordFailure,
recordSuccess,
reportBreakerChange,
)
import Ecluse.Core.Cve (AdvisoryRange (..), CveLookup (..), insideAffectedRange)
import Ecluse.Core.Ecosystem (Ecosystem)
import Ecluse.Core.Package
import Ecluse.Core.Rules.Types
import Ecluse.Core.Version (renderVersion)
data RuleDeps = RuleDeps
{ RuleDeps -> forall a. (Maybe CveLookup -> IO a) -> IO a
rdWithCveLookup :: forall a. (Maybe CveLookup -> IO a) -> IO a
, RuleDeps -> BreakerReporter
rdBreakerReporter :: BreakerReporter
}
inertRuleDeps :: RuleDeps
inertRuleDeps :: RuleDeps
inertRuleDeps =
RuleDeps
{ rdWithCveLookup :: forall a. (Maybe CveLookup -> IO a) -> IO a
rdWithCveLookup = \Maybe CveLookup -> IO a
use -> Maybe CveLookup -> IO a
use Maybe CveLookup
forall a. Maybe a
Nothing
, rdBreakerReporter :: BreakerReporter
rdBreakerReporter = BreakerReporter
noBreakerReporter
}
evalRule :: RuleDeps -> EvalContext -> Rule -> PackageDetails -> IO RuleResult
evalRule :: RuleDeps -> EvalContext -> Rule -> PackageDetails -> IO RuleResult
evalRule RuleDeps
_ EvalContext
_ (AllowScope Scope
scope) PackageDetails
pd =
RuleResult -> IO RuleResult
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (RuleResult -> IO RuleResult) -> RuleResult -> IO RuleResult
forall a b. (a -> b) -> a -> b
$ case PackageName -> Maybe Scope
pkgNamespace (PackageDetails -> PackageName
pkgName PackageDetails
pd) of
Just Scope
s
| Scope
s Scope -> Scope -> Bool
forall a. Eq a => a -> a -> Bool
== Scope
scope ->
Text -> RuleResult
Allow (Text
"scope " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Scope -> Text
renderScope Scope
scope Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" is allow-listed")
Maybe Scope
_ ->
Text -> RuleResult
NoDecision (Text
"scope is not the allow-listed " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Scope -> Text
renderScope Scope
scope)
evalRule RuleDeps
_ EvalContext
ctx (AllowIfOlderThan NominalDiffTime
minAge) PackageDetails
pd =
RuleResult -> IO RuleResult
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (RuleResult -> IO RuleResult) -> RuleResult -> IO RuleResult
forall a b. (a -> b) -> a -> b
$ case PackageDetails -> Maybe UTCTime
pkgPublishedAt PackageDetails
pd of
Maybe UTCTime
Nothing -> Text -> RuleResult
NoDecision Text
"publish time is unknown"
Just UTCTime
publishedAt ->
let age :: NominalDiffTime
age = UTCTime -> UTCTime -> NominalDiffTime
diffUTCTime (EvalContext -> UTCTime
ctxNow EvalContext
ctx) UTCTime
publishedAt
in if NominalDiffTime
age NominalDiffTime -> NominalDiffTime -> Bool
forall a. Ord a => a -> a -> Bool
>= NominalDiffTime
minAge
then
Text -> RuleResult
Allow
( Text
"published "
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> NominalDiffTime -> Text
renderDuration NominalDiffTime
age
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" ago (at least "
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> NominalDiffTime -> Text
renderDuration NominalDiffTime
minAge
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" old)"
)
else
Text -> RuleResult
NoDecision
( Text
"published only "
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> NominalDiffTime -> Text
renderDuration NominalDiffTime
age
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" ago, minimum age is "
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> NominalDiffTime -> Text
renderDuration NominalDiffTime
minAge
)
evalRule RuleDeps
_ EvalContext
_ Rule
DenyInstallTimeExecution PackageDetails
pd =
RuleResult -> IO RuleResult
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (RuleResult -> IO RuleResult) -> RuleResult -> IO RuleResult
forall a b. (a -> b) -> a -> b
$ case PackageDetails -> CodeExecSignal
pkgInstallCode PackageDetails
pd of
RunsCodeOnInstall Text
how -> Text -> RuleResult
Deny (Text
"runs code on install: " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
how)
CodeExecSignal
NoCodeOnInstall -> Text -> RuleResult
NoDecision Text
"no install-time code execution"
CodeExecSignal
CodeExecUnknown -> Text -> RuleResult
NoDecision Text
"install-time code execution not yet determined"
evalRule RuleDeps
_ EvalContext
_ (DenyByIdentity Text
ident) PackageDetails
pd =
RuleResult -> IO RuleResult
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (RuleResult -> IO RuleResult) -> RuleResult -> IO RuleResult
forall a b. (a -> b) -> a -> b
$
if Text -> PackageDetails -> Bool
matchesIdentity Text
ident PackageDetails
pd
then Text -> RuleResult
Deny (Text
"identity " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
ident Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" is revoked by operator")
else Text -> RuleResult
NoDecision (Text
"identity is not the revoked " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
ident)
evalRule RuleDeps
_ EvalContext
_ (AllowByIdentity Text
ident) PackageDetails
pd =
RuleResult -> IO RuleResult
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (RuleResult -> IO RuleResult) -> RuleResult -> IO RuleResult
forall a b. (a -> b) -> a -> b
$
if Text -> PackageDetails -> Bool
matchesIdentity Text
ident PackageDetails
pd
then Text -> RuleResult
Allow (Text
"identity " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
ident Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" is allow-listed by operator")
else Text -> RuleResult
NoDecision (Text
"identity is not the allow-listed " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
ident)
evalRule RuleDeps
deps EvalContext
_ Rule
AllowIfRemediatesCve PackageDetails
pd =
RuleDeps -> forall a. (Maybe CveLookup -> IO a) -> IO a
rdWithCveLookup RuleDeps
deps ((Maybe CveLookup -> IO RuleResult) -> IO RuleResult)
-> (Maybe CveLookup -> IO RuleResult) -> IO RuleResult
forall a b. (a -> b) -> a -> b
$ \case
Maybe CveLookup
Nothing -> RuleResult -> IO RuleResult
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Text -> RuleResult
NoDecision Text
"no advisory database is loaded")
Just CveLookup
cve -> CveLookup -> PackageDetails -> IO RuleResult
remediationVerdict CveLookup
cve PackageDetails
pd
remediationVerdict :: CveLookup -> PackageDetails -> IO RuleResult
remediationVerdict :: CveLookup -> PackageDetails -> IO RuleResult
remediationVerdict CveLookup
cve PackageDetails
pd = do
fixes <- CveLookup -> Text -> Text -> IO Bool
cveRemediationProbe CveLookup
cve Text
name Text
version
if not fixes
then pure (NoDecision "no advisory names this version as its fix")
else do
ranges <- cveAdvisoriesFor cve name
pure (classifyRanges (pkgEcosystem (pkgName pd)) version ranges)
where
name :: Text
name = PackageName -> Text
renderPackageName (PackageDetails -> PackageName
pkgName PackageDetails
pd)
version :: Text
version = Version -> Text
renderVersion (PackageDetails -> Version
pkgVersion PackageDetails
pd)
classifyRanges :: Ecosystem -> Text -> [AdvisoryRange] -> RuleResult
classifyRanges :: Ecosystem -> Text -> [AdvisoryRange] -> RuleResult
classifyRanges Ecosystem
eco Text
version [AdvisoryRange]
ranges =
case ([Text]
remediated, [Text]
stillOpen) of
([Text]
_, Text
_ : [Text]
_) ->
Text -> RuleResult
NoDecision
(Text
"fixes " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text -> [Text] -> Text
T.intercalate Text
", " [Text]
remediated Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" but is still affected by " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text -> [Text] -> Text
T.intercalate Text
", " [Text]
stillOpen)
([], []) ->
Text -> RuleResult
NoDecision Text
"no advisory names this version as its fix"
([Text]
ids, []) -> Text -> RuleResult
Allow (Text
"remediates " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text -> [Text] -> Text
T.intercalate Text
", " [Text]
ids)
where
remediated :: [Text]
remediated = [Text] -> [Text]
forall a. Ord a => [a] -> [a]
ordNub [AdvisoryRange -> Text
arCveId AdvisoryRange
ar | AdvisoryRange
ar <- [AdvisoryRange]
ranges, AdvisoryRange -> Maybe Text
arFixed AdvisoryRange
ar Maybe Text -> Maybe Text -> Bool
forall a. Eq a => a -> a -> Bool
== Text -> Maybe Text
forall a. a -> Maybe a
Just Text
version]
stillOpen :: [Text]
stillOpen = [Text] -> [Text]
forall a. Ord a => [a] -> [a]
ordNub [AdvisoryRange -> Text
arCveId AdvisoryRange
ar | AdvisoryRange
ar <- [AdvisoryRange]
ranges, Ecosystem -> Text -> AdvisoryRange -> Bool
insideAffectedRange Ecosystem
eco Text
version AdvisoryRange
ar]
matchesIdentity :: Text -> PackageDetails -> Bool
matchesIdentity :: Text -> PackageDetails -> Bool
matchesIdentity Text
ident PackageDetails
pd =
let pkgStr :: Text
pkgStr = PackageName -> Text
renderPackageName (PackageDetails -> PackageName
pkgName PackageDetails
pd)
pkgAtVer :: Text
pkgAtVer = Text
pkgStr Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"@" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Version -> Text
renderVersion (PackageDetails -> Version
pkgVersion PackageDetails
pd)
in Text
ident Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
== Text
pkgStr Bool -> Bool -> Bool
|| Text
ident Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
== Text
pkgAtVer
data PreparedRule = PreparedRule
{ PreparedRule -> Text
prepName :: Text
, PreparedRule -> Int
prepPrecedence :: Int
, PreparedRule -> Maybe Resilience
prepResilience :: Maybe Resilience
, PreparedRule -> EvalContext -> PackageDetails -> IO RuleResult
prepEval :: EvalContext -> PackageDetails -> IO RuleResult
}
data Resilience = Resilience
{ Resilience -> EffectfulConfig
resConfig :: EffectfulConfig
, Resilience -> FailureAlignment
resAlignment :: FailureAlignment
, Resilience -> TVar Breaker
resBreaker :: TVar Breaker
, Resilience -> BreakerReporter
resBreakerReporter :: BreakerReporter
}
prepare :: RuleDeps -> [PrecededRule] -> IO [PreparedRule]
prepare :: RuleDeps -> [PrecededRule] -> IO [PreparedRule]
prepare RuleDeps
deps = (PrecededRule -> IO PreparedRule)
-> [PrecededRule] -> IO [PreparedRule]
forall (t :: * -> *) (f :: * -> *) a b.
(Traversable t, Applicative f) =>
(a -> f b) -> t a -> f (t b)
forall (f :: * -> *) a b.
Applicative f =>
(a -> f b) -> [a] -> f [b]
traverse (RuleDeps -> PrecededRule -> IO PreparedRule
prepareRule RuleDeps
deps)
prepareRule :: RuleDeps -> PrecededRule -> IO PreparedRule
prepareRule :: RuleDeps -> PrecededRule -> IO PreparedRule
prepareRule RuleDeps
deps (PrecededRule Int
prec Rule
rule) = do
resilience <- RuleDeps -> Rule -> IO (Maybe Resilience)
resilienceFor RuleDeps
deps Rule
rule
pure
PreparedRule
{ prepName = ruleName rule
, prepPrecedence = prec
, prepResilience = resilience
, prepEval = \EvalContext
ctx -> RuleDeps -> EvalContext -> Rule -> PackageDetails -> IO RuleResult
evalRule RuleDeps
deps EvalContext
ctx Rule
rule
}
resilienceFor :: RuleDeps -> Rule -> IO (Maybe Resilience)
resilienceFor :: RuleDeps -> Rule -> IO (Maybe Resilience)
resilienceFor RuleDeps
deps = \case
Rule
AllowIfRemediatesCve -> do
breaker <- IO (TVar Breaker)
newBreaker
pure $
Just
Resilience
{ resConfig = defaultEffectfulConfig
, resAlignment = FailNoDecision
, resBreaker = breaker
, resBreakerReporter = rdBreakerReporter deps
}
Rule
_ -> Maybe Resilience -> IO (Maybe Resilience)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Maybe Resilience
forall a. Maybe a
Nothing
bootOrder :: [PreparedRule] -> [PreparedRule]
bootOrder :: [PreparedRule] -> [PreparedRule]
bootOrder = (PreparedRule -> (Down Int, Text))
-> [PreparedRule] -> [PreparedRule]
forall b a. Ord b => (a -> b) -> [a] -> [a]
sortOn (\PreparedRule
r -> Int -> Text -> (Down Int, Text)
bootKey (PreparedRule -> Int
prepPrecedence PreparedRule
r) (PreparedRule -> Text
prepName PreparedRule
r))
bootKey :: Int -> Text -> (Down Int, Text)
bootKey :: Int -> Text -> (Down Int, Text)
bootKey Int
prec Text
name = (Int -> Down Int
forall a. a -> Down a
Down Int
prec, Text
name)
renderBootOrder :: [PreparedRule] -> [Text]
renderBootOrder :: [PreparedRule] -> [Text]
renderBootOrder [PreparedRule]
rules = (Int -> PreparedRule -> Text) -> [Int] -> [PreparedRule] -> [Text]
forall a b c. (a -> b -> c) -> [a] -> [b] -> [c]
zipWith Int -> PreparedRule -> Text
forall {a}. Show a => a -> PreparedRule -> Text
line [Int
1 :: Int ..] ([PreparedRule] -> [PreparedRule]
bootOrder [PreparedRule]
rules)
where
line :: a -> PreparedRule -> Text
line a
i PreparedRule
r =
Text
"rule "
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> a -> Text
forall b a. (Show a, IsString b) => a -> b
show a
i
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
": "
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> PreparedRule -> Text
prepName PreparedRule
r
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" (precedence "
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Int -> Text
forall b a. (Show a, IsString b) => a -> b
show (PreparedRule -> Int
prepPrecedence PreparedRule
r)
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
")"
evalRules :: EvalContext -> [PreparedRule] -> PackageDetails -> IO Decision
evalRules :: EvalContext -> [PreparedRule] -> PackageDetails -> IO Decision
evalRules EvalContext
ctx [PreparedRule]
rules PackageDetails
pd = [PreparedRule] -> [Text] -> IO Decision
step ([PreparedRule] -> [PreparedRule]
bootOrder [PreparedRule]
rules) []
where
step :: [PreparedRule] -> [Reason] -> IO Decision
step :: [PreparedRule] -> [Text] -> IO Decision
step [] [Text]
reasons = Decision -> IO Decision
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ([Text] -> Decision
BlockedByDefault ([Text] -> [Text]
forall a. [a] -> [a]
reverse [Text]
reasons))
step (PreparedRule
r : [PreparedRule]
rs) [Text]
reasons
| Maybe Resilience -> Bool
forall a. Maybe a -> Bool
isNothing (PreparedRule -> Maybe Resilience
prepResilience PreparedRule
r) = do
res <- PreparedRule -> EvalContext -> PackageDetails -> IO RuleResult
prepEval PreparedRule
r EvalContext
ctx PackageDetails
pd
case decisive (prepName r) res of
Just Decision
d -> Decision -> IO Decision
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Decision
d
Maybe Decision
Nothing -> [PreparedRule] -> [Text] -> IO Decision
step [PreparedRule]
rs (RuleResult -> Text
reasonOf RuleResult
res Text -> [Text] -> [Text]
forall a. a -> [a] -> [a]
: [Text]
reasons)
| Bool
otherwise =
let ([PreparedRule]
block, [PreparedRule]
rest) = (PreparedRule -> Bool)
-> [PreparedRule] -> ([PreparedRule], [PreparedRule])
forall a. (a -> Bool) -> [a] -> ([a], [a])
span (Maybe Resilience -> Bool
forall a. Maybe a -> Bool
isJust (Maybe Resilience -> Bool)
-> (PreparedRule -> Maybe Resilience) -> PreparedRule -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. PreparedRule -> Maybe Resilience
prepResilience) (PreparedRule
r PreparedRule -> [PreparedRule] -> [PreparedRule]
forall a. a -> [a] -> [a]
: [PreparedRule]
rs)
in EvalContext
-> PackageDetails -> [PreparedRule] -> IO (Either Decision [Text])
evalBlock EvalContext
ctx PackageDetails
pd [PreparedRule]
block IO (Either Decision [Text])
-> (Either Decision [Text] -> IO Decision) -> IO Decision
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
Left Decision
d -> Decision -> IO Decision
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Decision
d
Right [Text]
blockReasons -> [PreparedRule] -> [Text] -> IO Decision
step [PreparedRule]
rest ([Text] -> [Text]
forall a. [a] -> [a]
reverse [Text]
blockReasons [Text] -> [Text] -> [Text]
forall a. Semigroup a => a -> a -> a
<> [Text]
reasons)
evalBlock :: EvalContext -> PackageDetails -> [PreparedRule] -> IO (Either Decision [Reason])
evalBlock :: EvalContext
-> PackageDetails -> [PreparedRule] -> IO (Either Decision [Text])
evalBlock EvalContext
ctx PackageDetails
pd [PreparedRule]
block =
IO [Async RuleResult]
-> ([Async RuleResult] -> IO ())
-> ([Async RuleResult] -> IO (Either Decision [Text]))
-> IO (Either Decision [Text])
forall (m :: * -> *) a b c.
MonadUnliftIO m =>
m a -> (a -> m b) -> (a -> m c) -> m c
bracket
((PreparedRule -> IO (Async RuleResult))
-> [PreparedRule] -> IO [Async RuleResult]
forall (t :: * -> *) (f :: * -> *) a b.
(Traversable t, Applicative f) =>
(a -> f b) -> t a -> f (t b)
forall (f :: * -> *) a b.
Applicative f =>
(a -> f b) -> [a] -> f [b]
traverse (\PreparedRule
r -> IO RuleResult -> IO (Async RuleResult)
forall (m :: * -> *) a. MonadUnliftIO m => m a -> m (Async a)
async (EvalContext -> PreparedRule -> PackageDetails -> IO RuleResult
runEffectfulRule EvalContext
ctx PreparedRule
r PackageDetails
pd)) [PreparedRule]
block)
((Async RuleResult -> IO ()) -> [Async RuleResult] -> IO ()
forall (t :: * -> *) (f :: * -> *) a b.
(Foldable t, Applicative f) =>
(a -> f b) -> t a -> f ()
traverse_ Async RuleResult -> IO ()
forall (m :: * -> *) a. MonadIO m => Async a -> m ()
uninterruptibleCancel)
(\[Async RuleResult]
asyncs -> [(PreparedRule, Async RuleResult)]
-> [Text] -> IO (Either Decision [Text])
awaitInOrder ([PreparedRule]
-> [Async RuleResult] -> [(PreparedRule, Async RuleResult)]
forall a b. [a] -> [b] -> [(a, b)]
zip [PreparedRule]
block [Async RuleResult]
asyncs) [])
awaitInOrder :: [(PreparedRule, Async RuleResult)] -> [Reason] -> IO (Either Decision [Reason])
awaitInOrder :: [(PreparedRule, Async RuleResult)]
-> [Text] -> IO (Either Decision [Text])
awaitInOrder [] [Text]
reasons = Either Decision [Text] -> IO (Either Decision [Text])
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ([Text] -> Either Decision [Text]
forall a b. b -> Either a b
Right ([Text] -> [Text]
forall a. [a] -> [a]
reverse [Text]
reasons))
awaitInOrder ((PreparedRule
r, Async RuleResult
a) : [(PreparedRule, Async RuleResult)]
rest) [Text]
reasons = do
res <- Async RuleResult -> IO RuleResult
forall (m :: * -> *) a. MonadIO m => Async a -> m a
wait Async RuleResult
a
case decisive (prepName r) res of
Just Decision
d -> do
((PreparedRule, Async RuleResult) -> IO ())
-> [(PreparedRule, Async RuleResult)] -> IO ()
forall (t :: * -> *) (f :: * -> *) a b.
(Foldable t, Applicative f) =>
(a -> f b) -> t a -> f ()
traverse_ (Async RuleResult -> IO ()
forall (m :: * -> *) a. MonadIO m => Async a -> m ()
cancel (Async RuleResult -> IO ())
-> ((PreparedRule, Async RuleResult) -> Async RuleResult)
-> (PreparedRule, Async RuleResult)
-> IO ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (PreparedRule, Async RuleResult) -> Async RuleResult
forall a b. (a, b) -> b
snd) [(PreparedRule, Async RuleResult)]
rest
Either Decision [Text] -> IO (Either Decision [Text])
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Decision -> Either Decision [Text]
forall a b. a -> Either a b
Left Decision
d)
Maybe Decision
Nothing -> [(PreparedRule, Async RuleResult)]
-> [Text] -> IO (Either Decision [Text])
awaitInOrder [(PreparedRule, Async RuleResult)]
rest (RuleResult -> Text
reasonOf RuleResult
res Text -> [Text] -> [Text]
forall a. a -> [a] -> [a]
: [Text]
reasons)
decisive :: Text -> RuleResult -> Maybe Decision
decisive :: Text -> RuleResult -> Maybe Decision
decisive Text
name = \case
Allow Text
reason -> Decision -> Maybe Decision
forall a. a -> Maybe a
Just (Text -> Text -> Decision
Admitted Text
name Text
reason)
Deny Text
reason -> Decision -> Maybe Decision
forall a. a -> Maybe a
Just (Text -> Text -> Decision
Blocked Text
name Text
reason)
Unavailable Transience
transience FailureAlignment
FailDeny Text
reason -> Decision -> Maybe Decision
forall a. a -> Maybe a
Just (Transience -> Text -> Decision
Undecidable Transience
transience Text
reason)
NoDecision Text
_ -> Maybe Decision
forall a. Maybe a
Nothing
Unavailable Transience
_ FailureAlignment
FailNoDecision Text
_ -> Maybe Decision
forall a. Maybe a
Nothing
reasonOf :: RuleResult -> Reason
reasonOf :: RuleResult -> Text
reasonOf = \case
Allow Text
reason -> Text
reason
Deny Text
reason -> Text
reason
NoDecision Text
reason -> Text
reason
Unavailable Transience
_ FailureAlignment
_ Text
reason -> Text
reason
runEffectfulRule :: EvalContext -> PreparedRule -> PackageDetails -> IO RuleResult
runEffectfulRule :: EvalContext -> PreparedRule -> PackageDetails -> IO RuleResult
runEffectfulRule EvalContext
ctx PreparedRule
rule PackageDetails
pd = case PreparedRule -> Maybe Resilience
prepResilience PreparedRule
rule of
Maybe Resilience
Nothing -> PreparedRule -> EvalContext -> PackageDetails -> IO RuleResult
prepEval PreparedRule
rule EvalContext
ctx PackageDetails
pd
Just Resilience
res -> do
let now :: UTCTime
now = EvalContext -> UTCTime
ctxNow EvalContext
ctx
admitted <- Resilience -> UTCTime -> IO Bool
admitProbe Resilience
res UTCTime
now
if not admitted
then
pure (exhausted res (prepName rule) (transientCause (resConfig res)) "the rule source circuit breaker is open")
else do
result <- attemptWithRetry res (prepEval rule ctx) pd
settleOutcome res (prepName rule) now result
settleOutcome :: Resilience -> Text -> UTCTime -> Either Transience RuleResult -> IO RuleResult
settleOutcome :: Resilience
-> Text -> UTCTime -> Either Transience RuleResult -> IO RuleResult
settleOutcome Resilience
res Text
name UTCTime
now = \case
Right RuleResult
outcome -> do
Resilience -> (Breaker -> Breaker) -> IO ()
commitBreaker Resilience
res Breaker -> Breaker
recordSuccess
RuleResult -> IO RuleResult
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure RuleResult
outcome
Left Transience
transience -> do
Resilience -> (Breaker -> Breaker) -> IO ()
commitBreaker Resilience
res (EffectfulConfig -> UTCTime -> Breaker -> Breaker
tripOnFailure (Resilience -> EffectfulConfig
resConfig Resilience
res) UTCTime
now)
RuleResult -> IO RuleResult
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Resilience -> Text -> Transience -> Text -> RuleResult
exhausted Resilience
res Text
name Transience
transience Text
"the rule could not be evaluated")
attemptWithRetry :: Resilience -> (PackageDetails -> IO RuleResult) -> PackageDetails -> IO (Either Transience RuleResult)
attemptWithRetry :: Resilience
-> (PackageDetails -> IO RuleResult)
-> PackageDetails
-> IO (Either Transience RuleResult)
attemptWithRetry Resilience
res PackageDetails -> IO RuleResult
evalAt PackageDetails
pd =
RetryPolicyM IO
-> (RetryStatus -> Either Transience RuleResult -> IO Bool)
-> (RetryStatus -> IO (Either Transience RuleResult))
-> IO (Either Transience RuleResult)
forall (m :: * -> *) b.
MonadIO m =>
RetryPolicyM m
-> (RetryStatus -> b -> m Bool) -> (RetryStatus -> m b) -> m b
retrying ([Int] -> RetryPolicyM IO
backoffPolicy (EffectfulConfig -> [Int]
ecBackoff (Resilience -> EffectfulConfig
resConfig Resilience
res))) RetryStatus -> Either Transience RuleResult -> IO Bool
forall {f :: * -> *} {p} {a} {b}.
Applicative f =>
p -> Either a b -> f Bool
shouldRetry (\RetryStatus
_ -> Resilience
-> (PackageDetails -> IO RuleResult)
-> PackageDetails
-> IO (Either Transience RuleResult)
attemptOnce Resilience
res PackageDetails -> IO RuleResult
evalAt PackageDetails
pd)
where
shouldRetry :: p -> Either a b -> f Bool
shouldRetry p
_ = Bool -> f Bool
forall a. a -> f a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Bool -> f Bool) -> (Either a b -> Bool) -> Either a b -> f Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Either a b -> Bool
forall a b. Either a b -> Bool
isLeft
backoffPolicy :: [Int] -> RetryPolicyM IO
backoffPolicy :: [Int] -> RetryPolicyM IO
backoffPolicy [Int]
backoffs = (RetryStatus -> IO (Maybe Int)) -> RetryPolicyM IO
forall (m :: * -> *).
(RetryStatus -> m (Maybe Int)) -> RetryPolicyM m
RetryPolicyM (\RetryStatus
rs -> Maybe Int -> IO (Maybe Int)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ([Int]
backoffs [Int] -> Int -> Maybe Int
forall a. [a] -> Int -> Maybe a
!!? RetryStatus -> Int
rsIterNumber RetryStatus
rs))
attemptOnce :: Resilience -> (PackageDetails -> IO RuleResult) -> PackageDetails -> IO (Either Transience RuleResult)
attemptOnce :: Resilience
-> (PackageDetails -> IO RuleResult)
-> PackageDetails
-> IO (Either Transience RuleResult)
attemptOnce Resilience
res PackageDetails -> IO RuleResult
evalAt PackageDetails
pd = do
result <- IO (Maybe RuleResult)
-> IO (Either SomeException (Maybe RuleResult))
forall (m :: * -> *) a.
MonadUnliftIO m =>
m a -> m (Either SomeException a)
tryAny (Int -> IO RuleResult -> IO (Maybe RuleResult)
forall (m :: * -> *) a.
MonadUnliftIO m =>
Int -> m a -> m (Maybe a)
timeout (EffectfulConfig -> Int
ecTimeout (Resilience -> EffectfulConfig
resConfig Resilience
res)) (PackageDetails -> IO RuleResult
evalAt PackageDetails
pd))
pure $ case result of
Left SomeException
_ -> Transience -> Either Transience RuleResult
forall a b. a -> Either a b
Left Transience
transient
Right Maybe RuleResult
Nothing -> Transience -> Either Transience RuleResult
forall a b. a -> Either a b
Left Transience
transient
Right (Just (Unavailable Transience
WontResolve FailureAlignment
_ Text
_)) -> Transience -> Either Transience RuleResult
forall a b. a -> Either a b
Left Transience
WontResolve
Right (Just (Unavailable (WillResolve Maybe RetryAfter
_) FailureAlignment
_ Text
_)) -> Transience -> Either Transience RuleResult
forall a b. a -> Either a b
Left Transience
transient
Right (Just RuleResult
clean) -> RuleResult -> Either Transience RuleResult
forall a b. b -> Either a b
Right RuleResult
clean
where
transient :: Transience
transient = EffectfulConfig -> Transience
transientCause (Resilience -> EffectfulConfig
resConfig Resilience
res)
exhausted :: Resilience -> Text -> Transience -> Text -> RuleResult
exhausted :: Resilience -> Text -> Transience -> Text -> RuleResult
exhausted Resilience
res Text
name Transience
transience Text
reason = Transience -> FailureAlignment -> Text -> RuleResult
Unavailable Transience
transience (Resilience -> FailureAlignment
resAlignment Resilience
res) (Text
name Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
": " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
reason)
transientCause :: EffectfulConfig -> Transience
transientCause :: EffectfulConfig -> Transience
transientCause EffectfulConfig
cfg = Maybe RetryAfter -> Transience
WillResolve (EffectfulConfig -> Maybe RetryAfter
ecRetryAfter EffectfulConfig
cfg)
admitProbe :: Resilience -> UTCTime -> IO Bool
admitProbe :: Resilience -> UTCTime -> IO Bool
admitProbe Resilience
res UTCTime
now = do
(permitted, old, new) <- STM (Bool, Breaker, Breaker) -> IO (Bool, Breaker, Breaker)
forall (m :: * -> *) a. MonadIO m => STM a -> m a
atomically (STM (Bool, Breaker, Breaker) -> IO (Bool, Breaker, Breaker))
-> STM (Bool, Breaker, Breaker) -> IO (Bool, Breaker, Breaker)
forall a b. (a -> b) -> a -> b
$ do
st <- TVar Breaker -> STM Breaker
forall a. TVar a -> STM a
readTVar (Resilience -> TVar Breaker
resBreaker Resilience
res)
let (p, st') = admit now st
writeTVar (resBreaker res) st'
pure (p, st, st')
reportBreakerChange (resBreakerReporter res) old new
pure permitted
commitBreaker :: Resilience -> (Breaker -> Breaker) -> IO ()
commitBreaker :: Resilience -> (Breaker -> Breaker) -> IO ()
commitBreaker Resilience
res Breaker -> Breaker
step = do
(old, new) <- STM (Breaker, Breaker) -> IO (Breaker, Breaker)
forall (m :: * -> *) a. MonadIO m => STM a -> m a
atomically (STM (Breaker, Breaker) -> IO (Breaker, Breaker))
-> STM (Breaker, Breaker) -> IO (Breaker, Breaker)
forall a b. (a -> b) -> a -> b
$ do
st <- TVar Breaker -> STM Breaker
forall a. TVar a -> STM a
readTVar (Resilience -> TVar Breaker
resBreaker Resilience
res)
let st' = Breaker -> Breaker
step Breaker
st
writeTVar (resBreaker res) st'
pure (st, st')
reportBreakerChange (resBreakerReporter res) old new
tripOnFailure :: EffectfulConfig -> UTCTime -> Breaker -> Breaker
tripOnFailure :: EffectfulConfig -> UTCTime -> Breaker -> Breaker
tripOnFailure EffectfulConfig
cfg = Int -> NominalDiffTime -> UTCTime -> Breaker -> Breaker
recordFailure (EffectfulConfig -> Int
ecBreakerThreshold EffectfulConfig
cfg) (EffectfulConfig -> NominalDiffTime
ecBreakerCooldown EffectfulConfig
cfg)
data EffectfulConfig = EffectfulConfig
{ EffectfulConfig -> Int
ecTimeout :: Int
, EffectfulConfig -> [Int]
ecBackoff :: [Int]
, EffectfulConfig -> Int
ecBreakerThreshold :: Int
, EffectfulConfig -> NominalDiffTime
ecBreakerCooldown :: NominalDiffTime
, EffectfulConfig -> Maybe RetryAfter
ecRetryAfter :: Maybe RetryAfter
}
defaultEffectfulConfig :: EffectfulConfig
defaultEffectfulConfig :: EffectfulConfig
defaultEffectfulConfig =
EffectfulConfig
{ ecTimeout :: Int
ecTimeout = Int
2_000_000
, ecBackoff :: [Int]
ecBackoff = [Int
100_000, Int
250_000]
, ecBreakerThreshold :: Int
ecBreakerThreshold = Int
5
, ecBreakerCooldown :: NominalDiffTime
ecBreakerCooldown = NominalDiffTime
30
, ecRetryAfter :: Maybe RetryAfter
ecRetryAfter = Maybe RetryAfter
forall a. Maybe a
Nothing
}
newBreaker :: IO (TVar Breaker)
newBreaker :: IO (TVar Breaker)
newBreaker = Breaker -> IO (TVar Breaker)
forall (m :: * -> *) a. MonadIO m => a -> m (TVar a)
newTVarIO Breaker
initialBreaker
renderDecision :: PackageDetails -> Decision -> Text
renderDecision :: PackageDetails -> Decision -> Text
renderDecision PackageDetails
pd Decision
decision =
let subject :: Text
subject = PackageName -> Text
renderPackageName (PackageDetails -> PackageName
pkgName PackageDetails
pd) Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"@" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Version -> Text
renderVersion (PackageDetails -> Version
pkgVersion PackageDetails
pd)
in case Decision
decision of
Admitted Text
name Text
reason ->
Text
subject Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" was approved by " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
name Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
": " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
reason
Blocked Text
name Text
reason ->
Text
subject Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" was denied by " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
name Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
": " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
reason
BlockedByDefault [Text]
reasons ->
Text
subject
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" was denied (no rule allowed it)"
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> if [Text] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [Text]
reasons
then Text
""
else Text
": " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text -> [Text] -> Text
T.intercalate Text
"; " [Text]
reasons
Undecidable Transience
_ Text
reason ->
Text
subject Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" could not be evaluated: " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
reason
renderDuration :: NominalDiffTime -> Text
renderDuration :: NominalDiffTime -> Text
renderDuration NominalDiffTime
d =
let secs :: Integer
secs = Integer -> Integer -> Integer
forall a. Ord a => a -> a -> a
max Integer
0 (Double -> Integer
forall b. Integral b => Double -> b
forall a b. (RealFrac a, Integral b) => a -> b
round (NominalDiffTime -> Double
forall a b. (Real a, Fractional b) => a -> b
realToFrac NominalDiffTime
d :: Double)) :: Integer
in [(Text, Integer)] -> Integer -> Text
forall {t} {t}.
(Semigroup t, Show t, IsString t, Integral t) =>
[(t, t)] -> t -> t
pick [(Text, Integer)]
units Integer
secs
where
units :: [(Text, Integer)]
units :: [(Text, Integer)]
units =
[ (Text
"day", Integer
86400)
, (Text
"hour", Integer
3600)
, (Text
"minute", Integer
60)
]
pick :: [(t, t)] -> t -> t
pick [] t
secs = t -> t -> t
forall {a} {a}.
(Semigroup a, Show a, IsString a, Eq a, Num a) =>
a -> a -> a
plural t
secs t
"second"
pick ((t
unit, t
size) : [(t, t)]
rest) t
secs
| t
secs t -> t -> Bool
forall a. Ord a => a -> a -> Bool
>= t
size = t -> t -> t
forall {a} {a}.
(Semigroup a, Show a, IsString a, Eq a, Num a) =>
a -> a -> a
plural (t
secs t -> t -> t
forall a. Integral a => a -> a -> a
`div` t
size) t
unit
| Bool
otherwise = [(t, t)] -> t -> t
pick [(t, t)]
rest t
secs
plural :: a -> a -> a
plural a
n a
unit = a -> a
forall b a. (Show a, IsString b) => a -> b
show a
n a -> a -> a
forall a. Semigroup a => a -> a -> a
<> a
" " a -> a -> a
forall a. Semigroup a => a -> a -> a
<> a
unit a -> a -> a
forall a. Semigroup a => a -> a -> a
<> (if a
n a -> a -> Bool
forall a. Eq a => a -> a -> Bool
== a
1 then a
"" else a
"s")