packages feed

wolf-0.3.0: main/Act.hs

{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE RecordWildCards     #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Act
  ( main
  ) where

import           BasicPrelude                 hiding (ByteString, find, hash, length,
                                               readFile, (<.>), (</>))
import           Codec.Compression.GZip
import           Control.Monad.Trans.Resource
import           Data.Aeson.Encode
import           Data.ByteString              (length)
import qualified Data.ByteString.Lazy         as BL
import           Data.Text                    (pack, strip)
import           Data.Text.Lazy               (toStrict)
import           Data.Text.Lazy.Builder       hiding (fromText)
import           Data.Yaml                    hiding (Parser)
import           Filesystem.Path              (dropExtension, (<.>))
import           Network.AWS.Data.Crypto
import           Network.AWS.Flow
import           Options
import           Options.Applicative          hiding (action)
import           Shelly                       hiding (FilePath, bash, (<.>))

data Args = Args
  { aConfig        :: FilePath
  , aQueue         :: Queue
  , aContainer     :: FilePath
  , aContainerless :: Maybe String
  , aGzip          :: Bool
  } deriving ( Eq, Read, Show )

args :: Parser Args
args = Args        <$>
  configFile       <*>
  (pack <$> queue) <*>
  containerFile    <*>
  containerless    <*>
  gzip

parser :: ParserInfo Args
parser =
  info ( helper <*> args ) $ fullDesc
    <> header   "act: Workflow activity"
    <> progDesc "Workflow activity"

data Container = Container
  { cImage       :: Text
  , cCommand     :: Text
  , cVolumes     :: [Text]
  , cDevices     :: [Text]
  , cEnvironment :: [Text]
  , cLink        :: [Text]
  } deriving ( Eq, Read, Show )

instance FromJSON Container where
  parseJSON (Object v) =
    Container                  <$>
    v .:  "image"              <*>
    v .:  "command"            <*>
    v .:? "volumes"     .!= [] <*>
    v .:? "devices"     .!= [] <*>
    v .:? "environment" .!= [] <*>
    v .:? "links"       .!= []
  parseJSON _ = mzero

data Control = Control
  { cUid :: Uid
  } deriving ( Eq, Read, Show )

instance ToJSON Control where
  toJSON Control{..} = object
    [ "run_uid" .= cUid
    ]

encodeText :: ToJSON a => a -> Text
encodeText = toStrict . toLazyText . encodeToTextBuilder . toJSON

handler :: MonadBaseControl IO m => m () -> m (Maybe SomeException)
handler a = handle (return . Just) $ a >> return Nothing

exec :: MonadIO m => Args -> Container -> Uid -> Metadata -> [Blob] -> m (Metadata, [Artifact], Maybe SomeException)
exec Args{..} container uid metadata blobs =
  shelly $ withDir $ \dir dataDir storeDir -> do
    control $ dataDir </> pack "control.json"
    storeInput $ storeDir </> pack "input"
    dataInput $ dataDir </> pack "input.json"
    e <- maybe (docker dataDir storeDir container) (bash dir container) aContainerless
    result <- dataOutput $ dataDir </> pack "output.json"
    artifacts <- storeOutput $ storeDir </> pack "output"
    return (result, artifacts, e) where
      withDir action =
        withTmpDir $ \dir -> do
          mkdir $ dir </> pack "data"
          mkdir $ dir </> pack "store"
          mkdir $ dir </> pack "store/input"
          mkdir $ dir </> pack "store/output"
          action dir (dir </> pack "data") (dir </> pack "store")
      control file =
        writefile file $ encodeText $ Control uid
      writeArtifact file blob =
        if aGzip then
          writeBinary (dropExtension file) $ BL.toStrict $ decompress blob
        else
          writeBinary file $ BL.toStrict blob
      readArtifact dir file =
        if aGzip then do
          key <- relativeTo dir file
          blob <- BL.toStrict . compress . BL.fromStrict <$> readBinary file
          return ( toTextIgnore (key <.> "gz")
                 , hash blob
                 , fromIntegral $ length blob
                 , BL.fromStrict blob
                 )
          else do
            key <- relativeTo dir file
            blob <- readBinary file
            return ( toTextIgnore key
                   , hash blob
                   , fromIntegral $ length blob
                   , BL.fromStrict blob
                   )
      dataInput file =
        maybe (return ()) (writefile file) metadata
      dataOutput file =
        catch_sh_maybe (readfile file) where
          catch_sh_maybe action =
            catch_sh (Just <$> action) $ \(_ :: SomeException) -> return Nothing
      storeInput dir =
        forM_ blobs $ \(key, blob) -> do
          paths <- strip <$> run "dirname" [key]
          mkdir_p $ dir </> paths
          writeArtifact (dir </> key) blob
      storeOutput dir = do
        artifacts <- findWhen test_f dir
        forM artifacts $ readArtifact dir
      docker dataDir storeDir Container{..} =
        handler $ do
          devices <- forM cDevices $ \device ->
            strip <$> run "readlink" ["-f", device]
          run_ "docker" $ concat
            [["run"]
            , concatMap (("--device" :)  . return) devices
            , concatMap (("--env"    :)  . return) cEnvironment
            , concatMap (("--link"    :) . return) cLink
            , concatMap (("--volume" :)  . return) $
                toTextIgnore dataDir  <> ":/app/data"  :
                toTextIgnore storeDir <> ":/app/store" : cVolumes
            , [cImage]
            , words cCommand
            ]
      bash dir Container{..} bashDir =
        handler $ do
          files <- ls $ fromText $ pack bashDir
          forM_ files $ flip cp_r dir
          cd dir
          maybe (return ()) (uncurry $ run_ . fromText) $ uncons $ words cCommand

call :: Args -> IO ()
call Args{..} = do
  config <- decodeFile aConfig >>= maybeThrow (userError "Bad Config")
  container <- decodeFile aContainer >>= maybeThrow (userError "Bad Container")
  env <- flowEnv config
  forever $ runResourceT $ runFlowT env $
    act aQueue $ exec Args{..} container

main :: IO ()
main = execParser parser >>= call