Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Workflow Definition DSL

Workflow Definition DSL

This code provides a very basic DSL for workflow definitions.

An example workflow can be found in Workflow.hs. Some actions are pre-defined in Jobsteps.hs.

You can evaluate the workflow and dump its declarative format (the data-structures defining the workflow) like this:

$ ghci
λ :load Workflow.hs
[1 of 3] Compiling WorkflowEngine   ( WorkflowEngine.hs, interpreted )
...
[2 of 3] Compiling Jobsteps         ( Jobsteps.hs, interpreted )
...
[3 of 3] Compiling Workflow         ( Workflow.hs, interpreted )
Ok, modules loaded: Workflow, WorkflowEngine, Jobsteps.
λ printWorkflow deployAndStart
...
LaunchJob 0 "createDiskClone" [VResultValue WorkflowArguments "templateName"]
LogValues "Created clone" [VResultValue (ActionResult 0) "cloneName",VInt 123]
LaunchJob 1 "exposeISCSI" [VResultValue (ActionResult 0) "cloneID"]
LaunchJob 2 "createMachine" [VResultValue WorkflowArguments "templateName",VResultValue (ActionResult 1) "targetID"]
LogValues "Created machine" [VResultValue (ActionResult 2) "machineID",VString "Just a value"]
LaunchJob 3 "startMachine" [VResultValue (ActionResult 2) "machineID"]
LogValues "Machine started" [VResultValue (ActionResult 2) "machineName"]
LaunchJob 4 "sendEmail" [VString "nicolas incubaid com",VString "A new machine was created!"]
{-# LANGUAGE FlexibleContexts #-}
module Jobsteps (
createDiskClone
, exposeISCSI
, createMachine
, startMachine
, runJob
) where
import Control.Monad.State
import WorkflowEngine (Activity, Action(..), Result(ActionResult), Value)
-- The job steps we provide
-- Note:
-- * JobStepN means: a jobstep returning some result, taking N argument
-- * VoidJobStepN means: a jobstep not returning a result, taking N argumentsarguments
createDiskClone :: JobStep1
createDiskClone = runJob1 "createDiskClone"
exposeISCSI :: JobStep1
exposeISCSI = runJob1 "exposeISCSI"
createMachine :: JobStep2
createMachine = runJob2 "createMachine"
startMachine :: VoidJobStep1
startMachine = runVoidJob1 "startMachine"
-- Here's some boring stuff, creating some type synonyms and helper
-- functions.
-- Anyone adding a new 'jobstep execution' command shouldn't botter too
-- much about this.
runJob :: MonadState ([Action], Int) m => String -> [Value] -> m Result
runJob name args = do
(acts, cnt) <- get
put (LaunchJob cnt name args : acts, cnt + 1)
return $ ActionResult cnt
-- Type synonyms for jobsteps taking 0, 1 and 2 arguments and returning
-- something useful
type JobStep a = Activity a
type JobStep0 = JobStep Result
type JobStep1 = Value -> JobStep Result
type JobStep2 = Value -> Value -> JobStep Result
-- Type synonym for jobsteps taking arguments and not returning anything
-- useful
type VoidJobStep0 = JobStep ()
type VoidJobStep1 = Value -> JobStep ()
type VoidJobStep2 = Value -> Value -> JobStep ()
runJob0 n = runJob n []
runJob1 n v = runJob n [v]
runJob2 n v1 v2 = runJob n [v1, v2]
runVoidJob0 n = runJob0 n >> return ()
runVoidJob1 n v = runJob1 n v >> return ()
runVoidJob2 n v1 v2 = runJob2 n v1 v2 >> return ()
{-# LANGUAGE OverloadedStrings #-}
module Workflow where
import Prelude hiding (lookup)
import Control.Monad (void)
import WorkflowEngine
import Jobsteps
-- For readability
type TemplateName = Value
-- This is a reusable activity, deploying a template
-- Note this type signature is not strictly required
deployTemplate :: TemplateName -> Activity Result
deployTemplate template = do
clone <- createDiskClone template
logValues "Created clone" [lookup "cloneName" clone, 123] -- The '123' is a demo
target <- exposeISCSI (lookup "cloneID" clone)
machine <- createMachine template (lookup "targetID" target)
logValues "Created machine" [lookup "machineID" machine, "Just a value"]
return machine
-- This is a final workflow
-- Note this signature isn't required either
deployAndStart :: Workflow
deployAndStart = makeWorkflow $ do
-- Look up the template name from the workflow invocation arguments
let template = lookup "templateName" workflowArguments
-- Call a sub-activity and capture its result
machine <- deployTemplate template
-- Call another sub-activity
startMachine (lookup "machineID" machine)
logValues "Machine started" [lookup "machineName" machine]
-- We can also call arbitrary jobsteps, not defined in the Jobsteps
-- module(s)
void $ runJob "sendEmail" ["nicolas incubaid com", "A new machine was created!"]
{-# LANGUAGE GeneralizedNewtypeDeriving, OverloadedStrings #-}
{- Some vocabulary:
- A workflow is a sequence of activities, and 'finalizes' (i.e. it has no
- result). It can take arguments though.
- An activity is a sequence of actions, and might return some result.
- An action is something which is executed and returns a result (which can
- be discarded).
-}
module WorkflowEngine (
(<$>)
, Activity
, Action(..)
, Result(ActionResult)
, Value(..)
, Workflow
, makeWorkflow
, workflowArguments
, lookup
, logValues
, calculateActions
, printWorkflow
) where
import Prelude hiding (lookup)
import Control.Applicative
import Control.Monad.State
import GHC.Exts (IsString(..))
-- The result of an action or an activity is an 'ActionResult'.
-- This can't be the actual value, of course: all we need is a reference to
-- the ID of the action or activity of which we want to look up the result
-- later on in the workflow.
-- So, 'ActionResult 10' is a representation of the result of action with ID 10
-- at runtime.
-- WorkflowArguments is a special Result type, containing the arguments
-- passed to the workflow at the very beginning
data Result = ActionResult Int
| WorkflowArguments
deriving (Show, Eq)
-- We can pass some arguments to actions. These are wrappers for these
-- value types.
-- A particular one is VResultValue, which is, at runtime, the value
-- connected to the given name of a given job result.
-- All other values are constants
data Value = VInt Int
| VString String
| VBool Bool
| VResultValue Result String
deriving (Show, Eq)
-- Syntactic sugar...
instance IsString Value where
fromString = VString
instance Num Value where
fromInteger i = VInt $ fromInteger i
-- We currently define 2 actions: launching a job, or logging some values
data Action = LaunchJob Int String [Value]
| LogValues String [Value]
deriving (Show)
newtype Activity a = Activity (State ([Action], Int) a)
deriving (Functor, Applicative, Monad, MonadState ([Action], Int))
-- A workflow is an activity which returns nothing at all
newtype Workflow = Workflow (Activity ())
makeWorkflow :: Activity () -> Workflow
makeWorkflow = Workflow
-- A function which allows us to look up the value of a given name from an
-- action result
lookup :: String -> Result -> Value
lookup name result = VResultValue result name
-- Return the arguments passed to the workflow in the very beginning
workflowArguments :: Result
workflowArguments = WorkflowArguments
-- A utility function (mainly for demonstration purposes): now we can emit
-- logging information from inside our workflows or activities!
logValues :: String -> [Value] -> Activity ()
logValues m vs = modify (\(acts, cnt) -> (LogValues m vs : acts, cnt))
calculateActions :: Activity a -> [Action]
calculateActions (Activity j) = reverse $ fst $ snd $ runState j ([], 0)
printWorkflow :: Workflow -> IO ()
printWorkflow (Workflow a) = mapM_ print $ calculateActions a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment