stackctl-1.5.0.0: src/Stackctl/AWS/Core.hs
module Stackctl.AWS.Core
( AwsEnv
, HasAwsEnv (..)
, awsEnvDiscover
, awsWithAuth
, awsSimple
, awsSend
, awsPaginate
, awsAwait
, awsAssumeRole
-- * Modifiers on 'AwsEnv'
, awsWithin
, awsTimeout
, awsSilently
-- * 'Amazonka' extensions
, AccountId (..)
-- * Error-handling
, handlingServiceError
, formatServiceError
-- * 'Amazonka'/'ResourceT' re-exports
, Region (..)
, FromText (..)
, ToText (..)
, MonadResource
) where
import Stackctl.Prelude hiding (timeout)
import Amazonka hiding (LogLevel (..))
import qualified Amazonka as AWS
import Amazonka.Auth.Keys (fromSession)
import Amazonka.Data.Text (FromText (..), ToText (..))
import Amazonka.Env (env_auth, env_logger, env_region)
import Amazonka.STS.AssumeRole
import Conduit (ConduitM)
import Control.Monad.Logger (defaultLoc, toLogStr)
import Control.Monad.Trans.Resource (MonadResource)
import Data.Typeable (typeRep)
import Stackctl.AWS.Orphans ()
import UnliftIO.Exception.Lens (handling)
newtype AwsEnv = AwsEnv
{ unAwsEnv :: Env
}
unL :: Lens' AwsEnv Env
unL = lens unAwsEnv $ \x y -> x {unAwsEnv = y}
awsEnvDiscover :: MonadLoggerIO m => m AwsEnv
awsEnvDiscover = do
env <- liftIO $ newEnv discover
AwsEnv <$> configureLogging env
configureLogging :: MonadLoggerIO m => Env -> m Env
configureLogging env = do
loggerIO <- askLoggerIO
let logger level = do
loggerIO
defaultLoc -- TODO: there may be a way to get a CallStack/Loc
"Amazonka"
( case level of
AWS.Info -> LevelInfo
AWS.Error -> LevelError
AWS.Debug -> LevelDebug
AWS.Trace -> LevelOther "trace"
)
. toLogStr
pure $ env & env_logger .~ logger
class HasAwsEnv env where
awsEnvL :: Lens' env AwsEnv
instance HasAwsEnv AwsEnv where
awsEnvL = id
awsWithAuth
:: (MonadIO m, MonadReader env m, HasAwsEnv env) => (AuthEnv -> m a) -> m a
awsWithAuth f = do
auth <- view $ awsEnvL . unL . env_auth . to runIdentity
withAuth auth f
awsSimple
:: forall a env m b
. ( HasCallStack
, MonadResource m
, MonadReader env m
, HasAwsEnv env
, AWSRequest a
, Typeable a
, Typeable (AWSResponse a)
)
=> a
-> (AWSResponse a -> Maybe b)
-> m b
awsSimple req post = do
resp <- awsSend req
let
name = show $ typeRep $ Proxy @a
err = name <> " successful, but processing the response failed"
maybe (throwString err) pure $ post resp
awsSend
:: ( MonadResource m
, MonadReader env m
, HasAwsEnv env
, AWSRequest a
, Typeable a
, Typeable (AWSResponse a)
)
=> a
-> m (AWSResponse a)
awsSend req = do
AwsEnv env <- view awsEnvL
send env req
awsPaginate
:: ( MonadResource m
, MonadReader env m
, HasAwsEnv env
, AWSPager a
, Typeable a
, Typeable (AWSResponse a)
)
=> a
-> ConduitM () (AWSResponse a) m ()
awsPaginate req = do
AwsEnv env <- view awsEnvL
paginateEither env req >>= hoistEither
hoistEither :: MonadIO m => Either Error a -> m a
hoistEither = either (liftIO . throwIO) pure
awsAwait
:: ( MonadResource m
, MonadReader env m
, HasAwsEnv env
, AWSRequest a
, Typeable a
)
=> Wait a
-> a
-> m Accept
awsAwait w req = do
AwsEnv env <- view awsEnvL
await env w req
awsAssumeRole
:: (MonadResource m, MonadReader env m, HasAwsEnv env)
=> Text
-- ^ Role ARN
-> Text
-- ^ Session name
-> m a
-- ^ Action to run as the assumed role
-> m a
awsAssumeRole role sessionName f = do
let req = newAssumeRole role sessionName
assumeEnv <- awsSimple req $ \resp -> do
let creds = resp ^. assumeRoleResponse_credentials
token <- creds ^. authEnv_sessionToken
let
accessKeyId = creds ^. authEnv_accessKeyId
secretAccessKey = creds ^. authEnv_secretAccessKey . _Sensitive
pure $ fromSession accessKeyId secretAccessKey $ token ^. _Sensitive
local (awsEnvL . unL %~ assumeEnv) f
awsWithin :: (MonadReader env m, HasAwsEnv env) => Region -> m a -> m a
awsWithin r = local $ awsEnvL . unL . env_region .~ r
awsTimeout :: (MonadReader env m, HasAwsEnv env) => Seconds -> m a -> m a
awsTimeout t = local $ awsEnvL . unL %~ globalTimeout t
awsSilently :: (MonadReader env m, HasAwsEnv env) => m a -> m a
awsSilently = local $ awsEnvL . unL . env_logger .~ noop
where
noop _level _msg = pure ()
newtype AccountId = AccountId
{ unAccountId :: Text
}
deriving newtype (Eq, Ord, Show, ToJSON)
-- | Handle 'ServiceError', log it and 'exitFailure'
--
-- This is useful at the top-level of the app, where we'd be crashing anyway. It
-- makes things more readable and easier to debug.
handlingServiceError :: (MonadUnliftIO m, MonadLogger m) => m a -> m a
handlingServiceError =
handling _ServiceError $ \e -> do
logError
$ "Exiting due to AWS Service error"
:# [ "code" .= toText (e ^. serviceError_code)
, "message" .= fmap toText (e ^. serviceError_message)
, "requestId" .= fmap toText (e ^. serviceError_requestId)
]
exitFailure
formatServiceError :: ServiceError -> Text
formatServiceError e =
mconcat
[ toText $ e ^. serviceError_code
, maybe "" ((": " <>) . toText) $ e ^. serviceError_message
, maybe "" (("\nRequest Id: " <>) . toText) $ e ^. serviceError_requestId
]