packages feed

groot-0.0.1.0: src/Groot/Compose/Service/AWS.hs

{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE LambdaCase            #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE RankNTypes            #-}

module Groot.Compose.Service.AWS
     ( serviceExists'
     , verifyActiveCluster'
     , awsServiceCompose
     ) where

import           Control.Lens
import           Control.Monad.Free
import           Control.Monad.Morph
import           Control.Monad.Reader         hiding (filterM)
import           Control.Monad.Trans.Maybe
import           Control.Monad.Trans.Resource
import qualified Data.HashMap.Strict          as Map
import           Data.Maybe
import           Data.Semigroup               ((<>))
import qualified Data.Text                    as T
import           Network.AWS
import qualified Network.AWS.ECS              as ECS
import           Network.AWS.Waiter

import           Groot.Compose.Service.Free
import           Groot.Console
import           Groot.Core
import           Groot.Data.Filter
import           Groot.Data.Text
import           Groot.Exception
import           Groot.Manifest
import           Groot.Types

-- Model mapping functions

serviceDeploymentConf :: DeploymentStrategy -> ECS.DeploymentConfiguration
serviceDeploymentConf DSBlueGreen =
    ECS.dcMinimumHealthyPercent ?~ 100
  $ ECS.dcMaximumPercent ?~ 200
  $ ECS.deploymentConfiguration
serviceDeploymentConf DSRolling =
    ECS.dcMinimumHealthyPercent ?~ 50
  $ ECS.dcMaximumPercent ?~ 200
  $ ECS.deploymentConfiguration
serviceDeploymentConf DSTearDown =
    ECS.dcMinimumHealthyPercent ?~ 0
  $ ECS.dcMaximumPercent ?~ 100
  $ ECS.deploymentConfiguration

createTaskDefinitionReq :: GrootManifest -> NamedServiceDeployment -> ECS.RegisterTaskDefinition
createTaskDefinitionReq manifest (serviceName, deployment) =
    ECS.rtdNetworkMode .~ (deployment ^. sdNetworkMode)
  $ ECS.rtdTaskRoleARN .~ (deployment ^. sdTaskRole)
  $ ECS.rtdContainerDefinitions .~ (containerDef <$> deployment ^. sdContainers)
  $ ECS.rtdVolumes .~ (taskVolume <$> Map.elems (manifest ^. gmVolumes))
  $ ECS.rtdPlacementConstraints .~ (concat $ taskPlacementConstaints <$> deployment ^. sdDeploymentConstraints)
  $ ECS.registerTaskDefinition serviceName
  where containerEnv =
          Map.foldrWithKey (\k v acc -> (ECS.kvpName ?~ k $ ECS.kvpValue ?~ v $ ECS.keyValuePair):acc) []

        containerPort port =
            ECS.pmProtocol .~ (port ^. pmProtocol)
          $ ECS.pmHostPort .~ (port ^. pmHostPort)
          $ ECS.pmContainerPort ?~ (port ^. pmContainerPort)
          $ ECS.portMapping

        taskVolume v =
            ECS.vName ?~ (v ^. vName)
          $ ECS.vHost ?~ (ECS.hvpSourcePath ?~ (v ^. vSourcePath) $ ECS.hostVolumeProperties)
          $ ECS.volume

        mountPoint mp =
            ECS.mpSourceVolume ?~ (mp ^. mpVolume)
          $ ECS.mpContainerPath ?~ (mp ^. mpTargetPath)
          $ ECS.mpReadOnly .~ (mp ^. mpReadOnly)
          $ ECS.mountPoint

        containerExtraHosts =
          Map.foldrWithKey (\host ip acc -> (ECS.hostEntry host ip):acc) []

        taskPlacementConstaints (InstanceAttributesConstraint attrs) =
          Map.foldrWithKey (\k v acc -> (attrExpr k v):acc) [] attrs
          where attrExpr k v =
                    ECS.tdpcType ?~ ECS.MemberOf
                  $ ECS.tdpcExpression ?~ (T.concat ["attribute:", k, " == ", v])
                  $ ECS.taskDefinitionPlacementConstraint

        containerDef c =
            ECS.cdImage ?~ (c ^. cImage)
          $ ECS.cdEnvironment .~ (containerEnv $ c ^. cEnvironment)
          $ ECS.cdPortMappings .~ (containerPort <$> c ^. cPortMappings)
          $ ECS.cdMemory .~ (c ^? cMemory . _AssignedMemory)
          $ ECS.cdMemoryReservation .~ (c ^? cMemory . _ReservedMemory)
          $ ECS.cdHostname .~ (c ^. cHostname)
          $ ECS.cdExtraHosts .~ (containerExtraHosts $ c ^. cExtraHosts)
          $ ECS.cdCpu .~ (c ^. cCpu)
          $ ECS.cdDockerLabels .~ (c ^. cLabels)
          $ ECS.cdLinks .~ (c ^. cLinks)
          $ ECS.cdLogConfiguration .~ (c ^. cLogConfig)
          $ ECS.cdName ?~ (c ^. cName)
          $ ECS.cdPrivileged .~ (c ^. cPriviledged)
          $ ECS.cdEssential .~ (c ^. cEssential)
          $ ECS.cdWorkingDirectory .~ (c ^. cWorkDir)
          $ ECS.cdMountPoints .~ (mountPoint <$> c ^. cMountPoints)
          $ ECS.cdEntryPoint .~ (c ^. cEntryPoint)
          $ ECS.cdDnsSearchDomains .~ (c ^. cDnsSearch)
          $ ECS.cdUser .~ (c ^. cUser)
          $ ECS.cdCommand .~ (c ^. cCommand)
          $ ECS.containerDefinition

createServiceReq :: ClusterRef -> NamedServiceDeployment -> TaskDefId -> ECS.CreateService
createServiceReq clusterRef (serviceName, deployment) tdId =
    ECS.cCluster ?~ (toText clusterRef)
  $ ECS.cRole .~ (deployment ^. sdServiceRole)
  $ ECS.cLoadBalancers .~ containerLoadBalancers
  $ ECS.cDeploymentConfiguration ?~ (serviceDeploymentConf $ deployment ^. sdDeploymentStrategy)
  $ ECS.cPlacementStrategy .~ (deployment ^. sdPlacementStrategy)
  $ ECS.createService serviceName (toText tdId) (deployment ^. sdDesiredCount)
  where loadBalancerConf container pm =
          let linkElb lnk =
                  ECS.lbContainerName ?~ (container ^. cName)
                $ ECS.lbContainerPort ?~ (pm ^. pmContainerPort)
                $ assignLink
                $ ECS.loadBalancer
                where assignLink = case lnk of
                        ELBNameLink     x -> ECS.lbLoadBalancerName ?~ x
                        TargetGroupLink x -> ECS.lbTargetGroupARN   ?~ x
          in linkElb <$> (pm ^. pmElbLink)

        containerLoadBalancers = catMaybes $ do
          cont <- deployment ^. sdContainers
          pm   <- cont ^. cPortMappings
          return $ loadBalancerConf cont pm

updateServiceReq :: ClusterRef -> NamedServiceDeployment -> TaskDefId -> ECS.UpdateService
updateServiceReq clusterRef (serviceName, deployment) tdId =
    ECS.usCluster ?~ (toText clusterRef)
  $ ECS.usTaskDefinition ?~ (toText tdId)
  $ ECS.usDesiredCount ?~ (deployment ^. sdDesiredCount)
  $ ECS.usDeploymentConfiguration ?~ (serviceDeploymentConf $ deployment ^. sdDeploymentStrategy)
  $ ECS.updateService serviceName

deleteServiceReq :: ClusterRef -> NamedServiceDeployment -> ECS.DeleteService
deleteServiceReq clusterRef (serviceName, _) =
    ECS.dsCluster ?~ (toText clusterRef)
  $ ECS.deleteService serviceName

-- Implementation helper functions

deploymentComplete :: TaskDefId -> Getter ECS.ContainerService Bool
deploymentComplete tdi = to $ isComplete . taskDeployment
  where taskDeployment :: ECS.ContainerService -> Maybe ECS.Deployment
        taskDeployment service = listToMaybe
          $ filter (\x -> maybe False (T.isSuffixOf (toText tdi)) (x ^. ECS.dTaskDefinition))
          $ service ^. ECS.csDeployments

        isComplete :: Maybe ECS.Deployment -> Bool
        isComplete mdeployment = maybe False id $ do
          deployment <- mdeployment
          running    <- deployment ^. ECS.dRunningCount
          desired    <- deployment ^. ECS.dDesiredCount
          return (running == desired)

serviceDeployed :: TaskDefId -> Wait ECS.DescribeServices
serviceDeployed taskId = Wait
  { _waitName = "serviceDeployed"
  , _waitAttempts = 100
  , _waitDelay = 6
  , _waitAcceptors =
    [ matchAny
        "MISSING"
        AcceptFailure
        (folding (concatOf ECS.dssrsFailures) . ECS.fReason . _Just . to toTextCI)
    , matchAny
        True
        AcceptSuccess
        (folding (concatOf ECS.dssrsServices) . (deploymentComplete taskId))
    ]
  }

waitServiceStateChange :: (MonadResource m, MonadBaseControl IO m)
                       => (ContainerServiceRef -> m ())
                       -> (ContainerServiceRef -> m ())
                       -> (ContainerServiceRef -> m ())
                       -> Wait ECS.DescribeServices
                       -> ECS.ContainerService
                       -> GrootM m ()
waitServiceStateChange onStart onSuccess onFailure waiter service = do
  env <- ask
  serviceRef <- case (service ^. ECS.csServiceName) of
    Nothing -> fail "No service name defined!"
    Just  x -> pure $ ContainerServiceRef x
  lift $ onStart serviceRef
  waitResult <- runAWS env $ await waiter describeIt
  lift $ case waitResult of
    AcceptSuccess -> onSuccess serviceRef
    _             -> onFailure serviceRef
  where describeIt :: ECS.DescribeServices
        describeIt =
            ECS.dCluster .~ (service ^. ECS.csClusterARN)
          $ ECS.dServices .~ (maybeToList $ service ^. ECS.csServiceARN)
          $ ECS.describeServices

waitForServiceDeployed :: (MonadConsole m, MonadResource m, MonadBaseControl IO m)
                       => ClusterRef
                       -> (ECS.ContainerService, Wait ECS.DescribeServices)
                       -> GrootM m ()
waitForServiceDeployed clusterRef (service, waiter) =
  let onStart serviceRef =
        putInfo $ "Waiting for service"
          <+> (styled yellowStyle $ toText serviceRef)
          <+> "to complete deployment on cluster"
          <+> (styled yellowStyle $ toText clusterRef)
          <+> "..."
      onSuccess serviceRef =
        putSuccess $ "Service"
          <+> (styled yellowStyle $ toText serviceRef)
          <+> "successfully deployed on cluster"
          <+> (styled yellowStyle $ toText clusterRef)
          <+> "."
      onFailure serviceRef = do
        errMsg <- pure "Service did not stabilize."
        throwM $ failedServiceDeployment serviceRef clusterRef (Just errMsg)
  in waitServiceStateChange onStart onSuccess onFailure waiter service

waitForServiceUndeployed :: (MonadConsole m, MonadResource m, MonadBaseControl IO m)
                         => ClusterRef
                         -> ECS.ContainerService
                         -> GrootM m ()
waitForServiceUndeployed clusterRef =
  let onStart serviceRef =
        putInfo $ "Waiting for service"
          <+> (styled yellowStyle $ toText serviceRef)
          <+> "to be removed from cluster"
          <+> (styled yellowStyle $ toText clusterRef)
          <+> "..."
      onSuccess serviceRef =
        putSuccess $ "Service"
          <+> (styled yellowStyle $ toText serviceRef)
          <+> "has been successfully removed from cluster"
          <+> (styled yellowStyle $ toText clusterRef)
          <+> "."
      onFailure serviceRef =
        throwM $ failedServiceDeletion serviceRef clusterRef
  in waitServiceStateChange onStart onSuccess onFailure ECS.servicesInactive

-- Free monad operations implementation

serviceExists' :: (MonadResource m, MonadBaseControl IO m)
               => Text
               -> ClusterRef
               -> GrootM m Bool
serviceExists' serviceName clusterRef = do
  env <- ask
  hoist (runAWS env) $ awsToGrootM $ (maybe False (const True) <$> check)
  where foundServices = findService (ContainerServiceRef serviceName) (Just clusterRef)
        check         = runMaybeT $ filterM isActiveContainerService foundServices

verifyActiveCluster' :: (MonadResource m, MonadBaseControl IO m) => ClusterRef -> GrootM m ()
verifyActiveCluster' clusterRef = do
  env <- ask
  runAWS env $ do
    cluster <- getCluster clusterRef
    case (maybeMatches isActiveCluster cluster) of
      Nothing -> throwM $ invalidClusterStatus clusterRef CSInactive (Just CSActive)
      Just  _ -> return ()

registerTask' :: (MonadConsole m, MonadResource m, MonadBaseControl IO m)
              => GrootManifest
              -> NamedServiceDeployment
              -> GrootM m TaskDefId
registerTask' manifest service@(serviceName, _) = do
  putInfo $ "Registering task definition for service " <> styled yellowStyle serviceName
  env   <- ask
  res   <- lift $ runAWS env . send $ createTaskDefinitionReq manifest service
  mtask <- pure $ res ^. ECS.rtdrsTaskDefinition >>= taskDefId
  case mtask of
    Nothing   -> throwM $ failedToRegisterTaskDef (TaskDefRef serviceName)
    Just task -> do
      putSuccess $ "Task for service"
        <+> styled yellowStyle serviceName
        <+> "has been registed as"
        <+> styled yellowStyle (toText task)
      return task

modifyService' :: (MonadConsole m, MonadResource m, MonadBaseControl IO m)
               => (NamedServiceDeployment -> ClusterRef -> TaskDefId -> GrootM m (Maybe ECS.ContainerService))
               -> NamedServiceDeployment
               -> ClusterRef
               -> TaskDefId
               -> GrootM m (ECS.ContainerService, Wait ECS.DescribeServices)
modifyService' awsAction service@(serviceName, _) clusterRef taskId = do
  mservice <- awsAction service clusterRef taskId
  case mservice of
    Nothing  -> throwM $ failedServiceDeployment (ContainerServiceRef serviceName) clusterRef Nothing
    Just srv -> return (srv, serviceDeployed taskId)

createService' :: (MonadConsole m, MonadResource m, MonadBaseControl IO m)
               => NamedServiceDeployment
               -> ClusterRef
               -> TaskDefId
               -> GrootM m (ECS.ContainerService, Wait ECS.DescribeServices)
createService' = modifyService' $ \service@(serviceName, _) clusterRef taskId -> do
  putInfo $ "Creating service" <+> styled yellowStyle serviceName <+> "with task" <+> (styled yellowStyle $ toText taskId)
  env       <- ask
  createReq <- pure $ createServiceReq clusterRef service taskId
  runAWS env $ fmap (\x -> x ^. ECS.csrsService) . send $ createReq

updateService' :: (MonadConsole m, MonadResource m, MonadBaseControl IO m)
               => NamedServiceDeployment
               -> ClusterRef
               -> TaskDefId
               -> GrootM m (ECS.ContainerService, Wait ECS.DescribeServices)
updateService' = modifyService' $ \service@(serviceName, _) clusterRef taskId -> do
  putInfo $ "Updating service" <+> styled yellowStyle serviceName <+> "with task" <+> (styled yellowStyle $ toText taskId)
  env       <- ask
  updateReq <- pure $ updateServiceReq clusterRef service taskId
  runAWS env $ fmap (\x -> x ^. ECS.usrsService) . send $ updateReq

removeService' :: (MonadConsole m, MonadResource m, MonadBaseControl IO m)
               => NamedServiceDeployment
               -> ClusterRef
               -> GrootM m ECS.ContainerService
removeService' service@(serviceName, _) clusterRef = do
  putInfo $ "Deleting service" <+> styled yellowStyle serviceName <> "."
  env       <- ask

  let csRef = ContainerServiceRef serviceName
  current   <- runAWS env $ runMaybeT $ findService csRef (Just clusterRef)

  case current of
    Nothing -> throwM $ serviceNotFound csRef (Just clusterRef)
    Just  c -> do
      updateReq <- pure $ ECS.usDesiredCount ?~ 0 $ ECS.usCluster ?~ (toText clusterRef) $ ECS.updateService serviceName
      runAWS env $ send updateReq
      deleteReq <- pure $ deleteServiceReq clusterRef service
      res <- runAWS env $ send deleteReq
      case (res ^. ECS.dsrsService) of
        Nothing -> throwM $ serviceNotFound csRef (Just clusterRef)
        Just  x -> return x

awsServiceCompose :: (MonadConsole m, MonadResource m, MonadBaseControl IO m) => GrootManifest -> ServiceComposeM a -> GrootM m a
awsServiceCompose manifest = foldFree $ \case
  RegisterTask service next ->
    next <$> registerTask' manifest service
  ServiceExists name clusterRef next ->
    next <$> serviceExists' name clusterRef
  CreateService service cluster taskId next ->
    (const next) <$> (createService' service cluster taskId >>= waitForServiceDeployed cluster)
  UpdateService service cluster taskId next ->
    (const next) <$> (updateService' service cluster taskId >>= waitForServiceDeployed cluster)
  RemoveService service cluster next ->
    (const next) <$> (removeService' service cluster >>= waitForServiceUndeployed cluster)
  VerifyActiveCluster cluster next ->
    (const next) <$> verifyActiveCluster' cluster