Skip to content

Commit c568999

Browse files
authored
Merge pull request #21 from rundeck/update-java-plugins-standards
Updated Java plugins standards
2 parents 43ecb31 + 3834c12 commit c568999

37 files changed

Lines changed: 1310 additions & 75 deletions

build.gradle

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ ext.distInstallPath = '/var/lib/rundeck-pb'
1919
defaultTasks 'clean', 'build'
2020

2121
dependencies {
22-
implementation 'org.codehaus.groovy:groovy-all:2.5.6'
22+
implementation 'org.codehaus.groovy:groovy-all:2.5.14'
2323
implementation 'com.github.rundeck.cli-toolbelt:toolbelt:0.2.2'
2424
implementation 'com.github.rundeck.cli-toolbelt:toolbelt-jewelcli:0.2.2'
2525
implementation 'org.apache.commons:commons-text:1.4'
@@ -48,7 +48,6 @@ allprojects {
4848
ext.rpmVersion=project.version.replaceAll('-', '.')
4949
}
5050

51-
5251
//force distZip/distTar artifacts to be overwritten by shadow versions
5352
shadowDistZip.mustRunAfter distZip
5453
shadowDistTar.mustRunAfter distTar

src/main/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGenerator.groovy

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,14 @@ class JavaPluginTemplateGenerator extends AbstractTemplateGenerator {
3535
templateProperties["providedService"] = providedService
3636
templateProperties["currentDate"] = Instant.now().toString()
3737
templateProperties["pluginLang"] = "java"
38-
templateProperties["rundeckVersion"] = "3.0.x"
38+
templateProperties["rundeckVersion"] = "5.0.2-20240212"
39+
templateProperties["groovyVersion"] = "3.0.9"
40+
templateProperties["apiKeyPath"] = "\${apiKeyPath}"
41+
templateProperties["data"] = "\${data}"
42+
templateProperties["resourceInfo"] = "resourceInfo"
43+
templateProperties["extra"] = "extra"
44+
templateProperties["hiddenTestValue"] = "hiddenTestValue"
45+
templateProperties["projectInfo"] = "projectInfo"
3946
return templateProperties
4047
}
4148

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package com.plugin.${javaPluginClass.toLowerCase()};
2+
3+
import com.dtolabs.rundeck.core.common.INodeEntry
4+
import com.dtolabs.rundeck.core.execution.ExecutionContext
5+
import com.dtolabs.rundeck.core.execution.ExecutionLogger
6+
import com.dtolabs.rundeck.core.execution.service.NodeExecutor
7+
import com.dtolabs.rundeck.core.execution.service.NodeExecutorResult
8+
import com.dtolabs.rundeck.core.execution.service.NodeExecutorResultImpl
9+
import com.dtolabs.rundeck.core.execution.utils.ResolverUtil
10+
import com.dtolabs.rundeck.core.plugins.Plugin
11+
import com.dtolabs.rundeck.core.plugins.configuration.Describable
12+
import com.dtolabs.rundeck.core.plugins.configuration.Description;
13+
import com.dtolabs.rundeck.core.plugins.configuration.StringRenderingConstants;
14+
import com.dtolabs.rundeck.plugins.ServiceNameConstants;
15+
import com.dtolabs.rundeck.plugins.descriptions.PluginDescription;
16+
import com.dtolabs.rundeck.plugins.util.DescriptionBuilder
17+
import com.dtolabs.rundeck.plugins.util.PropertyBuilder;
18+
19+
@Plugin(name = "${sanitizedPluginName}", service = ServiceNameConstants.NodeExecutor)
20+
@PluginDescription(title = "${pluginName}", description = "A node executor plugin that can execute commands on remote nodes")
21+
public class ${javaPluginClass} implements NodeExecutor, Describable {
22+
23+
public static final String SERVICE_PROVIDER_NAME = "${sanitizedPluginName}"
24+
25+
public static final String PROJ_PROP_PREFIX = "project."
26+
public static final String FRAMEWORK_PROP_PREFIX = "framework."
27+
28+
public static final String MOCK_FAILURE = "mockFailure"
29+
public static final String USERNAME = "username"
30+
public static final String PASSWORD = "password"
31+
32+
@Override
33+
Description getDescription() {
34+
DescriptionBuilder builder = DescriptionBuilder.builder()
35+
.name(SERVICE_PROVIDER_NAME)
36+
.title("${pluginName}")
37+
.description("A node executor plugin that can execute commands on remote nodes")
38+
.property(PropertyBuilder.builder()
39+
.title("Username")
40+
.string(USERNAME)
41+
.description("The username to use for the connection")
42+
.required(true)
43+
.renderingOption(StringRenderingConstants.INSTANCE_SCOPE_NODE_ATTRIBUTE_KEY, "username-key-path")
44+
.build()
45+
)
46+
.property(
47+
PropertyBuilder.builder()
48+
.title("Password")
49+
.string(PASSWORD)
50+
.description("The password to use for the connection")
51+
.required(true)
52+
.renderingOption(StringRenderingConstants.SELECTION_ACCESSOR_KEY, StringRenderingConstants.SelectionAccessor.STORAGE_PATH)
53+
.renderingOption(StringRenderingConstants.STORAGE_PATH_ROOT_KEY, "keys")
54+
.renderingOption(StringRenderingConstants.STORAGE_FILE_META_FILTER_KEY, "Rundeck-data-type=password")
55+
.build()
56+
)
57+
.property(
58+
PropertyBuilder.builder()
59+
.title("Mock Failure")
60+
.booleanType(MOCK_FAILURE)
61+
.description("Optionally select to mock a failure")
62+
.required(false)
63+
.defaultValue("false")
64+
.build()
65+
)
66+
67+
builder.mapping(USERNAME, PROJ_PROP_PREFIX + USERNAME)
68+
builder.frameworkMapping(USERNAME, FRAMEWORK_PROP_PREFIX + USERNAME)
69+
builder.mapping(PASSWORD, PROJ_PROP_PREFIX + PASSWORD)
70+
builder.frameworkMapping(PASSWORD, FRAMEWORK_PROP_PREFIX + PASSWORD)
71+
builder.mapping(MOCK_FAILURE, PROJ_PROP_PREFIX + MOCK_FAILURE)
72+
builder.frameworkMapping(MOCK_FAILURE, FRAMEWORK_PROP_PREFIX + MOCK_FAILURE)
73+
74+
return builder.build()
75+
}
76+
77+
@Override
78+
public NodeExecutorResult executeCommand(ExecutionContext context, String[] command, INodeEntry node) {
79+
80+
String username = ResolverUtil.resolveProperty(USERNAME, null, node,
81+
context.getIFramework().getFrameworkProjectMgr().getFrameworkProject(context.getFrameworkProject()),
82+
context.framework)
83+
String passwordKeyPath = ResolverUtil.resolveProperty(PASSWORD, null, node,
84+
context.getIFramework().getFrameworkProjectMgr().getFrameworkProject(context.getFrameworkProject()),
85+
context.framework)
86+
boolean mockFailure = Boolean.parseBoolean(ResolverUtil.resolveProperty(MOCK_FAILURE, "false", node,
87+
context.getIFramework().getFrameworkProjectMgr().getFrameworkProject(context.getFrameworkProject()),
88+
context.framework))
89+
90+
ExecutionLogger logger= context.getExecutionLogger()
91+
92+
//Here we can retrieve the password from key storage and use it to authenticate with the target node.
93+
String password = Util.getPasswordFromPath(passwordKeyPath, context)
94+
95+
logger.log(2, "Executing command: " + Arrays.asList(command) + " on node: " + node.getNodename() + " with username: " + username)
96+
97+
if(mockFailure) {
98+
return NodeExecutorResultImpl.createFailure(Util.PluginFailureReason.ConnectionError, "Failure due to mock failure", node)
99+
} else {
100+
return NodeExecutorResultImpl.createSuccess(node)
101+
}
102+
}
103+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.plugin.${javaPluginClass.toLowerCase()}
2+
3+
import com.dtolabs.rundeck.core.execution.ExecutionContext
4+
import com.dtolabs.rundeck.core.execution.workflow.steps.FailureReason
5+
import com.dtolabs.rundeck.core.storage.ResourceMeta
6+
7+
class Util {
8+
9+
static String getPasswordFromPath(String path, ExecutionContext context) throws IOException {
10+
ResourceMeta contents = context.getStorageTree().getResource(path).getContents();
11+
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
12+
contents.writeContent(byteArrayOutputStream);
13+
String password = new String(byteArrayOutputStream.toByteArray());
14+
return password;
15+
}
16+
17+
enum PluginFailureReason implements FailureReason {
18+
KeyStorageError,
19+
ConnectionError
20+
}
21+
22+
}

src/main/resources/templates/java-plugin/nodeexecutor/build.gradle.template

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ defaultTasks 'clean','build'
88
apply plugin: 'java'
99
apply plugin: 'groovy'
1010
apply plugin: 'idea'
11-
sourceCompatibility = 1.8
11+
sourceCompatibility = 11.0
1212
ext.rundeckPluginVersion= '2.0'
1313
ext.rundeckVersion= '${rundeckVersion}'
14-
ext.pluginClassNames='com.plugin.${sanitizedPluginName}.${javaPluginClass}'
14+
ext.pluginClassNames='com.plugin.${javaPluginClass.toLowerCase()}.${javaPluginClass}'
1515

1616

1717
repositories {
@@ -30,14 +30,14 @@ configurations{
3030
}
3131

3232
dependencies {
33-
implementation 'org.rundeck:rundeck-core:4.14.2-20230713'
34-
35-
//use pluginLibs to add dependecies, example:
33+
implementation 'org.rundeck:rundeck-core:${rundeckVersion}'
34+
implementation 'org.codehaus.groovy:groovy-all:${groovyVersion}'
35+
//use pluginLibs to add dependencies, example:
3636
//pluginLibs group: 'com.google.code.gson', name: 'gson', version: '2.8.2'
3737

38-
testImplementation 'junit:junit:4.12'
39-
testImplementation "org.codehaus.groovy:groovy-all:2.4.15"
40-
testImplementation "org.spockframework:spock-core:1.0-groovy-2.4"
38+
testImplementation 'junit:junit:4.13.2'
39+
testImplementation 'org.codehaus.groovy:groovy-all:${groovyVersion}'
40+
testImplementation "org.spockframework:spock-core:2.2-groovy-3.0"
4141
testImplementation "cglib:cglib-nodep:2.2.2"
4242
testImplementation group: 'org.objenesis', name: 'objenesis', version: '1.2'
4343
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
build.gradle.template->build.gradle
22
README.md.template->README.md
33
icon.png->src/main/resources/resources/icon.png
4-
Plugin.java.template->src/main/java/com/plugin/${javaPluginClass.toLowerCase()}/${javaPluginClass}.java
5-
PluginSpec.groovy.template->src/test/groovy/com/plugin/${javaPluginClass.toLowerCase()}/${javaPluginClass}Spec.groovy
6-
4+
Plugin.groovy.template->src/main/groovy/com/plugin/${javaPluginClass.toLowerCase()}/${javaPluginClass}.groovy
5+
Util.groovy.template->src/main/groovy/com/plugin/${javaPluginClass.toLowerCase()}/Util.groovy
6+
PluginSpec.groovy.template->src/test/groovy/com/plugin/${javaPluginClass.toLowerCase()}/${javaPluginClass}Spec.groovy
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.plugin.${javaPluginClass.toLowerCase()};
2+
3+
import okhttp3.MediaType
4+
import okhttp3.OkHttpClient
5+
import okhttp3.Request
6+
import okhttp3.Response
7+
import okhttp3.RequestBody
8+
import okhttp3.Credentials;
9+
10+
11+
class ExampleApis {
12+
13+
Properties configuration;
14+
15+
//Set constructor to use configuration from plugin properties
16+
ExampleApis(Properties configuration) {
17+
this.configuration = configuration;
18+
}
19+
20+
//Pass the customProperty from the plugin config to the JSON string that we'll pass to the API call
21+
private String json = '{"name":"' + configuration.getProperty("customProperty") + '"}';
22+
23+
//Set the media type for the API call request body
24+
public static final MediaType JSON = MediaType.get("application/json");
25+
26+
//Create a new OkHttpClient
27+
OkHttpClient client = new OkHttpClient();
28+
29+
//Post method that takes the API Key as an argument
30+
String post(String apiKey) throws IOException {
31+
32+
//Create a basic authentication credential
33+
String credential = Credentials.basic("name", apiKey);
34+
35+
RequestBody body = RequestBody.create(JSON, json);
36+
37+
Request request = new Request.Builder()
38+
.url("https://httpbin.org/post")
39+
.post(body)
40+
.header("Authorization", credential)
41+
.build();
42+
43+
Response response = null
44+
45+
try {
46+
response = client.newCall(request).execute()
47+
return response.body().string();
48+
} finally {
49+
response.close();
50+
}
51+
}
52+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.plugin.${javaPluginClass.toLowerCase()};
2+
3+
import com.dtolabs.rundeck.core.plugins.Plugin
4+
import com.dtolabs.rundeck.core.plugins.configuration.AcceptsServices
5+
import com.dtolabs.rundeck.core.storage.StorageTree
6+
import com.dtolabs.rundeck.plugins.descriptions.PluginDescription
7+
import com.dtolabs.rundeck.plugins.descriptions.PluginProperty
8+
import com.dtolabs.rundeck.plugins.notification.NotificationPlugin
9+
import com.dtolabs.rundeck.plugins.descriptions.RenderingOption
10+
import com.dtolabs.rundeck.plugins.descriptions.RenderingOptions
11+
import com.dtolabs.rundeck.core.plugins.configuration.StringRenderingConstants
12+
import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree
13+
import org.rundeck.app.spi.Services
14+
import org.slf4j.Logger
15+
import org.slf4j.LoggerFactory
16+
17+
@Plugin(service="Notification", name="${sanitizedPluginName}")
18+
@PluginDescription(title="${pluginName}", description="This is a notification plugin that integrated with ${pluginName}.")
19+
public class ${javaPluginClass} implements NotificationPlugin, AcceptsServices {
20+
21+
static Logger logger = LoggerFactory.getLogger(${javaPluginClass}.class);
22+
23+
@PluginProperty(name = "customProperty" ,title = "Custom Property", description = "A custom property to be passed to the API.")
24+
String customProperty;
25+
26+
@PluginProperty(
27+
title = "API Key Path",
28+
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.',
29+
required = true
30+
)
31+
@RenderingOptions([
32+
@RenderingOption(
33+
key = StringRenderingConstants.SELECTION_ACCESSOR_KEY,
34+
value = "STORAGE_PATH"
35+
),
36+
@RenderingOption(
37+
key = StringRenderingConstants.STORAGE_PATH_ROOT_KEY,
38+
value = "keys"
39+
),
40+
@RenderingOption(
41+
key = StringRenderingConstants.STORAGE_FILE_META_FILTER_KEY,
42+
value = "Rundeck-data-type=password"
43+
),
44+
@RenderingOption(
45+
key = StringRenderingConstants.GROUP_NAME,
46+
value = "API Configuration"
47+
)
48+
])
49+
String apiKeyPath
50+
51+
52+
//Implement services so that we can retrieve secret from key storage and pass to API call
53+
Services services
54+
@Override
55+
void setServices(Services services) {
56+
this.services = services
57+
}
58+
59+
public boolean postNotification(String trigger, Map executionData, Map config) {
60+
61+
//Get the secret from the key storage
62+
StorageTree keyStorage = services.getService(KeyStorageTree)
63+
String apiKeyPath = config.get("apiKeyPath")
64+
String apiKey = Util.getPasswordFromKeyStorage(apiKeyPath, keyStorage)
65+
66+
//Pass in config properties to the API so that secret can be used in api call
67+
ExampleApis api = new ExampleApis(config as Properties);
68+
69+
logger.warn(api.post(apiKey))
70+
71+
return true;
72+
}
73+
}
Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
package com.plugin.${javaPluginClass.toLowerCase()};
22

3-
import com.dtolabs.rundeck.plugins.notification.NotificationPlugin;
4-
import com.dtolabs.rundeck.core.plugins.Plugin;
5-
import com.dtolabs.rundeck.plugins.descriptions.PluginDescription;
6-
import com.dtolabs.rundeck.plugins.descriptions.PluginProperty;
7-
import java.util.*;
3+
import com.dtolabs.rundeck.core.plugins.Plugin
4+
import com.dtolabs.rundeck.plugins.descriptions.PluginDescription
5+
import com.dtolabs.rundeck.plugins.descriptions.PluginProperty
6+
import com.dtolabs.rundeck.plugins.notification.NotificationPlugin
7+
import org.slf4j.Logger
8+
import org.slf4j.LoggerFactory
89

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

13-
@PluginProperty(name = "example",title = "Example String",description = "Example description")
14-
private String example;
14+
static Logger logger = LoggerFactory.getLogger(Notificationplugin.class);
15+
16+
@PluginProperty(name = "test" ,title = "Test String", description = "a description")
17+
String test;
1518

1619
public boolean postNotification(String trigger, Map executionData, Map config) {
17-
System.err.printf("Trigger %s fired for %s, configuration: %s",trigger,executionData,config);
18-
System.err.println();
19-
System.err.printf("Local field example is: %s",example);
20+
21+
logger.info(new apiCall().post("{\"key\":\"value\"}"))
22+
2023
return true;
2124
}
22-
2325
}

0 commit comments

Comments
 (0)