diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..9ed0b19 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,102 @@ +# GitHub Actions Workflows + +This directory contains automated CI/CD workflows for the plugin-bootstrap project. + +## Workflows + +### build.yml - Continuous Integration + +**Triggers:** +- Push to `master` or `main` branches +- Pull requests to `master` or `main` branches + +**What it does:** +1. Checks out the code +2. Sets up JDK 11 (Temurin distribution) +3. Validates the Gradle wrapper for security +4. Runs `./gradlew clean build check` (compiles, runs tests) +5. Builds distribution packages (`.deb` and `.rpm`) + +**Purpose:** Ensures code quality and that the project builds successfully on every commit and PR. + +### release.yml - Release Automation + +**Triggers:** +- Push of a version tag matching pattern `v*` (e.g., `v1.2.0`) + +**What it does:** +1. Checks out the code with full history +2. Sets up JDK 11 (Temurin distribution) +3. Validates the Gradle wrapper +4. Runs `./gradlew clean build` (full build with tests) +5. Builds all distribution formats: + - Regular tar/zip archives + - Shadow (fat) tar/zip archives + - Debian package (`.deb`) + - RPM package (`.rpm`) +6. Extracts version from the tag +7. Creates a GitHub release with: + - Release name: "Release X.Y.Z" + - All distribution packages attached + - Auto-generated release notes from commits + +**Purpose:** Automates the release process, ensuring consistent builds and making distribution packages immediately available. + +## Creating a Release + +To create a new release: + +```bash +# 1. Update version and changelog (if needed) +# 2. Commit all changes +git add . +git commit -m "Prepare for release X.Y.Z" + +# 3. Create and push the tag +git tag -a vX.Y.Z -m "Release version X.Y.Z" +git push origin main +git push origin vX.Y.Z +``` + +The release workflow will automatically: +- Build all packages +- Create the GitHub release +- Upload distribution files +- Make them available at: https://github.com/rundeck/plugin-bootstrap/releases + +## Permissions + +Both workflows require specific permissions: + +- **build.yml**: Default permissions (read repository) +- **release.yml**: `contents: write` permission to create releases + +These permissions are configured in each workflow file. + +## Maintenance Notes + +### Updating Actions + +Keep actions up to date for security and features: +- `actions/checkout`: Currently v4 +- `actions/setup-java`: Currently v4 +- `gradle/wrapper-validation-action`: Currently v2 +- `softprops/action-gh-release`: Currently v1 + +Check for updates: https://github.com/marketplace?type=actions + +### Java Version + +The project uses Java 11 as the minimum version. This is set in: +- Both workflow files +- `build.gradle` (`sourceCompatibility = 11.0`) +- Generated plugin templates + +### Distribution Packages + +The workflows build multiple package formats: +- **Regular distributions**: Standard tar/zip with dependencies separate +- **Shadow distributions**: Fat archives with all dependencies bundled +- **System packages**: `.deb` for Debian/Ubuntu, `.rpm` for RedHat/CentOS + +Shadow distributions are recommended for most users as they're self-contained. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..6a06fc2 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,30 @@ +name: Build and Test + +on: + push: + branches: [ master, main ] + pull_request: + branches: [ master, main ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: 'temurin' + + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@v2 + + - name: Build and Test + run: ./gradlew clean build check + + - name: Build distribution packages + run: ./gradlew buildRpm buildDeb diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5a85f03 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,54 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: 'temurin' + + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@v2 + + - name: Build with Gradle + run: ./gradlew clean build + + - name: Build distribution packages + run: ./gradlew buildRpm buildDeb shadowDistZip shadowDistTar + + - name: Get version from tag + id: get_version + run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + name: Release ${{ steps.get_version.outputs.VERSION }} + draft: false + prerelease: false + generate_release_notes: true + files: | + build/distributions/rundeck-plugin-bootstrap-*.tar + build/distributions/rundeck-plugin-bootstrap-*.zip + build/distributions/rundeck-plugin-bootstrap-shadow-*.tar + build/distributions/rundeck-plugin-bootstrap-shadow-*.zip + build/distributions/rundeck-plugin-bootstrap_*_all.deb + build/distributions/rundeck-plugin-bootstrap-*.noarch.rpm + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 82a0206..3d74377 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ rundeck-plugin-bootstrap/ build/ *.iml .DS_Store +temp/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0312cf5..0000000 --- a/.travis.yml +++ /dev/null @@ -1,6 +0,0 @@ -language: java -sudo: false -jdk: -- oraclejdk8 -script: -- ./gradlew check diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b0c236c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,91 @@ +# Changelog + +All notable changes to the Rundeck Plugin Bootstrap project are documented in this file. + +## [1.2] - 2026-02-03 + +### Added +- **Examples folder** with complete, working examples of all plugin types + - 8 Java plugin examples (Notification, WorkflowStep, WorkflowNodeStep, ResourceModelSource, LogFilter, NodeExecutor, Orchestrator, Option) + - 5 Script plugin examples (NodeExecutor, WorkflowStep, ResourceModelSource, FileCopier, Option) + - 1 UI plugin example +- **Regeneration script** (`generate-examples.sh`) to easily recreate all examples with one command +- **Examples README** documenting the purpose and usage of each example +- **GitHub Actions** CI/CD workflow replacing Travis CI + - Automated build and test on push/PR + - Gradle wrapper validation + - Artifact uploads for distribution packages +- **Better error handling** with proper exit codes and optional debug output +- **Examples .gitignore** to prevent committing build artifacts + +### Changed +- **Updated Groovy** from 2.5.14 to 3.0.21 (matches current Rundeck) +- **Updated Spock** from 1.3-groovy-2.5 to 2.3-groovy-3.0 +- **Updated picocli** from 4.0.0-alpha-2 to 4.7.5 (stable release) +- **Updated commons-text** from 1.4 to 1.11.0 +- **Updated Gradle** wrapper from 7.2 to 7.6.4 (last stable 7.x) +- **Updated shadow plugin** from 7.1.0 to 7.1.2 +- **Updated axion-release plugin** from 1.13.4 to 1.18.2 +- **Updated Rundeck version** in templates from 5.0.2-20240212 to 5.7.0-20250101 +- **Updated Groovy version** in templates from 3.0.9 to 3.0.21 +- **Improved input validation** to preserve numbers in plugin names (e.g., "MyPlugin123" now stays as "myplugin123" instead of "myplugin") +- **Enhanced Generator.groovy** to use proper Callable return type for exit codes +- **Bumped version** from 1.1 to 1.2 +- **Updated README** with GitHub Actions badge and examples documentation + +### Fixed +- **Duplicate import** in notification PluginSpec template (removed duplicate `import spock.lang.Specification`) +- **Error handling** that was swallowing stack traces - now prints to stderr with optional debug mode +- **Exit codes** - now properly returns 0 on success, 1 on failure + +### Removed +- **Travis CI configuration** (`.travis.yml`) - replaced with GitHub Actions + +## [1.1] - Previous Release + +Initial working version with: +- Support for Java, Script, and UI plugins +- Multiple service types for each plugin type +- Template-based generation +- Basic testing + +--- + +## Migration Notes + +### From 1.1 to 1.2 + +1. **Groovy 3.0 Compatibility**: Generated plugins now use Groovy 3.0.21. If you have existing plugins generated with older versions, they should continue to work, but you may want to regenerate them to get the latest dependencies. + +2. **Build System**: The project now uses Gradle 7.6.4. If you're building from source, your existing Gradle daemon may need to be stopped: `./gradlew --stop` + +3. **CI/CD**: If you're maintaining a fork, update your CI to use GitHub Actions instead of Travis CI. The workflow file is at `.github/workflows/build.yml`. + +4. **Examples**: A new `examples/` directory has been added. You can regenerate it anytime with `./generate-examples.sh`. This is especially useful for documentation purposes. + +## Compatibility + +- **Rundeck**: Compatible with Rundeck 3.x, 4.x, and 5.x +- **Java**: Requires Java 11 or later +- **Gradle**: Uses Gradle 7.6.4 (included via wrapper) +- **Groovy**: Generated plugins use Groovy 3.0.21 + +## Release Process + +Releases are automated via GitHub Actions. To create a new release: + +1. Update version in relevant files if needed +2. Update CHANGELOG.md with release notes +3. Commit changes +4. Create and push a version tag: + ```bash + git tag -a v1.2.0 -m "Release version 1.2.0" + git push origin v1.2.0 + ``` +5. GitHub Actions will automatically build and create the release with all distribution packages + +The release workflow: +- Builds all distribution formats (tar, zip, deb, rpm) +- Creates a GitHub release +- Attaches distribution files +- Generates release notes from commits diff --git a/README.md b/README.md index c8fbaca..a0cad0e 100644 --- a/README.md +++ b/README.md @@ -1,102 +1,452 @@ # Rundeck Plugin Bootstrap -[![Build Status](https://travis-ci.org/rundeck/plugin-bootstrap.svg?branch=master)](https://travis-ci.org/rundeck/plugin-bootstrap) +[![Build Status](https://github.com/rundeck/plugin-bootstrap/actions/workflows/build.yml/badge.svg)](https://github.com/rundeck/plugin-bootstrap/actions/workflows/build.yml) -Bootstrap your Rundeck plugin development with this easy command line utility. +A command-line tool that generates scaffold code for Rundeck plugins, providing a fast way to start plugin development with best practices built in. +## Features -## Install +- **Multiple Plugin Types**: Generate Java, Script, or UI plugins +- **Service Type Coverage**: Support for all major Rundeck service types (Notification, WorkflowStep, ResourceModelSource, etc.) +- **Best Practices**: Generated code includes proper structure, tests, and documentation +- **Working Examples**: Ships with 14+ complete, buildable example plugins +- **Modern Stack**: Uses Groovy 3.0.21, Gradle 7.6, and current Rundeck APIs +- **Ready to Build**: Generated plugins include all necessary dependencies and build configuration +## Quick Start -* From zip file: -Download the tar or zip distribution, cd to the bin directory. +### Installation -* From deb package: +**From Release Package:** +Download the latest release from the [releases page](https://github.com/rundeck/plugin-bootstrap/releases): + +```bash +# Extract the archive +tar -xzf rundeck-plugin-bootstrap-X.Y.Z.tar.gz +cd rundeck-plugin-bootstrap-X.Y.Z + +# Run the tool +./bin/rundeck-plugin-bootstrap --help +``` + +**From Distribution Packages:** + +```bash +# Debian/Ubuntu +sudo dpkg -i rundeck-plugin-bootstrap_X.Y.Z-1_all.deb +rundeck-plugin-bootstrap --help + +# RedHat/CentOS +sudo rpm -i rundeck-plugin-bootstrap-X.Y.Z-1.noarch.rpm +rundeck-plugin-bootstrap --help ``` -sudo dpkg -i rundeck-plugin-bootstrap-X.Y.Z-1_all.deb + +**Build From Source:** + +```bash +git clone https://github.com/rundeck/plugin-bootstrap.git +cd plugin-bootstrap +./gradlew build +./run.sh --help ``` -* From rpm package: +### Create Your First Plugin +```bash +# Create a notification plugin in Java +rundeck-plugin-bootstrap \ + -n "My Notification Plugin" \ + -t java \ + -s Notification \ + -d ~/my-plugins + +# Build and test it +cd ~/my-plugins/my-notification-plugin +gradle build + +# The plugin JAR will be in build/libs/ ``` -sudo rpm -i rundeck-plugin-bootstrap-X.Y.Z-1.noarch.rpm + +## Usage + +```bash +rundeck-plugin-bootstrap [options] + +Required Options: + -n, --pluginName Name of your plugin (e.g., "My Awesome Plugin") + -t, --pluginType Plugin type: java, script, or ui + -s, --serviceType Rundeck service type (see list below) + -d, --destinationDirectory Directory where plugin will be created + +Other Options: + -h, --help Show help message + -V, --version Show version information +``` + +## Plugin Types and Services + +### Java Plugins (`-t java`) + +Provide full access to Rundeck's API with maximum flexibility: + +| Service Type (`-s`) | Description | +|---------------------|-------------| +| `Notification` | Send notifications when jobs complete, fail, or start | +| `WorkflowStep` | Add custom steps to job workflows | +| `WorkflowNodeStep` | Execute custom operations on individual nodes | +| `ResourceModelSource` | Provide dynamic node inventory from external sources | +| `LogFilter` | Process and transform job execution logs | +| `NodeExecutor` | Execute commands on nodes using custom protocols | +| `Orchestrator` | Control the order and conditions of workflow execution | +| `Option` | Generate dynamic option values for jobs | + +**Example:** +```bash +rundeck-plugin-bootstrap -n "Slack Notifier" -t java -s Notification -d ./plugins +``` + +### Script Plugins (`-t script`) + +Simpler plugins written in any scripting language: + +| Service Type (`-s`) | Description | +|---------------------|-------------| +| `WorkflowNodeStep` | Script-based node step execution | +| `RemoteScriptNodeStep` | Remote script execution on nodes | +| `NodeExecutor` | Custom script-based command execution | +| `FileCopier` | Script-based file transfer to nodes | +| `NodeExecutorFileCopier` | Combined executor and file copier | +| `ResourceModelSource` | Script-based node inventory | +| `Option` | Script-based dynamic option values | + +**Example:** +```bash +rundeck-plugin-bootstrap -n "Custom Node Executor" -t script -s NodeExecutor -d ./plugins +``` + +### UI Plugins (`-t ui`) + +Extend the Rundeck web interface: + +| Service Type (`-s`) | Description | +|---------------------|-------------| +| `UI` | Custom JavaScript and CSS for the Rundeck UI | + +**Example:** +```bash +rundeck-plugin-bootstrap -n "Dashboard Widget" -t ui -s UI -d ./plugins ``` -## How to use it +## Generated Plugin Structure -Run the following command to get the available options +### Java Plugin ``` -./rundeck-plugin-bootstrap help +my-plugin/ +├── build.gradle # Gradle build configuration +├── README.md # Plugin documentation +├── src/ +│ ├── main/ +│ │ ├── groovy/ # Plugin source code +│ │ │ └── com/plugin/myplugin/ +│ │ │ ├── MyPlugin.groovy +│ │ │ ├── Util.groovy +│ │ │ └── ExampleApis.groovy +│ │ └── resources/ +│ │ └── resources/ +│ │ └── icon.png # Plugin icon +│ └── test/ +│ └── groovy/ +│ └── com/plugin/myplugin/ +│ └── MyPluginSpec.groovy # Spock tests ``` -The options available are: +### Script Plugin +``` +my-script-plugin/ +├── build.gradle # Build configuration +├── Makefile # Alternative build tool +├── plugin.yaml # Plugin metadata +├── README.md +├── contents/ +│ └── script.sh # Your script +└── resources/ + └── icon.png +``` -* `--destinationDirectory or -d` : The directory in which the artifact directory will be generated -* `--pluginName or -n` : Plugin Name -* `--pluginType or -t` : Plugin Type -* `--serviceType or -s` : Rundeck Service Type +### Building Generated Plugins +**Java Plugins:** +```bash +cd my-plugin +gradle build +# JAR will be in build/libs/my-plugin-0.1.0.jar +``` -### Plugin Type options (`-t`) +**Script Plugins:** +```bash +cd my-script-plugin +gradle build +# Or use make +make +# ZIP will be created +``` + +**UI Plugins:** +```bash +cd my-ui-plugin +make +# ZIP will be created +``` -The plugins that can be created with the bootstrap client are: -* `script`: it creates a script plugin -* `java`: it creates a java plugin -* `ui`: it creates a UI plugin +### Installing in Rundeck -### Rundeck Service Type (`-s`) -Existing service plugins enabled on boostrap-plugin +1. Copy the generated `.jar` or `.zip` file to Rundeck's `libext/` directory +2. Restart Rundeck (or wait for hot-reload if configured) +3. The plugin will appear in the appropriate configuration section +## Example Plugins -#### for Java Plugins: -* ResourceModelSource -* Notification -* WorkflowStep -* WorkflowNodeStep -* LogFilter -* NodeExecutor -* Orchestrator -* Option +The `examples/` directory contains 14+ complete, working example plugins demonstrating every supported plugin type and service: -#### for Script Plugins: -* ResourceModelSource -* WorkflowNodeStep -* RemoteScriptNodeStep -* NodeExecutor -* FileCopier -* NodeExecutorFileCopier: Generate both, Node Executor and File Copier service -* Option +**Java Examples:** +- Notification, Workflow Step, Workflow Node Step +- Resource Model Source, Log Filter, Node Executor +- Orchestrator, Option Provider -#### for UI plugins -* UI +**Script Examples:** +- Node Executor, Workflow Step, Resource Model Source +- File Copier, Option Provider -## Examples: +**UI Examples:** +- Basic UI Plugin with JavaScript and CSS -* Create a script plugin: +Each example: +- ✅ Matches exactly what the bootstrap tool generates +- ✅ Includes complete source code and tests +- ✅ Can be built and installed immediately +- ✅ Serves as a template for your own plugins +- ✅ Provides documentation references +**Explore the examples:** +```bash +ls examples/ +# java-plugins/ script-plugins/ ui-plugins/ ``` -rundeck-plugin-bootstrap -n MyNodeExecutorPlugin -t script -s NodeExecutor -d /tmp + +**Build an example:** +```bash +cd examples/java-plugins/example-notification +gradle build +# Plugin ready to install: build/libs/example-notification-0.1.0.jar +``` + +**Regenerate all examples:** +```bash +./generate-examples.sh +``` + +See [`examples/README.md`](examples/README.md) for detailed documentation of each example. + +## Workflow Tips + +### Rapid Plugin Development + +Use this tool to accelerate plugin development with test-driven development: + +1. **Generate skeleton**: Create plugin scaffold with bootstrap tool +2. **Write tests first**: Define expected behavior in Spock tests +3. **Implement features**: Write plugin code to pass tests +4. **Test locally**: Run tests with `gradle test` (fast feedback) +5. **Test in Rundeck**: Install in actual Rundeck instance + +This approach is much faster than testing through the Rundeck UI during development. + +**Example test-driven workflow:** +```bash +# Generate plugin +rundeck-plugin-bootstrap -n "MyPlugin" -t java -s Notification -d ./ + +cd my-plugin + +# Write tests first (edit src/test/groovy/.../MyPluginSpec.groovy) +# Then implement features (edit src/main/groovy/.../MyPlugin.groovy) + +# Run tests frequently +gradle test + +# Build final plugin +gradle build ``` -A Script NodeExecutor plugin will be created at /tmp/mynodeexecutorplugin. -You can cd into that directory, run `gradle build` and you will have an installable plugin that you can put in your Rundeck installation. +See this [example test suite](https://github.com/rundeck-plugins/rundeck-ec2-nodes-plugin/blob/master/src/test/groovy/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSourceSpec.groovy) for inspiration. + +### Customizing Generated Plugins + +After generating a plugin, you'll want to customize it: + +1. **Update plugin metadata** in `build.gradle` (description, version, tags) +2. **Implement your logic** in the main plugin class +3. **Add dependencies** if needed (in `build.gradle` under `pluginLibs`) +4. **Write comprehensive tests** using Spock framework +5. **Update README.md** with usage instructions +6. **Replace icon.png** with your plugin's icon + +## Development + +### Building the Bootstrap Tool +```bash +# Clone the repository +git clone https://github.com/rundeck/plugin-bootstrap.git +cd plugin-bootstrap -* Create a UI script plugin: +# Build +./gradlew build +# Run locally +./run.sh --help + +# Or use the built distribution +./build/distributions/rundeck-plugin-bootstrap-shadow-*/bin/rundeck-plugin-bootstrap --help ``` -rundeck-plugin-bootstrap -n MyUIPlugin -t ui -s UI -d /tmp + +### Running Tests + +```bash +./gradlew test ``` -* Create a notification java plugin: +The test suite generates actual plugins and verifies they compile successfully. +### Creating Distribution Packages + +```bash +# Create all distribution formats +./gradlew clean build buildDeb buildRpm shadowDistZip shadowDistTar + +# Distributions will be in build/distributions/: +# - rundeck-plugin-bootstrap-X.Y.Z.tar +# - rundeck-plugin-bootstrap-X.Y.Z.zip +# - rundeck-plugin-bootstrap-shadow-X.Y.Z.tar +# - rundeck-plugin-bootstrap-shadow-X.Y.Z.zip +# - rundeck-plugin-bootstrap_X.Y.Z-1_all.deb +# - rundeck-plugin-bootstrap-X.Y.Z-1.noarch.rpm ``` -rundeck-plugin-bootstrap -n MyRundeckNotificationPlugin -t java -s Notification -d /tmp +### Creating a Release + +Releases are automated via GitHub Actions when you push a version tag: + +```bash +# Create and push a version tag +git tag -a v1.2.0 -m "Release version 1.2.0" +git push origin v1.2.0 +``` + +This will automatically: +1. Build all distribution packages +2. Create a GitHub release +3. Attach all distribution files to the release +4. Generate release notes from commits + +The release will be available at: `https://github.com/rundeck/plugin-bootstrap/releases` + +### Project Structure + +``` +plugin-bootstrap/ +├── src/ +│ ├── main/ +│ │ ├── groovy/ # Generator source code +│ │ │ └── com/rundeck/plugin/ +│ │ │ ├── Generator.groovy +│ │ │ ├── generator/ # Template generators +│ │ │ ├── template/ # Core template classes +│ │ │ └── utils/ # Utilities +│ │ └── resources/ +│ │ └── templates/ # Plugin templates +│ │ ├── java-plugin/ +│ │ ├── script-plugin/ +│ │ └── ui-script-plugin/ +│ └── test/ # Test suite +├── examples/ # Generated example plugins +├── generate-examples.sh # Example regeneration script +└── build.gradle # Build configuration ``` -## Speedy Plugin Development Testing -This repo can be used to test plugins while in development instead of testing through the Rundeck UI running in Development mode, because that can take a while. Instead, this repo can be used to make it faster. +## Requirements + +**To Run the Bootstrap Tool:** +- Java 11 or later +- No other dependencies (uses included Gradle wrapper) + +**Generated Plugins Require:** +- Java 11 or later (for Java plugins) +- Gradle 7.x or later (included in generated plugins via wrapper) +- Bash or compatible shell (for Script plugins) + +**Compatible with:** +- Rundeck 3.x, 4.x, and 5.x +- Groovy 3.0.21 +- Spock 2.3 (for tests) + +## Troubleshooting + +**Issue: "Command not found" after installation** +- Ensure `/usr/bin` is in your PATH +- Check symlink: `ls -la /usr/bin/rundeck-plugin-bootstrap` + +**Issue: Generated plugin doesn't appear in Rundeck** +- Verify plugin is in `libext/` directory +- Check Rundeck logs for loading errors: `service.log` +- Ensure plugin JAR/ZIP is not corrupted +- Restart Rundeck if auto-reload is disabled + +**Issue: Build fails with dependency errors** +- Run `gradle build --refresh-dependencies` +- Check internet connection (Gradle needs to download dependencies) +- Verify Gradle wrapper: `./gradlew --version` + +**Issue: Tests fail in generated plugin** +- This may indicate a template issue - please [report it](https://github.com/rundeck/plugin-bootstrap/issues) +- Check that you're using Java 11 or later + +## Contributing + +Contributions welcome! Please: + +1. Fork the repository +2. Create a feature branch +3. Make your changes with tests +4. Run `./gradlew check` to verify +5. Submit a pull request + +### Adding New Service Types + +To add support for a new Rundeck service type: + +1. Add the service type to `ServiceType.groovy` enum +2. Create templates in `src/main/resources/templates/` +3. Update the appropriate generator class (JavaPluginTemplateGenerator, etc.) +4. Add to the ALLOWED_TEMPLATES list +5. Create a test in the test suite +6. Update documentation + +## Resources + +- [Rundeck Plugin Development Guide](https://docs.rundeck.com/docs/developer/) +- [Plugin Developer Documentation](https://docs.rundeck.com/docs/developer/plugin-development.html) +- [Rundeck API Documentation](https://docs.rundeck.com/docs/api/) +- [Example Plugin Repository](https://github.com/rundeck-plugins/) + +## License + +Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) file for details. + +## Changelog + +See [CHANGELOG.md](CHANGELOG.md) for version history and release notes. + +--- -* To begin, you will need to follow the instructions above to create the skeleton of a plugin with the same type as the one in Development. -* Next, develop tests that can test the functionality of the plugin, by setting all of the properties in the test and executing the step as it normally would be executed. This should resemble the tests you would write at the end of development, but now you are writing them to help confirm development is complete. [Example Tests](https://github.com/rundeck-plugins/rundeck-ec2-nodes-plugin/blob/master/src/test/groovy/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSourceSpec.groovy). \ No newline at end of file +**Maintained by [Rundeck](https://www.rundeck.com/)** | **[Report Issues](https://github.com/rundeck/plugin-bootstrap/issues)** | **[View Examples](examples/)** \ No newline at end of file diff --git a/build.gradle b/build.gradle index aa989b4..40498c7 100644 --- a/build.gradle +++ b/build.gradle @@ -2,8 +2,8 @@ plugins { id 'groovy' id 'application' id "nebula.ospackage" version "9.1.1" - id 'pl.allegro.tech.build.axion-release' version '1.13.4' - id 'com.github.johnrengelman.shadow' version '7.1.0' + id 'pl.allegro.tech.build.axion-release' version '1.18.2' + id 'com.github.johnrengelman.shadow' version '7.1.2' } @@ -19,14 +19,14 @@ ext.distInstallPath = '/var/lib/rundeck-pb' defaultTasks 'clean', 'build' dependencies { - implementation 'org.codehaus.groovy:groovy-all:2.5.14' + implementation 'org.codehaus.groovy:groovy-all:3.0.21' implementation 'com.github.rundeck.cli-toolbelt:toolbelt:0.2.2' implementation 'com.github.rundeck.cli-toolbelt:toolbelt-jewelcli:0.2.2' - implementation 'org.apache.commons:commons-text:1.4' - implementation 'info.picocli:picocli:4.0.0-alpha-2' + implementation 'org.apache.commons:commons-text:1.11.0' + implementation 'info.picocli:picocli:4.7.5' - testImplementation 'org.spockframework:spock-core:1.3-groovy-2.5' + testImplementation 'org.spockframework:spock-core:2.3-groovy-3.0' } repositories { diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..c02ca2c --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,14 @@ +# Build artifacts in examples should be ignored +*/*/build/ +*/*/out/ +*/*/.gradle/ +*/*/*.jar +*/*/*.zip +*/*/*.tar +*/*/*.rpm +*/*/*.deb + +# IDE files +*/*/.idea/ +*/*/*.iml +*/*/.DS_Store diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..dfc4e57 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,123 @@ +# Rundeck Plugin Examples + +This directory contains example plugins generated by the Rundeck Plugin Bootstrap tool. These examples demonstrate the structure and code for each type of plugin that can be created. + +## Purpose + +These examples serve multiple purposes: + +1. **Documentation Reference** - Provide concrete examples that match the documentation +2. **Quick Start Templates** - Show developers exactly what the bootstrap tool generates +3. **Testing** - Verify that generated plugins build and work correctly +4. **Consistency** - Ensure documentation and generated code stay in sync + +## Plugin Types + +### Java Plugins (`java-plugins/`) + +Java plugins provide the most flexibility and access to Rundeck's full API: + +- **example-notification** - Notification plugin for sending alerts when jobs complete +- **example-workflow-step** - Workflow step plugin for custom job steps +- **example-workflow-node-step** - Node step plugin for operations on individual nodes +- **example-resource-model-source** - Resource model source for dynamic node inventory +- **example-log-filter** - Log filter plugin for processing job output +- **example-node-executor** - Node executor for custom command execution +- **example-orchestrator** - Orchestrator plugin for controlling workflow execution order +- **example-option** - Option plugin for dynamic option values + +### Script Plugins (`script-plugins/`) + +Script plugins are simpler and can be written in any scripting language: + +- **example-script-node-executor** - Script-based node executor +- **example-script-workflow-step** - Script-based workflow step +- **example-script-resource-model** - Script-based resource model source +- **example-script-file-copier** - Script-based file copier +- **example-script-option** - Script-based option provider + +### UI Plugins (`ui-plugins/`) + +UI plugins extend the Rundeck web interface: + +- **example-ui-plugin** - Basic UI plugin with JavaScript and CSS + +## Building Examples + +Each example plugin is a complete, buildable Rundeck plugin. + +### Java Plugins + +```bash +cd java-plugins/example-notification +gradle build +# Plugin jar will be in build/libs/ +``` + +### Script Plugins + +```bash +cd script-plugins/example-script-node-executor +gradle build +# Or use make +make +# Plugin zip will be created +``` + +### UI Plugins + +```bash +cd ui-plugins/example-ui-plugin +make +# Plugin zip will be created +``` + +## Installing Examples in Rundeck + +1. Build the plugin (see above) +2. Copy the resulting `.jar` or `.zip` file to Rundeck's `libext/` directory +3. Restart Rundeck (or it may hot-load depending on configuration) +4. The plugin will appear in the appropriate configuration section + +## Regenerating Examples + +To regenerate all examples (useful after updating the bootstrap tool): + +```bash +cd /path/to/plugin-bootstrap +./generate-examples.sh +``` + +This will: +1. Clean the existing examples directory +2. Build the latest bootstrap tool +3. Generate fresh examples for all plugin types + +## Using as Templates + +While these examples can be used as-is, they're designed to be starting points: + +1. Copy an example that matches your plugin type +2. Rename it to match your plugin's purpose +3. Modify the plugin code to implement your functionality +4. Update the README, build configuration, and tests +5. Build and install in Rundeck + +## Structure + +Each generated plugin includes: + +- **Source code** - Plugin implementation (Java/Groovy or scripts) +- **Tests** - Spock tests (Java plugins) demonstrating how to test the plugin +- **Build configuration** - Gradle build files with proper dependencies +- **README** - Basic documentation about the plugin +- **Resources** - Icons and other plugin resources + +## Version Information + +These examples were generated with: +- Rundeck Plugin Bootstrap version: 1.2 +- Rundeck version: 5.7.0-20250101 +- Groovy version: 3.0.21 + +For the latest version and updates, see: https://github.com/rundeck/plugin-bootstrap diff --git a/examples/java-plugins/example-log-filter/README.md b/examples/java-plugins/example-log-filter/README.md new file mode 100644 index 0000000..76d5c03 --- /dev/null +++ b/examples/java-plugins/example-log-filter/README.md @@ -0,0 +1,4 @@ +# Example Log Filter Rundeck Plugin + +This is a log filter plugin. + diff --git a/examples/java-plugins/example-log-filter/build.gradle b/examples/java-plugins/example-log-filter/build.gradle new file mode 100644 index 0000000..ad25b51 --- /dev/null +++ b/examples/java-plugins/example-log-filter/build.gradle @@ -0,0 +1,69 @@ +plugins { + id 'groovy' + id 'java' +} + +version = '0.1.0' +defaultTasks 'clean','build' +apply plugin: 'java' +apply plugin: 'groovy' +apply plugin: 'idea' +sourceCompatibility = 1.8 +ext.rundeckPluginVersion= '2.0' +ext.rundeckVersion= '5.7.0-20250101' +ext.pluginClassNames='com.plugin.examplelogfilter.ExampleLogFilter' + + +repositories { + mavenLocal() + mavenCentral() +} + +configurations{ + //declare custom pluginLibs configuration to include only libs for this plugin + pluginLibs + + //declare compile to extend from pluginLibs so it inherits the dependencies + implementation{ + extendsFrom pluginLibs + } +} + +dependencies { + implementation 'org.rundeck:rundeck-core:5.7.0-20250101' + + //use pluginLibs to add dependencies, example: + //pluginLibs group: 'com.google.code.gson', name: 'gson', version: '2.8.2' + + testImplementation 'junit:junit:4.12' + testImplementation "org.codehaus.groovy:groovy-all:3.0.21" + testImplementation "org.spockframework:spock-core:2.2-groovy-3.0" +} + +// task to copy plugin libs to output/lib dir +task copyToLib(type: Copy) { + into "$buildDir/output/lib" + from configurations.pluginLibs +} + +jar { + from "$buildDir/output" + manifest { + def libList = configurations.pluginLibs.collect{'lib/'+it.name}.join(' ') + + attributes 'Rundeck-Plugin-Classnames': pluginClassNames + attributes 'Rundeck-Plugin-File-Version': version + attributes 'Rundeck-Plugin-Name': 'Example Log Filter' + attributes 'Rundeck-Plugin-Description': 'Provide a short description of your plugin here.' + attributes 'Rundeck-Plugin-Rundeck-Compatibility-Version': '3.x' + attributes 'Rundeck-Plugin-Tags': 'java,logfilter' + attributes 'Rundeck-Plugin-License': 'Apache 2.0' + attributes 'Rundeck-Plugin-Source-Link': 'Please put the link to your source repo here' + attributes 'Rundeck-Plugin-Target-Host-Compatibility': 'all' + attributes 'Rundeck-Plugin-Version': rundeckPluginVersion + attributes 'Rundeck-Plugin-Archive': 'true' + attributes 'Rundeck-Plugin-Libs': "${libList}" + + } + dependsOn(copyToLib) +} diff --git a/examples/java-plugins/example-log-filter/src/main/java/com/plugin/examplelogfilter/ExampleLogFilter.java b/examples/java-plugins/example-log-filter/src/main/java/com/plugin/examplelogfilter/ExampleLogFilter.java new file mode 100644 index 0000000..127af32 --- /dev/null +++ b/examples/java-plugins/example-log-filter/src/main/java/com/plugin/examplelogfilter/ExampleLogFilter.java @@ -0,0 +1,83 @@ +package com.plugin.examplelogfilter; + +import com.dtolabs.rundeck.core.logging.LogEventControl; +import com.dtolabs.rundeck.core.logging.LogLevel; +import com.dtolabs.rundeck.core.logging.PluginLoggingContext; +import com.dtolabs.rundeck.core.plugins.Plugin; +import com.dtolabs.rundeck.plugins.descriptions.PluginDescription; +import com.dtolabs.rundeck.plugins.descriptions.PluginProperty; +import com.dtolabs.rundeck.plugins.descriptions.SelectLabels; +import com.dtolabs.rundeck.plugins.descriptions.SelectValues; +import com.dtolabs.rundeck.plugins.logging.LogFilterPlugin; +import java.util.HashMap; +import java.util.Map; + +@Plugin(service="LogFilter",name="example-log-filter") +@PluginDescription(title="Example Log Filter", description="My plugin description") +public class ExampleLogFilter implements LogFilterPlugin{ + + @PluginProperty(name = "example header",title = "Example String",description = "Example description") + private String header; + + @PluginProperty( + title = "Data type", + description = "Select datatype output", + required = false + ) + @SelectValues( + values = {"text/plain", "text/html"}, + freeSelect = true + ) + @SelectLabels(values = {"TEXT", "HTML"}) + String datatype = null; + + + private boolean started = false; + private StringBuilder buffer; + + @Override + public void init(final PluginLoggingContext context) { + started = true; + buffer = new StringBuilder(); + + if(datatype.equals("text/html")){ + buffer.append(""); + buffer.append(""); + } + } + + @Override + public void handleEvent(final PluginLoggingContext context, final LogEventControl event) { + if(event.getEventType().equals("log") && event.getLoglevel().equals(LogLevel.NORMAL) ){ + + if(datatype.equals("text/html")){ + buffer.append(""); + }else{ + buffer.append("[").append(header).append("] ").append(event.getMessage()).append("\n"); + } + + event.setLoglevel(LogLevel.DEBUG); + } + } + + @Override + public void complete(final PluginLoggingContext context) { + if (started && datatype!=null && buffer.length()>0) { + + if(datatype.equals("text/html")){ + buffer.append("
Log Output
").append("[").append(header).append("] ").append(event.getMessage()).append("
"); + } + + Map type = new HashMap<>(); + type.put("content-data-type", datatype); + + + context.log( + 2, + buffer.toString(), + type + ); + + } + } +} \ No newline at end of file diff --git a/examples/java-plugins/example-log-filter/src/main/resources/resources/icon.png b/examples/java-plugins/example-log-filter/src/main/resources/resources/icon.png new file mode 100644 index 0000000..7aaf7e8 Binary files /dev/null and b/examples/java-plugins/example-log-filter/src/main/resources/resources/icon.png differ diff --git a/examples/java-plugins/example-log-filter/src/test/groovy/com/plugin/examplelogfilter/ExampleLogFilterSpec.groovy b/examples/java-plugins/example-log-filter/src/test/groovy/com/plugin/examplelogfilter/ExampleLogFilterSpec.groovy new file mode 100644 index 0000000..ac1c24c --- /dev/null +++ b/examples/java-plugins/example-log-filter/src/test/groovy/com/plugin/examplelogfilter/ExampleLogFilterSpec.groovy @@ -0,0 +1,45 @@ +package com.plugin.examplelogfilter + +import com.dtolabs.rundeck.core.dispatcher.ContextView +import com.dtolabs.rundeck.core.execution.workflow.DataOutput +import com.dtolabs.rundeck.core.logging.LogEventControl +import com.dtolabs.rundeck.core.logging.LogLevel +import com.dtolabs.rundeck.core.logging.PluginLoggingContext +import spock.lang.Specification + +class ExampleLogFilterSpec extends Specification { + + def "test preset type "() { + given: + def plugin = new ExampleLogFilter() + plugin.datatype = datatype + plugin.header = "test" + def sharedoutput = new DataOutput(ContextView.global()) + def context = Mock(PluginLoggingContext) { + getOutputContext() >> sharedoutput + } + def events = [] + lines.each { line -> + events << Mock(LogEventControl) { + getMessage() >> line + getEventType() >> 'log' + getLoglevel() >> LogLevel.NORMAL + } + } + when: + plugin.init(context) + events.each { + plugin.handleEvent(context, it) + } + plugin.complete(context) + + then: + 1 * context.log(2, output, meta) + + where: + datatype | lines | output | meta + 'text/plain' | ['1,2,3', '---', 'a,b,c'] | '[test] 1,2,3\n[test] ---\n[test] a,b,c\n' | ['content-data-type': 'text/plain'] + 'text/html' | ['1,2,3', '---', 'a,b,c'] | "
Log Output
[test] 1,2,3
[test] ---
[test] a,b,c
" | ['content-data-type': 'text/html'] + } + +} \ No newline at end of file diff --git a/examples/java-plugins/example-node-executor/README.md b/examples/java-plugins/example-node-executor/README.md new file mode 100644 index 0000000..74590f9 --- /dev/null +++ b/examples/java-plugins/example-node-executor/README.md @@ -0,0 +1,4 @@ +# Example Node Executor Rundeck Plugin + +This is a node executor plugin. + diff --git a/examples/java-plugins/example-node-executor/build.gradle b/examples/java-plugins/example-node-executor/build.gradle new file mode 100644 index 0000000..0a29950 --- /dev/null +++ b/examples/java-plugins/example-node-executor/build.gradle @@ -0,0 +1,71 @@ +plugins { + id 'groovy' + id 'java' +} + +version = '0.1.0' +defaultTasks 'clean','build' +apply plugin: 'java' +apply plugin: 'groovy' +apply plugin: 'idea' +sourceCompatibility = 11.0 +ext.rundeckPluginVersion= '2.0' +ext.rundeckVersion= '5.7.0-20250101' +ext.pluginClassNames='com.plugin.examplenodeexecutor.ExampleNodeExecutor' + + +repositories { + mavenLocal() + mavenCentral() +} + +configurations{ + //declare custom pluginLibs configuration to include only libs for this plugin + pluginLibs + + //declare compile to extend from pluginLibs so it inherits the dependencies + implementation{ + extendsFrom pluginLibs + } +} + +dependencies { + implementation 'org.rundeck:rundeck-core:5.7.0-20250101' + implementation 'org.codehaus.groovy:groovy-all:3.0.21' + //use pluginLibs to add dependencies, example: + //pluginLibs group: 'com.google.code.gson', name: 'gson', version: '2.8.2' + + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.codehaus.groovy:groovy-all:3.0.21' + testImplementation "org.spockframework:spock-core:2.2-groovy-3.0" + testImplementation "cglib:cglib-nodep:2.2.2" + testImplementation group: 'org.objenesis', name: 'objenesis', version: '1.2' +} + +// task to copy plugin libs to output/lib dir +task copyToLib(type: Copy) { + into "$buildDir/output/lib" + from configurations.pluginLibs +} + +jar { + from "$buildDir/output" + manifest { + def libList = configurations.pluginLibs.collect{'lib/'+it.name}.join(' ') + + attributes 'Rundeck-Plugin-Classnames': pluginClassNames + attributes 'Rundeck-Plugin-File-Version': version + attributes 'Rundeck-Plugin-Name': 'Example Node Executor' + attributes 'Rundeck-Plugin-Description': 'Provide a short description of your plugin here.' + attributes 'Rundeck-Plugin-Rundeck-Compatibility-Version': '3.x' + attributes 'Rundeck-Plugin-Tags': 'java,executor' + attributes 'Rundeck-Plugin-License': 'Apache 2.0' + attributes 'Rundeck-Plugin-Source-Link': 'Please put the link to your source repo here' + attributes 'Rundeck-Plugin-Target-Host-Compatibility': 'all' + attributes 'Rundeck-Plugin-Version': rundeckPluginVersion + attributes 'Rundeck-Plugin-Archive': 'true' + attributes 'Rundeck-Plugin-Libs': "${libList}" + + } + dependsOn(copyToLib) +} diff --git a/examples/java-plugins/example-node-executor/src/main/groovy/com/plugin/examplenodeexecutor/ExampleNodeExecutor.groovy b/examples/java-plugins/example-node-executor/src/main/groovy/com/plugin/examplenodeexecutor/ExampleNodeExecutor.groovy new file mode 100644 index 0000000..3d4b92f --- /dev/null +++ b/examples/java-plugins/example-node-executor/src/main/groovy/com/plugin/examplenodeexecutor/ExampleNodeExecutor.groovy @@ -0,0 +1,103 @@ +package com.plugin.examplenodeexecutor; + +import com.dtolabs.rundeck.core.common.INodeEntry +import com.dtolabs.rundeck.core.execution.ExecutionContext +import com.dtolabs.rundeck.core.execution.ExecutionLogger +import com.dtolabs.rundeck.core.execution.service.NodeExecutor +import com.dtolabs.rundeck.core.execution.service.NodeExecutorResult +import com.dtolabs.rundeck.core.execution.service.NodeExecutorResultImpl +import com.dtolabs.rundeck.core.execution.utils.ResolverUtil +import com.dtolabs.rundeck.core.plugins.Plugin +import com.dtolabs.rundeck.core.plugins.configuration.Describable +import com.dtolabs.rundeck.core.plugins.configuration.Description; +import com.dtolabs.rundeck.core.plugins.configuration.StringRenderingConstants; +import com.dtolabs.rundeck.plugins.ServiceNameConstants; +import com.dtolabs.rundeck.plugins.descriptions.PluginDescription; +import com.dtolabs.rundeck.plugins.util.DescriptionBuilder +import com.dtolabs.rundeck.plugins.util.PropertyBuilder; + +@Plugin(name = "example-node-executor", service = ServiceNameConstants.NodeExecutor) +@PluginDescription(title = "Example Node Executor", description = "A node executor plugin that can execute commands on remote nodes") +public class ExampleNodeExecutor implements NodeExecutor, Describable { + + public static final String SERVICE_PROVIDER_NAME = "example-node-executor" + + public static final String PROJ_PROP_PREFIX = "project." + public static final String FRAMEWORK_PROP_PREFIX = "framework." + + public static final String MOCK_FAILURE = "mockFailure" + public static final String USERNAME = "username" + public static final String PASSWORD = "password" + + @Override + Description getDescription() { + DescriptionBuilder builder = DescriptionBuilder.builder() + .name(SERVICE_PROVIDER_NAME) + .title("Example Node Executor") + .description("A node executor plugin that can execute commands on remote nodes") + .property(PropertyBuilder.builder() + .title("Username") + .string(USERNAME) + .description("The username to use for the connection") + .required(true) + .renderingOption(StringRenderingConstants.INSTANCE_SCOPE_NODE_ATTRIBUTE_KEY, "username-key-path") + .build() + ) + .property( + PropertyBuilder.builder() + .title("Password") + .string(PASSWORD) + .description("The password to use for the connection") + .required(true) + .renderingOption(StringRenderingConstants.SELECTION_ACCESSOR_KEY, StringRenderingConstants.SelectionAccessor.STORAGE_PATH) + .renderingOption(StringRenderingConstants.STORAGE_PATH_ROOT_KEY, "keys") + .renderingOption(StringRenderingConstants.STORAGE_FILE_META_FILTER_KEY, "Rundeck-data-type=password") + .build() + ) + .property( + PropertyBuilder.builder() + .title("Mock Failure") + .booleanType(MOCK_FAILURE) + .description("Optionally select to mock a failure") + .required(false) + .defaultValue("false") + .build() + ) + + builder.mapping(USERNAME, PROJ_PROP_PREFIX + USERNAME) + builder.frameworkMapping(USERNAME, FRAMEWORK_PROP_PREFIX + USERNAME) + builder.mapping(PASSWORD, PROJ_PROP_PREFIX + PASSWORD) + builder.frameworkMapping(PASSWORD, FRAMEWORK_PROP_PREFIX + PASSWORD) + builder.mapping(MOCK_FAILURE, PROJ_PROP_PREFIX + MOCK_FAILURE) + builder.frameworkMapping(MOCK_FAILURE, FRAMEWORK_PROP_PREFIX + MOCK_FAILURE) + + return builder.build() + } + + @Override + public NodeExecutorResult executeCommand(ExecutionContext context, String[] command, INodeEntry node) { + + String username = ResolverUtil.resolveProperty(USERNAME, null, node, + context.getIFramework().getFrameworkProjectMgr().getFrameworkProject(context.getFrameworkProject()), + context.framework) + String passwordKeyPath = ResolverUtil.resolveProperty(PASSWORD, null, node, + context.getIFramework().getFrameworkProjectMgr().getFrameworkProject(context.getFrameworkProject()), + context.framework) + boolean mockFailure = Boolean.parseBoolean(ResolverUtil.resolveProperty(MOCK_FAILURE, "false", node, + context.getIFramework().getFrameworkProjectMgr().getFrameworkProject(context.getFrameworkProject()), + context.framework)) + + ExecutionLogger logger= context.getExecutionLogger() + + //Here we can retrieve the password from key storage and use it to authenticate with the target node. + String password = Util.getPasswordFromPath(passwordKeyPath, context) + + logger.log(2, "Executing command: " + Arrays.asList(command) + " on node: " + node.getNodename() + " with username: " + username) + + if(mockFailure) { + return NodeExecutorResultImpl.createFailure(Util.PluginFailureReason.ConnectionError, "Failure due to mock failure", node) + } else { + return NodeExecutorResultImpl.createSuccess(node) + } + } +} \ No newline at end of file diff --git a/examples/java-plugins/example-node-executor/src/main/groovy/com/plugin/examplenodeexecutor/Util.groovy b/examples/java-plugins/example-node-executor/src/main/groovy/com/plugin/examplenodeexecutor/Util.groovy new file mode 100644 index 0000000..8d6e075 --- /dev/null +++ b/examples/java-plugins/example-node-executor/src/main/groovy/com/plugin/examplenodeexecutor/Util.groovy @@ -0,0 +1,22 @@ +package com.plugin.examplenodeexecutor + +import com.dtolabs.rundeck.core.execution.ExecutionContext +import com.dtolabs.rundeck.core.execution.workflow.steps.FailureReason +import com.dtolabs.rundeck.core.storage.ResourceMeta + +class Util { + + static String getPasswordFromPath(String path, ExecutionContext context) throws IOException { + ResourceMeta contents = context.getStorageTree().getResource(path).getContents(); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + contents.writeContent(byteArrayOutputStream); + String password = new String(byteArrayOutputStream.toByteArray()); + return password; + } + + enum PluginFailureReason implements FailureReason { + KeyStorageError, + ConnectionError + } + +} diff --git a/examples/java-plugins/example-node-executor/src/main/resources/resources/icon.png b/examples/java-plugins/example-node-executor/src/main/resources/resources/icon.png new file mode 100644 index 0000000..7aaf7e8 Binary files /dev/null and b/examples/java-plugins/example-node-executor/src/main/resources/resources/icon.png differ diff --git a/examples/java-plugins/example-node-executor/src/test/groovy/com/plugin/examplenodeexecutor/ExampleNodeExecutorSpec.groovy b/examples/java-plugins/example-node-executor/src/test/groovy/com/plugin/examplenodeexecutor/ExampleNodeExecutorSpec.groovy new file mode 100644 index 0000000..26c0d17 --- /dev/null +++ b/examples/java-plugins/example-node-executor/src/test/groovy/com/plugin/examplenodeexecutor/ExampleNodeExecutorSpec.groovy @@ -0,0 +1,82 @@ +package com.plugin.examplenodeexecutor + +import com.dtolabs.rundeck.plugins.step.PluginStepContext +import com.dtolabs.rundeck.core.execution.ExecutionContext +import com.dtolabs.rundeck.core.execution.ExecutionLogger +import com.dtolabs.rundeck.core.execution.workflow.steps.StepException +import com.dtolabs.rundeck.plugins.PluginLogger +import com.dtolabs.rundeck.core.common.Framework +import com.dtolabs.rundeck.core.common.INodeEntry; +import com.dtolabs.rundeck.core.common.IRundeckProject +import spock.lang.Specification +import com.dtolabs.rundeck.core.common.ProjectManager +import com.dtolabs.rundeck.core.common.IRundeckProject + +class ExampleNodeExecutorSpec extends Specification { + + def getContext(ExecutionLogger logger, Boolean fail){ + + def manager = Mock(ProjectManager){ + getFrameworkProject(_)>> Mock(IRundeckProject) { + hasProperty(('project.exampleConfig')) >> true + getProperty(('project.exampleConfig')) >> "123345" + hasProperty(('project.exampleSelect')) >> true + getProperty(('project.exampleSelect')) >> "Blue" + hasProperty(('project.forceFail')) >> fail + getProperty(('project.forceFail')) >> fail + } + } + + Mock(ExecutionContext){ + getExecutionLogger()>>logger + getFrameworkProject() >> "test" + getFramework() >> Mock(Framework) { + getFrameworkProjectMgr() >> manager + } + + } + } + + def "check Boolean parameter"(){ + + given: + + String[] command = ["ls","-lrt"] + def logger = Mock(ExecutionLogger) + def example = new ExampleNodeExecutor() + def context = getContext(logger,true) + def node = Mock(INodeEntry){ + getNodename()>>"test" + getAttributes()>>["hostname":"Test","osFamily":"linux","forceFail":"true"] + } + + when: + example.executeCommand(context, command, node) + + then: + 1 * logger.log(0, '[demo-error] force to fail') + + } + + def "run OK"(){ + + given: + + String[] command = ["ls","-lrt"] + def logger = Mock(ExecutionLogger) + def example = new ExampleNodeExecutor() + def context = getContext(logger,false) + def node = Mock(INodeEntry){ + getNodename()>>"test" + getAttributes()>>["hostname":"Test","osFamily":"linux"] + } + + when: + example.executeCommand(context, command, node) + + then: + 1 * logger.log(2, '[demo-info] Running command: [ls, -lrt] on node test') + + } + +} \ No newline at end of file diff --git a/examples/java-plugins/example-notification/README.md b/examples/java-plugins/example-notification/README.md new file mode 100644 index 0000000..5a1c033 --- /dev/null +++ b/examples/java-plugins/example-notification/README.md @@ -0,0 +1,4 @@ +# Example Notification Rundeck Plugin + +This is a notification plugin. + diff --git a/examples/java-plugins/example-notification/build.gradle b/examples/java-plugins/example-notification/build.gradle new file mode 100644 index 0000000..c0fd565 --- /dev/null +++ b/examples/java-plugins/example-notification/build.gradle @@ -0,0 +1,69 @@ +plugins { + id 'groovy' + id 'java' +} + +version = '0.1.0' +defaultTasks 'clean','build' +apply plugin: 'java' +apply plugin: 'groovy' +apply plugin: 'idea' +sourceCompatibility = 11.0 +ext.rundeckPluginVersion= '2.0' +ext.rundeckVersion= '5.7.0-20250101' +ext.pluginClassNames='com.plugin.examplenotification.ExampleNotification' + + +repositories { + mavenLocal() + mavenCentral() +} + +configurations{ + //declare custom pluginLibs configuration to include only libs for this plugin + pluginLibs + + //declare compile to extend from pluginLibs so it inherits the dependencies + implementation{ + extendsFrom pluginLibs + } +} + +dependencies { + implementation 'org.rundeck:rundeck-core:5.7.0-20250101' + implementation 'org.codehaus.groovy:groovy-all:3.0.21' + + //use pluginLibs to add dependencies, example: + + testImplementation 'junit:junit:4.12' + testImplementation "org.codehaus.groovy:groovy-all:3.0.21" + testImplementation "org.spockframework:spock-core:2.2-groovy-3.0" +} + +// task to copy plugin libs to output/lib dir +task copyToLib(type: Copy) { + into "$buildDir/output/lib" + from configurations.pluginLibs +} + +jar { + from "$buildDir/output" + manifest { + def libList = configurations.pluginLibs.collect{'lib/'+it.name}.join(' ') + + attributes 'Rundeck-Plugin-Classnames': pluginClassNames + attributes 'Rundeck-Plugin-File-Version': version + attributes 'Rundeck-Plugin-Name': 'Example Notification' + attributes 'Rundeck-Plugin-Description': 'Provide a short description of your plugin here.' + attributes 'Rundeck-Plugin-Rundeck-Compatibility-Version': '3.x' + attributes 'Rundeck-Plugin-Tags': 'java,notification' + attributes 'Rundeck-Plugin-License': 'Apache 2.0' + attributes 'Rundeck-Plugin-Source-Link': 'Please put the link to your source repo here' + attributes 'Rundeck-Plugin-Target-Host-Compatibility': 'all' + attributes 'Rundeck-Plugin-Version': rundeckPluginVersion + attributes 'Rundeck-Plugin-Archive': 'true' + attributes 'Rundeck-Plugin-Libs': "${libList}" + + } + dependsOn(copyToLib) +} diff --git a/examples/java-plugins/example-notification/src/main/groovy/com/plugin/examplenotification/ExampleApis.groovy b/examples/java-plugins/example-notification/src/main/groovy/com/plugin/examplenotification/ExampleApis.groovy new file mode 100644 index 0000000..b9a9236 --- /dev/null +++ b/examples/java-plugins/example-notification/src/main/groovy/com/plugin/examplenotification/ExampleApis.groovy @@ -0,0 +1,52 @@ +package com.plugin.examplenotification; + +import okhttp3.MediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.RequestBody +import okhttp3.Credentials; + + +class ExampleApis { + + Properties configuration; + + //Set constructor to use configuration from plugin properties + ExampleApis(Properties configuration) { + this.configuration = configuration; + } + + //Pass the customProperty from the plugin config to the JSON string that we'll pass to the API call + private String json = '{"name":"' + configuration.getProperty("customProperty") + '"}'; + + //Set the media type for the API call request body + public static final MediaType JSON = MediaType.get("application/json"); + + //Create a new OkHttpClient + OkHttpClient client = new OkHttpClient(); + + //Post method that takes the API Key as an argument + String post(String apiKey) throws IOException { + + //Create a basic authentication credential + String credential = Credentials.basic("name", apiKey); + + RequestBody body = RequestBody.create(JSON, json); + + Request request = new Request.Builder() + .url("https://httpbin.org/post") + .post(body) + .header("Authorization", credential) + .build(); + + Response response = null + + try { + response = client.newCall(request).execute() + return response.body().string(); + } finally { + response.close(); + } + } +} \ No newline at end of file diff --git a/examples/java-plugins/example-notification/src/main/groovy/com/plugin/examplenotification/ExampleNotification.groovy b/examples/java-plugins/example-notification/src/main/groovy/com/plugin/examplenotification/ExampleNotification.groovy new file mode 100644 index 0000000..7a39dc6 --- /dev/null +++ b/examples/java-plugins/example-notification/src/main/groovy/com/plugin/examplenotification/ExampleNotification.groovy @@ -0,0 +1,75 @@ +package com.plugin.examplenotification; + +import com.dtolabs.rundeck.core.plugins.Plugin +import com.dtolabs.rundeck.core.plugins.configuration.AcceptsServices +import com.dtolabs.rundeck.core.storage.StorageTree +import com.dtolabs.rundeck.plugins.descriptions.PluginDescription +import com.dtolabs.rundeck.plugins.descriptions.PluginProperty +import com.dtolabs.rundeck.plugins.notification.NotificationPlugin +import com.dtolabs.rundeck.plugins.descriptions.RenderingOption +import com.dtolabs.rundeck.plugins.descriptions.RenderingOptions +import com.dtolabs.rundeck.core.plugins.configuration.StringRenderingConstants +import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree +import org.rundeck.app.spi.Services +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +@Plugin(service="Notification", name="example-notification") +@PluginDescription(title="Example Notification", description="This is a notification plugin that integrated with Example Notification.") +public class ExampleNotification implements NotificationPlugin, AcceptsServices { + + static Logger logger = LoggerFactory.getLogger(ExampleNotification.class); + + @PluginProperty(name = "customProperty" ,title = "Custom Property", description = "A custom property to be passed to the API.") + String customProperty; + + @PluginProperty( + title = "API Key Path", + description = 'REQUIRED: The path to the Key Storage entry for your API Key.\n If an error of `Unauthorized` occurs, be sure to add the proper policy to ACLs.', + required = true + ) + @RenderingOptions([ + @RenderingOption( + key = StringRenderingConstants.SELECTION_ACCESSOR_KEY, + value = "STORAGE_PATH" + ), + @RenderingOption( + key = StringRenderingConstants.STORAGE_PATH_ROOT_KEY, + value = "keys" + ), + @RenderingOption( + key = StringRenderingConstants.STORAGE_FILE_META_FILTER_KEY, + value = "Rundeck-data-type=password" + ), + @RenderingOption( + key = StringRenderingConstants.GROUP_NAME, + value = "API Configuration" + ) + ]) + String apiKeyPath + + + //Implement services so that we can retrieve secret from key storage and pass to API call + Services services + @Override + void setServices(Services services) { + this.services = services + } + + public boolean postNotification(String trigger, Map executionData, Map config) { + + //Get the secret from the key storage + StorageTree keyStorage = services.getService(KeyStorageTree) + String apiKeyPath = config.get("apiKeyPath") + String apiKey = Util.getPasswordFromKeyStorage(apiKeyPath, keyStorage) + + //Pass in config properties to the API so that secret can be used in api call + ExampleApis api = new ExampleApis(config as Properties); + + // Send notification - NOTE: Never log full API responses as they may contain sensitive data + def response = api.post(apiKey) + logger.info("Notification sent successfully") + + return true; + } +} \ No newline at end of file diff --git a/examples/java-plugins/example-notification/src/main/groovy/com/plugin/examplenotification/Util.groovy b/examples/java-plugins/example-notification/src/main/groovy/com/plugin/examplenotification/Util.groovy new file mode 100644 index 0000000..aab7835 --- /dev/null +++ b/examples/java-plugins/example-notification/src/main/groovy/com/plugin/examplenotification/Util.groovy @@ -0,0 +1,27 @@ +package com.plugin.examplenotification; + +import org.rundeck.storage.api.PathUtil +import org.rundeck.storage.api.StorageException +import com.dtolabs.rundeck.core.storage.ResourceMeta +import com.dtolabs.rundeck.core.storage.StorageTree + +/** + * A “Util” class should be written to handle common methods for renderingOptions, retrieving keys from KeyStorage, + * auth-settings, and any other generic methods that can be used for support across your suite of plugins. + */ +class Util { + static String getPasswordFromKeyStorage(String path, StorageTree storage) { + try{ + ResourceMeta contents = storage.getResource(path).getContents() + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream() + contents.writeContent(byteArrayOutputStream) + String password = new String(byteArrayOutputStream.toByteArray()) + + return password + }catch(Exception e){ + throw StorageException.readException( + PathUtil.asPath(path), e.getMessage() + ) + } + } +} \ No newline at end of file diff --git a/examples/java-plugins/example-notification/src/main/resources/resources/icon.png b/examples/java-plugins/example-notification/src/main/resources/resources/icon.png new file mode 100644 index 0000000..7aaf7e8 Binary files /dev/null and b/examples/java-plugins/example-notification/src/main/resources/resources/icon.png differ diff --git a/examples/java-plugins/example-notification/src/test/groovy/com/plugin/examplenotification/ExampleNotificationSpec.groovy b/examples/java-plugins/example-notification/src/test/groovy/com/plugin/examplenotification/ExampleNotificationSpec.groovy new file mode 100644 index 0000000..89b57e5 --- /dev/null +++ b/examples/java-plugins/example-notification/src/test/groovy/com/plugin/examplenotification/ExampleNotificationSpec.groovy @@ -0,0 +1,71 @@ +package com.plugin.examplenotification + +import spock.lang.Specification +import org.rundeck.app.spi.Services +import org.rundeck.storage.api.Resource +import com.dtolabs.rundeck.core.storage.ResourceMeta +import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree + +class ExampleNotificationSpec extends Specification { + //Some Possible trigger names + public static final String TRIGGER_START = "start"; + public static final String TRIGGER_SUCCESS = "success"; + public static final String TRIGGER_FAILURE = "failure"; + + + Map sampleExecutionData() { + [ + id : 1, + href : 'http://example.com/dummy/execution/1', + status : 'succeeded', + user : 'rduser', + dateStarted : new Date(0), + 'dateStartedUnixtime' : 0, + 'dateStartedW3c' : '1970-01-01T00:00:00Z', + dateEnded : new Date(10000), + 'dateEndedUnixtime' : 10000, + 'dateEndedW3c' : '1970-01-01T00:00:10Z', + description : 'a job', + argstring : '-opt1 value', + project : 'rdproject1', + succeededNodeListString: 'nodea,nodeb', + succeededNodeList : ['nodea', 'nodeb'], + loglevel : 'INFO' + ] + } + + def "Post Notification basic success"() { + given: + + ExampleNotification plugin = new ExampleNotification(); + //TODO: set additional properties for your plugin + String trigger = TRIGGER_SUCCESS + + def executionData = sampleExecutionData() + def configuration = [apiKeyPath:"keys/apiKey"] + + //TODO: add mock implementations of any objects which your plugin uses, such as HTTP clients, etc. + def storageTree = Mock(KeyStorageTree) + storageTree.getResource(_) >> Mock(Resource) { + getContents() >> Mock(ResourceMeta) { + writeContent(_) >> { args -> + args[0].write('password'.bytes) + return 6L + } + } + } + def services = Mock(Services) { + getService(KeyStorageTree.class) >> storageTree + } + + plugin.setServices(services) + + when: + + def result = plugin.postNotification(trigger, executionData, configuration) + + then: + result + } + +} \ No newline at end of file diff --git a/examples/java-plugins/example-option/README.md b/examples/java-plugins/example-option/README.md new file mode 100644 index 0000000..519f2fe --- /dev/null +++ b/examples/java-plugins/example-option/README.md @@ -0,0 +1,4 @@ +# Example Option Rundeck Plugin + +This is a option plugin. + diff --git a/examples/java-plugins/example-option/build.gradle b/examples/java-plugins/example-option/build.gradle new file mode 100644 index 0000000..86be7ed --- /dev/null +++ b/examples/java-plugins/example-option/build.gradle @@ -0,0 +1,65 @@ +plugins { + id 'groovy' + id 'java' +} + +version = '0.1.0' +defaultTasks 'clean','build' +sourceCompatibility = 1.8 +ext.rundeckPluginVersion= '2.0' +ext.rundeckVersion= '5.7.0-20250101' +ext.pluginClassNames='com.plugin.exampleoption.ExampleOption' + +repositories { + mavenLocal() + mavenCentral() +} + +configurations{ + //declare custom pluginLibs configuration to include only libs for this plugin + pluginLibs + + //declare compile to extend from pluginLibs so it inherits the dependencies + implementation{ + extendsFrom pluginLibs + } +} + +dependencies { + implementation 'org.rundeck:rundeck-core:5.7.0-20250101' + + //use pluginLibs to add dependencies, example: + //pluginLibs group: 'com.google.code.gson', name: 'gson', version: '2.8.2' + + testImplementation 'junit:junit:4.12' + testImplementation "org.codehaus.groovy:groovy-all:3.0.21" + testImplementation "org.spockframework:spock-core:2.2-groovy-3.0" +} + +// task to copy plugin libs to output/lib dir +task copyToLib(type: Copy) { + into "$buildDir/output/lib" + from configurations.pluginLibs +} + +jar { + from "$buildDir/output" + manifest { + def libList = configurations.pluginLibs.collect{'lib/'+it.name}.join(' ') + + attributes 'Rundeck-Plugin-Classnames': pluginClassNames + attributes 'Rundeck-Plugin-File-Version': version + attributes 'Rundeck-Plugin-Name': 'Example Option' + attributes 'Rundeck-Plugin-Description': 'Provide a short description of your plugin here.' + attributes 'Rundeck-Plugin-Rundeck-Compatibility-Version': '3.x' + attributes 'Rundeck-Plugin-Tags': 'java,option' + attributes 'Rundeck-Plugin-License': 'Apache 2.0' + attributes 'Rundeck-Plugin-Source-Link': 'Please put the link to your source repo here' + attributes 'Rundeck-Plugin-Target-Host-Compatibility': 'all' + attributes 'Rundeck-Plugin-Version': rundeckPluginVersion + attributes 'Rundeck-Plugin-Archive': 'true' + attributes 'Rundeck-Plugin-Libs': "${libList}" + + } + dependsOn(copyToLib) +} diff --git a/examples/java-plugins/example-option/src/main/java/com/plugin/exampleoption/ExampleOption.java b/examples/java-plugins/example-option/src/main/java/com/plugin/exampleoption/ExampleOption.java new file mode 100644 index 0000000..6be9993 --- /dev/null +++ b/examples/java-plugins/example-option/src/main/java/com/plugin/exampleoption/ExampleOption.java @@ -0,0 +1,80 @@ +package com.plugin.exampleoption; + +import com.dtolabs.rundeck.core.plugins.Plugin; +import com.dtolabs.rundeck.core.plugins.configuration.Describable; +import com.dtolabs.rundeck.core.plugins.configuration.Description; +import com.dtolabs.rundeck.plugins.ServiceNameConstants; +import com.dtolabs.rundeck.plugins.descriptions.PluginDescription; +import com.dtolabs.rundeck.plugins.option.OptionValue; +import com.dtolabs.rundeck.plugins.option.OptionValuesPlugin; +import com.dtolabs.rundeck.plugins.util.DescriptionBuilder; +import com.dtolabs.rundeck.plugins.util.PropertyBuilder; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Plugin(service=ServiceNameConstants.OptionValues,name="example-option") +@PluginDescription(title="Example Option", description="My Option plugin description") +public class ExampleOption implements OptionValuesPlugin, Describable{ + + public static final String SERVICE_PROVIDER_NAME = "example-option"; + + /** + * Overriding this method gives the plugin a chance to take part in building the {@link + * com.dtolabs.rundeck.core.plugins.configuration.Description} presented by this plugin. This subclass can use the + * {@link DescriptionBuilder} to modify all aspects of the description, add or remove properties, etc. + */ + @Override + public Description getDescription() { + return DescriptionBuilder.builder() + .name(SERVICE_PROVIDER_NAME) + .title("Example Option") + .description("Example Workflow Step") + .property(PropertyBuilder.builder() + .string("example") + .title("Example String") + .description("Example description") + .required(false) + .build() + ) + .property(PropertyBuilder.builder() + .booleanType("exampleBoolean") + .title("Example Boolean") + .description("Example Boolean?") + .required(false) + .defaultValue("false") + .build() + ) + .build(); + } + + @Override + public List getOptionValues(final Map config) { + List options = new ArrayList<>(); + options.add(new StandardOptionValue("Alpha","alpha")); + options.add(new StandardOptionValue("Beta","beta")); + options.add(new StandardOptionValue("Gamma","gamma")); + return options; + } + + static class StandardOptionValue implements OptionValue { + + private String name; + private String value; + StandardOptionValue(String name, String value) { + this.name = name; + this.value = value; + } + @Override + public String getName() { + return name; + } + + @Override + public String getValue() { + return value; + } + } + +} \ No newline at end of file diff --git a/examples/java-plugins/example-option/src/main/resources/resources/icon.png b/examples/java-plugins/example-option/src/main/resources/resources/icon.png new file mode 100644 index 0000000..7aaf7e8 Binary files /dev/null and b/examples/java-plugins/example-option/src/main/resources/resources/icon.png differ diff --git a/examples/java-plugins/example-option/src/test/groovy/com/plugin/exampleoption/ExampleOptionSpec.groovy b/examples/java-plugins/example-option/src/test/groovy/com/plugin/exampleoption/ExampleOptionSpec.groovy new file mode 100644 index 0000000..0a8284d --- /dev/null +++ b/examples/java-plugins/example-option/src/test/groovy/com/plugin/exampleoption/ExampleOptionSpec.groovy @@ -0,0 +1,30 @@ +package com.plugin.exampleoption + +import com.dtolabs.rundeck.plugins.step.PluginStepContext +import com.dtolabs.rundeck.core.execution.workflow.steps.StepException +import com.dtolabs.rundeck.plugins.PluginLogger +import spock.lang.Specification + +class ExampleOptionSpec extends Specification { + + def getContext(PluginLogger logger){ + Mock(PluginStepContext){ + getLogger()>>logger + } + } + + def "get options"(){ + given: + + def example = new ExampleOption() + def configuration = [example:"example123",exampleBoolean:"false",] + + when: + def options = example.getOptionValues(configuration) + + then: + options.size() > 0 + } + + +} \ No newline at end of file diff --git a/examples/java-plugins/example-orchestrator/README.md b/examples/java-plugins/example-orchestrator/README.md new file mode 100644 index 0000000..68f75f3 --- /dev/null +++ b/examples/java-plugins/example-orchestrator/README.md @@ -0,0 +1,4 @@ +# Example Orchestrator Rundeck Plugin + +This is a orchestrator plugin. + diff --git a/examples/java-plugins/example-orchestrator/build.gradle b/examples/java-plugins/example-orchestrator/build.gradle new file mode 100644 index 0000000..546b2f7 --- /dev/null +++ b/examples/java-plugins/example-orchestrator/build.gradle @@ -0,0 +1,69 @@ +plugins { + id 'groovy' + id 'java' +} + +version = '0.1.0' +defaultTasks 'clean','build' +apply plugin: 'java' +apply plugin: 'groovy' +apply plugin: 'idea' +sourceCompatibility = 1.8 +ext.rundeckPluginVersion= '2.0' +ext.rundeckVersion= '5.7.0-20250101' +ext.pluginClassNames='com.plugin.exampleorchestrator.ExampleOrchestrator' + + +repositories { + mavenLocal() + mavenCentral() +} + +configurations{ + //declare custom pluginLibs configuration to include only libs for this plugin + pluginLibs + + //declare compile to extend from pluginLibs so it inherits the dependencies + implementation{ + extendsFrom pluginLibs + } +} + +dependencies { + implementation 'org.rundeck:rundeck-core:5.7.0-20250101' + + //use pluginLibs to add dependencies, example: + //pluginLibs group: 'com.google.code.gson', name: 'gson', version: '2.8.2' + + testImplementation 'junit:junit:4.12' + testImplementation "org.codehaus.groovy:groovy-all:3.0.21" + testImplementation "org.spockframework:spock-core:2.2-groovy-3.0" +} + +// task to copy plugin libs to output/lib dir +task copyToLib(type: Copy) { + into "$buildDir/output/lib" + from configurations.pluginLibs +} + +jar { + from "$buildDir/output" + manifest { + def libList = configurations.pluginLibs.collect{'lib/'+it.name}.join(' ') + + attributes 'Rundeck-Plugin-Classnames': pluginClassNames + attributes 'Rundeck-Plugin-File-Version': version + attributes 'Rundeck-Plugin-Name': 'Example Orchestrator' + attributes 'Rundeck-Plugin-Description': 'Provide a short description of your plugin here.' + attributes 'Rundeck-Plugin-Rundeck-Compatibility-Version': '3.x' + attributes 'Rundeck-Plugin-Tags': 'java,orchestrator' + attributes 'Rundeck-Plugin-License': 'Apache 2.0' + attributes 'Rundeck-Plugin-Source-Link': 'Please put the link to your source repo here' + attributes 'Rundeck-Plugin-Target-Host-Compatibility': 'all' + attributes 'Rundeck-Plugin-Version': rundeckPluginVersion + attributes 'Rundeck-Plugin-Archive': 'true' + attributes 'Rundeck-Plugin-Libs': "${libList}" + + } + dependsOn(copyToLib) +} diff --git a/examples/java-plugins/example-orchestrator/src/main/java/com/plugin/exampleorchestrator/ExampleOrchestrator.java b/examples/java-plugins/example-orchestrator/src/main/java/com/plugin/exampleorchestrator/ExampleOrchestrator.java new file mode 100644 index 0000000..cb3f303 --- /dev/null +++ b/examples/java-plugins/example-orchestrator/src/main/java/com/plugin/exampleorchestrator/ExampleOrchestrator.java @@ -0,0 +1,35 @@ +package com.plugin.exampleorchestrator; + +import com.dtolabs.rundeck.core.common.INodeEntry; +import com.dtolabs.rundeck.core.execution.workflow.StepExecutionContext; +import com.dtolabs.rundeck.core.plugins.Plugin; +import com.dtolabs.rundeck.plugins.ServiceNameConstants; +import com.dtolabs.rundeck.plugins.descriptions.PluginDescription; +import com.dtolabs.rundeck.plugins.descriptions.PluginProperty; +import com.dtolabs.rundeck.plugins.orchestrator.Orchestrator; +import com.dtolabs.rundeck.plugins.orchestrator.OrchestratorPlugin; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; + +@Plugin(service=ServiceNameConstants.Orchestrator,name="example-orchestrator") +@PluginDescription(title="Example Orchestrator", description="My Orchestrator plugin description") +public class ExampleOrchestrator implements OrchestratorPlugin{ + + @PluginProperty(title = "Count", description = "Number of nodes to select from the pool", defaultValue = "1") + protected int count; + + @Override + public Orchestrator createOrchestrator(StepExecutionContext context, Collection nodes) { + return new ExampleOrchestratorOrchestrator(count, context, nodes); + } + + +} \ No newline at end of file diff --git a/examples/java-plugins/example-orchestrator/src/main/java/com/plugin/exampleorchestrator/ExampleOrchestratorOrchestrator.java b/examples/java-plugins/example-orchestrator/src/main/java/com/plugin/exampleorchestrator/ExampleOrchestratorOrchestrator.java new file mode 100644 index 0000000..6530dff --- /dev/null +++ b/examples/java-plugins/example-orchestrator/src/main/java/com/plugin/exampleorchestrator/ExampleOrchestratorOrchestrator.java @@ -0,0 +1,69 @@ +package com.plugin.exampleorchestrator; + +import com.dtolabs.rundeck.core.common.INodeEntry; +import com.dtolabs.rundeck.core.execution.workflow.StepExecutionContext; +import com.dtolabs.rundeck.core.execution.workflow.steps.node.NodeStepResult; +import com.dtolabs.rundeck.plugins.orchestrator.Orchestrator; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Random; + +/** + * Selects a random subset of the nodes + */ +public class ExampleOrchestratorOrchestrator implements Orchestrator { + + Random random; + final int count; + List nodes; + + public ExampleOrchestratorOrchestrator( + int count, + StepExecutionContext context, + Collection nodes + ) + { + this.random = new Random(); + this.count = count; + this.nodes = select(count, nodes); + } + + /** + * Select count random items from the input nodes, or if nodes is smaller than count, reorders them + * @param count number of nodes + * @param nodes input nodes + * @return list of count nodes + */ + private List select(final int count, final Collection nodes) { + List source = new ArrayList<>(nodes); + List selected = new ArrayList<>(); + int total = Math.min(count, nodes.size()); + for (int i = 0; i < total; i++) { + selected.add(source.remove(random.nextInt(source.size()))); + } + return selected; + } + + + @Override + public INodeEntry nextNode() { + if (nodes.size() > 0) { + return nodes.remove(0); + } else { + return null; + } + } + + @Override + public void returnNode( final INodeEntry node, final boolean success, final NodeStepResult result) + { + + } + + @Override + public boolean isComplete() { + return nodes.size() == 0; + } +} \ No newline at end of file diff --git a/examples/java-plugins/example-orchestrator/src/main/resources/resources/icon.png b/examples/java-plugins/example-orchestrator/src/main/resources/resources/icon.png new file mode 100644 index 0000000..7aaf7e8 Binary files /dev/null and b/examples/java-plugins/example-orchestrator/src/main/resources/resources/icon.png differ diff --git a/examples/java-plugins/example-orchestrator/src/test/groovy/com/plugin/exampleorchestrator/ExampleOrchestratorSpec.groovy b/examples/java-plugins/example-orchestrator/src/test/groovy/com/plugin/exampleorchestrator/ExampleOrchestratorSpec.groovy new file mode 100644 index 0000000..770f468 --- /dev/null +++ b/examples/java-plugins/example-orchestrator/src/test/groovy/com/plugin/exampleorchestrator/ExampleOrchestratorSpec.groovy @@ -0,0 +1,43 @@ +package com.plugin.exampleorchestrator + +import com.dtolabs.rundeck.core.common.INodeEntry +import com.dtolabs.rundeck.core.common.NodeEntryImpl +import com.dtolabs.rundeck.core.execution.workflow.StepExecutionContext +import spock.lang.Specification + +class ExampleOrchestratorSpec extends Specification { + + private INodeEntry create(String hostname, String rankAttr=null, String rankValue=null){ + INodeEntry iNodeEntry = NodeEntryImpl.create(hostname, hostname); + + if(null!=rankAttr) { + iNodeEntry.getAttributes().put(rankAttr, rankValue); + } + return iNodeEntry; + } + + def "simple test"() { + + given: + INodeEntry node1 = create("Centos6", "memory", "1048"); + INodeEntry node2 = create("Centos7", "memory", "2048"); + INodeEntry node3 = create("Windows2016", "memory", "10240"); + INodeEntry node4 = create("Windows10", "memory", "4096"); + + List nodes = Arrays.asList(node1, node2, node3, node4); + + def count=1 + + StepExecutionContext context=Mock(StepExecutionContext) + + when: + def orchestrator=new ExampleOrchestratorOrchestrator(count,context,nodes) + + then: + orchestrator.nextNode()?.hostname!=null + orchestrator.nextNode()?.hostname==null + + } + + +} diff --git a/examples/java-plugins/example-resource-model-source/README.md b/examples/java-plugins/example-resource-model-source/README.md new file mode 100644 index 0000000..bdcde83 --- /dev/null +++ b/examples/java-plugins/example-resource-model-source/README.md @@ -0,0 +1,4 @@ +# Example Resource Model Source Rundeck Plugin + +This is a resource model plugin. + diff --git a/examples/java-plugins/example-resource-model-source/build.gradle b/examples/java-plugins/example-resource-model-source/build.gradle new file mode 100644 index 0000000..afb34ef --- /dev/null +++ b/examples/java-plugins/example-resource-model-source/build.gradle @@ -0,0 +1,69 @@ +plugins { + id 'groovy' + id 'java' +} + +version = '0.1.0' +defaultTasks 'clean','build' +apply plugin: 'java' +apply plugin: 'groovy' +apply plugin: 'idea' +sourceCompatibility = 11.0 +ext.rundeckPluginVersion= '2.0' +ext.rundeckVersion= '5.7.0-20250101' +ext.pluginClassNames='com.plugin.exampleresourcemodelsource.ExampleResourceModelSourceFactory' + + +repositories { + mavenLocal() + mavenCentral() +} + +configurations{ + //declare custom pluginLibs configuration to include only libs for this plugin + pluginLibs + + //declare compile to extend from pluginLibs so it inherits the dependencies + implementation{ + extendsFrom pluginLibs + } +} + +dependencies { + implementation 'org.rundeck:rundeck-core:5.7.0-20250101' + implementation 'org.codehaus.groovy:groovy-all:3.0.21' + + //use pluginLibs to add dependencies, example: + + testImplementation 'junit:junit:4.12' + testImplementation "org.codehaus.groovy:groovy-all:3.0.21" + testImplementation "org.spockframework:spock-core:2.2-groovy-3.0" +} + +// task to copy plugin libs to output/lib dir +task copyToLib(type: Copy) { + into "$buildDir/output/lib" + from configurations.pluginLibs +} + +jar { + from "$buildDir/output" + manifest { + def libList = configurations.pluginLibs.collect{'lib/'+it.name}.join(' ') + + attributes 'Rundeck-Plugin-Classnames': pluginClassNames + attributes 'Rundeck-Plugin-File-Version': version + attributes 'Rundeck-Plugin-Name': 'Example Resource Model Source' + attributes 'Rundeck-Plugin-Description': 'Provide a short description of your plugin here.' + attributes 'Rundeck-Plugin-Rundeck-Compatibility-Version': '3.x' + attributes 'Rundeck-Plugin-Tags': 'java,resourceModel' + attributes 'Rundeck-Plugin-License': 'Apache 2.0' + attributes 'Rundeck-Plugin-Source-Link': 'Please put the link to your source repo here' + attributes 'Rundeck-Plugin-Target-Host-Compatibility': 'all' + attributes 'Rundeck-Plugin-Version': rundeckPluginVersion + attributes 'Rundeck-Plugin-Archive': 'true' + attributes 'Rundeck-Plugin-Libs': "${libList}" + + } + dependsOn(copyToLib) +} diff --git a/examples/java-plugins/example-resource-model-source/src/main/groovy/com/plugin/exampleresourcemodelsource/ExampleResourceModelSource.groovy b/examples/java-plugins/example-resource-model-source/src/main/groovy/com/plugin/exampleresourcemodelsource/ExampleResourceModelSource.groovy new file mode 100644 index 0000000..467d811 --- /dev/null +++ b/examples/java-plugins/example-resource-model-source/src/main/groovy/com/plugin/exampleresourcemodelsource/ExampleResourceModelSource.groovy @@ -0,0 +1,74 @@ +package com.plugin.exampleresourcemodelsource; + +import com.dtolabs.rundeck.core.common.INodeSet +import com.dtolabs.rundeck.core.common.NodeEntryImpl +import com.dtolabs.rundeck.core.common.NodeSetImpl +import com.dtolabs.rundeck.core.resources.ResourceModelSource +import com.dtolabs.rundeck.core.resources.ResourceModelSourceException +import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree +import org.rundeck.app.spi.Services + +import groovy.json.JsonSlurper + +class ExampleResourceModelSource implements ResourceModelSource{ + + //Properties object to hold the configuration from the plugin properties + //Services object used for retrieving secrets from KeyStorage + Properties configuration; + Services services; + + public ExampleResourceModelSource(Properties configuration, Services services) { + this.configuration = configuration; + this.services = services + } + + @Override + public INodeSet getNodes() throws ResourceModelSourceException { + + String tags=configuration.getProperty("tags"); + + //Optional: if a secret was needed from KeyStorage, it would be retrieved like this: + KeyStorageTree keyStorage = services.getService(KeyStorageTree.class) + String apiKeyPath = configuration.getProperty("apiKeyPath") + String apiKey = Util.getPasswordFromKeyStorage(apiKeyPath, keyStorage) + + //This is the object for the collection of nodes + final NodeSetImpl nodeSet = new NodeSetImpl(); + + //Let's say we have a collection of nodes returned to us from an API call or other source: + String nodes = ''' + {"nodes": + [ + {"name":"host1","hostname":"10.0.0.1","properties":[{"username":"rundeck","os":"windows"}]}, + {"name":"host2","hostname":"10.0.0.2","properties":[{"username":"rundeck","os":"linux"}]}, + {"name":"host3","hostname":"10.0.0.3","properties":[{"username":"rundeck","os":"linux"}]} + ] + } + ''' + + //Parse the JSON and then loop through them to add them and their properties to the NodeSet + def parser = new JsonSlurper() + def jsonNodes = parser.parseText(nodes) + + for (node in jsonNodes["nodes"]) { + + NodeEntryImpl nodeEntry = new NodeEntryImpl(); + + //Set the node name and hostname + nodeEntry.setNodename(node["name"] as String) + nodeEntry.setHostname(node["hostname"] as String) + + nodeEntry.setAttribute("username", node.properties[0]["username"] as String) + nodeEntry.setAttribute("os", node.properties[0]["os"] as String) + + //Set the tags from the configuration property + HashSet tagset = new HashSet<>(); + tagset.add(tags); + nodeEntry.setTags(tagset); + + nodeSet.putNode(nodeEntry); + } + + return nodeSet; + } +} \ No newline at end of file diff --git a/examples/java-plugins/example-resource-model-source/src/main/groovy/com/plugin/exampleresourcemodelsource/ExampleResourceModelSourceFactory.groovy b/examples/java-plugins/example-resource-model-source/src/main/groovy/com/plugin/exampleresourcemodelsource/ExampleResourceModelSourceFactory.groovy new file mode 100644 index 0000000..946ac4b --- /dev/null +++ b/examples/java-plugins/example-resource-model-source/src/main/groovy/com/plugin/exampleresourcemodelsource/ExampleResourceModelSourceFactory.groovy @@ -0,0 +1,81 @@ +package com.plugin.exampleresourcemodelsource; + +import com.dtolabs.rundeck.core.resources.ResourceModelSource; +import com.dtolabs.rundeck.core.resources.ResourceModelSourceFactory; +import com.dtolabs.rundeck.plugins.ServiceNameConstants +import com.dtolabs.rundeck.core.plugins.Plugin; +import com.dtolabs.rundeck.core.plugins.configuration.Description; +import com.dtolabs.rundeck.plugins.descriptions.PluginDescription +import com.dtolabs.rundeck.plugins.descriptions.PluginProperty +import com.dtolabs.rundeck.plugins.descriptions.RenderingOption +import com.dtolabs.rundeck.plugins.descriptions.RenderingOptions +import com.dtolabs.rundeck.core.plugins.configuration.StringRenderingConstants +import com.dtolabs.rundeck.core.plugins.configuration.ConfigurationException; +import com.dtolabs.rundeck.core.plugins.configuration.PropertyUtil; +import com.dtolabs.rundeck.plugins.util.DescriptionBuilder; +import org.rundeck.app.spi.Services +import static com.dtolabs.rundeck.core.plugins.configuration.StringRenderingConstants.GROUP_NAME + +@Plugin(name = ExampleResourceModelSourceFactory.PLUGIN_NAME, service=ServiceNameConstants.ResourceModelSource) +@PluginDescription(title = ExampleResourceModelSourceFactory.PLUGIN_TITLE, description = ExampleResourceModelSourceFactory.PLUGIN_DESCRIPTION) +public class ExampleResourceModelSourceFactory implements ResourceModelSourceFactory { + + public static final String PLUGIN_NAME = "example-resource-model-source" + public static final String PLUGIN_TITLE = "Example Resource Model Source" + public static final String PLUGIN_DESCRIPTION = "Test Resource Model"; + + /** + * Overriding this method gives the plugin a chance to take part in building the {@link + * com.dtolabs.rundeck.core.plugins.configuration.Description} presented by this plugin. This subclass can use the + * {@link DescriptionBuilder} to modify all aspects of the description, add or remove properties, etc. + */ + @PluginProperty( + title = "Tags", + description = "Custom Tags example.", + defaultValue = "custom tags", + required = false + ) + @RenderingOption(key = GROUP_NAME, value = "Configuration") + String tags + + @PluginProperty( + title = "API Key Path", + description = 'REQUIRED: The path to the Key Storage entry for your API Key.\n If an error of `Unauthorized` occurs, be sure to add the proper policy to ACLs.', + required = true + ) + @RenderingOptions([ + @RenderingOption( + key = StringRenderingConstants.SELECTION_ACCESSOR_KEY, + value = "STORAGE_PATH" + ), + @RenderingOption( + key = StringRenderingConstants.STORAGE_PATH_ROOT_KEY, + value = "keys" + ), + @RenderingOption( + key = StringRenderingConstants.STORAGE_FILE_META_FILTER_KEY, + value = "Rundeck-data-type=password" + ), + @RenderingOption( + key = StringRenderingConstants.GROUP_NAME, + value = "API Configuration" + ) + ]) + String apiKeyPath + + @Override + ResourceModelSource createResourceModelSource(Properties configuration) throws ConfigurationException { + + //We implement this method with just the Properties input because it is required by the interface, but we don't use it. + //Instead, we use the other method that receives a Services object, which is the one that we need to use in order to access the Key Storage service. + null + } + + @Override + ResourceModelSource createResourceModelSource(Services services, Properties properties) throws ConfigurationException { + + def resource = new ExampleResourceModelSource(properties, services) + return resource; + } + +} \ No newline at end of file diff --git a/examples/java-plugins/example-resource-model-source/src/main/groovy/com/plugin/exampleresourcemodelsource/Util.groovy b/examples/java-plugins/example-resource-model-source/src/main/groovy/com/plugin/exampleresourcemodelsource/Util.groovy new file mode 100644 index 0000000..4cdc77c --- /dev/null +++ b/examples/java-plugins/example-resource-model-source/src/main/groovy/com/plugin/exampleresourcemodelsource/Util.groovy @@ -0,0 +1,28 @@ +package com.plugin.exampleresourcemodelsource; + +import org.rundeck.storage.api.PathUtil +import org.rundeck.storage.api.StorageException +import com.dtolabs.rundeck.core.storage.ResourceMeta +import com.dtolabs.rundeck.plugins.step.PluginStepContext +import com.dtolabs.rundeck.core.storage.StorageTree + +/** + * A “Util” class should be written to handle common methods for renderingOptions, retrieving keys from KeyStorage, + * auth-settings, and any other generic methods that can be used for support across your suite of plugins. + */ +class Util { + static String getPasswordFromKeyStorage(String path, StorageTree storage) { + try{ + ResourceMeta contents = storage.getResource(path).getContents() + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream() + contents.writeContent(byteArrayOutputStream) + String password = new String(byteArrayOutputStream.toByteArray()) + + return password + }catch(Exception e){ + throw StorageException.readException( + PathUtil.asPath(path), e.getMessage() + ) + } + } +} \ No newline at end of file diff --git a/examples/java-plugins/example-resource-model-source/src/main/resources/resources/icon.png b/examples/java-plugins/example-resource-model-source/src/main/resources/resources/icon.png new file mode 100644 index 0000000..7aaf7e8 Binary files /dev/null and b/examples/java-plugins/example-resource-model-source/src/main/resources/resources/icon.png differ diff --git a/examples/java-plugins/example-resource-model-source/src/test/groovy/com/plugin/exampleresourcemodelsource/ExampleResourceModelSourceFactorySpec.groovy b/examples/java-plugins/example-resource-model-source/src/test/groovy/com/plugin/exampleresourcemodelsource/ExampleResourceModelSourceFactorySpec.groovy new file mode 100644 index 0000000..2ce9e09 --- /dev/null +++ b/examples/java-plugins/example-resource-model-source/src/test/groovy/com/plugin/exampleresourcemodelsource/ExampleResourceModelSourceFactorySpec.groovy @@ -0,0 +1,46 @@ +package com.plugin.exampleresourcemodelsource + +import spock.lang.Specification +import org.rundeck.app.spi.Services +import org.rundeck.storage.api.Resource +import com.dtolabs.rundeck.core.storage.ResourceMeta +import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree + + +class ExampleResourceModelSourceFactorySpec extends Specification { + + def "retrieve resource success"(){ + given: + //TODO: set additional properties for your plugin + Properties configuration = new Properties() + configuration.put("tags","example") + configuration.put("apiKeyPath","keys/api-key") + + def storageTree = Mock(KeyStorageTree) + storageTree.getResource(_) >> Mock(Resource) { + getContents() >> Mock(ResourceMeta) { + writeContent(_) >> { args -> + args[0].write('password'.bytes) + return 6L + } + } + } + def services = Mock(Services) { + getService(KeyStorageTree.class) >> storageTree + } + + //def factory = new ExampleResourceModelSourceFactory() + + def vmList = ["node1","node2","node3"] + + when: + // def result = factory.createResourceModelSource(services, configuration) + ExampleResourceModelSource plugin = new ExampleResourceModelSource(configuration, services) + def nodes = plugin.getNodes() + + then: + nodes.size()==vmList.size() + } + + +} \ No newline at end of file diff --git a/examples/java-plugins/example-workflow-node-step/README.md b/examples/java-plugins/example-workflow-node-step/README.md new file mode 100644 index 0000000..2bbd97c --- /dev/null +++ b/examples/java-plugins/example-workflow-node-step/README.md @@ -0,0 +1,3 @@ +# Example Workflow Node Step Rundeck Plugin + +This is a template node step plugin that was build using the [rundeck-plugin-bootstrap](https://github.com/rundeck/plugin-bootstrap) diff --git a/examples/java-plugins/example-workflow-node-step/build.gradle b/examples/java-plugins/example-workflow-node-step/build.gradle new file mode 100644 index 0000000..8389fbc --- /dev/null +++ b/examples/java-plugins/example-workflow-node-step/build.gradle @@ -0,0 +1,70 @@ +plugins { + id 'groovy' + id 'java' +} + +version = '0.1.0' +defaultTasks 'clean','build' +apply plugin: 'java' +apply plugin: 'groovy' +apply plugin: 'idea' +sourceCompatibility = 11.0 +ext.rundeckPluginVersion= '2.0' +ext.rundeckVersion= '5.7.0-20250101' +ext.pluginClassNames='com.plugin.exampleworkflownodestep.ExampleWorkflowNodeStep' + + +repositories { + mavenLocal() + mavenCentral() +} + +configurations{ + //declare custom pluginLibs configuration to include only libs for this plugin + pluginLibs + + //declare compile to extend from pluginLibs so it inherits the dependencies + implementation{ + extendsFrom pluginLibs + } +} + +dependencies { + implementation 'org.rundeck:rundeck-core:5.7.0-20250101' + implementation 'org.codehaus.groovy:groovy-all:3.0.21' + + //use pluginLibs to add dependecies, example: + //pluginLibs group: 'com.google.code.gson', name: 'gson', version: '2.8.2' + + testImplementation 'junit:junit:4.12' + testImplementation "org.codehaus.groovy:groovy-all:3.0.21" + testImplementation "org.spockframework:spock-core:2.2-groovy-3.0" +} + +// task to copy plugin libs to output/lib dir +task copyToLib(type: Copy) { + into "$buildDir/output/lib" + from configurations.pluginLibs +} + +jar { + from "$buildDir/output" + manifest { + def libList = configurations.pluginLibs.collect{'lib/'+it.name}.join(' ') + + attributes 'Rundeck-Plugin-Classnames': pluginClassNames + attributes 'Rundeck-Plugin-File-Version': version + attributes 'Rundeck-Plugin-Name': 'Example Workflow Node Step' + attributes 'Rundeck-Plugin-Description': 'Provide a short description of your plugin here.' + attributes 'Rundeck-Plugin-Rundeck-Compatibility-Version': '3.x' + attributes 'Rundeck-Plugin-Tags': 'java,NodeStep' + attributes 'Rundeck-Plugin-License': 'Apache 2.0' + attributes 'Rundeck-Plugin-Source-Link': 'Please put the link to your source repo here' + attributes 'Rundeck-Plugin-Target-Host-Compatibility': 'all' + attributes 'Rundeck-Plugin-Version': rundeckPluginVersion + attributes 'Rundeck-Plugin-Archive': 'true' + attributes 'Rundeck-Plugin-Libs': "${libList}" + + } + dependsOn(copyToLib) +} diff --git a/examples/java-plugins/example-workflow-node-step/src/main/groovy/com/plugin/exampleworkflownodestep/Constants.groovy b/examples/java-plugins/example-workflow-node-step/src/main/groovy/com/plugin/exampleworkflownodestep/Constants.groovy new file mode 100644 index 0000000..0bb7568 --- /dev/null +++ b/examples/java-plugins/example-workflow-node-step/src/main/groovy/com/plugin/exampleworkflownodestep/Constants.groovy @@ -0,0 +1,10 @@ +package com.plugin.exampleworkflownodestep; + +/** + * If other functions are required for purposes of modularity or clarity, they should either be added to a Util Class + * (if generic enough), or a PluginHelper Class that is accessible to the Plugin Class. + */ +class Constants { + public static final String BASE_API_URL = "http://localhost:4440/api/" + public static final String API_VERSION = "41" +} \ No newline at end of file diff --git a/examples/java-plugins/example-workflow-node-step/src/main/groovy/com/plugin/exampleworkflownodestep/ExampleApis.groovy b/examples/java-plugins/example-workflow-node-step/src/main/groovy/com/plugin/exampleworkflownodestep/ExampleApis.groovy new file mode 100644 index 0000000..cea1a30 --- /dev/null +++ b/examples/java-plugins/example-workflow-node-step/src/main/groovy/com/plugin/exampleworkflownodestep/ExampleApis.groovy @@ -0,0 +1,88 @@ +package com.plugin.exampleworkflownodestep; + +import okhttp3.Headers +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response + +/** + * If other functions are required for purposes of modularity or clarity, they should either be added to a Util Class + * (if generic enough), or a PluginHelper Class that is accessible to the Plugin Class. + */ +class ExampleApis { + String userRundeckBaseApiUrl + String userRundeckApiVersion + Headers headers + OkHttpClient client + + ExampleApis(String userBaseApiUrl, String userApiVersion, String userAuthToken) { + this.client = new OkHttpClient() + this.headers = new Headers.Builder() + .add('Accept', 'application/json') + .add('Content-Type', 'application/json') + .add('X-Rundeck-Auth-Token', userAuthToken) + .build() + + if (!userBaseApiUrl) { + userRundeckBaseApiUrl = Constants.BASE_API_URL + } else { + userRundeckBaseApiUrl = userBaseApiUrl + } + + if (!userApiVersion) { + userRundeckApiVersion = Constants.API_VERSION + } else { + userRundeckApiVersion = userApiVersion + } + } + + /** + * Requests info on a single node by name, from a given Rundeck project by name. + * https://docs.rundeck.com/docs/api/rundeck-api.html#getting-resource-info + */ + String getResourceInfoByName( + String projectName, + String nodeName + ) throws IOException { + String resourceUrl = "/project/" + projectName + "/resource/" + nodeName + String fullUrl = createFullUrl(userRundeckBaseApiUrl, userRundeckApiVersion, resourceUrl) + + Request request = new Request.Builder() + .url(fullUrl) + .headers(headers) + .build() + + try (Response response = client.newCall(request).execute()) { + return response.body().string(); + } + } + + /** + * Requests info on a Rundeck project by name. + * https://docs.rundeck.com/docs/api/rundeck-api.html#getting-project-info + */ + String getProjectInfoByName( + String projectName + ) throws IOException { + String resourceUrl = "/project/" + projectName + String fullUrl = createFullUrl(userRundeckBaseApiUrl, userRundeckApiVersion, resourceUrl) + + Request request = new Request.Builder() + .url(fullUrl) + .headers(headers) + .build() + + try (Response response = client.newCall(request).execute()) { + return response.body().string(); + } + } + + private static String createFullUrl(String baseApiUrl, String apiVersion, String apiPath) { + + // Handle for user trailing forward slash + if(baseApiUrl.endsWith("/")) { + baseApiUrl = baseApiUrl.substring(0, baseApiUrl.length() - 1) + } + return baseApiUrl + "/" + apiVersion + "/" + apiPath + } +} \ No newline at end of file diff --git a/examples/java-plugins/example-workflow-node-step/src/main/groovy/com/plugin/exampleworkflownodestep/ExampleWorkflowNodeStep.groovy b/examples/java-plugins/example-workflow-node-step/src/main/groovy/com/plugin/exampleworkflownodestep/ExampleWorkflowNodeStep.groovy new file mode 100644 index 0000000..66b6986 --- /dev/null +++ b/examples/java-plugins/example-workflow-node-step/src/main/groovy/com/plugin/exampleworkflownodestep/ExampleWorkflowNodeStep.groovy @@ -0,0 +1,220 @@ +package com.plugin.exampleworkflownodestep; + +/** + * Dependencies: + * any Java SDK must be officially recognized by the vendor for that technology + * (e.g. AWS Java SDK, SumoLogic, Zendesk) and show reasonably recent (within past year) development. Any SDK used must + * have an open source license such as Apache-2 or MIT. + */ + +import com.dtolabs.rundeck.core.common.INodeEntry +import com.dtolabs.rundeck.core.execution.workflow.steps.node.NodeStepException +import com.dtolabs.rundeck.core.plugins.Plugin +import com.dtolabs.rundeck.core.plugins.configuration.StringRenderingConstants +import com.dtolabs.rundeck.plugins.ServiceNameConstants +import com.dtolabs.rundeck.plugins.step.NodeStepPlugin +import com.dtolabs.rundeck.plugins.step.PluginStepContext +import com.dtolabs.rundeck.plugins.descriptions.PluginDescription +import com.dtolabs.rundeck.plugins.descriptions.PluginProperty +import com.dtolabs.rundeck.plugins.descriptions.RenderingOption +import com.dtolabs.rundeck.plugins.descriptions.RenderingOptions +import groovy.json.JsonBuilder +import groovy.json.JsonOutput +import org.rundeck.storage.api.StorageException +import com.dtolabs.rundeck.core.execution.ExecutionListener +import static com.dtolabs.rundeck.core.plugins.configuration.StringRenderingConstants.GROUPING +import static com.dtolabs.rundeck.core.plugins.configuration.StringRenderingConstants.GROUP_NAME + +/** +* ExampleNodeStepPlugin demonstrates a basic {@link com.dtolabs.rundeck.plugins.step.NodeStepPlugin}, and how to +* programmatically build all of the plugin's Properties exposed in the GUI. +*

