Skip to content

Scala plugin development guide

ddurnev edited this page Jan 18, 2013 · 14 revisions

Genesis project : Scala plugin development guide

Plugin context

Plugin context is implemented as as spring configuration bean

Plugin context should be annotated with @GenesisPlugin with mandatory id field.

It's main responsibility is to provide step coordination factories and step builder factories to genesis workflow context.
It's advised for plugin context not to export other types of beans to global genesis spring context

@Configuration
@GenesisPlugin(id = "ping", description = "simple ping plugin")
class PingPluginContex {

}

Step and step builders

To introduce new steps definition to genesis template dsl one should provide:

  • new step class marked with com.griddynamics.genesis.workflow.Step trait - step definition to be processed by appropriate executors
  • custom com.griddynamics.genesis.plugin.StepBuilder trait implementation - the class is respobsible for collecting attribute values from template and creating Step definition.
  • custom com.griddynamics.genesis.plugin.StepBuilderFactory trait implementaion - the class responsible for connecting StepBuilder with step name representation in template dsl
    Example:

Lets say you want to introduce new ping step with one attibute url:

ping {
      url = "http://google.com"
   }

To support this syntax the followin code is needed:

case class PingStep(url: String) extends Step {
 override val stepDescription = "Ping signal"
}

class PingStepBuilderFactory extends com.griddynamics.genesis.plugin.StepBuilderFactory {
    val stepName = "ping"  //the value should be exactly the same as it's expected to be in template

    override def newStepBuilder = new StepBuilder {
        @BeanProperty var url : String = _  //the name of the variable should be exactly the same as the name of the parameter in template

        def getDetails = new PingStep(url)
    }
}

In order for genesis workflow system to support this new step PingStepBuilderFactory should be exported in plugin context

@Configuration
@GenesisPlugin(id = "ping", description = "simple ping plugin")
class PingPluginContex {
   @Bean def stepBuilderFactory = new PingStepBuilderFactory
}

Simple step definitions

In case of step builder doesn't having some conversion logic plugin developer can use com.griddynamics.genesis.plugin.StepDefinition class to declare new step without providing new custom StepBuilderFactory and StepBuilder

@Configuration
@GenesisPlugin(id = "ping", description = "simple ping plugin")
class PingPluginContex {
   @Bean def pingStepDefinition = new StepDefinition("ping", classOf[PingStep])
}

In this case genesis core is capable of instantion of PingStep based on constructor arguments.

Following basic convertions from groovy to scala is supported:

  • value/null (or absense of parameter) to Some(_)/None
  • groovy list(java.lang.List) to scala Seq, Set, List
  • groovy map(java.lang.Map) to scala.Map

Case class default constructor parameters is also supported.

More complex example

Given:

case class ComplexStep(optString: Option[String], defaultInt:Int = 5, list: Seq[String]) extends Step
...
@Bean def complexStepDefinition = new StepDefinition("complexStep", classOf[ComplexStep])

Followin template code:

complexStep {
   list = ["one", "two"]
}

will construct object equal to new ComplexStep(optString = None, list = Seq("one", "two"))

Actions

Action is the tiniest peace of work workflow engine can execute. Each workflow step can consist of several actions.

class PingAction(url: String) extends Action

Actions have no representations in template dsl. Information of how to execute each action is coded in action executor. 
Information how to split step into actions and coordinate action executors is coded in step coordinator

Action executors

Actions are data objects, to provide appropriate handling of each action one should provide specific action executor. Genesis provide several trait that should be used to implement asynchronous or synchronous executors:

  • ActionExecutor - base executor class
  • AsyncActionExecutor - for async execution
  • SyncActionExecutor - for sync execution
class PingResult(val success: Boolean) extends ActionResult

class PingActionExecutor(val action: PingAction) SyncActionExecutor extends ActionExecutor {
  def startSync() = {
      try {
         new Url(action.url).openConnection()
         new PingResult(true)
      } catch {
         case _ => new PingResult(false)
      }
  }
}

Step coordinator

To coordinate execution of several actions while executing particular step one should implement specific com.griddynamics.genesis.workflow.StepCoordinator

Genesis provide several template classes for different variations, like ActionOrientedStepCoordinator.

Because in this sample step consist of only one action coordinator looks simplified

class PingStepCoordinator(val step: PingStep, context: StepExecutionContext) extends ActionOrientedStepCoordinator {
  var failed = false;

  def getActionExecutor(action: Action) = action match { 
     case a: PingAction => new PingActionExecutor(a)   // in case of step consisting of several actions, several matches will be here
  }

  def getStepResult = {
    GenesisStepResult(context.step, isStepFailed = failed)
  }

