module Network.CloudSeeder.Main
( Command(..)
, StackName(..)
, CliError(..)
, HasCliError(..)
, AsCliError(..)
, cli
, cliIO
) where
import Control.Applicative.Lift (Errors, failure, runErrors)
import Control.Lens ((^.), (^..), each, has, only, to)
import Control.Lens.TH (makeClassy, makeClassyPrisms)
import Control.Monad.Base (MonadBase)
import Control.Monad.Catch (MonadCatch, MonadThrow)
import Control.Monad.Error.Lens (throwing)
import Control.Monad.Except (MonadError(..), ExceptT, runExceptT)
import Control.Monad.IO.Class (MonadIO)
import Control.Monad.Logger (LoggingT, MonadLogger, runStderrLoggingT)
import Control.Monad.Reader (MonadReader, ReaderT, runReaderT)
import Control.Monad.Trans.Control (MonadBaseControl(..))
import Data.List (find, sort)
import Data.Semigroup ((<>))
import Network.AWS (Credentials(Discover), Env, newEnv)
import System.Exit (exitFailure)
import Prelude hiding (readFile)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Network.CloudSeeder.CommandLine
import Network.CloudSeeder.DSL
import Network.CloudSeeder.Interfaces
data CliError
= CliMissingEnvVars [T.Text]
| CliFileSystemError FileSystemError
| CliStackNotConfigured T.Text
| CliMissingDependencyStacks [T.Text]
deriving (Eq, Show)
makeClassy ''CliError
makeClassyPrisms ''CliError
renderCliError :: CliError -> T.Text
renderCliError (CliMissingEnvVars vars)
= "the following required environment variables were not set:\n"
<> T.unlines (map (" " <>) vars)
renderCliError (CliFileSystemError (FileNotFound path))
= "file not found: ‘" <> path <> "’\n"
renderCliError (CliStackNotConfigured stackName)
= "stack name not present in configuration: ‘" <> stackName <> "’\n"
renderCliError (CliMissingDependencyStacks stackNames)
= "the following dependency stacks do not exist in AWS:\n"
<> T.unlines (map (" " <>) stackNames)
newtype AppM a = AppM (ReaderT Env (ExceptT CliError (LoggingT IO)) a)
deriving ( Functor, Applicative, Monad, MonadIO, MonadBase IO
, MonadCatch, MonadThrow, MonadReader Env, MonadError CliError
, MonadLogger, MonadArguments, MonadEnvironment )
instance MonadBaseControl IO AppM where
type StM AppM a = StM (ReaderT Env (ExceptT CliError (LoggingT IO))) a
liftBaseWith f = AppM (liftBaseWith (\g -> f (\(AppM x) -> g x)))
restoreM = AppM . restoreM
instance MonadFileSystem CliError AppM where
readFile = readFile'
instance MonadCloud AppM where
computeChangeset = computeChangeset'
getStackOutputs = getStackOutputs'
runChangeSet = runChangeSet'
runAppM :: AppM a -> IO a
runAppM (AppM x) = do
env <- newEnv Discover
result <- runStderrLoggingT . runExceptT $ runReaderT x env
either (\err -> T.putStr (renderCliError err) >> exitFailure) return result
instance AsFileSystemError CliError where
_FileSystemError = _CliFileSystemError
cli :: (MonadCloud m, MonadFileSystem CliError m, MonadEnvironment m) => Command -> DeploymentConfiguration -> m ()
cli (DeployStack nameToDeploy) config = do
let allNames = config ^.. stacks.each.name
dependencies = takeWhile (/= nameToDeploy) allNames
appName = config ^. name
maybeStackToDeploy = config ^. stacks.to (find (has (name.only nameToDeploy)))
stackToDeploy <- maybe (throwing _CliStackNotConfigured nameToDeploy) return maybeStackToDeploy
let requiredGlobalEnvVars = "Env" : (config ^. environmentVariables)
requiredStackEnvVars = stackToDeploy ^. environmentVariables
requiredEnvVars = requiredGlobalEnvVars ++ requiredStackEnvVars
maybeEnvValues <- mapM (\envVarKey -> (envVarKey,) <$> getEnv envVarKey) requiredEnvVars
let envVarsOrFailure = runErrors $ traverse (extractResult (,)) maybeEnvValues
envVars <- either (throwError . CliMissingEnvVars . sort) return envVarsOrFailure
let env = snd $ head envVars
let mkStackName s = StackName $ env <> "-" <> appName <> "-" <> s
templateBody <- readFile $ nameToDeploy <> ".yaml"
maybeOutputs <- mapM (\stackName -> (stackName,) <$> getStackOutputs (mkStackName stackName)) dependencies
let outputsOrFailure = runErrors $ traverse (extractResult (flip const)) maybeOutputs
outputs <- either (throwing _CliMissingDependencyStacks) return outputsOrFailure
let parameters = envVars ++ concat outputs
csId <- computeChangeset (mkStackName nameToDeploy) templateBody parameters
runChangeSet csId
cliIO :: IO DeploymentConfiguration -> IO ()
cliIO mConfig = do
config <- mConfig
cmd <- getArgs
runAppM (cli cmd config)
extractResult :: (a -> b -> c) -> (a, Maybe b) -> Errors [a] c
extractResult f (k, m) = do
v <- maybe (failure [k]) pure m
pure $ f k v