+* The plugin class is annotated with {@link Plugin} to define the service and name of this service provider plugin. +*

+* The provider name of this plugin is statically defined in the class. The service name makes use of {@link +* ServiceNameConstants} to provide the known Rundeck service names. +*/ +@Plugin(name = PLUGIN_NAME, service = ServiceNameConstants.WorkflowNodeStep) +@PluginDescription(title = PLUGIN_TITLE, description = PLUGIN_DESCRIPTION) +class ExampleWorkflowNodeStep implements NodeStepPlugin { + /** + * Define a name used to identify your plugin. It is a good idea to use a fully qualified package-style name. + */ + public static final String PLUGIN_NAME = "example-workflow-node-step" + public static final String PLUGIN_TITLE = "Example Workflow Node Step" + public static final String PLUGIN_DESCRIPTION = "Template Node Step plugin that makes a call to an API and retrieves a response." + + Map meta = Collections.singletonMap("content-data-type", "application/json") + ExampleApis exapis + + /** + * Plugin Properties must: + * * be laid out at the top of the Plugin class, just after any class/instance variables. + * * be intuitive for the user to understand, and inform the end-user what is expected for that field. + * * follow the method/conventions of renderingOptions below + * * use KeyStorage for storage/retrieval of secrets. See 'API Key Path' property below. + */ + @PluginProperty( + title = "API URL", + description = """Provide the base URL for the API to connect to. It will be used by the plugin to get information from the API. If left blank, the call will use a default base API URL. + + +When carriage returns are used in the description, any part of the string after them—such as this—will also be collapsed. **Markdown** can also be used in this _expanded_ block. + + +Want to learn more about the Rundeck API? Check out [our docs](https://docs.rundeck.com/docs/api/rundeck-api.html).""", + defaultValue = Constants.BASE_API_URL, + required = false + ) + @RenderingOptions( + [ + @RenderingOption(key = GROUP_NAME, value = "API Configuration") + ] + ) + String userBaseApiUrl + + /** + * Here, we're requesting an integer, which will restrict this field in the GUI to only accept integers. + */ + @PluginProperty( + title = "API Version", + description = "Overrides the API version used to make the call. If left blank, the call will use a default API version.", + defaultValue = Constants.API_VERSION, + required = false + ) + @RenderingOption(key = GROUP_NAME, value = "API Configuration") + Integer userApiVersion + + /** + * Here we're requesting the user provides the path to the API key in Key Storage. + * For security and accessibility, any secure strings of information should always be saved into Key Storage. That includes + * tokens, passwords, certificates, or any other authentication information. + * Here, we're setting up the RenderingOptions to display this as a field for keys of the 'password' type (Rundeck-data-type=password). + * The value of this property will only be a path to the necessary key. You'll see how the actual key is resolved below. + */ + @PluginProperty( + title = "API Key Path", + description = "REQUIRED: The path to the Key Storage entry for your API Key.", + required = true + ) + @RenderingOptions([ + @RenderingOption( + key = StringRenderingConstants.SELECTION_ACCESSOR_KEY, + value = "STORAGE_PATH" + ), + @RenderingOption( + key = StringRenderingConstants.STORAGE_PATH_ROOT_KEY, + value = "keys" + ), + @RenderingOption( + key = StringRenderingConstants.STORAGE_FILE_META_FILTER_KEY, + value = "Rundeck-data-type=password" + ), + @RenderingOption( + key = StringRenderingConstants.GROUP_NAME, + value = "API Configuration" + ) + ]) + String apiKeyPath + + @PluginProperty( + title = "Collapsed test value", + description = """This is another test property to be output at the end of the execution.By default, it will be collapsed in the list of properties, thanks to the '@RenderingOption' 'GROUPING' key being set to 'secondary'.""", + required = false + ) + @RenderingOption(key = GROUP_NAME, value = "Collapsed Configuration") + /** The secondary grouping RenderingOption is what collapses the field by default in the GUI */ + @RenderingOption(key = GROUPING, value = "secondary") + String hiddenTestValue + + /** + * Plugins should make good use of logging and log levels in order to provide the user with the right amount + * of information on execution. Use 'context.getExecutionContext().getExecutionListener().log' to handle logging. + * Any failure in the execution should be caught and thrown as a NodeStepException + * NodeStepExceptions require a message, FailureReason, and node name to be provided + * @param context + * @param configuration + * @param entry + * @throws NodeStepException + */ + @Override + void executeNodeStep(final PluginStepContext context, + final Map configuration, + final INodeEntry entry) throws NodeStepException { + + /** + * We'll resolve the name of the current project and node. We'll use them to make an + * API GET request. + */ + String projectName = context.getFrameworkProject() + String currentNodeName = entry.getNodename() + String resourceInfo + String userApiVersionString = null + String userApiKey + + /** + * Next, we'll resolve the API token itself. There's a perfect function for this in the Util class, + * getPasswordFromKeyStorage. You can see more about how the process works in the Util file. + */ + try { + userApiKey = Util.getPasswordFromKeyStorage(apiKeyPath, context) + } catch (StorageException e) { + throw new NodeStepException( + 'Error accessing ${apiKeyPath}:' + e.getMessage(), + PluginFailureReason.KeyStorageError, + entry.getNodename() + ) + } + + /** + * The preferred method of logging is to write into, and then print out, + * the executionContext log. First, we add to our logging object from before. + */ + ExecutionListener logger = context.getExecutionContext().getExecutionListener() + + logger.log(3, "Here is a single line log entry. We'll print this as a logLevel 2, along with our next log lines.") + logger.log(3, "Plugins use configurable logging levels that determines when a log is generated. Here's how it works:") + //Note that log levels 3 and 4 are only visible in the GUI if the user has selected the 'Run with Debug Output' option. + logger.log(3, '["0": "Error","1": "Warning","2": "Notice","3": "Info","4": "Debug"]') + + /** Cast the API Version, if it was provided */ + if (userApiVersion) { + userApiVersionString = userApiVersion.toString() + } + + /** + * Secrets should be retrieved from Key Storage using a try/catch block that fetches credentials/passwords using + * the user provided path, and the PluginStepContext object. + */ + try { + if (!exapis) { + exapis = new ExampleApis(userBaseApiUrl, userApiVersionString, userApiKey) + } + resourceInfo = exapis.getResourceInfoByName(projectName, currentNodeName) + } catch (IOException e) { + throw new NodeStepException( + 'Failed to get resource info with error:' + e.getMessage(), + PluginFailureReason.ResourceInfoError, + entry.getNodename() + ) + } + + /** + * At this point, we have our result data in hand with resourceInfo. + * Let's save it to outputContext, which will allow the job runner to pass the results + * to another job step automatically by the context name. + * In this instance, the resource information in 'resourceInfo' can be interpolated into any subsequent job steps by + * using '${data}.resourceInfo'. + */ + context.getExecutionContext().getOutputContext().addOutput("data", "resourceInfo", resourceInfo) + /** Here, we'll get access to 'hiddenTestValue' via 'extra.hiddenTestValue' */ + context.getExecutionContext().getOutputContext().addOutput("extra", "hiddenTestValue", hiddenTestValue) + + /** Now, we'll add it to the log, print for the user, and call it a day. */ + logger.log(2, "Job run complete! Results from API call:") + + def Json = JsonOutput.toJson(resourceInfo) + logger.log(2, Json, meta) + + } +} \ No newline at end of file diff --git a/examples/java-plugins/example-workflow-node-step/src/main/groovy/com/plugin/exampleworkflownodestep/FailureReason.groovy b/examples/java-plugins/example-workflow-node-step/src/main/groovy/com/plugin/exampleworkflownodestep/FailureReason.groovy new file mode 100644 index 0000000..aba7fa6 --- /dev/null +++ b/examples/java-plugins/example-workflow-node-step/src/main/groovy/com/plugin/exampleworkflownodestep/FailureReason.groovy @@ -0,0 +1,15 @@ +package com.plugin.exampleworkflownodestep; + +import com.dtolabs.rundeck.core.execution.workflow.steps.FailureReason + +/** + * This enum lists the known reasons this plugin might fail. + * + * There should be a FailureReason enum that implements the FailureReason interface. + * There should regularly be failure reasons for Authentication errors, Key Storage errors, etc. + * Use these to represent reasons your plugin may fail to execute. + */ +enum PluginFailureReason implements FailureReason { + KeyStorageError, + ResourceInfoError +} \ No newline at end of file diff --git a/examples/java-plugins/example-workflow-node-step/src/main/groovy/com/plugin/exampleworkflownodestep/Util.groovy b/examples/java-plugins/example-workflow-node-step/src/main/groovy/com/plugin/exampleworkflownodestep/Util.groovy new file mode 100644 index 0000000..525d140 --- /dev/null +++ b/examples/java-plugins/example-workflow-node-step/src/main/groovy/com/plugin/exampleworkflownodestep/Util.groovy @@ -0,0 +1,27 @@ +package com.plugin.exampleworkflownodestep; + +import org.rundeck.storage.api.PathUtil +import org.rundeck.storage.api.StorageException +import com.dtolabs.rundeck.core.storage.ResourceMeta +import com.dtolabs.rundeck.plugins.step.PluginStepContext + +/** + * A “Util” class should be written to handle common methods for renderingOptions, retrieving keys from KeyStorage, + * auth-settings, and any other generic methods that can be used for support across your suite. + */ +class Util { + static String getPasswordFromKeyStorage(String path, PluginStepContext context){ + try{ + ResourceMeta contents = context.getExecutionContext().getStorageTree().getResource(path).getContents() + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream() + contents.writeContent(byteArrayOutputStream) + String password = new String(byteArrayOutputStream.toByteArray()) + + return password + } catch (Exception e){ + throw StorageException.readException( + PathUtil.asPath(path), e.getMessage() + ) + } + } +} \ No newline at end of file diff --git a/examples/java-plugins/example-workflow-node-step/src/main/resources/resources/icon.png b/examples/java-plugins/example-workflow-node-step/src/main/resources/resources/icon.png new file mode 100644 index 0000000..7aaf7e8 Binary files /dev/null and b/examples/java-plugins/example-workflow-node-step/src/main/resources/resources/icon.png differ diff --git a/examples/java-plugins/example-workflow-node-step/src/test/groovy/com/plugin/exampleworkflownodestep/ExampleWorkflowNodeStepSpec.groovy b/examples/java-plugins/example-workflow-node-step/src/test/groovy/com/plugin/exampleworkflownodestep/ExampleWorkflowNodeStepSpec.groovy new file mode 100644 index 0000000..ab40f66 --- /dev/null +++ b/examples/java-plugins/example-workflow-node-step/src/test/groovy/com/plugin/exampleworkflownodestep/ExampleWorkflowNodeStepSpec.groovy @@ -0,0 +1,58 @@ +package com.plugin.exampleworkflownodestep + +import com.dtolabs.rundeck.plugins.step.PluginStepContext +import com.dtolabs.rundeck.core.execution.workflow.steps.StepException +import com.dtolabs.rundeck.plugins.PluginLogger +import com.dtolabs.rundeck.core.common.INodeEntry +import spock.lang.Specification + +class ExampleWorkflowNodeStepSpec extends Specification { + + def getContext(PluginLogger logger){ + Mock(PluginStepContext){ + getLogger()>>logger + } + } + + def "check Boolean parameter"(){ + + given: + + def example = new ExampleWorkflowNodeStep() + def context = getContext(Mock(PluginLogger)) + def node = Mock(INodeEntry){ + getNodename()>>"Test" + getAttributes()>>["attr:name":"Test"] + } + + def configuration = [example:"example123",exampleBoolean:"true"] + + when: + example.executeNodeStep(context,configuration,node) + + then: + thrown StepException + } + + def "run OK"(){ + + given: + + def example = new ExampleWorkflowNodeStep() + def logger = Mock(PluginLogger) + def context = getContext(logger) + def node = Mock(INodeEntry){ + getNodename()>>"Test" + getAttributes()>>["attr:name":"Test"] + } + + def configuration = [example:"example123",exampleBoolean:"false"] + + when: + example.executeNodeStep(context,configuration,node) + + then: + 1 * logger.log(2, "Example node step executing on node: Test") + } + +} \ No newline at end of file diff --git a/examples/java-plugins/example-workflow-step/README.md b/examples/java-plugins/example-workflow-step/README.md new file mode 100644 index 0000000..28f1c5e --- /dev/null +++ b/examples/java-plugins/example-workflow-step/README.md @@ -0,0 +1,3 @@ +# Example Workflow Step Node Step Plugin + +This is a template node step plugin that was build using the [rundeck-plugin-bootstrap](https://github.com/rundeck/plugin-bootstrap) diff --git a/examples/java-plugins/example-workflow-step/build.gradle b/examples/java-plugins/example-workflow-step/build.gradle new file mode 100644 index 0000000..38f27ea --- /dev/null +++ b/examples/java-plugins/example-workflow-step/build.gradle @@ -0,0 +1,70 @@ +plugins { + id 'groovy' + id 'java' +} + +version = '0.1.0' +defaultTasks 'clean','build' +apply plugin: 'java' +apply plugin: 'groovy' +apply plugin: 'idea' +sourceCompatibility = 11.0 +ext.rundeckPluginVersion= '2.0' +ext.rundeckVersion= '5.7.0-20250101' +ext.pluginClassNames='com.plugin.exampleworkflowstep.ExampleWorkflowStep' + + +repositories { + mavenLocal() + mavenCentral() +} + +configurations{ + //declare custom pluginLibs configuration to include only libs for this plugin + pluginLibs + + //declare compile to extend from pluginLibs so it inherits the dependencies + implementation{ + extendsFrom pluginLibs + } +} + +dependencies { + implementation 'org.rundeck:rundeck-core:5.7.0-20250101' + implementation 'org.codehaus.groovy:groovy-all:3.0.21' + + //use pluginLibs to add dependencies, example: + //pluginLibs group: 'com.google.code.gson', name: 'gson', version: '2.8.2' + + testImplementation 'junit:junit:4.12' + testImplementation "org.codehaus.groovy:groovy-all:3.0.21" + testImplementation "org.spockframework:spock-core:2.2-groovy-3.0" +} + +// task to copy plugin libs to output/lib dir +task copyToLib(type: Copy) { + into "$buildDir/output/lib" + from configurations.pluginLibs +} + +jar { + from "$buildDir/output" + manifest { + def libList = configurations.pluginLibs.collect{'lib/'+it.name}.join(' ') + + attributes 'Rundeck-Plugin-Classnames': pluginClassNames + attributes 'Rundeck-Plugin-File-Version': version + attributes 'Rundeck-Plugin-Name': 'Example Workflow Step' + attributes 'Rundeck-Plugin-Description': 'Provide a short description of your plugin here.' + attributes 'Rundeck-Plugin-Rundeck-Compatibility-Version': '3.x' + attributes 'Rundeck-Plugin-Tags': 'java,NodeStep' + attributes 'Rundeck-Plugin-License': 'Apache 2.0' + attributes 'Rundeck-Plugin-Source-Link': 'Please put the link to your source repo here' + attributes 'Rundeck-Plugin-Target-Host-Compatibility': 'all' + attributes 'Rundeck-Plugin-Version': rundeckPluginVersion + attributes 'Rundeck-Plugin-Archive': 'true' + attributes 'Rundeck-Plugin-Libs': "${libList}" + + } + dependsOn(copyToLib) +} diff --git a/examples/java-plugins/example-workflow-step/src/main/groovy/com/plugin/exampleworkflowstep/Constants.groovy b/examples/java-plugins/example-workflow-step/src/main/groovy/com/plugin/exampleworkflowstep/Constants.groovy new file mode 100644 index 0000000..fe073a6 --- /dev/null +++ b/examples/java-plugins/example-workflow-step/src/main/groovy/com/plugin/exampleworkflowstep/Constants.groovy @@ -0,0 +1,10 @@ +package com.plugin.exampleworkflowstep; + +/** + * If other functions are required for purposes of modularity or clarity, they should either be added to a Util Class + * (if generic enough), or a PluginHelper Class that is accessible to the Plugin Class. + */ +class Constants { + public static final String BASE_API_URL = "http://localhost:4440/api/" + public static final String API_VERSION = "41" +} \ No newline at end of file diff --git a/examples/java-plugins/example-workflow-step/src/main/groovy/com/plugin/exampleworkflowstep/ExampleApis.groovy b/examples/java-plugins/example-workflow-step/src/main/groovy/com/plugin/exampleworkflowstep/ExampleApis.groovy new file mode 100644 index 0000000..d64417d --- /dev/null +++ b/examples/java-plugins/example-workflow-step/src/main/groovy/com/plugin/exampleworkflowstep/ExampleApis.groovy @@ -0,0 +1,88 @@ +package com.plugin.exampleworkflowstep; + +import okhttp3.Headers +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response + +/** + * If other functions are required for purposes of modularity or clarity, they should either be added to a Util Class + * (if generic enough), or a PluginHelper Class that is accessible to the Plugin Class. + */ +class ExampleApis { + String userRundeckBaseApiUrl + String userRundeckApiVersion + Headers headers + OkHttpClient client + + ExampleApis(String userBaseApiUrl, String userApiVersion, String userAuthToken) { + this.client = new OkHttpClient() + this.headers = new Headers.Builder() + .add('Accept', 'application/json') + .add('Content-Type', 'application/json') + .add('X-Rundeck-Auth-Token', userAuthToken) + .build() + + if (!userBaseApiUrl) { + userRundeckBaseApiUrl = Constants.BASE_API_URL + } else { + userRundeckBaseApiUrl = userBaseApiUrl + } + + if (!userApiVersion) { + userRundeckApiVersion = Constants.API_VERSION + } else { + userRundeckApiVersion = userApiVersion + } + } + + /** + * Requests info on a single node by name, from a given Rundeck project by name. + * https://docs.rundeck.com/docs/api/rundeck-api.html#getting-resource-info + */ + String getResourceInfoByName( + String projectName, + String nodeName + ) throws IOException { + String resourceUrl = "/project/" + projectName + "/resource/" + nodeName + String fullUrl = createFullUrl(userRundeckBaseApiUrl, userRundeckApiVersion, resourceUrl) + + Request request = new Request.Builder() + .url(fullUrl) + .headers(headers) + .build() + + try (Response response = client.newCall(request).execute()) { + return response.body().string(); + } + } + + /** + * Requests info on a Rundeck project by name. + * https://docs.rundeck.com/docs/api/rundeck-api.html#getting-project-info + */ + String getProjectInfoByName( + String projectName + ) throws IOException { + String resourceUrl = "/project/" + projectName + String fullUrl = createFullUrl(userRundeckBaseApiUrl, userRundeckApiVersion, resourceUrl) + + Request request = new Request.Builder() + .url(fullUrl) + .headers(headers) + .build() + + try (Response response = client.newCall(request).execute()) { + return response.body().string(); + } + } + + private static String createFullUrl(String baseApiUrl, String apiVersion, String apiPath) { + + // Handle for user trailing forward slash + if(baseApiUrl.endsWith("/")) { + baseApiUrl = baseApiUrl.substring(0, baseApiUrl.length() - 1) + } + return baseApiUrl + "/" + apiVersion + "/" + apiPath + } +} \ No newline at end of file diff --git a/examples/java-plugins/example-workflow-step/src/main/groovy/com/plugin/exampleworkflowstep/ExampleWorkflowStep.groovy b/examples/java-plugins/example-workflow-step/src/main/groovy/com/plugin/exampleworkflowstep/ExampleWorkflowStep.groovy new file mode 100644 index 0000000..2f6539f --- /dev/null +++ b/examples/java-plugins/example-workflow-step/src/main/groovy/com/plugin/exampleworkflowstep/ExampleWorkflowStep.groovy @@ -0,0 +1,215 @@ +package com.plugin.exampleworkflowstep; + +/** + * Dependency Recommendations: + * Any Java SDK must be officially recognized by the vendor for that technology + * (e.g. AWS Java SDK, SumoLogic, Zendesk) and show reasonably recent development. Any SDK used must + * have an open source license such as Apache-2 or MIT. + */ + +import com.dtolabs.rundeck.core.plugins.Plugin +import com.dtolabs.rundeck.plugins.step.StepPlugin +import com.dtolabs.rundeck.core.execution.workflow.steps.StepException +import com.dtolabs.rundeck.core.plugins.configuration.StringRenderingConstants +import com.dtolabs.rundeck.plugins.ServiceNameConstants +import com.dtolabs.rundeck.plugins.step.PluginStepContext +import com.dtolabs.rundeck.plugins.descriptions.PluginDescription +import com.dtolabs.rundeck.plugins.descriptions.PluginProperty +import com.dtolabs.rundeck.plugins.descriptions.RenderingOption +import com.dtolabs.rundeck.plugins.descriptions.RenderingOptions +import com.dtolabs.rundeck.core.execution.ExecutionListener +import groovy.json.JsonBuilder +import groovy.json.JsonOutput +import org.rundeck.storage.api.StorageException +import static com.dtolabs.rundeck.core.plugins.configuration.StringRenderingConstants.GROUPING +import static com.dtolabs.rundeck.core.plugins.configuration.StringRenderingConstants.GROUP_NAME + +/** +* WorkflowStepPlugin demonstrates a basic {@link com.dtolabs.rundeck.plugins.step.StepPlugin}, and how to +* programmatically build all of the plugin's Properties exposed in the GUI. +*

+* The plugin class is annotated with {@link Plugin} to define the service and name of this service provider plugin. +*

+* The provider name of this plugin is statically defined in the class. The service name makes use of {@link +* ServiceNameConstants} to provide the known Rundeck service names. +*/ +@Plugin(name = PLUGIN_NAME, service = ServiceNameConstants.WorkflowStep) +@PluginDescription(title = PLUGIN_TITLE, description = PLUGIN_DESCRIPTION) +class ExampleWorkflowStep implements StepPlugin { + /** + * Define a name used to identify your plugin. It is a good idea to use a fully qualified package-style name. + */ + public static final String PLUGIN_NAME = "example-workflow-step" + public static final String PLUGIN_TITLE = "Example Workflow Step" + public static final String PLUGIN_DESCRIPTION = "Template Workflow Step plugin that makes a call to an API and retrieves a response." + + Map meta = Collections.singletonMap("content-data-type", "application/json") + ExampleApis exapis + + /** + * Plugin Properties must: + * * be laid out at the top of the Plugin class, just after any class/instance variables. + * * be intuitive for the user to understand, and inform the end-user what is expected for that field. + * * follow the method/conventions of renderingOptions below + * * use KeyStorage for storage/retrieval of secrets. See 'API Key Path' property below. + */ + @PluginProperty( + title = "API URL", + description = """Provide the base URL for the API to connect to. It will be used by the plugin to get information from the API. If left blank, the call will use a default base API URL. + + +When carriage returns are used in the description, any part of the string after them—such as this—will also be collapsed. **Markdown** can also be used in this _expanded_ block. + + +Want to learn more about the Rundeck API? Check out [our docs](https://docs.rundeck.com/docs/api/rundeck-api.html).""", + defaultValue = Constants.BASE_API_URL, + required = false + ) + @RenderingOptions( + [ + @RenderingOption(key = GROUP_NAME, value = "API Configuration") + ] + ) + String userBaseApiUrl + + /** + * Here, we're requesting an integer, which will restrict this field in the GUI to only accept integers. + */ + @PluginProperty( + title = "API Version", + description = "Overrides the API version used to make the call. If left blank, the call will use a default API version.", + defaultValue = Constants.API_VERSION, + required = false + ) + @RenderingOption(key = GROUP_NAME, value = "API Configuration") + Integer userApiVersion + + /** + * Here we're requesting the user provides the path to the API key in Key Storage. + * For security and accessibility, any secure strings of information should always be saved into Key Storage. That includes + * tokens, passwords, certificates, or any other authentication information. + * Here, we're setting up the RenderingOptions to display this as a field for keys of the 'password' type (Rundeck-data-type=password). + * The value of this property will only be a path to the necessary key. You'll see how the actual key is resolved below. + */ + @PluginProperty( + title = "API Key Path", + description = "REQUIRED: The path to the Key Storage entry for your API Key.", + required = true + ) + @RenderingOptions([ + @RenderingOption( + key = StringRenderingConstants.SELECTION_ACCESSOR_KEY, + value = "STORAGE_PATH" + ), + @RenderingOption( + key = StringRenderingConstants.STORAGE_PATH_ROOT_KEY, + value = "keys" + ), + @RenderingOption( + key = StringRenderingConstants.STORAGE_FILE_META_FILTER_KEY, + value = "Rundeck-data-type=password" + ), + @RenderingOption( + key = StringRenderingConstants.GROUP_NAME, + value = "API Configuration" + ) + ]) + String apiKeyPath + + @PluginProperty( + title = "Collapsed test value", + description = """This is another test property to be output at the end of the execution.By default, it will be collapsed in the list of properties, thanks to the '@RenderingOption' 'GROUPING' key being set to 'secondary'.""", + required = false + ) + @RenderingOption(key = GROUP_NAME, value = "Collapsed Configuration") + /** The secondary grouping RenderingOption is what collapses the field by default in the GUI */ + @RenderingOption(key = GROUPING, value = "secondary") + String hiddenTestValue + + /** + * Plugins should make good use of logging and log levels in order to provide the user with the right amount + * of information on execution. Use 'context.getExecutionContext().getExecutionListener().log' to handle logging. + * Any failure in the execution should be caught and thrown as a StepException + * StepExceptions require a message, FailureReason to be provided + * @param context + * @param configuration + * @param entry + * @throws StepException + */ + @Override + void executeStep(final PluginStepContext context, + final Map configuration) { + + /** + * We'll resolve the name of the current project. We'll use them to make an + * API GET request. + */ + String projectName = context.getFrameworkProject() + String projectInfo + String userApiVersionString = null + String userApiKey + + /** + * Next, we'll resolve the API token itself. There's a perfect function for this in the Util class, + * getPasswordFromKeyStorage. You can see more about how the process works in the Util file. + */ + try { + userApiKey = Util.getPasswordFromKeyStorage(apiKeyPath, context) + } catch (StorageException e) { + throw new StepException( + 'Error accessing ${apiKeyPath}:' + e.getMessage(), + PluginFailureReason.KeyStorageError + ) + } + + /** + * The preferred method of logging is to write into, and then print out, + * the executionContext log. First, we add to our logging object from before. + */ + ExecutionListener logger = context.getExecutionContext().getExecutionListener() + + logger.log(3, "Here is a single line log entry. We'll print this as a logLevel 3, along with our next log lines.") + logger.log(3, "Plugins use configurable logging levels that determines when log is generated. Here's how it works:") + //Note that log levels 3 and 4 are only visible in the GUI if the user has selected the 'Run with Debug Output' option. + logger.log(3, '["0": "Error","1": "Warning","2": "Notice","3": "Info","4": "Debug"]') + + /** Cast the API Version, if it was provided */ + if (userApiVersion) { + userApiVersionString = userApiVersion.toString() + } + + /** + * Secrets should be retrieved from Key Storage using a try/catch block that fetches credentials/passwords using + * the user provided path, and the PluginStepContext object. + */ + try { + if (!exapis) { + exapis = new ExampleApis(userBaseApiUrl, userApiVersionString, userApiKey) + } + projectInfo = exapis.getProjectInfoByName(projectName) + } catch (IOException e) { + throw new StepException( + 'Failed to get resource info with error:' + e.getMessage(), + PluginFailureReason.ResourceInfoError + ) + } + + /** + * At this point, we have our result data in hand with resourceInfo. + * Let's save it to outputContext, which will allow the job runner to pass the results + * to another job step automatically by the context name. + * In this instance, the resource information in 'projectInfo' can be interpolated into any subsequent job steps by + * using '${data}.projectInfo'. + */ + context.getExecutionContext().getOutputContext().addOutput("data", "projectInfo", projectInfo) + /** Here, we'll get access to 'hiddenTestValue' via 'extra.hiddenTestValue' */ + context.getExecutionContext().getOutputContext().addOutput("extra", "hiddenTestValue", hiddenTestValue) + + /** Now, we'll add it to the log, print for the user, and call it a day. */ + logger.log(2, "Job run complete! Results from API call:") + + def json = JsonOutput.toJson(projectInfo) + logger.log(2, json, meta) + + } +} \ No newline at end of file diff --git a/examples/java-plugins/example-workflow-step/src/main/groovy/com/plugin/exampleworkflowstep/FailureReason.groovy b/examples/java-plugins/example-workflow-step/src/main/groovy/com/plugin/exampleworkflowstep/FailureReason.groovy new file mode 100644 index 0000000..eb279d6 --- /dev/null +++ b/examples/java-plugins/example-workflow-step/src/main/groovy/com/plugin/exampleworkflowstep/FailureReason.groovy @@ -0,0 +1,15 @@ +package com.plugin.exampleworkflowstep; + +import com.dtolabs.rundeck.core.execution.workflow.steps.FailureReason + +/** + * This enum lists the known reasons this plugin might fail. + * + * There should be a FailureReason enum that implements the FailureReason interface. + * There should regularly be failure reasons for Authentication errors, Key Storage errors, etc. + * Use these to represent reasons your plugin may fail to execute. + */ +enum PluginFailureReason implements FailureReason { + KeyStorageError, + ResourceInfoError +} \ No newline at end of file diff --git a/examples/java-plugins/example-workflow-step/src/main/groovy/com/plugin/exampleworkflowstep/Util.groovy b/examples/java-plugins/example-workflow-step/src/main/groovy/com/plugin/exampleworkflowstep/Util.groovy new file mode 100644 index 0000000..af38282 --- /dev/null +++ b/examples/java-plugins/example-workflow-step/src/main/groovy/com/plugin/exampleworkflowstep/Util.groovy @@ -0,0 +1,27 @@ +package com.plugin.exampleworkflowstep; + +import org.rundeck.storage.api.PathUtil +import org.rundeck.storage.api.StorageException +import com.dtolabs.rundeck.core.storage.ResourceMeta +import com.dtolabs.rundeck.plugins.step.PluginStepContext + +/** + * A “Util” class should be written to handle common methods for renderingOptions, retrieving keys from KeyStorage, + * auth-settings, and any other generic methods that can be used for support across your suite. + */ +class Util { + static String getPasswordFromKeyStorage(String path, PluginStepContext context){ + try{ + ResourceMeta contents = context.getExecutionContext().getStorageTree().getResource(path).getContents() + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream() + contents.writeContent(byteArrayOutputStream) + String password = new String(byteArrayOutputStream.toByteArray()) + + return password + } catch (Exception e){ + throw StorageException.readException( + PathUtil.asPath(path), e.getMessage() + ) + } + } +} \ No newline at end of file diff --git a/examples/java-plugins/example-workflow-step/src/main/resources/resources/icon.png b/examples/java-plugins/example-workflow-step/src/main/resources/resources/icon.png new file mode 100644 index 0000000..7aaf7e8 Binary files /dev/null and b/examples/java-plugins/example-workflow-step/src/main/resources/resources/icon.png differ diff --git a/examples/java-plugins/example-workflow-step/src/test/groovy/com/plugin/exampleworkflowstep/ExampleWorkflowStepSpec.groovy b/examples/java-plugins/example-workflow-step/src/test/groovy/com/plugin/exampleworkflowstep/ExampleWorkflowStepSpec.groovy new file mode 100644 index 0000000..3454196 --- /dev/null +++ b/examples/java-plugins/example-workflow-step/src/test/groovy/com/plugin/exampleworkflowstep/ExampleWorkflowStepSpec.groovy @@ -0,0 +1,47 @@ +package com.plugin.exampleworkflowstep + +import com.dtolabs.rundeck.plugins.step.PluginStepContext +import com.dtolabs.rundeck.core.execution.workflow.steps.StepException +import com.dtolabs.rundeck.plugins.PluginLogger +import spock.lang.Specification + +class ExampleWorkflowStepSpec extends Specification { + + def getContext(PluginLogger logger){ + Mock(PluginStepContext){ + getLogger()>>logger + } + } + + def "check Boolean parameter"(){ + + given: + + def example = new ExampleWorkflowStep() + def context = getContext(Mock(PluginLogger)) + def configuration = [example:"example123",exampleBoolean:"true"] + + when: + example.executeStep(context,configuration) + + then: + thrown StepException + } + + def "run OK"(){ + + given: + + def example = new ExampleWorkflowStep() + def logger = Mock(PluginLogger) + def context = getContext(logger) + def configuration = [example:"example123",exampleBoolean:"false",exampleFreeSelect:"Beige"] + + when: + example.executeStep(context,configuration) + + then: + 1 * logger.log(2, 'Example step configuration: {example=example123, exampleBoolean=false, exampleFreeSelect=Beige}') + } + +} \ No newline at end of file diff --git a/examples/script-plugins/example-script-file-copier/Makefile b/examples/script-plugins/example-script-file-copier/Makefile new file mode 100644 index 0000000..355e58e --- /dev/null +++ b/examples/script-plugins/example-script-file-copier/Makefile @@ -0,0 +1,11 @@ +all: install + +clean: + rm -rf build + +build: + mkdir -p build/libs build/zip-content/example-script-file-copier + cp -r contents resources plugin.yaml build/zip-content/example-script-file-copier + cd build/zip-content; zip -r example-script-file-copier.zip * + mv build/zip-content/example-script-file-copier.zip build/libs + diff --git a/examples/script-plugins/example-script-file-copier/README.md b/examples/script-plugins/example-script-file-copier/README.md new file mode 100644 index 0000000..3e31fc1 --- /dev/null +++ b/examples/script-plugins/example-script-file-copier/README.md @@ -0,0 +1,22 @@ +# Example Script File Copier Rundeck Plugin + +This is a FileCopier plugin. + +## Build + +* Using gradle +``` +gradle clean build +``` + +* Using make + +``` +make clean build +``` + +## Install + +``` +cp build/libs/example-script-file-copier.zip $RDECK_BASE/libext +``` \ No newline at end of file diff --git a/examples/script-plugins/example-script-file-copier/build.gradle b/examples/script-plugins/example-script-file-copier/build.gradle new file mode 100644 index 0000000..b555d67 --- /dev/null +++ b/examples/script-plugins/example-script-file-copier/build.gradle @@ -0,0 +1,24 @@ +buildscript { + repositories { + mavenCentral() + } +} +plugins { + id 'pl.allegro.tech.build.axion-release' version '1.7.0' +} + +ext.pluginName = 'Example Script File Copier' +ext.pluginDescription = "Provide a short description of your plugin here" +ext.sopsCopyright = "© 2018, Rundeck, Inc." +ext.sopsUrl = "http://rundeck.com" +ext.buildDateString=new Date().format("yyyy-MM-dd'T'HH:mm:ssX") +ext.archivesBaseName = "example-script-file-copier" +ext.pluginBaseFolder = "." + +project.version = "0.1.0-SNAPSHOT" +ext.archiveFilename = ext.archivesBaseName + '-' + version + +// NOTE: Uses gradle-5.6 branch for stability. For production use, consider: +// 1. Copying the script into your repository, OR +// 2. Pinning to a specific commit hash +apply from: 'https://raw.githubusercontent.com/rundeck-plugins/build-zip/gradle-5.6/build.gradle' \ No newline at end of file diff --git a/examples/script-plugins/example-script-file-copier/contents/filecopier b/examples/script-plugins/example-script-file-copier/contents/filecopier new file mode 100644 index 0000000..2ec8731 --- /dev/null +++ b/examples/script-plugins/example-script-file-copier/contents/filecopier @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +#Your script here +HOST=$1 +shift +SOURCE=$1 +shift +DESTINATION="$*" + +#do a dry run +if [[ "true" == "$RD_CONFIG_DRY_RUN" ]] ; then + # WARNING: Never log all RD_CONFIG_* variables as they may contain secrets from Key Storage + echo "Dry run mode - copying file from $SOURCE to $HOST: $DESTINATION" + exit 0 + +fi + +echo "running example-script-file-copier" +echo "Example Config: $RD_CONFIG_EXAMPLE" +echo "Example Select Config: $RD_CONFIG_EXAMPLESELECT" +echo "Coping file from $SOURCE to $HOST: $DESTINATION " + +## Copy file from $SOURCE to $DESTINATION \ No newline at end of file diff --git a/examples/script-plugins/example-script-file-copier/plugin.yaml b/examples/script-plugins/example-script-file-copier/plugin.yaml new file mode 100644 index 0000000..9e2b821 --- /dev/null +++ b/examples/script-plugins/example-script-file-copier/plugin.yaml @@ -0,0 +1,61 @@ +name: Example Script File Copier +rundeckPluginVersion: 2.0 +author: Rundeck Dev +description: Describe your plugin here +rundeckCompatibilityVersion: 3.x +targetHostCompatibility: unix +license: Apache 2.0 +tags: + - script + - FileCopier +date: 2026-02-03T23:15:55.076223Z +version: 1.0.0 +providers: + - name: example-script-file-copier + service: FileCopier + title: Example Script File Copier + description: The description of example-script-file-copier plugin + plugin-type: script + script-interpreter: /bin/bash + script-file: filecopier + script-args: ${file-copy.file} ${file-copy.destination} + config: + - type: String + name: example + title: 'Example String' + description: 'Example String' + required: true + - type: Select + name: exampleSelect + title: ExampleSelect + description: 'Example Select' + default: Beige + values: + - Blue + - Beige + - Black + - type: Boolean + name: dry_run + title: Dry Run? + description: 'Just echo what would be done' + default: true + renderingOptions: + groupName: 'Config' + - type: String + name: storageprivatekey + title: Storage Private Key + description: Access to storage private key example + renderingOptions: + selectionAccessor: "STORAGE_PATH" + valueConversion: "STORAGE_PATH_AUTOMATIC_READ" + storage-path-root: "keys" + storage-file-meta-filter: "Rundeck-key-type=private" + - type: String + name: storagepassword + title: Storage Password + description: Access to storage password example + renderingOptions: + selectionAccessor: "STORAGE_PATH" + valueConversion: "STORAGE_PATH_AUTOMATIC_READ" + storage-path-root: "keys" + storage-file-meta-filter: "Rundeck-data-type=password" \ No newline at end of file diff --git a/examples/script-plugins/example-script-file-copier/resources/icon.png b/examples/script-plugins/example-script-file-copier/resources/icon.png new file mode 100644 index 0000000..7aaf7e8 Binary files /dev/null and b/examples/script-plugins/example-script-file-copier/resources/icon.png differ diff --git a/examples/script-plugins/example-script-node-executor/Makefile b/examples/script-plugins/example-script-node-executor/Makefile new file mode 100644 index 0000000..cff448b --- /dev/null +++ b/examples/script-plugins/example-script-node-executor/Makefile @@ -0,0 +1,11 @@ +all: install + +clean: + rm -rf build + +build: + mkdir -p build/libs build/zip-content/example-script-node-executor + cp -r contents resources plugin.yaml build/zip-content/example-script-node-executor + cd build/zip-content; zip -r example-script-node-executor.zip * + mv build/zip-content/example-script-node-executor.zip build/libs + diff --git a/examples/script-plugins/example-script-node-executor/README.md b/examples/script-plugins/example-script-node-executor/README.md new file mode 100644 index 0000000..86a8c5f --- /dev/null +++ b/examples/script-plugins/example-script-node-executor/README.md @@ -0,0 +1,22 @@ +# Example Script Node Executor Rundeck Plugin + +This is a NodeExecutor plugin. + +## Build + +* Using gradle +``` +gradle clean build +``` + +* Using make + +``` +make clean build +``` + +## Install + +``` +cp build/libs/example-script-node-executor.zip $RDECK_BASE/libext +``` \ No newline at end of file diff --git a/examples/script-plugins/example-script-node-executor/build.gradle b/examples/script-plugins/example-script-node-executor/build.gradle new file mode 100644 index 0000000..4acaedd --- /dev/null +++ b/examples/script-plugins/example-script-node-executor/build.gradle @@ -0,0 +1,24 @@ +buildscript { + repositories { + mavenCentral() + } +} +plugins { + id 'pl.allegro.tech.build.axion-release' version '1.7.0' +} + +ext.pluginName = 'Example Script Node Executor' +ext.pluginDescription = "Provide a short description of your plugin here" +ext.sopsCopyright = "© 2018, Rundeck, Inc." +ext.sopsUrl = "http://rundeck.com" +ext.buildDateString=new Date().format("yyyy-MM-dd'T'HH:mm:ssX") +ext.archivesBaseName = "example-script-node-executor" +ext.pluginBaseFolder = "." + +project.version = "0.1.0-SNAPSHOT" +ext.archiveFilename = ext.archivesBaseName + '-' + version + +// NOTE: Uses gradle-5.6 branch for stability. For production use, consider: +// 1. Copying the script into your repository, OR +// 2. Pinning to a specific commit hash +apply from: 'https://raw.githubusercontent.com/rundeck-plugins/build-zip/gradle-5.6/build.gradle' \ No newline at end of file diff --git a/examples/script-plugins/example-script-node-executor/contents/nodeexecutor b/examples/script-plugins/example-script-node-executor/contents/nodeexecutor new file mode 100644 index 0000000..22e4731 --- /dev/null +++ b/examples/script-plugins/example-script-node-executor/contents/nodeexecutor @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -eu + +#Your script here +HOST=$1 +shift +CMD="$*" + +#do a dry run +if [[ "true" == "$RD_CONFIG_DRY_RUN" ]] ; then + # WARNING: Never log all RD_CONFIG_* variables as they may contain secrets from Key Storage + echo "[example-script-node-executor] Dry run mode - command to run on node $HOST: $CMD" + exit 0 +fi + + +echo "running example-script-node-executor" +echo "Example Config: $RD_CONFIG_EXAMPLE" +echo "Example Select Config: $RD_CONFIG_EXAMPLESELECT" +echo "Connecting with host $HOST" + +## Connect to the host $HOST and run command $CMD \ No newline at end of file diff --git a/examples/script-plugins/example-script-node-executor/plugin.yaml b/examples/script-plugins/example-script-node-executor/plugin.yaml new file mode 100644 index 0000000..fd509e8 --- /dev/null +++ b/examples/script-plugins/example-script-node-executor/plugin.yaml @@ -0,0 +1,61 @@ +name: Example Script Node Executor +rundeckPluginVersion: 2.0 +author: Rundeck Dev +description: Describe your plugin here +rundeckCompatibilityVersion: 3.x +targetHostCompatibility: unix +license: Apache 2.0 +tags: + - script + - NodeExecutor +date: 2026-02-03T23:15:53.480885Z +version: 1.0.0 +providers: + - name: example-script-node-executor + service: NodeExecutor + title: Example Script Node Executor + description: The description of example-script-node-executor plugin + plugin-type: script + script-interpreter: /bin/bash + script-file: nodeexecutor + script-args: ${node.name} ${exec.command} + config: + - type: String + name: example + title: 'Example String' + description: 'Example String' + required: true + - type: Select + name: exampleSelect + title: ExampleSelect + description: 'Example Select' + default: Beige + values: + - Blue + - Beige + - Black + - type: Boolean + name: dry_run + title: Dry Run? + description: 'Just echo what would be done' + default: true + renderingOptions: + groupName: 'Config' + - type: String + name: storageprivatekey + title: Storage Private Key + description: Access to storage private key example + renderingOptions: + selectionAccessor: "STORAGE_PATH" + valueConversion: "STORAGE_PATH_AUTOMATIC_READ" + storage-path-root: "keys" + storage-file-meta-filter: "Rundeck-key-type=private" + - type: String + name: storagepassword + title: Storage Password + description: Access to storage password example + renderingOptions: + selectionAccessor: "STORAGE_PATH" + valueConversion: "STORAGE_PATH_AUTOMATIC_READ" + storage-path-root: "keys" + storage-file-meta-filter: "Rundeck-data-type=password" \ No newline at end of file diff --git a/examples/script-plugins/example-script-node-executor/resources/icon.png b/examples/script-plugins/example-script-node-executor/resources/icon.png new file mode 100644 index 0000000..7aaf7e8 Binary files /dev/null and b/examples/script-plugins/example-script-node-executor/resources/icon.png differ diff --git a/examples/script-plugins/example-script-option/Makefile b/examples/script-plugins/example-script-option/Makefile new file mode 100644 index 0000000..d7cef59 --- /dev/null +++ b/examples/script-plugins/example-script-option/Makefile @@ -0,0 +1,11 @@ +all: install + +clean: + rm -rf build + +build: + mkdir -p build/libs build/zip-content/example-script-option + cp -r contents resources plugin.yaml build/zip-content/example-script-option + cd build/zip-content; zip -r example-script-option.zip * + mv build/zip-content/example-script-option.zip build/libs + diff --git a/examples/script-plugins/example-script-option/README.md b/examples/script-plugins/example-script-option/README.md new file mode 100644 index 0000000..9d8ab01 --- /dev/null +++ b/examples/script-plugins/example-script-option/README.md @@ -0,0 +1,22 @@ +# Example Script Option Rundeck Plugin + +This is a Option plugin. + +## Build + +* Using gradle +``` +gradle clean build +``` + +* Using make + +``` +make clean build +``` + +## Install + +``` +cp build/libs/example-script-option.zip $RDECK_BASE/libext +``` \ No newline at end of file diff --git a/examples/script-plugins/example-script-option/build.gradle b/examples/script-plugins/example-script-option/build.gradle new file mode 100644 index 0000000..9d1daf1 --- /dev/null +++ b/examples/script-plugins/example-script-option/build.gradle @@ -0,0 +1,24 @@ +buildscript { + repositories { + mavenCentral() + } +} +plugins { + id 'pl.allegro.tech.build.axion-release' version '1.7.0' +} + +ext.pluginName = 'Example Script Option' +ext.pluginDescription = "Provide a short description of your plugin here" +ext.sopsCopyright = "© 2018, Rundeck, Inc." +ext.sopsUrl = "http://rundeck.com" +ext.buildDateString=new Date().format("yyyy-MM-dd'T'HH:mm:ssX") +ext.archivesBaseName = "example-script-option" +ext.pluginBaseFolder = "." + +project.version = "0.1.0-SNAPSHOT" +ext.archiveFilename = ext.archivesBaseName + '-' + version + +// NOTE: Uses gradle-5.6 branch for stability. For production use, consider: +// 1. Copying the script into your repository, OR +// 2. Pinning to a specific commit hash +apply from: 'https://raw.githubusercontent.com/rundeck-plugins/build-zip/gradle-5.6/build.gradle' \ No newline at end of file diff --git a/examples/script-plugins/example-script-option/contents/option b/examples/script-plugins/example-script-option/contents/option new file mode 100644 index 0000000..51f937c --- /dev/null +++ b/examples/script-plugins/example-script-option/contents/option @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -eu + +echo "==START_OPTIONS==" +echo "opt1:First Option" +echo "opt2:Second Option" +echo "==END_OPTIONS==" \ No newline at end of file diff --git a/examples/script-plugins/example-script-option/plugin.yaml b/examples/script-plugins/example-script-option/plugin.yaml new file mode 100644 index 0000000..ac31559 --- /dev/null +++ b/examples/script-plugins/example-script-option/plugin.yaml @@ -0,0 +1,30 @@ +name: Example Script Option +rundeckPluginVersion: 2.0 +author: Rundeck Dev +description: Describe your plugin here +rundeckCompatibilityVersion: 3.x +targetHostCompatibility: unix +license: Apache 2.0 +tags: + - option + - Option +date: 2026-02-03T23:15:55.603266Z +version: 1.0.0 +providers: + - name: example-script-option + service: OptionValues + title: Example Script Option + description: The description of example-script-option plugin + plugin-type: script + script-interpreter: /bin/bash + script-file: option + config: + - type: String + name: example + title: 'Example String' + description: 'Example String' + required: false + - type: Boolean + name: debug + title: Debug? + description: 'Write debug messages to stderr' \ No newline at end of file diff --git a/examples/script-plugins/example-script-option/resources/icon.png b/examples/script-plugins/example-script-option/resources/icon.png new file mode 100644 index 0000000..7aaf7e8 Binary files /dev/null and b/examples/script-plugins/example-script-option/resources/icon.png differ diff --git a/examples/script-plugins/example-script-resource-model/Makefile b/examples/script-plugins/example-script-resource-model/Makefile new file mode 100644 index 0000000..eb95ef4 --- /dev/null +++ b/examples/script-plugins/example-script-resource-model/Makefile @@ -0,0 +1,11 @@ +all: install + +clean: + rm -rf build + +build: + mkdir -p build/libs build/zip-content/example-script-resource-model + cp -r contents resources plugin.yaml build/zip-content/example-script-resource-model + cd build/zip-content; zip -r example-script-resource-model.zip * + mv build/zip-content/example-script-resource-model.zip build/libs + diff --git a/examples/script-plugins/example-script-resource-model/README.md b/examples/script-plugins/example-script-resource-model/README.md new file mode 100644 index 0000000..961e354 --- /dev/null +++ b/examples/script-plugins/example-script-resource-model/README.md @@ -0,0 +1,22 @@ +# Example Script Resource Model Rundeck Plugin + +This is a ResourceModelSource plugin. + +## Build + +* Using gradle +``` +gradle clean build +``` + +* Using make + +``` +make clean build +``` + +## Install + +``` +cp build/libs/example-script-resource-model.zip $RDECK_BASE/libext +``` \ No newline at end of file diff --git a/examples/script-plugins/example-script-resource-model/build.gradle b/examples/script-plugins/example-script-resource-model/build.gradle new file mode 100644 index 0000000..3a3d4da --- /dev/null +++ b/examples/script-plugins/example-script-resource-model/build.gradle @@ -0,0 +1,24 @@ +buildscript { + repositories { + mavenCentral() + } +} +plugins { + id 'pl.allegro.tech.build.axion-release' version '1.7.0' +} + +ext.pluginName = 'Example Script Resource Model' +ext.pluginDescription = "Provide a short description of your plugin here" +ext.sopsCopyright = "© 2018, Rundeck, Inc." +ext.sopsUrl = "http://rundeck.com" +ext.buildDateString=new Date().format("yyyy-MM-dd'T'HH:mm:ssX") +ext.archivesBaseName = "example-script-resource-model" +ext.pluginBaseFolder = "." + +project.version = "0.1.0-SNAPSHOT" +ext.archiveFilename = ext.archivesBaseName + '-' + version + +// NOTE: Uses gradle-5.6 branch for stability. For production use, consider: +// 1. Copying the script into your repository, OR +// 2. Pinning to a specific commit hash +apply from: 'https://raw.githubusercontent.com/rundeck-plugins/build-zip/gradle-5.6/build.gradle' \ No newline at end of file diff --git a/examples/script-plugins/example-script-resource-model/contents/resource b/examples/script-plugins/example-script-resource-model/contents/resource new file mode 100644 index 0000000..5b45083 --- /dev/null +++ b/examples/script-plugins/example-script-resource-model/contents/resource @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -eu + +#Your script here +cat <<-END +{ + "madmartigan.local": { + "tags": "$RD_CONFIG_CUSTOMTAGS", + "osFamily": "unix", + "username": "rundeckuser", + "osVersion": "10.10.3", + "osArch": "x86_64", + "description": "Rundeck server node", + "hostname": "localhost", + "nodename": "madmartigan.local", + "osName": "Mac OS X" + }, + "test": { + "tags": "$RD_CONFIG_CUSTOMTAGS", + "osFamily": "unix", + "ssh-key-storage-path": "keys/testkey1.pem", + "username": "vagrant", + "osVersion": "10.10.3", + "osArch": "x86_64", + "description": "Rundeck server node", + "hostname": "127.0.0.1", + "nodename": "test", + "osName": "Mac OS X" + } +} +END + diff --git a/examples/script-plugins/example-script-resource-model/plugin.yaml b/examples/script-plugins/example-script-resource-model/plugin.yaml new file mode 100644 index 0000000..84d72a7 --- /dev/null +++ b/examples/script-plugins/example-script-resource-model/plugin.yaml @@ -0,0 +1,42 @@ +name: Example Script Resource Model +rundeckPluginVersion: 2.0 +author: Rundeck Dev +description: Describe your plugin here +rundeckCompatibilityVersion: 3.x +targetHostCompatibility: unix +license: Apache 2.0 +tags: + - script + - ResourceModelSource +date: 2026-02-03T23:15:54.549683Z +version: 1.0.0 +providers: + - name: example-script-resource-model + service: ResourceModelSource + title: Example Script Resource Model + description: The description of example-script-resource-model plugin + plugin-type: script + script-interpreter: /bin/bash + script-file: resource + resource-format: resourcejson + config: + - type: String + name: customtags + title: 'Example Custom Tags' + description: 'Add Custom Tags' + required: true + - type: Select + name: exampleSelect + title: ExampleSelect + description: 'Node Filter' + default: app + values: + - app + - db + - www + - type: Boolean + name: running + title: Just running nodes? + description: 'Filter by running nodes' + renderingOptions: + groupName: 'Config' \ No newline at end of file diff --git a/examples/script-plugins/example-script-resource-model/resources/icon.png b/examples/script-plugins/example-script-resource-model/resources/icon.png new file mode 100644 index 0000000..7aaf7e8 Binary files /dev/null and b/examples/script-plugins/example-script-resource-model/resources/icon.png differ diff --git a/examples/script-plugins/example-script-workflow-step/Makefile b/examples/script-plugins/example-script-workflow-step/Makefile new file mode 100644 index 0000000..4abde92 --- /dev/null +++ b/examples/script-plugins/example-script-workflow-step/Makefile @@ -0,0 +1,11 @@ +all: install + +clean: + rm -rf build + +build: + mkdir -p build/libs build/zip-content/example-script-workflow-step + cp -r contents resources plugin.yaml build/zip-content/example-script-workflow-step + cd build/zip-content; zip -r example-script-workflow-step.zip * + mv build/zip-content/example-script-workflow-step.zip build/libs + diff --git a/examples/script-plugins/example-script-workflow-step/README.md b/examples/script-plugins/example-script-workflow-step/README.md new file mode 100644 index 0000000..b06dd5a --- /dev/null +++ b/examples/script-plugins/example-script-workflow-step/README.md @@ -0,0 +1,22 @@ +# Example Script Workflow Step Rundeck Plugin + +This is a WorkflowNodeStep plugin. + +## Build + +* Using gradle +``` +gradle clean build +``` + +* Using make + +``` +make clean build +``` + +## Install + +``` +cp build/libs/example-script-workflow-step.zip $RDECK_BASE/libext +``` \ No newline at end of file diff --git a/examples/script-plugins/example-script-workflow-step/build.gradle b/examples/script-plugins/example-script-workflow-step/build.gradle new file mode 100644 index 0000000..ce92c1b --- /dev/null +++ b/examples/script-plugins/example-script-workflow-step/build.gradle @@ -0,0 +1,24 @@ +buildscript { + repositories { + mavenCentral() + } +} +plugins { + id 'pl.allegro.tech.build.axion-release' version '1.7.0' +} + +ext.pluginName = 'Example Script Workflow Step' +ext.pluginDescription = "Provide a short description of your plugin here" +ext.sopsCopyright = "© 2018, Rundeck, Inc." +ext.sopsUrl = "http://rundeck.com" +ext.buildDateString=new Date().format("yyyy-MM-dd'T'HH:mm:ssX") +ext.archivesBaseName = "example-script-workflow-step" +ext.pluginBaseFolder = "." + +project.version = "0.1.0-SNAPSHOT" +ext.archiveFilename = ext.archivesBaseName + '-' + version + +// NOTE: Uses gradle-5.6 branch for stability. For production use, consider: +// 1. Copying the script into your repository, OR +// 2. Pinning to a specific commit hash +apply from: 'https://raw.githubusercontent.com/rundeck-plugins/build-zip/gradle-5.6/build.gradle' \ No newline at end of file diff --git a/examples/script-plugins/example-script-workflow-step/contents/exec b/examples/script-plugins/example-script-workflow-step/contents/exec new file mode 100644 index 0000000..be4a4de --- /dev/null +++ b/examples/script-plugins/example-script-workflow-step/contents/exec @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -eu + +#Your script here + +if [[ "true" == "$RD_CONFIG_DEBUG" ]] ; then + # WARNING: Never log all RD_CONFIG_* variables as they may contain secrets from Key Storage + # Only log specific non-sensitive configuration values needed for debugging + echo "Debug mode enabled" + echo "Plugin: example-script-workflow-step" +fi + +echo "running example-script-workflow-step" +echo "Example Config: $RD_CONFIG_EXAMPLE" +echo "Example Multi-Line Config: $RD_CONFIG_MULTILINEEXAMPLE" +echo "Example Select Config: $RD_CONFIG_EXAMPLESELECT" diff --git a/examples/script-plugins/example-script-workflow-step/plugin.yaml b/examples/script-plugins/example-script-workflow-step/plugin.yaml new file mode 100644 index 0000000..87b4405 --- /dev/null +++ b/examples/script-plugins/example-script-workflow-step/plugin.yaml @@ -0,0 +1,65 @@ +name: Example Script Workflow Step +rundeckPluginVersion: 2.0 +author: Rundeck Dev +description: Describe your plugin here +rundeckCompatibilityVersion: 3.x +targetHostCompatibility: unix +license: Apache 2.0 +tags: + - script + - WorkflowNodeStep +date: 2026-02-03T23:15:54.018721Z +version: 1.0.0 +providers: + - name: example-script-workflow-step + service: WorkflowNodeStep + title: Example Script Workflow Step + description: The description of example-script-workflow-step plugin + plugin-type: script + script-interpreter: /bin/bash + script-file: exec + config: + - type: String + name: example + title: 'Example String' + description: 'Example String' + required: true + - type: String + name: multilineexample + title: 'Multi-line Example String' + description: 'Multi-line Example String' + renderingOptions: + displayType: MULTI_LINE + - type: Select + name: exampleSelect + title: ExampleSelect + description: 'Example Select' + default: Beige + values: + - Blue + - Beige + - Black + - type: Boolean + name: debug + title: Debug? + description: 'Write debug messages to stderr' + renderingOptions: + groupName: 'Config' + - type: String + name: storageprivatekey + title: Storage Private Key + description: Access to storage private key example + renderingOptions: + selectionAccessor: "STORAGE_PATH" + valueConversion: "STORAGE_PATH_AUTOMATIC_READ" + storage-path-root: "keys" + storage-file-meta-filter: "Rundeck-key-type=private" + - type: String + name: storagepassword + title: Storage Password + description: Access to storage password example + renderingOptions: + selectionAccessor: "STORAGE_PATH" + valueConversion: "STORAGE_PATH_AUTOMATIC_READ" + storage-path-root: "keys" + storage-file-meta-filter: "Rundeck-data-type=password" \ No newline at end of file diff --git a/examples/script-plugins/example-script-workflow-step/resources/icon.png b/examples/script-plugins/example-script-workflow-step/resources/icon.png new file mode 100644 index 0000000..7aaf7e8 Binary files /dev/null and b/examples/script-plugins/example-script-workflow-step/resources/icon.png differ diff --git a/examples/ui-plugins/example-ui-plugin/Makefile b/examples/ui-plugins/example-ui-plugin/Makefile new file mode 100644 index 0000000..8eb62c3 --- /dev/null +++ b/examples/ui-plugins/example-ui-plugin/Makefile @@ -0,0 +1,11 @@ +all: install + +clean: + rm -rf build + +build: + mkdir -p build/libs build/zip-content/example-ui-plugin + cp -r resources plugin.yaml build/zip-content/example-ui-plugin + cd build/zip-content; zip -r example-ui-plugin.zip * + mv build/zip-content/example-ui-plugin.zip build/libs + diff --git a/examples/ui-plugins/example-ui-plugin/README.md b/examples/ui-plugins/example-ui-plugin/README.md new file mode 100644 index 0000000..9b4d6da --- /dev/null +++ b/examples/ui-plugins/example-ui-plugin/README.md @@ -0,0 +1,16 @@ +# Example UI Plugin Rundeck Plugin + +This is a UI plugin. + +## Build + +Using make: +``` +make clean build +``` + +## Install + +``` +cp build/libs/example-ui-plugin.zip $RDECK_BASE/libext +``` \ No newline at end of file diff --git a/examples/ui-plugins/example-ui-plugin/plugin.yaml b/examples/ui-plugins/example-ui-plugin/plugin.yaml new file mode 100644 index 0000000..5c2ee27 --- /dev/null +++ b/examples/ui-plugins/example-ui-plugin/plugin.yaml @@ -0,0 +1,25 @@ +name: Example UI Plugin +rundeckPluginVersion: 2.0 +author: Rundeck Dev +description: Describe your plugin here +rundeckCompatibilityVersion: 3.x +targetHostCompatibility: unix +license: Apache 2.0 +tags: + - script + - UI +date: 2026-02-03T23:15:56.135349Z +version: 1.0.0 +providers: + - name: example-ui-plugin + service: UI + plugin-type: ui + title: Example UI Plugin + description: The description of your plugin + ui: + - pages: '*' + scripts: + - js/example-ui-plugin-init.js + - js/main.js + styles: + - css/example-ui-plugin-styles.css \ No newline at end of file diff --git a/examples/ui-plugins/example-ui-plugin/resources/css/example-ui-plugin-styles.css b/examples/ui-plugins/example-ui-plugin/resources/css/example-ui-plugin-styles.css new file mode 100644 index 0000000..ca0aaa6 --- /dev/null +++ b/examples/ui-plugins/example-ui-plugin/resources/css/example-ui-plugin-styles.css @@ -0,0 +1,3 @@ +.ui-plugin-text { + font-size: 24pt; +} \ No newline at end of file diff --git a/examples/ui-plugins/example-ui-plugin/resources/icon.png b/examples/ui-plugins/example-ui-plugin/resources/icon.png new file mode 100644 index 0000000..7aaf7e8 Binary files /dev/null and b/examples/ui-plugins/example-ui-plugin/resources/icon.png differ diff --git a/examples/ui-plugins/example-ui-plugin/resources/js/example-ui-plugin-init.js b/examples/ui-plugins/example-ui-plugin/resources/js/example-ui-plugin-init.js new file mode 100644 index 0000000..6d1bd36 --- /dev/null +++ b/examples/ui-plugins/example-ui-plugin/resources/js/example-ui-plugin-init.js @@ -0,0 +1,6 @@ + +console.log("Sample UI plugin init complete") + +jQuery(document).on('load.rundeck.page', function () { + addMyText() +}); diff --git a/examples/ui-plugins/example-ui-plugin/resources/js/main.js b/examples/ui-plugins/example-ui-plugin/resources/js/main.js new file mode 100644 index 0000000..0b7f44b --- /dev/null +++ b/examples/ui-plugins/example-ui-plugin/resources/js/main.js @@ -0,0 +1,4 @@ + +function addMyText() { + jQuery('.main-panel').prepend("

Sample UI plugin loaded
") +} diff --git a/generate-examples.sh b/generate-examples.sh new file mode 100755 index 0000000..b9a4f7b --- /dev/null +++ b/generate-examples.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash + +# +# Script to generate all plugin examples +# This provides a consistent set of examples for documentation purposes +# + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EXAMPLES_DIR="${SCRIPT_DIR}/examples" +BOOTSTRAP_CMD="${SCRIPT_DIR}/run.sh" + +echo "==========================================" +echo "Rundeck Plugin Bootstrap - Example Generator" +echo "==========================================" +echo "" + +# Clean examples directory if it exists +if [ -d "$EXAMPLES_DIR" ]; then + echo "Cleaning existing examples directory..." + rm -rf "$EXAMPLES_DIR" +fi + +mkdir -p "$EXAMPLES_DIR" + +# Build the bootstrap tool first +echo "Building rundeck-plugin-bootstrap..." +./gradlew clean build shadowDistZip > /dev/null 2>&1 +echo "✓ Build complete" +echo "" + +# Extract the shadow distribution +cd build/distributions +SHADOW_ZIP=$(ls rundeck-plugin-bootstrap-shadow-*.zip | head -1) +unzip -q "$SHADOW_ZIP" +SHADOW_DIR=$(basename "$SHADOW_ZIP" .zip) +cd "$SCRIPT_DIR" + +BOOTSTRAP_BIN="./build/distributions/${SHADOW_DIR}/bin/rundeck-plugin-bootstrap" + +echo "Generating example plugins..." +echo "" + +# Java Plugin Examples +echo "Java Plugins:" +echo " → Notification Plugin" +$BOOTSTRAP_BIN -n "Example Notification" -t java -s Notification -d "$EXAMPLES_DIR/java-plugins" + +echo " → Workflow Step Plugin" +$BOOTSTRAP_BIN -n "Example Workflow Step" -t java -s WorkflowStep -d "$EXAMPLES_DIR/java-plugins" + +echo " → Workflow Node Step Plugin" +$BOOTSTRAP_BIN -n "Example Workflow Node Step" -t java -s WorkflowNodeStep -d "$EXAMPLES_DIR/java-plugins" + +echo " → Resource Model Source Plugin" +$BOOTSTRAP_BIN -n "Example Resource Model Source" -t java -s ResourceModelSource -d "$EXAMPLES_DIR/java-plugins" + +echo " → Log Filter Plugin" +$BOOTSTRAP_BIN -n "Example Log Filter" -t java -s LogFilter -d "$EXAMPLES_DIR/java-plugins" + +echo " → Node Executor Plugin" +$BOOTSTRAP_BIN -n "Example Node Executor" -t java -s NodeExecutor -d "$EXAMPLES_DIR/java-plugins" + +echo " → Orchestrator Plugin" +$BOOTSTRAP_BIN -n "Example Orchestrator" -t java -s Orchestrator -d "$EXAMPLES_DIR/java-plugins" + +echo " → Option Plugin" +$BOOTSTRAP_BIN -n "Example Option" -t java -s Option -d "$EXAMPLES_DIR/java-plugins" + +echo "" + +# Script Plugin Examples +echo "Script Plugins:" +echo " → Node Executor Plugin" +$BOOTSTRAP_BIN -n "Example Script Node Executor" -t script -s NodeExecutor -d "$EXAMPLES_DIR/script-plugins" + +echo " → Workflow Node Step Plugin" +$BOOTSTRAP_BIN -n "Example Script Workflow Step" -t script -s WorkflowNodeStep -d "$EXAMPLES_DIR/script-plugins" + +echo " → Resource Model Source Plugin" +$BOOTSTRAP_BIN -n "Example Script Resource Model" -t script -s ResourceModelSource -d "$EXAMPLES_DIR/script-plugins" + +echo " → File Copier Plugin" +$BOOTSTRAP_BIN -n "Example Script File Copier" -t script -s FileCopier -d "$EXAMPLES_DIR/script-plugins" + +echo " → Option Plugin" +$BOOTSTRAP_BIN -n "Example Script Option" -t script -s Option -d "$EXAMPLES_DIR/script-plugins" + +echo "" + +# UI Plugin Examples +echo "UI Plugins:" +echo " → UI Plugin" +$BOOTSTRAP_BIN -n "Example UI Plugin" -t ui -s UI -d "$EXAMPLES_DIR/ui-plugins" + +echo "" +echo "==========================================" +echo "✓ All examples generated successfully!" +echo "==========================================" +echo "" +echo "Examples are located in: $EXAMPLES_DIR" +echo "" +echo "Directory structure:" +tree -L 2 "$EXAMPLES_DIR" 2>/dev/null || find "$EXAMPLES_DIR" -maxdepth 2 -type d | sed 's|[^/]*/| |g' +echo "" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ffed3a2..3994438 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/groovy/com/rundeck/plugin/Generator.groovy b/src/main/groovy/com/rundeck/plugin/Generator.groovy index 4bda330..d896500 100644 --- a/src/main/groovy/com/rundeck/plugin/Generator.groovy +++ b/src/main/groovy/com/rundeck/plugin/Generator.groovy @@ -27,15 +27,21 @@ import java.util.concurrent.Callable */ @Command(description = "Create a Rundeck plugin artifact.", - name = "plugin-bootstrap", mixinStandardHelpOptions = true, version = "1.1") -class Generator implements Callable{ + name = "plugin-bootstrap", mixinStandardHelpOptions = true, version = "1.2") +class Generator implements Callable{ static void main(String[] args) throws Exception { + int exitCode = 0 try{ - CommandLine.call(new Generator(), args) + exitCode = new CommandLine(new Generator()).execute(args) }catch(Exception e){ - println(e.getMessage()) + System.err.println("Error: ${e.getMessage()}") + if (System.getProperty("debug") != null) { + e.printStackTrace(System.err) + } + exitCode = 1 } + System.exit(exitCode) } @Option(names = [ "-n", "--pluginName" ], description = "Plugin Name." , required = true) @@ -48,7 +54,7 @@ class Generator implements Callable{ String destinationDirectory @Override - Void call() throws Exception { + Integer call() throws Exception { FilesystemArtifactTemplateGenerator generator = new FilesystemArtifactTemplateGenerator() println generator.generate(this.pluginName, @@ -56,6 +62,6 @@ class Generator implements Callable{ this.serviceType.toString(), this.destinationDirectory) - return null + return 0 } } diff --git a/src/main/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGenerator.groovy b/src/main/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGenerator.groovy index 3ddd978..becfd35 100644 --- a/src/main/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGenerator.groovy +++ b/src/main/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGenerator.groovy @@ -35,8 +35,8 @@ class JavaPluginTemplateGenerator extends AbstractTemplateGenerator { templateProperties["providedService"] = providedService templateProperties["currentDate"] = Instant.now().toString() templateProperties["pluginLang"] = "java" - templateProperties["rundeckVersion"] = "5.0.2-20240212" - templateProperties["groovyVersion"] = "3.0.9" + templateProperties["rundeckVersion"] = "5.7.0-20250101" + templateProperties["groovyVersion"] = "3.0.21" templateProperties["apiKeyPath"] = "\${apiKeyPath}" templateProperties["data"] = "\${data}" templateProperties["resourceInfo"] = "resourceInfo" diff --git a/src/main/groovy/com/rundeck/plugin/utils/GeneratorUtils.groovy b/src/main/groovy/com/rundeck/plugin/utils/GeneratorUtils.groovy index 2deafd2..c4502f0 100644 --- a/src/main/groovy/com/rundeck/plugin/utils/GeneratorUtils.groovy +++ b/src/main/groovy/com/rundeck/plugin/utils/GeneratorUtils.groovy @@ -18,6 +18,6 @@ package com.rundeck.plugin.utils class GeneratorUtils { static String sanitizedPluginName(final String pluginName) { - return pluginName.replace(" ", "-").replaceAll("[^a-zA-Z\\-]","").toLowerCase() + return pluginName.replace(" ", "-").replaceAll("[^a-zA-Z0-9\\-]","").toLowerCase() } } diff --git a/src/main/resources/templates/java-plugin/logfilter/build.gradle.template b/src/main/resources/templates/java-plugin/logfilter/build.gradle.template index 466dc5a..90c3771 100644 --- a/src/main/resources/templates/java-plugin/logfilter/build.gradle.template +++ b/src/main/resources/templates/java-plugin/logfilter/build.gradle.template @@ -11,7 +11,7 @@ apply plugin: 'idea' sourceCompatibility = 1.8 ext.rundeckPluginVersion= '2.0' ext.rundeckVersion= '${rundeckVersion}' -ext.pluginClassNames='com.plugin.${sanitizedPluginName}.${javaPluginClass}' +ext.pluginClassNames='com.plugin.${javaPluginClass.toLowerCase()}.${javaPluginClass}' repositories { @@ -30,14 +30,14 @@ configurations{ } dependencies { - implementation 'org.rundeck:rundeck-core:4.14.2-20230713' + implementation 'org.rundeck:rundeck-core:${rundeckVersion}' - //use pluginLibs to add dependecies, example: + //use pluginLibs to add dependencies, example: //pluginLibs group: 'com.google.code.gson', name: 'gson', version: '2.8.2' testImplementation 'junit:junit:4.12' - testImplementation "org.codehaus.groovy:groovy-all:2.4.15" - testImplementation "org.spockframework:spock-core:1.0-groovy-2.4" + testImplementation "org.codehaus.groovy:groovy-all:${groovyVersion}" + testImplementation "org.spockframework:spock-core:2.2-groovy-3.0" } // task to copy plugin libs to output/lib dir diff --git a/src/main/resources/templates/java-plugin/notification/Plugin.groovy.template b/src/main/resources/templates/java-plugin/notification/Plugin.groovy.template index 70e74f4..970f290 100644 --- a/src/main/resources/templates/java-plugin/notification/Plugin.groovy.template +++ b/src/main/resources/templates/java-plugin/notification/Plugin.groovy.template @@ -66,7 +66,9 @@ public class ${javaPluginClass} implements NotificationPlugin, AcceptsServices { //Pass in config properties to the API so that secret can be used in api call ExampleApis api = new ExampleApis(config as Properties); - logger.warn(api.post(apiKey)) + // Send notification - NOTE: Never log full API responses as they may contain sensitive data + def response = api.post(apiKey) + logger.info("Notification sent successfully") return true; } diff --git a/src/main/resources/templates/java-plugin/notification/PluginSpec.groovy.template b/src/main/resources/templates/java-plugin/notification/PluginSpec.groovy.template index 12cc8b1..3708a40 100644 --- a/src/main/resources/templates/java-plugin/notification/PluginSpec.groovy.template +++ b/src/main/resources/templates/java-plugin/notification/PluginSpec.groovy.template @@ -1,6 +1,5 @@ package com.plugin.${javaPluginClass.toLowerCase()} -import spock.lang.Specification import spock.lang.Specification import org.rundeck.app.spi.Services import org.rundeck.storage.api.Resource diff --git a/src/main/resources/templates/java-plugin/option/Plugin.java.template b/src/main/resources/templates/java-plugin/option/Plugin.java.template index 1d67154..e1d23c5 100644 --- a/src/main/resources/templates/java-plugin/option/Plugin.java.template +++ b/src/main/resources/templates/java-plugin/option/Plugin.java.template @@ -58,7 +58,7 @@ public class ${javaPluginClass} implements OptionValuesPlugin, Describable{ return options; } - class StandardOptionValue implements OptionValue { + static class StandardOptionValue implements OptionValue { private String name; private String value; diff --git a/src/main/resources/templates/java-plugin/option/build.gradle.template b/src/main/resources/templates/java-plugin/option/build.gradle.template index bed0892..f4a01fc 100644 --- a/src/main/resources/templates/java-plugin/option/build.gradle.template +++ b/src/main/resources/templates/java-plugin/option/build.gradle.template @@ -8,7 +8,7 @@ defaultTasks 'clean','build' sourceCompatibility = 1.8 ext.rundeckPluginVersion= '2.0' ext.rundeckVersion= '${rundeckVersion}' -ext.pluginClassNames='com.plugin.${sanitizedPluginName}.${javaPluginClass}' +ext.pluginClassNames='com.plugin.${javaPluginClass.toLowerCase()}.${javaPluginClass}' repositories { mavenLocal() @@ -26,14 +26,14 @@ configurations{ } dependencies { - implementation 'org.rundeck:rundeck-core:4.14.2-20230713' + implementation 'org.rundeck:rundeck-core:${rundeckVersion}' - //use pluginLibs to add dependecies, example: + //use pluginLibs to add dependencies, example: //pluginLibs group: 'com.google.code.gson', name: 'gson', version: '2.8.2' testImplementation 'junit:junit:4.12' - testImplementation "org.codehaus.groovy:groovy-all:2.4.15" - testImplementation "org.spockframework:spock-core:1.0-groovy-2.4" + testImplementation "org.codehaus.groovy:groovy-all:${groovyVersion}" + testImplementation "org.spockframework:spock-core:2.2-groovy-3.0" } // task to copy plugin libs to output/lib dir diff --git a/src/main/resources/templates/java-plugin/orchestrator/Plugin.java.template b/src/main/resources/templates/java-plugin/orchestrator/Plugin.java.template index e102f70..d4db2a2 100644 --- a/src/main/resources/templates/java-plugin/orchestrator/Plugin.java.template +++ b/src/main/resources/templates/java-plugin/orchestrator/Plugin.java.template @@ -13,14 +13,11 @@ import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.Collection; -import java.util.List; -import java.util.stream.Collectors; - import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Random; +import java.util.stream.Collectors; @Plugin(service=ServiceNameConstants.Orchestrator,name="${sanitizedPluginName}") @PluginDescription(title="${pluginName}", description="My Orchestrator plugin description") diff --git a/src/main/resources/templates/java-plugin/orchestrator/build.gradle.template b/src/main/resources/templates/java-plugin/orchestrator/build.gradle.template index 0b15044..b0e4a47 100644 --- a/src/main/resources/templates/java-plugin/orchestrator/build.gradle.template +++ b/src/main/resources/templates/java-plugin/orchestrator/build.gradle.template @@ -11,7 +11,7 @@ apply plugin: 'idea' sourceCompatibility = 1.8 ext.rundeckPluginVersion= '2.0' ext.rundeckVersion= '${rundeckVersion}' -ext.pluginClassNames='com.plugin.${sanitizedPluginName}.${javaPluginClass}' +ext.pluginClassNames='com.plugin.${javaPluginClass.toLowerCase()}.${javaPluginClass}' repositories { @@ -30,14 +30,14 @@ configurations{ } dependencies { - implementation 'org.rundeck:rundeck-core:4.14.2-20230713' + implementation 'org.rundeck:rundeck-core:${rundeckVersion}' - //use pluginLibs to add dependecies, example: + //use pluginLibs to add dependencies, example: //pluginLibs group: 'com.google.code.gson', name: 'gson', version: '2.8.2' testImplementation 'junit:junit:4.12' - testImplementation "org.codehaus.groovy:groovy-all:2.4.15" - testImplementation "org.spockframework:spock-core:1.0-groovy-2.4" + testImplementation "org.codehaus.groovy:groovy-all:${groovyVersion}" + testImplementation "org.spockframework:spock-core:2.2-groovy-3.0" } // task to copy plugin libs to output/lib dir diff --git a/src/main/resources/templates/java-plugin/workflownodestep/Plugin.groovy.template b/src/main/resources/templates/java-plugin/workflownodestep/Plugin.groovy.template index c9661e0..6f5c265 100644 --- a/src/main/resources/templates/java-plugin/workflownodestep/Plugin.groovy.template +++ b/src/main/resources/templates/java-plugin/workflownodestep/Plugin.groovy.template @@ -154,7 +154,7 @@ By default, it will be collapsed in the list of properties, thanks to the '@Rend /** * Next, we'll resolve the API token itself. There's a perfect function for this in the Util class, - * getPasswordFromKeyStorage. YOu can see more about how the process works in the Util file. + * getPasswordFromKeyStorage. You can see more about how the process works in the Util file. */ try { userApiKey = Util.getPasswordFromKeyStorage(apiKeyPath, context) diff --git a/src/main/resources/templates/java-plugin/workflowstep/Plugin.groovy.template b/src/main/resources/templates/java-plugin/workflowstep/Plugin.groovy.template index c8f5dad..745c29d 100644 --- a/src/main/resources/templates/java-plugin/workflowstep/Plugin.groovy.template +++ b/src/main/resources/templates/java-plugin/workflowstep/Plugin.groovy.template @@ -151,7 +151,7 @@ By default, it will be collapsed in the list of properties, thanks to the '@Rend /** * Next, we'll resolve the API token itself. There's a perfect function for this in the Util class, - * getPasswordFromKeyStorage. YOu can see more about how the process works in the Util file. + * getPasswordFromKeyStorage. You can see more about how the process works in the Util file. */ try { userApiKey = Util.getPasswordFromKeyStorage(apiKeyPath, context) diff --git a/src/main/resources/templates/script-plugin/filecopier/build.gradle.template b/src/main/resources/templates/script-plugin/filecopier/build.gradle.template index fd9b92a..10a1993 100644 --- a/src/main/resources/templates/script-plugin/filecopier/build.gradle.template +++ b/src/main/resources/templates/script-plugin/filecopier/build.gradle.template @@ -18,4 +18,7 @@ ext.pluginBaseFolder = "." project.version = "0.1.0-SNAPSHOT" ext.archiveFilename = ext.archivesBaseName + '-' + version +// WARNING: This loads build logic from a remote URL. For production use, consider: +// 1. Copying the script into your repository, OR +// 2. Pinning to a specific commit hash instead of 'master' apply from: 'https://raw.githubusercontent.com/rundeck-plugins/build-zip/master/build.gradle' \ No newline at end of file diff --git a/src/main/resources/templates/script-plugin/filecopier/filecopier.template b/src/main/resources/templates/script-plugin/filecopier/filecopier.template index 8b6fb58..2dfe450 100644 --- a/src/main/resources/templates/script-plugin/filecopier/filecopier.template +++ b/src/main/resources/templates/script-plugin/filecopier/filecopier.template @@ -9,8 +9,8 @@ DESTINATION="\$*" #do a dry run if [[ "true" == "\$RD_CONFIG_DRY_RUN" ]] ; then - env | grep RD_CONFIG - echo "Coping file from \$SOURCE to \$HOST: \$DESTINATION " + # WARNING: Never log all RD_CONFIG_* variables as they may contain secrets from Key Storage + echo "Dry run mode - copying file from \$SOURCE to \$HOST: \$DESTINATION" exit 0 fi diff --git a/src/main/resources/templates/script-plugin/nodeexecutor-filecopier/build.gradle.template b/src/main/resources/templates/script-plugin/nodeexecutor-filecopier/build.gradle.template index fd9b92a..10a1993 100644 --- a/src/main/resources/templates/script-plugin/nodeexecutor-filecopier/build.gradle.template +++ b/src/main/resources/templates/script-plugin/nodeexecutor-filecopier/build.gradle.template @@ -18,4 +18,7 @@ ext.pluginBaseFolder = "." project.version = "0.1.0-SNAPSHOT" ext.archiveFilename = ext.archivesBaseName + '-' + version +// WARNING: This loads build logic from a remote URL. For production use, consider: +// 1. Copying the script into your repository, OR +// 2. Pinning to a specific commit hash instead of 'master' apply from: 'https://raw.githubusercontent.com/rundeck-plugins/build-zip/master/build.gradle' \ No newline at end of file diff --git a/src/main/resources/templates/script-plugin/nodeexecutor-filecopier/filecopier.template b/src/main/resources/templates/script-plugin/nodeexecutor-filecopier/filecopier.template index 319c85f..124954a 100644 --- a/src/main/resources/templates/script-plugin/nodeexecutor-filecopier/filecopier.template +++ b/src/main/resources/templates/script-plugin/nodeexecutor-filecopier/filecopier.template @@ -10,8 +10,8 @@ DESTINATION="\$*" #do a dry run if [[ "true" == "\$RD_CONFIG_DRY_RUN" ]] ; then - env | grep RD_CONFIG - echo "Coping file from \$SOURCE to \$HOST: \$DESTINATION " + # WARNING: Never log all RD_CONFIG_* variables as they may contain secrets from Key Storage + echo "Dry run mode - copying file from \$SOURCE to \$HOST: \$DESTINATION" exit 0 fi diff --git a/src/main/resources/templates/script-plugin/nodeexecutor-filecopier/nodeexecutor.template b/src/main/resources/templates/script-plugin/nodeexecutor-filecopier/nodeexecutor.template index 6e92b1e..e594d8e 100644 --- a/src/main/resources/templates/script-plugin/nodeexecutor-filecopier/nodeexecutor.template +++ b/src/main/resources/templates/script-plugin/nodeexecutor-filecopier/nodeexecutor.template @@ -8,8 +8,8 @@ CMD="\$*" #do a dry run if [[ "true" == "\$RD_CONFIG_DRY_RUN" ]] ; then - env | grep RD_CONFIG - echo "[${sanitizedPluginName}] command to run on node \$HOST: \$CMD" + # WARNING: Never log all RD_CONFIG_* variables as they may contain secrets from Key Storage + echo "[${sanitizedPluginName}] Dry run mode - command to run on node \$HOST: \$CMD" exit 0 fi diff --git a/src/main/resources/templates/script-plugin/nodeexecutor/build.gradle.template b/src/main/resources/templates/script-plugin/nodeexecutor/build.gradle.template index fd9b92a..10a1993 100644 --- a/src/main/resources/templates/script-plugin/nodeexecutor/build.gradle.template +++ b/src/main/resources/templates/script-plugin/nodeexecutor/build.gradle.template @@ -18,4 +18,7 @@ ext.pluginBaseFolder = "." project.version = "0.1.0-SNAPSHOT" ext.archiveFilename = ext.archivesBaseName + '-' + version +// WARNING: This loads build logic from a remote URL. For production use, consider: +// 1. Copying the script into your repository, OR +// 2. Pinning to a specific commit hash instead of 'master' apply from: 'https://raw.githubusercontent.com/rundeck-plugins/build-zip/master/build.gradle' \ No newline at end of file diff --git a/src/main/resources/templates/script-plugin/nodeexecutor/nodeexecutor.template b/src/main/resources/templates/script-plugin/nodeexecutor/nodeexecutor.template index 6e92b1e..e594d8e 100644 --- a/src/main/resources/templates/script-plugin/nodeexecutor/nodeexecutor.template +++ b/src/main/resources/templates/script-plugin/nodeexecutor/nodeexecutor.template @@ -8,8 +8,8 @@ CMD="\$*" #do a dry run if [[ "true" == "\$RD_CONFIG_DRY_RUN" ]] ; then - env | grep RD_CONFIG - echo "[${sanitizedPluginName}] command to run on node \$HOST: \$CMD" + # WARNING: Never log all RD_CONFIG_* variables as they may contain secrets from Key Storage + echo "[${sanitizedPluginName}] Dry run mode - command to run on node \$HOST: \$CMD" exit 0 fi diff --git a/src/main/resources/templates/script-plugin/option/build.gradle.template b/src/main/resources/templates/script-plugin/option/build.gradle.template index fd9b92a..10a1993 100644 --- a/src/main/resources/templates/script-plugin/option/build.gradle.template +++ b/src/main/resources/templates/script-plugin/option/build.gradle.template @@ -18,4 +18,7 @@ ext.pluginBaseFolder = "." project.version = "0.1.0-SNAPSHOT" ext.archiveFilename = ext.archivesBaseName + '-' + version +// WARNING: This loads build logic from a remote URL. For production use, consider: +// 1. Copying the script into your repository, OR +// 2. Pinning to a specific commit hash instead of 'master' apply from: 'https://raw.githubusercontent.com/rundeck-plugins/build-zip/master/build.gradle' \ No newline at end of file diff --git a/src/main/resources/templates/script-plugin/resourcemodelsource/build.gradle.template b/src/main/resources/templates/script-plugin/resourcemodelsource/build.gradle.template index fd9b92a..10a1993 100644 --- a/src/main/resources/templates/script-plugin/resourcemodelsource/build.gradle.template +++ b/src/main/resources/templates/script-plugin/resourcemodelsource/build.gradle.template @@ -18,4 +18,7 @@ ext.pluginBaseFolder = "." project.version = "0.1.0-SNAPSHOT" ext.archiveFilename = ext.archivesBaseName + '-' + version +// WARNING: This loads build logic from a remote URL. For production use, consider: +// 1. Copying the script into your repository, OR +// 2. Pinning to a specific commit hash instead of 'master' apply from: 'https://raw.githubusercontent.com/rundeck-plugins/build-zip/master/build.gradle' \ No newline at end of file diff --git a/src/main/resources/templates/script-plugin/workflow/build.gradle.template b/src/main/resources/templates/script-plugin/workflow/build.gradle.template index fd9b92a..10a1993 100644 --- a/src/main/resources/templates/script-plugin/workflow/build.gradle.template +++ b/src/main/resources/templates/script-plugin/workflow/build.gradle.template @@ -18,4 +18,7 @@ ext.pluginBaseFolder = "." project.version = "0.1.0-SNAPSHOT" ext.archiveFilename = ext.archivesBaseName + '-' + version +// WARNING: This loads build logic from a remote URL. For production use, consider: +// 1. Copying the script into your repository, OR +// 2. Pinning to a specific commit hash instead of 'master' apply from: 'https://raw.githubusercontent.com/rundeck-plugins/build-zip/master/build.gradle' \ No newline at end of file diff --git a/src/main/resources/templates/script-plugin/workflow/exec.template b/src/main/resources/templates/script-plugin/workflow/exec.template index f56d4b4..e037cc7 100644 --- a/src/main/resources/templates/script-plugin/workflow/exec.template +++ b/src/main/resources/templates/script-plugin/workflow/exec.template @@ -4,7 +4,10 @@ set -eu #Your script here if [[ "true" == "\$RD_CONFIG_DEBUG" ]] ; then - env | grep RD_CONFIG + # WARNING: Never log all RD_CONFIG_* variables as they may contain secrets from Key Storage + # Only log specific non-sensitive configuration values needed for debugging + echo "Debug mode enabled" + echo "Plugin: ${sanitizedPluginName}" fi echo "running ${sanitizedPluginName}"