  def onActionFinish(result: ActionResult) = {
    result match {
      case a: PingResult => {
        failed = !a.success
        Seq()
      }
      case _: ExecutorThrowable => {
        failed = true
        Seq()
      }
      case _: ExecutorInterrupt => {
        Seq()
      }
    }
  }

  def onStepInterrupt(signal: Signal) = Seq() 

  def onStepStart() = List(new PingAction(step.url))  // list of actions to be executed to finish step. only one action in our case 

}

Step coordinator factory

The last peace of the puzzle is a factory class that should map specific step(constructed from template) to appropriate step coordinator.

To provide such class one should implement com.griddynamics.genesis.plugin.PartialStepCoordinatorFactoryor use one of template classes.

class PingStepCoordinatorFactory extends PartialStepCoordinatorFactory {
  def isDefinedAt(step: Step) = step.isInstanceOf[PingStep]

  def apply(step: Step, context: StepExecutionContext) = {
    new PingStepCoordinator(step.asInstanceOf[PingStep], context)   
  }
}

This implementation should be exported by plugin context

@Configuration
@GenesisPlugin(id = "ping", description = "simple ping plugin")
class PingPluginContex {
   @Bean def stepBuilderFactory = new PingStepBuilderFactory

   @Bean def stepCoordinator = new PingStepCoordinatorFactory()
}

Steps with single action

In case of step execution does not requiring multiple actions simplified form of step implementation might be used. Only step executor com.griddynamics.genesis.workflow.TrivialStepExecutor should be implemented (i.e. no step coordinators, no actions, no coordinator factories are required).

Following code is the only code required to implement ping step

case class PingStep(url: String) extends Step

class PingResult(val step: PingStep, val isStepFailed: Boolean) extends FallibleResult //step result

class PingActionExecutor extends TrivialStepExecutor[PingStep, PingResult] {
  override def execute(step: PingStep, context: StepExecutionContext): PingResult = {
      try {
         new Url(step.url).openConnection()
         new PingResult(step, true)
      } catch {
         case _ => new PingResult(step, false)
      }
  }
}

@Configuration
@GenesisPlugin(id = "ping", description = "simple ping plugin")
class PingPluginContex {
   @Bean def pingStepDefinition = new StepDefinition("ping", classOf[PingStep])

   @Bean def pingStepExecutor = new PingActionExecutor ()
}

Plugin configuration

Plugin can use genesis main configuration subsystem. To get access to it plugin must conform to following rules:

  • All properties must start with genesis.plugin.<plugin_id>. prefix
  • Plugin should provide genesis-plugin.json in package root directory with all supported properties metadata in json format: json map should contain metadata object for each property name - name is key, metadata object is value. Metadata object contains the following properties:
Parameter Name Description Mandatory
default Default value of the specified property yes
description Property description text no
type Property type: string - one of text or password. If password is specified then property value is not displayed on UI. If not specified 'text' is assumed. no
validation Property validation rules: map with validation error message strings as keys and property validator names as values. In case of validation fails the specified error message is displayed. no
restartRequired Boolean value indicating if Genesis restart is required to apply change of property value no
important Boolean value indicating if genesis should fail on start if property value is invalid no
Example: ``` json { "genesis.plugin.build-jenkins.password": { "default": "", "type": "password", "restartRequired": true}, "genesis.plugin.build-jenkins.username": { "default": "", "restartRequired": true}, "genesis.plugin.build-jenkins.baseUrl": { "default": "", "restartRequired": true, "validation": {"Must be a valid URL": "url"} } } ``` If the plugin has no mean of runtime reconfiguration it's possible to use spring ` @Value ` annotation for easy configuration properties access
@Value("${genesis.plugin.ping.timeout}") var timeout: Int = _

Genesis Configuration Property Validation

The following standard validators are supported by genesis commons library [see com.griddynamics.genesis.configuration.CommonValidationContext class]:

  • required - not empty
  • host - valid host name
  • port - valid port
  • int_nonnegative - integer value >= 0
  • int_positive - integer value > 0
  • email - valid e-mail
  • url - valid URL
  • default_length - length <= 128 characters

Any other validator could be added by your plugin by implementing trait com.griddynamics.genesis.validation.ConfigValueValidator and registering your validator instance as bean in spring context. Then your validator could be referenced in property metadata by its spring bean name. If no validator with specified name exists then regexp validator is used and name is treated as regular expression. Validation passes if value is matched by the regexp.

Genesis services

Genesis provide several services that can be discovered and used by plugins. The definition of services can be found in internal-api module in com.griddynamics.genesis.service package.

Discovery of the services is implemented via @Autowired spring feature

@Autowired var sshService: SshService = _
Clone this wiki locally