Groovy Plugin Development
Groovy Plugin Development
Overview
Groovy plugins provide a middle ground between Script and Java plugins, offering:
- Simple Development - Create plugins in a single
.groovyfile - Dynamic Scripting - Groovy's powerful scripting features
- Java Ecosystem Access - Use Java libraries and classes
- Hot Reloading - Update plugins without restarting Rundeck (after initial load)
- DSL Syntax - Simplified domain-specific language for plugin definition
Limitations:
- Currently supports only Notification and Logging plugin types
- For other plugin types, use Java or Script plugins
Supported Plugin Types
Groovy plugins currently support these service types:
- Notification Plugin - Send notifications on job events
- Streaming Log Reader - Read execution logs
- Streaming Log Writer - Write execution logs
- Execution File Storage - Store execution files
- Log Filter - Filter/transform log output
- Content Converter - Render content as HTML
Quick Start
1. Create Plugin File
Create a file named MyPlugin.groovy in Rundeck's plugins directory:
- Launcher:
$RDECK_BASE/libext - RPM/DEB:
/var/lib/rundeck/libext
2. Define Plugin
import com.dtolabs.rundeck.plugins.notification.NotificationPlugin
rundeckPlugin(NotificationPlugin) {
title = 'My Notification'
description = 'Sends custom notifications'
version = '1.0'
author = 'Your Name'
configuration {
webhook_url(
title: 'Webhook URL',
description: 'URL to send notification',
required: true
)
message(
title: 'Message',
description: 'Custom message',
defaultValue: 'Job ${job.name} completed'
)
}
onstart { Map executionData, Map config ->
println("Job starting: ${executionData.job.name}")
true
}
onsuccess { Map executionData, Map config ->
def url = config.webhook_url
def message = config.message
// Send notification
sendWebhook(url, message, executionData)
true
}
onfailure { Map executionData, Map config ->
def url = config.webhook_url
sendWebhook(url, "Job failed: ${executionData.job.name}", executionData)
true
}
}
def sendWebhook(String url, String message, Map data) {
// Implementation here
println("Sending to ${url}: ${message}")
}
3. Deploy and Use
- Save the
.groovyfile to the plugins directory - Restart Rundeck (required for first-time load)
- Configure the plugin in your Job's notifications
- After the initial load, you can update the
.groovyfile without restarting
Groovy DSL Syntax
rundeckPlugin Method
The rundeckPlugin method defines your plugin. It takes two arguments:
- The plugin interface class
- A closure containing the plugin definition
import com.dtolabs.rundeck.plugins.notification.NotificationPlugin
rundeckPlugin(NotificationPlugin) {
// Plugin definition
}
Important
Always import the necessary types used in your Groovy script.
Plugin Properties
Basic Properties
Set these properties to configure how your plugin appears in the Rundeck GUI:
rundeckPlugin(NotificationPlugin) {
title = 'My Plugin' // Display name
description = 'Does something' // Description text
version = '1.0.0' // Plugin version
url = 'https://example.com' // Documentation URL
author = '© 2026, Your Name' // Author information
metadata = [ // Provider metadata
faicon: 'bell',
glyphicon: 'bell'
]
}
Provider Metadata
The metadata property is a map defining additional metadata:
metadata = [
faicon: 'check-circle', // Font Awesome icon
fabicon: 'github', // Font Awesome brand icon
glyphicon: 'ok-sign' // Glyphicon icon
]
See Plugin Icons for available options.
Configuration Properties
Use the configuration closure to define user-configurable properties:
configuration {
// Property definitions
}
Note
Not all plugin types support the configuration closure. Check the specific plugin type documentation.
Property Definition Syntax
There are two ways to define properties:
1. Method Call Form (Recommended for full control):
property_name(
title: 'Display Name',
description: 'Help text',
type: 'String',
required: true,
defaultValue: 'default'
)
2. Assignment Form (Quick and simple):
// String property with default
property_name = 'default value'
// Equivalent to:
// property_name(defaultValue: 'default value', type: 'String')
// Free select with values
property_list = ['value1', 'value2', 'value3']
// Equivalent to:
// property_list(type: 'FreeSelect', values: ['value1', 'value2', 'value3'])
Property Attributes
| Attribute | Required | Description |
|---|---|---|
name | Yes | Unique identifier (automatically derived from property name) |
type | No | Data type (default: String) |
title | No | User-readable label |
description | No | Help text for users |
required | No | Whether value is required (default: false) |
defaultValue | No | Default value |
scope | No | Property scope (default: Instance) |
values | For Select | List of selectable values |
Property Types
| Type | Description | User Experience |
|---|---|---|
String | Text input | Single-line text field |
Integer | Whole number | Numeric input |
Long | Large number | Numeric input |
Boolean | True/false | Checkbox |
Select | Choose from list | Dropdown (strict) |
FreeSelect | Choose or enter | Dropdown (allows custom) |
Property Examples
Simple string:
api_endpoint(
title: 'API Endpoint',
description: 'The API URL',
required: true,
defaultValue: 'https://api.example.com'
)
Integer with default:
timeout(
title: 'Timeout (seconds)',
type: 'Integer',
defaultValue: '30'
)
Boolean checkbox:
enable_debug(
title: 'Enable Debug Logging',
type: 'Boolean',
defaultValue: 'false'
)
Select dropdown:
environment(
title: 'Environment',
type: 'Select',
values: ['dev', 'staging', 'production'],
defaultValue: 'dev'
)
Free select (allows custom values):
region(
title: 'Region',
type: 'FreeSelect',
values: ['us-east-1', 'us-west-2', 'eu-west-1'],
description: 'Select or enter AWS region'
)
Using assignment form:
configuration {
// Quick string property
message = 'Default message'
// Quick select property
priority = ['low', 'medium', 'high']
// Detailed property with validation
timeout(
title: 'Timeout',
type: 'Integer',
required: true
)
}
Property Validation
Add custom validation by providing a closure. The it variable contains the property value:
phone_number(
title: 'Phone Number',
description: 'Enter 10-digit phone number'
) {
it.replaceAll(/[^\d]/, '') ==~ /^\d{10}$/
}
More validation examples:
// Email validation
email(title: 'Email Address') {
it ==~ /^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$/
}
// URL validation
webhook_url(title: 'Webhook URL') {
it.startsWith('http://') || it.startsWith('https://')
}
// Range validation
port(title: 'Port Number', type: 'Integer') {
def portNum = it as Integer
portNum >= 1 && portNum <= 65535
}
Property Scopes
Control where property values are resolved from:
api_key(
title: 'API Key',
scope: PropertyScope.Project, // Or just 'Project'
required: true
)
Available scopes:
Instance(default) - Job-level configurationProject- Project propertiesFramework- Framework propertiesProjectOnly- Only project (no framework)InstanceOnly- Only instance (no project/framework)
See Property Scopes for complete documentation.
Validation and Scopes
Properties with Instance scope show validation errors when saving the Job. Properties with Project or Framework scope are not shown in the GUI, so validation only occurs when the plugin executes.
Plugin Implementation
Notification Plugin Example
Complete notification plugin with configuration and event handlers:
import com.dtolabs.rundeck.plugins.notification.NotificationPlugin
import groovy.json.JsonOutput
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.net.URI
rundeckPlugin(NotificationPlugin) {
title = 'Slack Notifier'
description = 'Sends notifications to Slack'
version = '1.0'
author = 'Ops Team'
metadata = [
faicon: 'slack'
]
configuration {
webhook_url(
title: 'Slack Webhook URL',
description: 'Your Slack incoming webhook URL',
required: true
)
channel(
title: 'Channel',
description: 'Slack channel (optional)',
required: false
)
username(
title: 'Bot Username',
defaultValue: 'Rundeck'
)
notify_on_start(
title: 'Notify on Start',
type: 'Boolean',
defaultValue: 'false'
)
}
onstart { Map executionData, Map config ->
if (config.notify_on_start == 'true') {
def message = "Job *${executionData.job.name}* started"
return sendSlackMessage(config, message, 'good')
}
true
}
onsuccess { Map executionData, Map config ->
def duration = executionData.execution.dateEnded.time -
executionData.execution.dateStarted.time
def message = "Job *${executionData.job.name}* succeeded in ${duration}ms"
sendSlackMessage(config, message, 'good')
}
onfailure { Map executionData, Map config ->
def message = "Job *${executionData.job.name}* failed!"
sendSlackMessage(config, message, 'danger')
}
onavgduration { Map executionData, Map config ->
def message = "Job *${executionData.job.name}* exceeded average duration"
sendSlackMessage(config, message, 'warning')
}
}
def sendSlackMessage(Map config, String message, String color) {
try {
def payload = [
text: message,
username: config.username,
attachments: [[
color: color,
text: message
]]
]
if (config.channel) {
payload.channel = config.channel
}
def client = HttpClient.newHttpClient()
def request = HttpRequest.newBuilder()
.uri(URI.create(config.webhook_url))
.header('Content-Type', 'application/json')
.POST(HttpRequest.BodyPublishers.ofString(JsonOutput.toJson(payload)))
.build()
def response = client.send(request, HttpResponse.BodyHandlers.ofString())
if (response.statusCode() == 200) {
println("Notification sent successfully")
return true
} else {
System.err.println("Failed to send notification: ${response.statusCode()}")
return false
}
} catch (Exception e) {
System.err.println("Error sending notification: ${e.message}")
e.printStackTrace()
return false
}
}
Log Filter Plugin Example
import com.dtolabs.rundeck.plugins.logs.LogFilterPlugin
rundeckPlugin(LogFilterPlugin) {
title = 'Metric Extractor'
description = 'Extracts metrics from log output'
version = '1.0'
configuration {
pattern(
title: 'Metric Pattern',
description: 'Regex pattern to match metrics',
required: true,
defaultValue: 'METRIC: (\\w+)=(\\d+)'
)
}
// Initialize the filter
def metrics = [:]
// Process each log line
handleEvent { event ->
def matcher = event.message =~ config.pattern
if (matcher) {
def name = matcher[0][1]
def value = matcher[0][2] as Integer
metrics[name] = value
}
// Pass through the event
event
}
// Called when complete
complete {
if (metrics) {
println("Collected metrics: ${metrics}")
// Export to data context
outputContext.metrics = metrics
}
}
}
Accessing Execution Data
Available Data in Notification Plugins
The executionData map contains:
executionData.job.name // Job name
executionData.job.group // Job group
executionData.job.project // Project name
executionData.execution.id // Execution ID
executionData.execution.status // Status (succeeded, failed, etc.)
executionData.execution.user // User who ran the job
executionData.execution.dateStarted // Start time
executionData.execution.dateEnded // End time (if finished)
Accessing Configuration
Configuration values are available in the config map:
onsuccess { Map executionData, Map config ->
def url = config.webhook_url
def channel = config.channel
def enableDebug = config.enable_debug == 'true'
// Use configuration values
}
Best Practices
Error Handling
Always handle errors gracefully:
onsuccess { Map executionData, Map config ->
try {
sendNotification(config, executionData)
return true // Success
} catch (Exception e) {
System.err.println("Failed to send notification: ${e.message}")
e.printStackTrace()
return false // Failure
}
}
Logging
Use println for info and System.err.println for errors:
println("Sending notification to ${config.webhook_url}")
System.err.println("ERROR: Failed to connect")
Return Values
Notification methods should return:
truefor successfalsefor failure
onsuccess { executionData, config ->
def success = performAction()
return success // Return boolean
}
Validation
Validate configuration early:
onsuccess { executionData, config ->
if (!config.webhook_url) {
System.err.println("ERROR: Webhook URL not configured")
return false
}
if (!config.webhook_url.startsWith('http')) {
System.err.println("ERROR: Invalid webhook URL")
return false
}
// Continue with valid config
}
Resource Cleanup
Clean up resources properly:
def client = null
try {
client = createHttpClient()
// Use client
} finally {
if (client) {
client.close()
}
}
Development Workflow
Initial Development
- Create your
.groovyfile in the plugins directory - Restart Rundeck to load the plugin
- Test the plugin in a Job
Iterative Development
After the initial load:
- Edit the
.groovyfile - No restart needed! - Changes take effect on next use
- Test your changes
Hot Reloading
Groovy plugins support hot reloading after the initial load. This makes development much faster than Java plugins.
Debugging
Add debug logging:
if (config.enable_debug == 'true') {
println("DEBUG: Configuration: ${config}")
println("DEBUG: Execution data: ${executionData}")
}
Test with simple executions first:
// Add a simple test handler
onstart { executionData, config ->
println("Plugin loaded successfully!")
println("Config: ${config}")
true
}
Limitations
Groovy plugins currently have some limitations:
Supported Plugin Types:
- ✅ Notification
- ✅ Logging (StreamingLogWriter, StreamingLogReader, ExecutionFileStorage)
- ✅ Log Filter
- ✅ Content Converter
- ❌ Node Steps
- ❌ Workflow Steps
- ❌ Node Executors
- ❌ File Copiers
- ❌ Resource Model Sources
- ❌ And others...
Workarounds:
- For unsupported types, use Java plugins or Script plugins
- For simple use cases, Script plugins are often easier
- For complex logic, Java plugins provide full control
Related Documentation
- Plugin Properties Reference - Property configuration details
- Notification Plugins - Groovy notification plugins
- Logging Plugins - Groovy logging plugins
- Log Filter Plugins - Groovy log filters
- Java Plugin Development - For unsupported types
- Script Plugin Development - Alternative approach