aws-secrets-0.0.0.0: library/AWS/Secrets/Fetch.hs
module AWS.Secrets.Fetch where
import AWS.Secrets.Config (SecretsConfig)
import qualified AWS.Secrets.Config as Config
import AWS.Secrets.Name (SecretName, getSecretNameText)
import Control.Applicative (pure)
import Control.Monad.Except (MonadError, throwError)
import Control.Monad.IO.Class (MonadIO)
import qualified Data.Aeson as JSON
import qualified Data.ByteString.Lazy as Lazy
import qualified Data.ByteString.Lazy as Lazy.ByteString
import Data.Either (Either (..))
import Data.Foldable (fold)
import Data.Function (($), (.))
import Data.Int (Int)
import qualified Data.List as List
import Data.Semigroup ((<>))
import Data.String (String)
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Lazy as Lazy.Text
import qualified Data.Text.Lazy.Builder as Text
import qualified Data.Text.Lazy.Builder as Text.Builder
import qualified System.Exit as Exit
import System.IO (FilePath)
import qualified System.Process.Typed as Process
import Text.Show (Show, show)
-- | Type @result@ may be e.g. 'AWS.Secrets.SecretType.Secret'.
fetchSecret ::
forall m result.
(MonadIO m, MonadError Text m, JSON.FromJSON result) =>
SecretsConfig ->
SecretName ->
m result
fetchSecret config name = do
let secretNameText :: Text
secretNameText = getSecretNameText name
secretNameString :: String
secretNameString = Text.unpack secretNameText
secretNameTextBuilder :: Text.Builder
secretNameTextBuilder = Text.Builder.fromText secretNameText
awsRegionText :: Text
awsRegionText = Config.getAwsRegionText (Config.getAwsRegion config)
awsRegionString :: String
awsRegionString = Text.unpack awsRegionText
awsRegionTextBuilder :: Text.Builder
awsRegionTextBuilder = Text.Builder.fromText awsRegionText
executableFilePath :: FilePath
executableFilePath = Config.getAwsCliFilePath (Config.getAwsCli config)
executableTextBuilder :: Text.Builder
executableTextBuilder = Text.Builder.fromString executableFilePath
stringArgs :: [String]
stringArgs =
[ "secretsmanager",
"get-secret-value",
"--secret-id",
secretNameString,
"--region",
awsRegionString
]
fullCommandTextBuilder :: Text.Builder
fullCommandTextBuilder =
unwords
[ "The exact command executed was:",
showBuilder @[String] (executableFilePath : stringArgs)
]
descriptionTextBuilder :: Text.Builder
descriptionTextBuilder =
unwords
[ "AWS command",
quote executableTextBuilder,
"to get secret",
quote secretNameTextBuilder,
"from region",
quote $ awsRegionTextBuilder
]
(exitCode :: Exit.ExitCode, output :: Lazy.ByteString, error :: Lazy.ByteString) <-
Process.readProcess (Process.proc executableFilePath stringArgs)
let -- Shows what came out on stdout
normalOutputMessage :: Text.Builder
normalOutputMessage =
if Lazy.ByteString.null output
then "It produced no output."
else
unwords
[ "Its output was:",
showBuilder @Lazy.ByteString output
]
-- Shows what came out on stderr
errorOutputMessage :: Text.Builder
errorOutputMessage =
if Lazy.ByteString.null error
then "It produced no error output."
else
unwords
[ "Its error output was:",
showBuilder @Lazy.ByteString error
]
-- What to do if the JSON on stdout couldn't be parsed
throwParseError :: forall x. String -> m x
throwParseError parseError =
(throwError . render . unlines)
[ unwords
[ descriptionTextBuilder,
"failed to produce valid JSON"
],
fullCommandTextBuilder,
normalOutputMessage,
unwords
[ "The output from the parser is:",
Text.Builder.fromString parseError
]
]
-- What to do if AWS command returns a failure exit code
throwExitCodeError :: forall x. Int -> m x
throwExitCodeError exitCodeInt =
(throwError . render . unlines)
[ unwords
[ descriptionTextBuilder,
"failed with exit code",
quote (showBuilder @Int exitCodeInt)
],
fullCommandTextBuilder,
errorOutputMessage
]
case exitCode of
Exit.ExitSuccess -> case JSON.eitherDecode @result output of
Right x -> pure x
Left e -> throwParseError e
Exit.ExitFailure exitCodeInt ->
throwExitCodeError exitCodeInt
quote :: Text.Builder -> Text.Builder
quote x = "‘" <> x <> "’"
unwords :: [Text.Builder] -> Text.Builder
unwords = fold . List.intersperse " "
unlines :: [Text.Builder] -> Text.Builder
unlines = fold . List.intersperse "\n"
showBuilder :: Show a => a -> Text.Builder
showBuilder = Text.Builder.fromString . show
render :: Text.Builder -> Text
render = Lazy.Text.toStrict . Text.Builder.toLazyText