Skip to content

Commit

Permalink
Merge pull request #21 from rundeck/update-java-plugins-standards
Browse files Browse the repository at this point in the history
Updated Java plugins standards
  • Loading branch information
jsboak authored Apr 24, 2024
2 parents 43ecb31 + 3834c12 commit c568999
Show file tree
Hide file tree
Showing 37 changed files with 1,310 additions and 75 deletions.
3 changes: 1 addition & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ ext.distInstallPath = '/var/lib/rundeck-pb'
defaultTasks 'clean', 'build'

dependencies {
implementation 'org.codehaus.groovy:groovy-all:2.5.6'
implementation 'org.codehaus.groovy:groovy-all:2.5.14'
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'
Expand Down Expand Up @@ -48,7 +48,6 @@ allprojects {
ext.rpmVersion=project.version.replaceAll('-', '.')
}


//force distZip/distTar artifacts to be overwritten by shadow versions
shadowDistZip.mustRunAfter distZip
shadowDistTar.mustRunAfter distTar
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,14 @@ class JavaPluginTemplateGenerator extends AbstractTemplateGenerator {
templateProperties["providedService"] = providedService
templateProperties["currentDate"] = Instant.now().toString()
templateProperties["pluginLang"] = "java"
templateProperties["rundeckVersion"] = "3.0.x"
templateProperties["rundeckVersion"] = "5.0.2-20240212"
templateProperties["groovyVersion"] = "3.0.9"
templateProperties["apiKeyPath"] = "\${apiKeyPath}"
templateProperties["data"] = "\${data}"
templateProperties["resourceInfo"] = "resourceInfo"
templateProperties["extra"] = "extra"
templateProperties["hiddenTestValue"] = "hiddenTestValue"
templateProperties["projectInfo"] = "projectInfo"
return templateProperties
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.plugin.${javaPluginClass.toLowerCase()};

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 = "${sanitizedPluginName}", service = ServiceNameConstants.NodeExecutor)
@PluginDescription(title = "${pluginName}", description = "A node executor plugin that can execute commands on remote nodes")
public class ${javaPluginClass} implements NodeExecutor, Describable {

public static final String SERVICE_PROVIDER_NAME = "${sanitizedPluginName}"

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("${pluginName}")
.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)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.plugin.${javaPluginClass.toLowerCase()}

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
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ defaultTasks 'clean','build'
apply plugin: 'java'
apply plugin: 'groovy'
apply plugin: 'idea'
sourceCompatibility = 1.8
sourceCompatibility = 11.0
ext.rundeckPluginVersion= '2.0'
ext.rundeckVersion= '${rundeckVersion}'
ext.pluginClassNames='com.plugin.${sanitizedPluginName}.${javaPluginClass}'
ext.pluginClassNames='com.plugin.${javaPluginClass.toLowerCase()}.${javaPluginClass}'


repositories {
Expand All @@ -30,14 +30,14 @@ configurations{
}

dependencies {
implementation 'org.rundeck:rundeck-core:4.14.2-20230713'

//use pluginLibs to add dependecies, example:
implementation 'org.rundeck:rundeck-core:${rundeckVersion}'
implementation 'org.codehaus.groovy:groovy-all:${groovyVersion}'
//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 'junit:junit:4.13.2'
testImplementation 'org.codehaus.groovy:groovy-all:${groovyVersion}'
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'
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
build.gradle.template->build.gradle
README.md.template->README.md
icon.png->src/main/resources/resources/icon.png
Plugin.java.template->src/main/java/com/plugin/${javaPluginClass.toLowerCase()}/${javaPluginClass}.java
PluginSpec.groovy.template->src/test/groovy/com/plugin/${javaPluginClass.toLowerCase()}/${javaPluginClass}Spec.groovy

Plugin.groovy.template->src/main/groovy/com/plugin/${javaPluginClass.toLowerCase()}/${javaPluginClass}.groovy
Util.groovy.template->src/main/groovy/com/plugin/${javaPluginClass.toLowerCase()}/Util.groovy
PluginSpec.groovy.template->src/test/groovy/com/plugin/${javaPluginClass.toLowerCase()}/${javaPluginClass}Spec.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.plugin.${javaPluginClass.toLowerCase()};

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();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.plugin.${javaPluginClass.toLowerCase()};

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="${sanitizedPluginName}")
@PluginDescription(title="${pluginName}", description="This is a notification plugin that integrated with ${pluginName}.")
public class ${javaPluginClass} implements NotificationPlugin, AcceptsServices {

static Logger logger = LoggerFactory.getLogger(${javaPluginClass}.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);

logger.warn(api.post(apiKey))

return true;
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
package com.plugin.${javaPluginClass.toLowerCase()};

import com.dtolabs.rundeck.plugins.notification.NotificationPlugin;
import com.dtolabs.rundeck.core.plugins.Plugin;
import com.dtolabs.rundeck.plugins.descriptions.PluginDescription;
import com.dtolabs.rundeck.plugins.descriptions.PluginProperty;
import java.util.*;
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.notification.NotificationPlugin
import org.slf4j.Logger
import org.slf4j.LoggerFactory

@Plugin(service="Notification",name="${sanitizedPluginName}")
@PluginDescription(title="${pluginName}", description="My plugin description")
public class ${javaPluginClass} implements NotificationPlugin{
@Plugin(service="Notification", name="${sanitizedPluginName}")
@PluginDescription(title="${pluginName}", description="This is a notification plugin that integrated with ${pluginName}.")
public class ${javaPluginClass} implements NotificationPlugin {

@PluginProperty(name = "example",title = "Example String",description = "Example description")
private String example;
static Logger logger = LoggerFactory.getLogger(Notificationplugin.class);

@PluginProperty(name = "test" ,title = "Test String", description = "a description")
String test;

public boolean postNotification(String trigger, Map executionData, Map config) {
System.err.printf("Trigger %s fired for %s, configuration: %s",trigger,executionData,config);
System.err.println();
System.err.printf("Local field example is: %s",example);

logger.info(new apiCall().post("{\"key\":\"value\"}"))

return true;
}

}
Loading

0 comments on commit c568999

Please sign in to comment.