packages feed

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