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 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 :: ( MonadResource m , MonadReader env m , HasAwsEnv env , AWSRequest a , Typeable a , Typeable (AWSResponse a) ) => Text -> a -> (AWSResponse a -> Maybe b) -> m b awsSimple name req post = do resp <- awsSend req maybe (throwString err) pure $ post resp where err = unpack name <> " successful, but processing the response failed" 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 "sts:AssumeRole" 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 ]