From 01d8d4002a76e63f6705db9416465b5b2c21222d Mon Sep 17 00:00:00 2001 From: Jake Cohen Date: Wed, 7 Feb 2024 16:23:03 -0800 Subject: [PATCH 01/10] update WorkflowNodeStep --- .../JavaPluginTemplateGenerator.groovy | 5 + .../Constants.groovy.template | 10 + .../ExampleApis.groovy.template | 85 ++++++ .../FailureReason.groovy.template | 15 ++ .../workflownodestep/Plugin.groovy.template | 250 ++++++++++++++++++ .../workflownodestep/Util.groovy.template | 27 ++ .../workflownodestep/build.gradle.template | 3 +- .../workflownodestep/java-plugin.structure | 6 +- .../JavaPluginTemplateGeneratorTest.groovy | 3 +- 9 files changed, 401 insertions(+), 3 deletions(-) create mode 100644 src/main/resources/templates/java-plugin/workflownodestep/Constants.groovy.template create mode 100644 src/main/resources/templates/java-plugin/workflownodestep/ExampleApis.groovy.template create mode 100644 src/main/resources/templates/java-plugin/workflownodestep/FailureReason.groovy.template create mode 100644 src/main/resources/templates/java-plugin/workflownodestep/Plugin.groovy.template create mode 100644 src/main/resources/templates/java-plugin/workflownodestep/Util.groovy.template diff --git a/src/main/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGenerator.groovy b/src/main/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGenerator.groovy index bcbedcf..e753787 100644 --- a/src/main/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGenerator.groovy +++ b/src/main/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGenerator.groovy @@ -36,6 +36,11 @@ class JavaPluginTemplateGenerator extends AbstractTemplateGenerator { templateProperties["currentDate"] = Instant.now().toString() templateProperties["pluginLang"] = "java" templateProperties["rundeckVersion"] = "3.0.x" + templateProperties["apiKeyPath"] = "\${apiKeyPath}" + templateProperties["data"] = "\${data}" + templateProperties["resourceInfo"] = "resourceInfo" + templateProperties["extra"] = "extra" + templateProperties["hiddenTestValue"] = "hiddenTestValue" return templateProperties } diff --git a/src/main/resources/templates/java-plugin/workflownodestep/Constants.groovy.template b/src/main/resources/templates/java-plugin/workflownodestep/Constants.groovy.template new file mode 100644 index 0000000..c130aa1 --- /dev/null +++ b/src/main/resources/templates/java-plugin/workflownodestep/Constants.groovy.template @@ -0,0 +1,10 @@ +package com.plugin.${javaPluginClass.toLowerCase()}; + +/** + * 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/src/main/resources/templates/java-plugin/workflownodestep/ExampleApis.groovy.template b/src/main/resources/templates/java-plugin/workflownodestep/ExampleApis.groovy.template new file mode 100644 index 0000000..311f201 --- /dev/null +++ b/src/main/resources/templates/java-plugin/workflownodestep/ExampleApis.groovy.template @@ -0,0 +1,85 @@ +package com.plugin.${javaPluginClass.toLowerCase()}; + +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 = ExampleConstants.BASE_API_URL + } else { + userRundeckBaseApiUrl = userBaseApiUrl + } + + if (!userApiVersion) { + userRundeckApiVersion = ExampleConstants.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(); + } + } + +// Handle for user query path inconsistencies + private static String createFullUrl(String baseApiUrl, String apiVersion, String apiPath) { + String correctedBaseUrl = baseApiUrl.replaceAll('/', "") + return correctedBaseUrl + "/" + apiVersion + apiPath + } +} \ No newline at end of file diff --git a/src/main/resources/templates/java-plugin/workflownodestep/FailureReason.groovy.template b/src/main/resources/templates/java-plugin/workflownodestep/FailureReason.groovy.template new file mode 100644 index 0000000..16b7fc2 --- /dev/null +++ b/src/main/resources/templates/java-plugin/workflownodestep/FailureReason.groovy.template @@ -0,0 +1,15 @@ +package com.plugin.${javaPluginClass.toLowerCase()}; + +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 FailureReason implements FailureReason { + KeyStorageError, + ResourceInfoError +} \ No newline at end of file diff --git a/src/main/resources/templates/java-plugin/workflownodestep/Plugin.groovy.template b/src/main/resources/templates/java-plugin/workflownodestep/Plugin.groovy.template new file mode 100644 index 0000000..f21a302 --- /dev/null +++ b/src/main/resources/templates/java-plugin/workflownodestep/Plugin.groovy.template @@ -0,0 +1,250 @@ +package com.plugin.${javaPluginClass.toLowerCase()}; + +/** + * 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 + +/** + * 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. + */ +import com.plugin.${javaPluginClass.toLowerCase()}.ExampleApis +import com.plugin.${javaPluginClass.toLowerCase()}.Constants +import com.plugin.${javaPluginClass.toLowerCase()}.Util + +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 Rundeck 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 ${javaPluginClass} 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 = "${sanitizedPluginName}" + public static final String PLUGIN_TITLE = "${pluginName}" + public static final String PLUGIN_DESCRIPTION = "EXAMPLE NODE STEP: Make a call to an API and retrieve response." + + /** Sets up the logging and meta objects for use during execution. + * log - We'll add objects to it as the step executes, and then print them and clear the log + * for its next use + * meta - Holds any metadata for use when the log is printed. Usually will just contain the + * content type of the log data ("application/json") + */ + List log = new ArrayList() + 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 'Rundeck API Key Path' property below. + */ + @PluginProperty( + title = "Rundeck API URL", + description = """Provide the base URL for this Rundeck instance. 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.\n\n +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.\n\n +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. + * However, the version will need to be a string. So, we'll cast it below. + */ + @PluginProperty( + title = "Rundeck 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 the Rundeck 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 = "Rundeck API Key Path", + description = "REQUIRED: The path to the Rundeck Key Storage entry for your Rundeck 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 + + /** + * In the main NodeStepPlugin class, executeNodeStep() should be the only method. + * Any other methods supporting the execution should be in another supporting class. + * + * 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 call to Rundeck itself, and get info about the current node. + */ + 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(), + FailureReason.KeyStorageError, + entry.getNodename() + ) + } + + /** Messages can be logged out for the user using print/println */ + System.out.println("Example node step executing on node: " + entry.getNodename()) + + /** + * But 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. + */ + log.add("Here is a single line log entry. We'll print this as a logLevel 2, along with our next log lines.") + log.add("Rundeck Plugins use a log level based on the standard syslog model. Here's how it works:") + log.add(["0": "Error","1": "Warning","2": "Notice","3": "Info","4": "Debug"]) + /** + * Now that we've added that all to the log, let's print it at logLevel 2, the highest level that + * the user will see by default. + */ + context.getExecutionContext() + .getExecutionListener() + .log(2, JsonOutput.toJson(log), meta) + + /** Lastly, we'll clear the log. Otherwise, the next time we print, we'll print the previous entries again. */ + log.clear() + + /** 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(), + FailureReason.ResourceInfoError, + entry.getNodename() + ) + } + + /** + * At this point, if we haven't failed, 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. */ + System.out.println("Job run complete! Results from API call:") + log.add(resourceInfo) + context.getExecutionContext() + .getExecutionListener() + .log(2, JsonOutput.toJson(log), meta) + } +} \ No newline at end of file diff --git a/src/main/resources/templates/java-plugin/workflownodestep/Util.groovy.template b/src/main/resources/templates/java-plugin/workflownodestep/Util.groovy.template new file mode 100644 index 0000000..94ac31d --- /dev/null +++ b/src/main/resources/templates/java-plugin/workflownodestep/Util.groovy.template @@ -0,0 +1,27 @@ +package com.plugin.${javaPluginClass.toLowerCase()}; + +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/src/main/resources/templates/java-plugin/workflownodestep/build.gradle.template b/src/main/resources/templates/java-plugin/workflownodestep/build.gradle.template index 11f8db7..188b4bd 100644 --- a/src/main/resources/templates/java-plugin/workflownodestep/build.gradle.template +++ b/src/main/resources/templates/java-plugin/workflownodestep/build.gradle.template @@ -31,13 +31,14 @@ configurations{ dependencies { implementation 'org.rundeck:rundeck-core:4.14.2-20230713' + implementation 'org.codehaus.groovy:groovy-all:3.0.9' //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:2.4.15" - testImplementation "org.spockframework:spock-core:1.0-groovy-2.4" + testImplementation "org.spockframework:spock-core:2.0-groovy-3.0" } // task to copy plugin libs to output/lib dir diff --git a/src/main/resources/templates/java-plugin/workflownodestep/java-plugin.structure b/src/main/resources/templates/java-plugin/workflownodestep/java-plugin.structure index d0ce03e..37b3d6f 100644 --- a/src/main/resources/templates/java-plugin/workflownodestep/java-plugin.structure +++ b/src/main/resources/templates/java-plugin/workflownodestep/java-plugin.structure @@ -1,6 +1,10 @@ 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 +ExampleApis.groovy.template->src/main/groovy/com/plugin/${javaPluginClass.toLowerCase()}/ExampleApis.groovy +Constants.groovy.template->src/main/groovy/com/plugin/${javaPluginClass.toLowerCase()}/Constants.groovy +FailureReason.groovy.template->src/main/groovy/com/plugin/${javaPluginClass.toLowerCase()}/FailureReason.groovy +Util.groovy.template->src/main/groovy/com/plugin/${javaPluginClass.toLowerCase()}/Util.groovy +Plugin.groovy.template->src/main/groovy/com/plugin/${javaPluginClass.toLowerCase()}/${javaPluginClass}.groovy PluginSpec.groovy.template->src/test/groovy/com/plugin/${javaPluginClass.toLowerCase()}/${javaPluginClass}Spec.groovy diff --git a/src/test/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGeneratorTest.groovy b/src/test/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGeneratorTest.groovy index b2debb2..b87a16a 100644 --- a/src/test/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGeneratorTest.groovy +++ b/src/test/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGeneratorTest.groovy @@ -78,10 +78,11 @@ class JavaPluginTemplateGeneratorTest extends Specification { then: compileResult == 0 + new File(tmpDir,"/my-workflownodestep-plugin/src/main/groovy/com/plugin/myworkflownodestepplugin/Constants.groovy").exists() new File(tmpDir,"/my-workflownodestep-plugin/build.gradle").exists() new File(tmpDir,"/my-workflownodestep-plugin/src/main/resources/resources/icon.png").exists() new File(tmpDir,"/my-workflownodestep-plugin/README.md").exists() - new File(tmpDir,"/my-workflownodestep-plugin/src/main/java/com/plugin/myworkflownodestepplugin/MyWorkflownodestepPlugin.java").exists() + new File(tmpDir,"/my-workflownodestep-plugin/src/main/groovy/com/plugin/myworkflownodestepplugin/MyWorkflownodestepPlugin.groovy").exists() new File(tmpDir,"/my-workflownodestep-plugin/src/test/groovy/com/plugin/myworkflownodestepplugin/MyWorkflownodestepPluginSpec.groovy").exists() } From 61dcaa152fdd2405626e881134fa8f2e18d3f16d Mon Sep 17 00:00:00 2001 From: Jake Cohen Date: Wed, 7 Feb 2024 17:58:50 -0800 Subject: [PATCH 02/10] remove Rundeck specifics from property names and descriptions --- .../workflownodestep/Plugin.groovy.template | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) 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 f21a302..8744aa4 100644 --- a/src/main/resources/templates/java-plugin/workflownodestep/Plugin.groovy.template +++ b/src/main/resources/templates/java-plugin/workflownodestep/Plugin.groovy.template @@ -35,7 +35,7 @@ import static com.dtolabs.rundeck.core.plugins.configuration.StringRenderingCons /** * 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 Rundeck GUI. +* 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. *

@@ -67,11 +67,11 @@ class ${javaPluginClass} implements NodeStepPlugin { * * 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 'Rundeck API Key Path' property below. + * * use KeyStorage for storage/retrieval of secrets. See 'API Key Path' property below. */ @PluginProperty( - title = "Rundeck API URL", - description = """Provide the base URL for this Rundeck instance. It will be used by the plugin to get information \ + 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.\n\n 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.\n\n @@ -91,7 +91,7 @@ Want to learn more about the Rundeck API? Check out [our docs](https://docs.rund * However, the version will need to be a string. So, we'll cast it below. */ @PluginProperty( - title = "Rundeck API Version", + 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 @@ -100,15 +100,15 @@ Want to learn more about the Rundeck API? Check out [our docs](https://docs.rund Integer userApiVersion /** - * Here we're requesting the user provides the path to the API key in the Rundeck Key Storage. + * 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 = "Rundeck API Key Path", - description = "REQUIRED: The path to the Rundeck Key Storage entry for your Rundeck API Key.", + title = "API Key Path", + description = "REQUIRED: The path to the Key Storage entry for your API Key.", required = true ) @RenderingOptions([ @@ -164,7 +164,7 @@ By default, it will be collapsed in the list of properties, thanks to the '@Rend /** * We'll resolve the name of the current project and node. We'll use them to make an - * API call to Rundeck itself, and get info about the current node. + * API GET request. */ String projectName = context.getFrameworkProject() String currentNodeName = entry.getNodename() @@ -194,7 +194,7 @@ By default, it will be collapsed in the list of properties, thanks to the '@Rend * the executionContext log. First, we add to our logging object from before. */ log.add("Here is a single line log entry. We'll print this as a logLevel 2, along with our next log lines.") - log.add("Rundeck Plugins use a log level based on the standard syslog model. Here's how it works:") + log.add("Plugins use a log level based on the standard syslog model. Here's how it works:") log.add(["0": "Error","1": "Warning","2": "Notice","3": "Info","4": "Debug"]) /** * Now that we've added that all to the log, let's print it at logLevel 2, the highest level that From b621d88b68dc19a02aade6d02b21f01424bfc187 Mon Sep 17 00:00:00 2001 From: Jake Cohen Date: Thu, 8 Feb 2024 09:39:45 -0800 Subject: [PATCH 03/10] update workflow step plugin --- .../JavaPluginTemplateGenerator.groovy | 3 +- .../workflownodestep/Plugin.groovy.template | 37 +-- .../workflownodestep/README.md.template | 3 +- .../workflownodestep/build.gradle.template | 2 +- .../workflowstep/Constants.groovy.template | 10 + .../workflowstep/ExampleApis.groovy.template | 85 +++++++ .../FailureReason.groovy.template | 15 ++ .../workflowstep/Plugin.groovy.template | 233 ++++++++++++++++++ .../workflowstep/README.md.template | 5 +- .../workflowstep/Util.groovy.template | 27 ++ .../workflowstep/build.gradle.template | 7 +- .../workflowstep/java-plugin.structure | 6 +- .../JavaPluginTemplateGeneratorTest.groovy | 2 +- 13 files changed, 399 insertions(+), 36 deletions(-) create mode 100644 src/main/resources/templates/java-plugin/workflowstep/Constants.groovy.template create mode 100644 src/main/resources/templates/java-plugin/workflowstep/ExampleApis.groovy.template create mode 100644 src/main/resources/templates/java-plugin/workflowstep/FailureReason.groovy.template create mode 100644 src/main/resources/templates/java-plugin/workflowstep/Plugin.groovy.template create mode 100644 src/main/resources/templates/java-plugin/workflowstep/Util.groovy.template diff --git a/src/main/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGenerator.groovy b/src/main/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGenerator.groovy index e753787..f88df66 100644 --- a/src/main/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGenerator.groovy +++ b/src/main/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGenerator.groovy @@ -35,12 +35,13 @@ class JavaPluginTemplateGenerator extends AbstractTemplateGenerator { templateProperties["providedService"] = providedService templateProperties["currentDate"] = Instant.now().toString() templateProperties["pluginLang"] = "java" - templateProperties["rundeckVersion"] = "3.0.x" + templateProperties["rundeckVersion"] = "4.17.4-20231216" templateProperties["apiKeyPath"] = "\${apiKeyPath}" templateProperties["data"] = "\${data}" templateProperties["resourceInfo"] = "resourceInfo" templateProperties["extra"] = "extra" templateProperties["hiddenTestValue"] = "hiddenTestValue" + templateProperties["projectInfo"] = "projectInfo" return templateProperties } 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 8744aa4..705504c 100644 --- a/src/main/resources/templates/java-plugin/workflownodestep/Plugin.groovy.template +++ b/src/main/resources/templates/java-plugin/workflownodestep/Plugin.groovy.template @@ -21,6 +21,8 @@ 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 + /** * If other functions are required for purposes of modularity or clarity, they should either be added to a Util Class @@ -50,7 +52,7 @@ class ${javaPluginClass} implements NodeStepPlugin { */ public static final String PLUGIN_NAME = "${sanitizedPluginName}" public static final String PLUGIN_TITLE = "${pluginName}" - public static final String PLUGIN_DESCRIPTION = "EXAMPLE NODE STEP: Make a call to an API and retrieve response." + public static final String PLUGIN_DESCRIPTION = "Template Node Step plugin that makes a call to an API and retrieves a response." /** Sets up the logging and meta objects for use during execution. * log - We'll add objects to it as the step executes, and then print them and clear the log @@ -58,7 +60,7 @@ class ${javaPluginClass} implements NodeStepPlugin { * meta - Holds any metadata for use when the log is printed. Usually will just contain the * content type of the log data ("application/json") */ - List log = new ArrayList() + ExecutionListener log = context.getExecutionContext().getExecutionListener() Map meta = Collections.singletonMap("content-data-type", "application/json") ExampleApis exapis @@ -186,26 +188,13 @@ By default, it will be collapsed in the list of properties, thanks to the '@Rend ) } - /** Messages can be logged out for the user using print/println */ - System.out.println("Example node step executing on node: " + entry.getNodename()) - /** - * But the preferred method of logging is to write into, and then print out, + * 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. */ - log.add("Here is a single line log entry. We'll print this as a logLevel 2, along with our next log lines.") - log.add("Plugins use a log level based on the standard syslog model. Here's how it works:") - log.add(["0": "Error","1": "Warning","2": "Notice","3": "Info","4": "Debug"]) - /** - * Now that we've added that all to the log, let's print it at logLevel 2, the highest level that - * the user will see by default. - */ - context.getExecutionContext() - .getExecutionListener() - .log(2, JsonOutput.toJson(log), meta) - - /** Lastly, we'll clear the log. Otherwise, the next time we print, we'll print the previous entries again. */ - log.clear() + 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 a log level based on the standard syslog model. Here's how it works:") + logger.log(3, '["0": "Error","1": "Warning","2": "Notice","3": "Info","4": "Debug"]') /** Cast the API Version, if it was provided */ if (userApiVersion) { @@ -241,10 +230,10 @@ By default, it will be collapsed in the list of properties, thanks to the '@Rend context.getExecutionContext().getOutputContext().addOutput("extra", "hiddenTestValue", hiddenTestValue) /** Now, we'll add it to the log, print for the user, and call it a day. */ - System.out.println("Job run complete! Results from API call:") - log.add(resourceInfo) - context.getExecutionContext() - .getExecutionListener() - .log(2, JsonOutput.toJson(log), meta) + logger.log(3, "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/src/main/resources/templates/java-plugin/workflownodestep/README.md.template b/src/main/resources/templates/java-plugin/workflownodestep/README.md.template index 0ef12dd..7e6bac0 100644 --- a/src/main/resources/templates/java-plugin/workflownodestep/README.md.template +++ b/src/main/resources/templates/java-plugin/workflownodestep/README.md.template @@ -1,4 +1,3 @@ # ${pluginName} Rundeck Plugin -This is a notification 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/src/main/resources/templates/java-plugin/workflownodestep/build.gradle.template b/src/main/resources/templates/java-plugin/workflownodestep/build.gradle.template index 188b4bd..08a84c4 100644 --- a/src/main/resources/templates/java-plugin/workflownodestep/build.gradle.template +++ b/src/main/resources/templates/java-plugin/workflownodestep/build.gradle.template @@ -30,7 +30,7 @@ configurations{ } dependencies { - implementation 'org.rundeck:rundeck-core:4.14.2-20230713' + implementation 'org.rundeck:rundeck-core:${rundeckVersion}' implementation 'org.codehaus.groovy:groovy-all:3.0.9' //use pluginLibs to add dependecies, example: diff --git a/src/main/resources/templates/java-plugin/workflowstep/Constants.groovy.template b/src/main/resources/templates/java-plugin/workflowstep/Constants.groovy.template new file mode 100644 index 0000000..c130aa1 --- /dev/null +++ b/src/main/resources/templates/java-plugin/workflowstep/Constants.groovy.template @@ -0,0 +1,10 @@ +package com.plugin.${javaPluginClass.toLowerCase()}; + +/** + * 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/src/main/resources/templates/java-plugin/workflowstep/ExampleApis.groovy.template b/src/main/resources/templates/java-plugin/workflowstep/ExampleApis.groovy.template new file mode 100644 index 0000000..311f201 --- /dev/null +++ b/src/main/resources/templates/java-plugin/workflowstep/ExampleApis.groovy.template @@ -0,0 +1,85 @@ +package com.plugin.${javaPluginClass.toLowerCase()}; + +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 = ExampleConstants.BASE_API_URL + } else { + userRundeckBaseApiUrl = userBaseApiUrl + } + + if (!userApiVersion) { + userRundeckApiVersion = ExampleConstants.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(); + } + } + +// Handle for user query path inconsistencies + private static String createFullUrl(String baseApiUrl, String apiVersion, String apiPath) { + String correctedBaseUrl = baseApiUrl.replaceAll('/', "") + return correctedBaseUrl + "/" + apiVersion + apiPath + } +} \ No newline at end of file diff --git a/src/main/resources/templates/java-plugin/workflowstep/FailureReason.groovy.template b/src/main/resources/templates/java-plugin/workflowstep/FailureReason.groovy.template new file mode 100644 index 0000000..16b7fc2 --- /dev/null +++ b/src/main/resources/templates/java-plugin/workflowstep/FailureReason.groovy.template @@ -0,0 +1,15 @@ +package com.plugin.${javaPluginClass.toLowerCase()}; + +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 FailureReason implements FailureReason { + KeyStorageError, + ResourceInfoError +} \ No newline at end of file diff --git a/src/main/resources/templates/java-plugin/workflowstep/Plugin.groovy.template b/src/main/resources/templates/java-plugin/workflowstep/Plugin.groovy.template new file mode 100644 index 0000000..a5210b1 --- /dev/null +++ b/src/main/resources/templates/java-plugin/workflowstep/Plugin.groovy.template @@ -0,0 +1,233 @@ +package com.plugin.${javaPluginClass.toLowerCase()}; + +/** + * 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.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 + +/** + * 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. + */ +import com.plugin.${javaPluginClass.toLowerCase()}.ExampleApis +import com.plugin.${javaPluginClass.toLowerCase()}.Constants +import com.plugin.${javaPluginClass.toLowerCase()}.Util + +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.WorkflowStep) +@PluginDescription(title = PLUGIN_TITLE, description = PLUGIN_DESCRIPTION) +class ${javaPluginClass} 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 = "${sanitizedPluginName}" + public static final String PLUGIN_TITLE = "${pluginName}" + public static final String PLUGIN_DESCRIPTION = "Template Workflow Step plugin that makes a call to an API and retrieves a response." + + /** Sets up the logging and meta objects for use during execution. + * log - We'll add objects to it as the step executes, and then print them and clear the log + * for its next use + * meta - Holds any metadata for use when the log is printed. Usually will just contain the + * content type of the log data ("application/json") + */ + ExecutionListener log = context.getExecutionContext().getExecutionListener() + 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.\n\n +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.\n\n +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. + * However, the version will need to be a string. So, we'll cast it below. + */ + @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 + + /** + * In the Plugin class (this file) we try to limit executeStep() to be the only method. + * Any other methods supporting the execution should be in another supporting class. + * + * 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 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 StepException( + 'Error accessing ${apiKeyPath}:' + e.getMessage(), + FailureReason.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. + */ + 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 a log level based on the standard syslog model. Here's how it works:") + 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(), + FailureReason.ResourceInfoError + ) + } + + /** + * At this point, if we haven't failed, 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", "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(3, "Job run complete! Results from API call:") + + def json = Jsonoutput.toJson(resourceInfo) + logger.log(3, json, meta) + + } +} \ No newline at end of file diff --git a/src/main/resources/templates/java-plugin/workflowstep/README.md.template b/src/main/resources/templates/java-plugin/workflowstep/README.md.template index 0ef12dd..01f758b 100644 --- a/src/main/resources/templates/java-plugin/workflowstep/README.md.template +++ b/src/main/resources/templates/java-plugin/workflowstep/README.md.template @@ -1,4 +1,3 @@ -# ${pluginName} Rundeck Plugin - -This is a notification plugin. +# ${pluginName} 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/src/main/resources/templates/java-plugin/workflowstep/Util.groovy.template b/src/main/resources/templates/java-plugin/workflowstep/Util.groovy.template new file mode 100644 index 0000000..94ac31d --- /dev/null +++ b/src/main/resources/templates/java-plugin/workflowstep/Util.groovy.template @@ -0,0 +1,27 @@ +package com.plugin.${javaPluginClass.toLowerCase()}; + +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/src/main/resources/templates/java-plugin/workflowstep/build.gradle.template b/src/main/resources/templates/java-plugin/workflowstep/build.gradle.template index fd147e8..08a84c4 100644 --- a/src/main/resources/templates/java-plugin/workflowstep/build.gradle.template +++ b/src/main/resources/templates/java-plugin/workflowstep/build.gradle.template @@ -30,14 +30,15 @@ configurations{ } dependencies { - implementation 'org.rundeck:rundeck-core:4.14.2-20230713' + implementation 'org.rundeck:rundeck-core:${rundeckVersion}' + implementation 'org.codehaus.groovy:groovy-all:3.0.9' //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:2.4.15" - testImplementation "org.spockframework:spock-core:1.0-groovy-2.4" + testImplementation "org.spockframework:spock-core:2.0-groovy-3.0" } // task to copy plugin libs to output/lib dir @@ -56,7 +57,7 @@ jar { attributes 'Rundeck-Plugin-Name': '${pluginName}' 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,WorkflowStep' + 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' diff --git a/src/main/resources/templates/java-plugin/workflowstep/java-plugin.structure b/src/main/resources/templates/java-plugin/workflowstep/java-plugin.structure index d0ce03e..f6a4c04 100644 --- a/src/main/resources/templates/java-plugin/workflowstep/java-plugin.structure +++ b/src/main/resources/templates/java-plugin/workflowstep/java-plugin.structure @@ -1,6 +1,10 @@ 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 +Plugin.groovy.template->src/main/groovy/com/plugin/${javaPluginClass.toLowerCase()}/${javaPluginClass}.groovy +Constants.groovy.template->src/main/groovy/com/plugin/${javaPluginClass.toLowerCase()}/Constants.groovy +ExampleApis.groovy.template->src/main/groovy/com/plugin/${javaPluginClass.toLowerCase()}/ExampleApis.groovy +FailureReason.groovy.template->src/main/groovy/com/plugin/${javaPluginClass.toLowerCase()}/FailureReason.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 diff --git a/src/test/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGeneratorTest.groovy b/src/test/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGeneratorTest.groovy index b87a16a..1b0c680 100644 --- a/src/test/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGeneratorTest.groovy +++ b/src/test/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGeneratorTest.groovy @@ -65,7 +65,7 @@ class JavaPluginTemplateGeneratorTest extends Specification { new File(tmpDir,"/my-workflowstep-plugin/build.gradle").exists() new File(tmpDir,"/my-workflowstep-plugin/src/main/resources/resources/icon.png").exists() new File(tmpDir,"/my-workflowstep-plugin/README.md").exists() - new File(tmpDir,"/my-workflowstep-plugin/src/main/java/com/plugin/myworkflowstepplugin/MyWorkflowstepPlugin.java").exists() + new File(tmpDir,"/my-workflowstep-plugin/src/main/groovy/com/plugin/myworkflowstepplugin/MyWorkflowstepPlugin.groovy").exists() new File(tmpDir,"/my-workflowstep-plugin/src/test/groovy/com/plugin/myworkflowstepplugin/MyWorkflowstepPluginSpec.groovy").exists() } From e5e0563ea53fd08e964c826a7a94f0ab9d91ae07 Mon Sep 17 00:00:00 2001 From: Jake Cohen Date: Thu, 8 Feb 2024 10:49:01 -0800 Subject: [PATCH 04/10] finish workflow step and other touch-ups --- .../ExampleApis.groovy.template | 13 ++++++++----- .../FailureReason.groovy.template | 2 +- .../workflownodestep/Plugin.groovy.template | 11 +++++++---- .../workflownodestep/build.gradle.template | 2 +- .../workflowstep/ExampleApis.groovy.template | 13 ++++++++----- .../FailureReason.groovy.template | 2 +- .../workflowstep/Plugin.groovy.template | 19 +++++++++++-------- .../workflowstep/build.gradle.template | 2 +- 8 files changed, 38 insertions(+), 26 deletions(-) diff --git a/src/main/resources/templates/java-plugin/workflownodestep/ExampleApis.groovy.template b/src/main/resources/templates/java-plugin/workflownodestep/ExampleApis.groovy.template index 311f201..0085f05 100644 --- a/src/main/resources/templates/java-plugin/workflownodestep/ExampleApis.groovy.template +++ b/src/main/resources/templates/java-plugin/workflownodestep/ExampleApis.groovy.template @@ -24,13 +24,13 @@ class ExampleApis { .build() if (!userBaseApiUrl) { - userRundeckBaseApiUrl = ExampleConstants.BASE_API_URL + userRundeckBaseApiUrl = Constants.BASE_API_URL } else { userRundeckBaseApiUrl = userBaseApiUrl } if (!userApiVersion) { - userRundeckApiVersion = ExampleConstants.API_VERSION + userRundeckApiVersion = Constants.API_VERSION } else { userRundeckApiVersion = userApiVersion } @@ -77,9 +77,12 @@ class ExampleApis { } } -// Handle for user query path inconsistencies private static String createFullUrl(String baseApiUrl, String apiVersion, String apiPath) { - String correctedBaseUrl = baseApiUrl.replaceAll('/', "") - return correctedBaseUrl + "/" + apiVersion + 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/src/main/resources/templates/java-plugin/workflownodestep/FailureReason.groovy.template b/src/main/resources/templates/java-plugin/workflownodestep/FailureReason.groovy.template index 16b7fc2..6ac765c 100644 --- a/src/main/resources/templates/java-plugin/workflownodestep/FailureReason.groovy.template +++ b/src/main/resources/templates/java-plugin/workflownodestep/FailureReason.groovy.template @@ -9,7 +9,7 @@ import com.dtolabs.rundeck.core.execution.workflow.steps.FailureReason * 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 FailureReason implements FailureReason { +enum PluginFailureReason implements FailureReason { KeyStorageError, ResourceInfoError } \ No newline at end of file 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 705504c..f0d3432 100644 --- a/src/main/resources/templates/java-plugin/workflownodestep/Plugin.groovy.template +++ b/src/main/resources/templates/java-plugin/workflownodestep/Plugin.groovy.template @@ -60,7 +60,7 @@ class ${javaPluginClass} implements NodeStepPlugin { * meta - Holds any metadata for use when the log is printed. Usually will just contain the * content type of the log data ("application/json") */ - ExecutionListener log = context.getExecutionContext().getExecutionListener() + Map meta = Collections.singletonMap("content-data-type", "application/json") ExampleApis exapis @@ -183,7 +183,7 @@ By default, it will be collapsed in the list of properties, thanks to the '@Rend } catch (StorageException e) { throw new NodeStepException( 'Error accessing ${apiKeyPath}:' + e.getMessage(), - FailureReason.KeyStorageError, + PluginFailureReason.KeyStorageError, entry.getNodename() ) } @@ -192,8 +192,11 @@ By default, it will be collapsed in the list of properties, thanks to the '@Rend * 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 a log level based on the standard syslog model. 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 */ @@ -213,7 +216,7 @@ By default, it will be collapsed in the list of properties, thanks to the '@Rend } catch (IOException e) { throw new NodeStepException( 'Failed to get resource info with error:' + e.getMessage(), - FailureReason.ResourceInfoError, + PluginFailureReason.ResourceInfoError, entry.getNodename() ) } @@ -230,7 +233,7 @@ By default, it will be collapsed in the list of properties, thanks to the '@Rend 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(3, "Job run complete! Results from API call:") + logger.log(2, "Job run complete! Results from API call:") def Json = JsonOutput.toJson(resourceInfo) logger.log(2, Json, meta) diff --git a/src/main/resources/templates/java-plugin/workflownodestep/build.gradle.template b/src/main/resources/templates/java-plugin/workflownodestep/build.gradle.template index 08a84c4..630552a 100644 --- a/src/main/resources/templates/java-plugin/workflownodestep/build.gradle.template +++ b/src/main/resources/templates/java-plugin/workflownodestep/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 { diff --git a/src/main/resources/templates/java-plugin/workflowstep/ExampleApis.groovy.template b/src/main/resources/templates/java-plugin/workflowstep/ExampleApis.groovy.template index 311f201..0085f05 100644 --- a/src/main/resources/templates/java-plugin/workflowstep/ExampleApis.groovy.template +++ b/src/main/resources/templates/java-plugin/workflowstep/ExampleApis.groovy.template @@ -24,13 +24,13 @@ class ExampleApis { .build() if (!userBaseApiUrl) { - userRundeckBaseApiUrl = ExampleConstants.BASE_API_URL + userRundeckBaseApiUrl = Constants.BASE_API_URL } else { userRundeckBaseApiUrl = userBaseApiUrl } if (!userApiVersion) { - userRundeckApiVersion = ExampleConstants.API_VERSION + userRundeckApiVersion = Constants.API_VERSION } else { userRundeckApiVersion = userApiVersion } @@ -77,9 +77,12 @@ class ExampleApis { } } -// Handle for user query path inconsistencies private static String createFullUrl(String baseApiUrl, String apiVersion, String apiPath) { - String correctedBaseUrl = baseApiUrl.replaceAll('/', "") - return correctedBaseUrl + "/" + apiVersion + 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/src/main/resources/templates/java-plugin/workflowstep/FailureReason.groovy.template b/src/main/resources/templates/java-plugin/workflowstep/FailureReason.groovy.template index 16b7fc2..6ac765c 100644 --- a/src/main/resources/templates/java-plugin/workflowstep/FailureReason.groovy.template +++ b/src/main/resources/templates/java-plugin/workflowstep/FailureReason.groovy.template @@ -9,7 +9,7 @@ import com.dtolabs.rundeck.core.execution.workflow.steps.FailureReason * 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 FailureReason implements FailureReason { +enum PluginFailureReason implements FailureReason { KeyStorageError, ResourceInfoError } \ No newline at end of file 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 a5210b1..de7b87d 100644 --- a/src/main/resources/templates/java-plugin/workflowstep/Plugin.groovy.template +++ b/src/main/resources/templates/java-plugin/workflowstep/Plugin.groovy.template @@ -58,7 +58,7 @@ class ${javaPluginClass} implements StepPlugin { * meta - Holds any metadata for use when the log is printed. Usually will just contain the * content type of the log data ("application/json") */ - ExecutionListener log = context.getExecutionContext().getExecutionListener() + Map meta = Collections.singletonMap("content-data-type", "application/json") ExampleApis exapis @@ -166,7 +166,7 @@ By default, it will be collapsed in the list of properties, thanks to the '@Rend * API GET request. */ String projectName = context.getFrameworkProject() - String resourceInfo + String projectInfo String userApiVersionString = null String userApiKey @@ -179,7 +179,7 @@ By default, it will be collapsed in the list of properties, thanks to the '@Rend } catch (StorageException e) { throw new StepException( 'Error accessing ${apiKeyPath}:' + e.getMessage(), - FailureReason.KeyStorageError + PluginFailureReason.KeyStorageError ) } @@ -187,8 +187,11 @@ By default, it will be collapsed in the list of properties, thanks to the '@Rend * 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 a log level based on the standard syslog model. 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 */ @@ -208,7 +211,7 @@ By default, it will be collapsed in the list of properties, thanks to the '@Rend } catch (IOException e) { throw new StepException( 'Failed to get resource info with error:' + e.getMessage(), - FailureReason.ResourceInfoError + PluginFailureReason.ResourceInfoError ) } @@ -219,15 +222,15 @@ By default, it will be collapsed in the list of properties, thanks to the '@Rend * 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", "resourceInfo", resourceInfo) + 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(3, "Job run complete! Results from API call:") + logger.log(2, "Job run complete! Results from API call:") - def json = Jsonoutput.toJson(resourceInfo) - logger.log(3, json, meta) + def json = JsonOutput.toJson(projectInfo) + logger.log(2, json, meta) } } \ No newline at end of file diff --git a/src/main/resources/templates/java-plugin/workflowstep/build.gradle.template b/src/main/resources/templates/java-plugin/workflowstep/build.gradle.template index 08a84c4..630552a 100644 --- a/src/main/resources/templates/java-plugin/workflowstep/build.gradle.template +++ b/src/main/resources/templates/java-plugin/workflowstep/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 { From c32eeb32e235a5fda45d065285ee80082453c23c Mon Sep 17 00:00:00 2001 From: Jake Cohen Date: Fri, 9 Feb 2024 14:07:41 -0800 Subject: [PATCH 05/10] fix up resourcemodelsource --- .../notification/Plugin.java.template | 3 + .../Plugin.groovy.template | 62 +++++++++++++++++++ .../resourcemodelsource/Plugin.java.template | 2 +- .../PluginFactory.groovy.template | 45 ++++++++++++++ .../PluginSpec.groovy.template | 2 +- .../resourcemodelsource/build.gradle.template | 4 +- .../resourcemodelsource/java-plugin.structure | 3 +- .../JavaPluginTemplateGeneratorTest.groovy | 2 +- 8 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 src/main/resources/templates/java-plugin/resourcemodelsource/Plugin.groovy.template create mode 100644 src/main/resources/templates/java-plugin/resourcemodelsource/PluginFactory.groovy.template diff --git a/src/main/resources/templates/java-plugin/notification/Plugin.java.template b/src/main/resources/templates/java-plugin/notification/Plugin.java.template index 4295bf2..a31baa1 100644 --- a/src/main/resources/templates/java-plugin/notification/Plugin.java.template +++ b/src/main/resources/templates/java-plugin/notification/Plugin.java.template @@ -14,6 +14,9 @@ public class ${javaPluginClass} implements NotificationPlugin{ private String example; public boolean postNotification(String trigger, Map executionData, Map config) { + + //implement your notification here. Rundeck will call this plugin if configured for onStart, onSuccess, onFailure, etc. + 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); diff --git a/src/main/resources/templates/java-plugin/resourcemodelsource/Plugin.groovy.template b/src/main/resources/templates/java-plugin/resourcemodelsource/Plugin.groovy.template new file mode 100644 index 0000000..0ec5446 --- /dev/null +++ b/src/main/resources/templates/java-plugin/resourcemodelsource/Plugin.groovy.template @@ -0,0 +1,62 @@ +package com.plugin.${javaPluginClass.toLowerCase()}; + +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 groovy.json.JsonSlurper + +class ${javaPluginClass} implements ResourceModelSource{ + + private final Properties configuration; + + public ${javaPluginClass}(Properties configuration) { + this.configuration = configuration; + } + + @Override + public INodeSet getNodes() throws ResourceModelSourceException { + + String tags=configuration.getProperty("tags"); + + //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"}]} + ] + } + ''' + + 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/src/main/resources/templates/java-plugin/resourcemodelsource/Plugin.java.template b/src/main/resources/templates/java-plugin/resourcemodelsource/Plugin.java.template index 88de54a..4258185 100644 --- a/src/main/resources/templates/java-plugin/resourcemodelsource/Plugin.java.template +++ b/src/main/resources/templates/java-plugin/resourcemodelsource/Plugin.java.template @@ -16,7 +16,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Properties; -@Plugin(service="ResourceModelSource",name="${sanitizedPluginName}") +@Plugin(service="ResourceModelSource",name=PROVIDER_NAME) public class ${javaPluginClass}Factory implements ResourceModelSourceFactory, Describable{ public static final String PROVIDER_NAME = "${sanitizedPluginName}"; diff --git a/src/main/resources/templates/java-plugin/resourcemodelsource/PluginFactory.groovy.template b/src/main/resources/templates/java-plugin/resourcemodelsource/PluginFactory.groovy.template new file mode 100644 index 0000000..f4b6f6d --- /dev/null +++ b/src/main/resources/templates/java-plugin/resourcemodelsource/PluginFactory.groovy.template @@ -0,0 +1,45 @@ +package com.plugin.${javaPluginClass.toLowerCase()}; + +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.ConfigurationException; +import com.dtolabs.rundeck.core.plugins.configuration.PropertyUtil; +import com.dtolabs.rundeck.plugins.util.DescriptionBuilder; +import static com.dtolabs.rundeck.core.plugins.configuration.StringRenderingConstants.GROUP_NAME + +@Plugin(name = ${javaPluginClass}Factory.PLUGIN_NAME, service=ServiceNameConstants.ResourceModelSource) +@PluginDescription(title = ${javaPluginClass}Factory.PLUGIN_TITLE, description = ${javaPluginClass}Factory.PLUGIN_DESCRIPTION) +public class ${javaPluginClass}Factory implements ResourceModelSourceFactory { + + public static final String PLUGIN_NAME = "${sanitizedPluginName}" + public static final String PLUGIN_TITLE = "${pluginName}" + 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 + + @Override + public ResourceModelSource createResourceModelSource(final Properties properties) throws ConfigurationException { + final ${javaPluginClass} resource = new ${javaPluginClass}(properties); + return resource; + } + +} \ No newline at end of file diff --git a/src/main/resources/templates/java-plugin/resourcemodelsource/PluginSpec.groovy.template b/src/main/resources/templates/java-plugin/resourcemodelsource/PluginSpec.groovy.template index f634e46..c25cf8a 100644 --- a/src/main/resources/templates/java-plugin/resourcemodelsource/PluginSpec.groovy.template +++ b/src/main/resources/templates/java-plugin/resourcemodelsource/PluginSpec.groovy.template @@ -13,7 +13,7 @@ class ${javaPluginClass}FactorySpec extends Specification { def factory = new ${javaPluginClass}Factory() - def vmList = ["localhost"] + def vmList = ["node1","node2","node3"] when: def result = factory.createResourceModelSource(configuration) diff --git a/src/main/resources/templates/java-plugin/resourcemodelsource/build.gradle.template b/src/main/resources/templates/java-plugin/resourcemodelsource/build.gradle.template index 8bf5e65..615c093 100644 --- a/src/main/resources/templates/java-plugin/resourcemodelsource/build.gradle.template +++ b/src/main/resources/templates/java-plugin/resourcemodelsource/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}Factory' +ext.pluginClassNames='com.plugin.${javaPluginClass.toLowerCase()}.${javaPluginClass}Factory' repositories { @@ -31,9 +31,9 @@ configurations{ dependencies { implementation 'org.rundeck:rundeck-core:4.14.2-20230713' + implementation 'org.codehaus.groovy:groovy-all:2.4.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:2.4.15" diff --git a/src/main/resources/templates/java-plugin/resourcemodelsource/java-plugin.structure b/src/main/resources/templates/java-plugin/resourcemodelsource/java-plugin.structure index 001e484..813262a 100644 --- a/src/main/resources/templates/java-plugin/resourcemodelsource/java-plugin.structure +++ b/src/main/resources/templates/java-plugin/resourcemodelsource/java-plugin.structure @@ -1,6 +1,7 @@ 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}Factory.java +Plugin.groovy.template->src/main/groovy/com/plugin/${javaPluginClass.toLowerCase()}/${javaPluginClass}.groovy +PluginFactory.groovy.template->src/main/groovy/com/plugin/${javaPluginClass.toLowerCase()}/${javaPluginClass}Factory.groovy PluginSpec.groovy.template->src/test/groovy/com/plugin/${javaPluginClass.toLowerCase()}/${javaPluginClass}FactorySpec.groovy diff --git a/src/test/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGeneratorTest.groovy b/src/test/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGeneratorTest.groovy index 1b0c680..c3ea55b 100644 --- a/src/test/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGeneratorTest.groovy +++ b/src/test/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGeneratorTest.groovy @@ -48,7 +48,7 @@ class JavaPluginTemplateGeneratorTest extends Specification { new File(tmpDir,"/my-resourcemodel-plugin/build.gradle").exists() new File(tmpDir,"/my-resourcemodel-plugin/src/main/resources/resources/icon.png").exists() new File(tmpDir,"/my-resourcemodel-plugin/README.md").exists() - new File(tmpDir,"/my-resourcemodel-plugin/src/main/java/com/plugin/myresourcemodelplugin/MyResourcemodelPluginFactory.java").exists() + new File(tmpDir,"/my-resourcemodel-plugin/src/main/groovy/com/plugin/myresourcemodelplugin/MyResourcemodelPluginFactory.groovy").exists() new File(tmpDir,"/my-resourcemodel-plugin/src/test/groovy/com/plugin/myresourcemodelplugin/MyResourcemodelPluginFactorySpec.groovy").exists() } From 0e5183a2977e4269267dfddc9ddc4ebcdd90f166 Mon Sep 17 00:00:00 2001 From: Jake Cohen Date: Tue, 13 Feb 2024 12:56:33 -0800 Subject: [PATCH 06/10] add util to resource model source plugin and add notification plugin --- .../notification/ExampleApis.groovy.template | 34 +++++++++++++++ .../notification/Plugin.groovy.template | 25 +++++++++++ .../notification/Plugin.java.template | 29 +++++++------ .../notification/build.gradle.template | 4 +- .../notification/java-plugin.structure | 4 +- .../Plugin.groovy.template | 15 ++++++- .../PluginFactory.groovy.template | 42 +++++++++++++++++-- .../PluginSpec.groovy.template | 30 ++++++++++--- .../resourcemodelsource/Util.groovy.template | 28 +++++++++++++ .../resourcemodelsource/java-plugin.structure | 1 + .../JavaPluginTemplateGeneratorTest.groovy | 2 +- 11 files changed, 185 insertions(+), 29 deletions(-) create mode 100644 src/main/resources/templates/java-plugin/notification/ExampleApis.groovy.template create mode 100644 src/main/resources/templates/java-plugin/notification/Plugin.groovy.template create mode 100644 src/main/resources/templates/java-plugin/resourcemodelsource/Util.groovy.template diff --git a/src/main/resources/templates/java-plugin/notification/ExampleApis.groovy.template b/src/main/resources/templates/java-plugin/notification/ExampleApis.groovy.template new file mode 100644 index 0000000..bbb3766 --- /dev/null +++ b/src/main/resources/templates/java-plugin/notification/ExampleApis.groovy.template @@ -0,0 +1,34 @@ +package com.plugin.${javaPluginClass.toLowerCase()}; + +import okhttp3.MediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.RequestBody + +class apiCall { + + public static final MediaType JSON = MediaType.get("application/json"); + + OkHttpClient client = new OkHttpClient(); + + String post(String json) throws IOException { + + RequestBody body = RequestBody.create(JSON, json); + + Request request = new Request.Builder() + .url("https://httpbin.org/post") + .post(body) + .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/src/main/resources/templates/java-plugin/notification/Plugin.groovy.template b/src/main/resources/templates/java-plugin/notification/Plugin.groovy.template new file mode 100644 index 0000000..51b7cda --- /dev/null +++ b/src/main/resources/templates/java-plugin/notification/Plugin.groovy.template @@ -0,0 +1,25 @@ +package com.plugin.${javaPluginClass.toLowerCase()}; + +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="This is a notification plugin that integrated with ${pluginName}.") +public class ${javaPluginClass} implements NotificationPlugin { + + static Logger logger = LoggerFactory.getLogger(${javaPluginClass}.class); + + @PluginProperty(name = "test" ,title = "Test String", description = "a description") + String test; + + public boolean postNotification(String trigger, Map executionData, Map config) { + + logger.info(new apiCall().post("{\"key\":\"value\"}")) + + return true; + } +} \ No newline at end of file diff --git a/src/main/resources/templates/java-plugin/notification/Plugin.java.template b/src/main/resources/templates/java-plugin/notification/Plugin.java.template index a31baa1..6b7c1e2 100644 --- a/src/main/resources/templates/java-plugin/notification/Plugin.java.template +++ b/src/main/resources/templates/java-plugin/notification/Plugin.java.template @@ -1,26 +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) { - //implement your notification here. Rundeck will call this plugin if configured for onStart, onSuccess, onFailure, etc. + logger.info(new apiCall().post("{\"key\":\"value\"}")) - 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); return true; } - } \ No newline at end of file diff --git a/src/main/resources/templates/java-plugin/notification/build.gradle.template b/src/main/resources/templates/java-plugin/notification/build.gradle.template index 71bdd50..199f5a8 100644 --- a/src/main/resources/templates/java-plugin/notification/build.gradle.template +++ b/src/main/resources/templates/java-plugin/notification/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 { @@ -31,9 +31,9 @@ configurations{ dependencies { implementation 'org.rundeck:rundeck-core:4.14.2-20230713' + implementation 'org.codehaus.groovy:groovy-all:2.4.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:2.4.15" diff --git a/src/main/resources/templates/java-plugin/notification/java-plugin.structure b/src/main/resources/templates/java-plugin/notification/java-plugin.structure index d0ce03e..ff4b22b 100644 --- a/src/main/resources/templates/java-plugin/notification/java-plugin.structure +++ b/src/main/resources/templates/java-plugin/notification/java-plugin.structure @@ -1,6 +1,8 @@ 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 +ExampleApis.groovy.template->src/main/groovy/com/plugin/${javaPluginClass.toLowerCase()}/ExampleApis.groovy +Plugin.groovy.template->src/main/groovy/com/plugin/${javaPluginClass.toLowerCase()}/${javaPluginClass}.groovy + PluginSpec.groovy.template->src/test/groovy/com/plugin/${javaPluginClass.toLowerCase()}/${javaPluginClass}Spec.groovy diff --git a/src/main/resources/templates/java-plugin/resourcemodelsource/Plugin.groovy.template b/src/main/resources/templates/java-plugin/resourcemodelsource/Plugin.groovy.template index 0ec5446..6cc3d25 100644 --- a/src/main/resources/templates/java-plugin/resourcemodelsource/Plugin.groovy.template +++ b/src/main/resources/templates/java-plugin/resourcemodelsource/Plugin.groovy.template @@ -5,15 +5,21 @@ 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 ${javaPluginClass} implements ResourceModelSource{ - private final Properties configuration; + //Properties object to hold the configuration from the plugin properties + //Services object used for retrieving secrets from KeyStorage + Properties configuration; + Services services; - public ${javaPluginClass}(Properties configuration) { + public ${javaPluginClass}(Properties configuration, Services services) { this.configuration = configuration; + this.services = services } @Override @@ -21,6 +27,11 @@ class ${javaPluginClass} implements ResourceModelSource{ 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(); diff --git a/src/main/resources/templates/java-plugin/resourcemodelsource/PluginFactory.groovy.template b/src/main/resources/templates/java-plugin/resourcemodelsource/PluginFactory.groovy.template index f4b6f6d..ae5f499 100644 --- a/src/main/resources/templates/java-plugin/resourcemodelsource/PluginFactory.groovy.template +++ b/src/main/resources/templates/java-plugin/resourcemodelsource/PluginFactory.groovy.template @@ -9,9 +9,11 @@ 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 = ${javaPluginClass}Factory.PLUGIN_NAME, service=ServiceNameConstants.ResourceModelSource) @@ -32,13 +34,47 @@ public class ${javaPluginClass}Factory implements ResourceModelSourceFactory { 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 - public ResourceModelSource createResourceModelSource(final Properties properties) throws ConfigurationException { - final ${javaPluginClass} resource = new ${javaPluginClass}(properties); + 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 ${javaPluginClass}(properties, services) return resource; } diff --git a/src/main/resources/templates/java-plugin/resourcemodelsource/PluginSpec.groovy.template b/src/main/resources/templates/java-plugin/resourcemodelsource/PluginSpec.groovy.template index c25cf8a..3b98d25 100644 --- a/src/main/resources/templates/java-plugin/resourcemodelsource/PluginSpec.groovy.template +++ b/src/main/resources/templates/java-plugin/resourcemodelsource/PluginSpec.groovy.template @@ -1,25 +1,45 @@ package com.plugin.${javaPluginClass.toLowerCase()} 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 ${javaPluginClass}FactorySpec extends Specification { def "retrieve resource success"(){ given: - //TODO: set additional properties for your plugin Properties configuration = new Properties() configuration.put("tags","example") - - def factory = new ${javaPluginClass}Factory() + 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 ${javaPluginClass}Factory() def vmList = ["node1","node2","node3"] when: - def result = factory.createResourceModelSource(configuration) + // def result = factory.createResourceModelSource(services, configuration) + ${javaPluginClass} plugin = new ${javaPluginClass}(configuration, services) + def nodes = plugin.getNodes() then: - result.getNodes().size()==vmList.size() + nodes.size()==vmList.size() } diff --git a/src/main/resources/templates/java-plugin/resourcemodelsource/Util.groovy.template b/src/main/resources/templates/java-plugin/resourcemodelsource/Util.groovy.template new file mode 100644 index 0000000..42b696c --- /dev/null +++ b/src/main/resources/templates/java-plugin/resourcemodelsource/Util.groovy.template @@ -0,0 +1,28 @@ +package com.plugin.${javaPluginClass.toLowerCase()}; + +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/src/main/resources/templates/java-plugin/resourcemodelsource/java-plugin.structure b/src/main/resources/templates/java-plugin/resourcemodelsource/java-plugin.structure index 813262a..46e8e4b 100644 --- a/src/main/resources/templates/java-plugin/resourcemodelsource/java-plugin.structure +++ b/src/main/resources/templates/java-plugin/resourcemodelsource/java-plugin.structure @@ -3,5 +3,6 @@ README.md.template->README.md icon.png->src/main/resources/resources/icon.png Plugin.groovy.template->src/main/groovy/com/plugin/${javaPluginClass.toLowerCase()}/${javaPluginClass}.groovy PluginFactory.groovy.template->src/main/groovy/com/plugin/${javaPluginClass.toLowerCase()}/${javaPluginClass}Factory.groovy +Util.groovy.template->src/main/groovy/com/plugin/${javaPluginClass.toLowerCase()}/Util.groovy PluginSpec.groovy.template->src/test/groovy/com/plugin/${javaPluginClass.toLowerCase()}/${javaPluginClass}FactorySpec.groovy diff --git a/src/test/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGeneratorTest.groovy b/src/test/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGeneratorTest.groovy index c3ea55b..9b2ceb4 100644 --- a/src/test/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGeneratorTest.groovy +++ b/src/test/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGeneratorTest.groovy @@ -30,7 +30,7 @@ class JavaPluginTemplateGeneratorTest extends Specification { new File(tmpDir,"/my-great-plugin/build.gradle").exists() new File(tmpDir,"/my-great-plugin/src/main/resources/resources/icon.png").exists() new File(tmpDir,"/my-great-plugin/README.md").exists() - new File(tmpDir,"/my-great-plugin/src/main/java/com/plugin/mygreatplugin/MyGreatPlugin.java").exists() + new File(tmpDir,"/my-great-plugin/src/main/groovy/com/plugin/mygreatplugin/MyGreatPlugin.groovy").exists() new File(tmpDir,"/my-great-plugin/src/test/groovy/com/plugin/mygreatplugin/MyGreatPluginSpec.groovy").exists() } From b83bac9de514c21e3c67d804dd57201ac1cc0429 Mon Sep 17 00:00:00 2001 From: Jake Cohen Date: Tue, 13 Feb 2024 18:42:52 -0800 Subject: [PATCH 07/10] bump java version to 11 --- build.gradle | 3 +- .../JavaPluginTemplateGenerator.groovy | 2 +- .../notification/ExampleApis.groovy.template | 24 +++++++- .../notification/Plugin.groovy.template | 56 +++++++++++++++++-- .../notification/PluginSpec.groovy.template | 21 ++++++- .../notification/Util.groovy.template | 27 +++++++++ .../notification/build.gradle.template | 6 +- .../notification/java-plugin.structure | 2 +- .../Plugin.groovy.template | 1 + .../resourcemodelsource/build.gradle.template | 4 +- .../workflownodestep/build.gradle.template | 2 +- .../workflowstep/build.gradle.template | 2 +- 12 files changed, 131 insertions(+), 19 deletions(-) create mode 100644 src/main/resources/templates/java-plugin/notification/Util.groovy.template diff --git a/build.gradle b/build.gradle index dc1b524..aa989b4 100644 --- a/build.gradle +++ b/build.gradle @@ -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' @@ -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 diff --git a/src/main/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGenerator.groovy b/src/main/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGenerator.groovy index f88df66..7887966 100644 --- a/src/main/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGenerator.groovy +++ b/src/main/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGenerator.groovy @@ -35,7 +35,7 @@ class JavaPluginTemplateGenerator extends AbstractTemplateGenerator { templateProperties["providedService"] = providedService templateProperties["currentDate"] = Instant.now().toString() templateProperties["pluginLang"] = "java" - templateProperties["rundeckVersion"] = "4.17.4-20231216" + templateProperties["rundeckVersion"] = "5.0.2-20240212" templateProperties["apiKeyPath"] = "\${apiKeyPath}" templateProperties["data"] = "\${data}" templateProperties["resourceInfo"] = "resourceInfo" diff --git a/src/main/resources/templates/java-plugin/notification/ExampleApis.groovy.template b/src/main/resources/templates/java-plugin/notification/ExampleApis.groovy.template index bbb3766..3e7d99d 100644 --- a/src/main/resources/templates/java-plugin/notification/ExampleApis.groovy.template +++ b/src/main/resources/templates/java-plugin/notification/ExampleApis.groovy.template @@ -5,20 +5,39 @@ import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import okhttp3.RequestBody +import okhttp3.Credentials; -class apiCall { +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(); - String post(String json) throws IOException { + //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 @@ -30,5 +49,4 @@ class apiCall { response.close(); } } - } \ No newline at end of file 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 51b7cda..70e74f4 100644 --- a/src/main/resources/templates/java-plugin/notification/Plugin.groovy.template +++ b/src/main/resources/templates/java-plugin/notification/Plugin.groovy.template @@ -1,24 +1,72 @@ 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 { +public class ${javaPluginClass} implements NotificationPlugin, AcceptsServices { static Logger logger = LoggerFactory.getLogger(${javaPluginClass}.class); - @PluginProperty(name = "test" ,title = "Test String", description = "a description") - String test; + @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) { - logger.info(new apiCall().post("{\"key\":\"value\"}")) + //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; } 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 8a5b4e0..12cc8b1 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,11 @@ 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 +import com.dtolabs.rundeck.core.storage.ResourceMeta +import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree class ${javaPluginClass}Spec extends Specification { //Some Possible trigger names @@ -38,9 +43,23 @@ class ${javaPluginClass}Spec extends Specification { String trigger = TRIGGER_SUCCESS def executionData = sampleExecutionData() - def configuration = [:] + 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: diff --git a/src/main/resources/templates/java-plugin/notification/Util.groovy.template b/src/main/resources/templates/java-plugin/notification/Util.groovy.template new file mode 100644 index 0000000..1b4ff8e --- /dev/null +++ b/src/main/resources/templates/java-plugin/notification/Util.groovy.template @@ -0,0 +1,27 @@ +package com.plugin.${javaPluginClass.toLowerCase()}; + +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/src/main/resources/templates/java-plugin/notification/build.gradle.template b/src/main/resources/templates/java-plugin/notification/build.gradle.template index 199f5a8..ea340f8 100644 --- a/src/main/resources/templates/java-plugin/notification/build.gradle.template +++ b/src/main/resources/templates/java-plugin/notification/build.gradle.template @@ -8,7 +8,7 @@ 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.${javaPluginClass.toLowerCase()}.${javaPluginClass}' @@ -30,10 +30,10 @@ configurations{ } dependencies { - implementation 'org.rundeck:rundeck-core:4.14.2-20230713' + implementation 'org.rundeck:rundeck-core:${rundeckVersion}' implementation 'org.codehaus.groovy:groovy-all:2.4.21' - //use pluginLibs to add dependecies, example: + //use pluginLibs to add dependencies, example: testImplementation 'junit:junit:4.12' testImplementation "org.codehaus.groovy:groovy-all:2.4.15" diff --git a/src/main/resources/templates/java-plugin/notification/java-plugin.structure b/src/main/resources/templates/java-plugin/notification/java-plugin.structure index ff4b22b..8a94c7d 100644 --- a/src/main/resources/templates/java-plugin/notification/java-plugin.structure +++ b/src/main/resources/templates/java-plugin/notification/java-plugin.structure @@ -3,6 +3,6 @@ README.md.template->README.md icon.png->src/main/resources/resources/icon.png ExampleApis.groovy.template->src/main/groovy/com/plugin/${javaPluginClass.toLowerCase()}/ExampleApis.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 diff --git a/src/main/resources/templates/java-plugin/resourcemodelsource/Plugin.groovy.template b/src/main/resources/templates/java-plugin/resourcemodelsource/Plugin.groovy.template index 6cc3d25..36de94e 100644 --- a/src/main/resources/templates/java-plugin/resourcemodelsource/Plugin.groovy.template +++ b/src/main/resources/templates/java-plugin/resourcemodelsource/Plugin.groovy.template @@ -46,6 +46,7 @@ class ${javaPluginClass} implements ResourceModelSource{ } ''' + //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) diff --git a/src/main/resources/templates/java-plugin/resourcemodelsource/build.gradle.template b/src/main/resources/templates/java-plugin/resourcemodelsource/build.gradle.template index 615c093..555b16d 100644 --- a/src/main/resources/templates/java-plugin/resourcemodelsource/build.gradle.template +++ b/src/main/resources/templates/java-plugin/resourcemodelsource/build.gradle.template @@ -8,7 +8,7 @@ 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.${javaPluginClass.toLowerCase()}.${javaPluginClass}Factory' @@ -30,7 +30,7 @@ configurations{ } dependencies { - implementation 'org.rundeck:rundeck-core:4.14.2-20230713' + implementation 'org.rundeck:rundeck-core:${rundeckVersion}' implementation 'org.codehaus.groovy:groovy-all:2.4.21' //use pluginLibs to add dependecies, example: diff --git a/src/main/resources/templates/java-plugin/workflownodestep/build.gradle.template b/src/main/resources/templates/java-plugin/workflownodestep/build.gradle.template index 630552a..261294e 100644 --- a/src/main/resources/templates/java-plugin/workflownodestep/build.gradle.template +++ b/src/main/resources/templates/java-plugin/workflownodestep/build.gradle.template @@ -8,7 +8,7 @@ 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.${javaPluginClass.toLowerCase()}.${javaPluginClass}' diff --git a/src/main/resources/templates/java-plugin/workflowstep/build.gradle.template b/src/main/resources/templates/java-plugin/workflowstep/build.gradle.template index 630552a..261294e 100644 --- a/src/main/resources/templates/java-plugin/workflowstep/build.gradle.template +++ b/src/main/resources/templates/java-plugin/workflowstep/build.gradle.template @@ -8,7 +8,7 @@ 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.${javaPluginClass.toLowerCase()}.${javaPluginClass}' From eddb73b4b56e288d21a6986e58ed573b19d3aaca Mon Sep 17 00:00:00 2001 From: Jake Cohen Date: Wed, 10 Apr 2024 14:12:48 -0700 Subject: [PATCH 08/10] upgrade Node Executor plugin --- .../nodeexecutor/Plugin.groovy.template | 103 ++++++++++++++++++ .../nodeexecutor/Util.groovy.template | 22 ++++ .../nodeexecutor/build.gradle.template | 16 +-- .../nodeexecutor/java-plugin.structure | 6 +- .../JavaPluginTemplateGeneratorTest.groovy | 2 +- 5 files changed, 137 insertions(+), 12 deletions(-) create mode 100644 src/main/resources/templates/java-plugin/nodeexecutor/Plugin.groovy.template create mode 100644 src/main/resources/templates/java-plugin/nodeexecutor/Util.groovy.template diff --git a/src/main/resources/templates/java-plugin/nodeexecutor/Plugin.groovy.template b/src/main/resources/templates/java-plugin/nodeexecutor/Plugin.groovy.template new file mode 100644 index 0000000..01f4a65 --- /dev/null +++ b/src/main/resources/templates/java-plugin/nodeexecutor/Plugin.groovy.template @@ -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) + } + } +} \ No newline at end of file diff --git a/src/main/resources/templates/java-plugin/nodeexecutor/Util.groovy.template b/src/main/resources/templates/java-plugin/nodeexecutor/Util.groovy.template new file mode 100644 index 0000000..c03d5b1 --- /dev/null +++ b/src/main/resources/templates/java-plugin/nodeexecutor/Util.groovy.template @@ -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 + } + +} diff --git a/src/main/resources/templates/java-plugin/nodeexecutor/build.gradle.template b/src/main/resources/templates/java-plugin/nodeexecutor/build.gradle.template index c82ff0c..4830534 100644 --- a/src/main/resources/templates/java-plugin/nodeexecutor/build.gradle.template +++ b/src/main/resources/templates/java-plugin/nodeexecutor/build.gradle.template @@ -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 { @@ -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:2.4.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: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:3.0.9' + 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' } diff --git a/src/main/resources/templates/java-plugin/nodeexecutor/java-plugin.structure b/src/main/resources/templates/java-plugin/nodeexecutor/java-plugin.structure index d0ce03e..4f53de7 100644 --- a/src/main/resources/templates/java-plugin/nodeexecutor/java-plugin.structure +++ b/src/main/resources/templates/java-plugin/nodeexecutor/java-plugin.structure @@ -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 \ No newline at end of file diff --git a/src/test/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGeneratorTest.groovy b/src/test/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGeneratorTest.groovy index 9b2ceb4..8ddea50 100644 --- a/src/test/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGeneratorTest.groovy +++ b/src/test/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGeneratorTest.groovy @@ -114,7 +114,7 @@ class JavaPluginTemplateGeneratorTest extends Specification { new File(tmpDir,"/my-nodeexecutor-plugin/build.gradle").exists() new File(tmpDir,"/my-nodeexecutor-plugin/src/main/resources/resources/icon.png").exists() new File(tmpDir,"/my-nodeexecutor-plugin/README.md").exists() - new File(tmpDir,"/my-nodeexecutor-plugin/src/main/java/com/plugin/mynodeexecutorplugin/MyNodeexecutorPlugin.java").exists() + new File(tmpDir,"/my-nodeexecutor-plugin/src/main/groovy/com/plugin/mynodeexecutorplugin/MyNodeexecutorPlugin.groovy").exists() new File(tmpDir,"/my-nodeexecutor-plugin/src/test/groovy/com/plugin/mynodeexecutorplugin/MyNodeexecutorPluginSpec.groovy").exists() } From 432b52a00188d5249527f85c537f5d0c7a1e0a6f Mon Sep 17 00:00:00 2001 From: Jake Cohen Date: Thu, 11 Apr 2024 12:14:34 -0700 Subject: [PATCH 09/10] cleaning up unnecessary imports --- .../workflownodestep/Plugin.groovy.template | 26 ++------------- .../workflowstep/Plugin.groovy.template | 33 ++++--------------- 2 files changed, 8 insertions(+), 51 deletions(-) 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 f0d3432..c9661e0 100644 --- a/src/main/resources/templates/java-plugin/workflownodestep/Plugin.groovy.template +++ b/src/main/resources/templates/java-plugin/workflownodestep/Plugin.groovy.template @@ -22,16 +22,6 @@ import groovy.json.JsonBuilder import groovy.json.JsonOutput import org.rundeck.storage.api.StorageException import com.dtolabs.rundeck.core.execution.ExecutionListener - - -/** - * 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. - */ -import com.plugin.${javaPluginClass.toLowerCase()}.ExampleApis -import com.plugin.${javaPluginClass.toLowerCase()}.Constants -import com.plugin.${javaPluginClass.toLowerCase()}.Util - import static com.dtolabs.rundeck.core.plugins.configuration.StringRenderingConstants.GROUPING import static com.dtolabs.rundeck.core.plugins.configuration.StringRenderingConstants.GROUP_NAME @@ -54,13 +44,6 @@ class ${javaPluginClass} implements NodeStepPlugin { public static final String PLUGIN_TITLE = "${pluginName}" public static final String PLUGIN_DESCRIPTION = "Template Node Step plugin that makes a call to an API and retrieves a response." - /** Sets up the logging and meta objects for use during execution. - * log - We'll add objects to it as the step executes, and then print them and clear the log - * for its next use - * meta - Holds any metadata for use when the log is printed. Usually will just contain the - * content type of the log data ("application/json") - */ - Map meta = Collections.singletonMap("content-data-type", "application/json") ExampleApis exapis @@ -90,7 +73,6 @@ Want to learn more about the Rundeck API? Check out [our docs](https://docs.rund /** * Here, we're requesting an integer, which will restrict this field in the GUI to only accept integers. - * However, the version will need to be a string. So, we'll cast it below. */ @PluginProperty( title = "API Version", @@ -146,12 +128,8 @@ By default, it will be collapsed in the list of properties, thanks to the '@Rend String hiddenTestValue /** - * In the main NodeStepPlugin class, executeNodeStep() should be the only method. - * Any other methods supporting the execution should be in another supporting class. - * * 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 @@ -195,7 +173,7 @@ By default, it will be collapsed in the list of properties, thanks to the '@Rend 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 a log level based on the standard syslog model. Here's how it works:") + 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"]') @@ -222,7 +200,7 @@ By default, it will be collapsed in the list of properties, thanks to the '@Rend } /** - * At this point, if we haven't failed, we have our result data in hand with resourceInfo. + * 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 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 de7b87d..c8f5dad 100644 --- a/src/main/resources/templates/java-plugin/workflowstep/Plugin.groovy.template +++ b/src/main/resources/templates/java-plugin/workflowstep/Plugin.groovy.template @@ -1,9 +1,9 @@ package com.plugin.${javaPluginClass.toLowerCase()}; /** - * 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 + * 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. */ @@ -21,20 +21,11 @@ import com.dtolabs.rundeck.core.execution.ExecutionListener import groovy.json.JsonBuilder import groovy.json.JsonOutput import org.rundeck.storage.api.StorageException - -/** - * 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. - */ -import com.plugin.${javaPluginClass.toLowerCase()}.ExampleApis -import com.plugin.${javaPluginClass.toLowerCase()}.Constants -import com.plugin.${javaPluginClass.toLowerCase()}.Util - 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 +* 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. @@ -52,13 +43,6 @@ class ${javaPluginClass} implements StepPlugin { public static final String PLUGIN_TITLE = "${pluginName}" public static final String PLUGIN_DESCRIPTION = "Template Workflow Step plugin that makes a call to an API and retrieves a response." - /** Sets up the logging and meta objects for use during execution. - * log - We'll add objects to it as the step executes, and then print them and clear the log - * for its next use - * meta - Holds any metadata for use when the log is printed. Usually will just contain the - * content type of the log data ("application/json") - */ - Map meta = Collections.singletonMap("content-data-type", "application/json") ExampleApis exapis @@ -88,7 +72,6 @@ Want to learn more about the Rundeck API? Check out [our docs](https://docs.rund /** * Here, we're requesting an integer, which will restrict this field in the GUI to only accept integers. - * However, the version will need to be a string. So, we'll cast it below. */ @PluginProperty( title = "API Version", @@ -144,12 +127,8 @@ By default, it will be collapsed in the list of properties, thanks to the '@Rend String hiddenTestValue /** - * In the Plugin class (this file) we try to limit executeStep() to be the only method. - * Any other methods supporting the execution should be in another supporting class. - * * 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 @@ -190,7 +169,7 @@ By default, it will be collapsed in the list of properties, thanks to the '@Rend 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 a log level based on the standard syslog model. Here's how it works:") + 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"]') @@ -216,7 +195,7 @@ By default, it will be collapsed in the list of properties, thanks to the '@Rend } /** - * At this point, if we haven't failed, we have our result data in hand with resourceInfo. + * 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 From 3834c1203316959b332a02a8ddf5efc720c0974e Mon Sep 17 00:00:00 2001 From: Jake Cohen Date: Thu, 11 Apr 2024 12:37:23 -0700 Subject: [PATCH 10/10] templatize groovy version --- .../plugin/generator/JavaPluginTemplateGenerator.groovy | 1 + .../java-plugin/nodeexecutor/build.gradle.template | 4 ++-- .../java-plugin/notification/build.gradle.template | 6 +++--- .../java-plugin/resourcemodelsource/build.gradle.template | 8 ++++---- .../java-plugin/workflownodestep/build.gradle.template | 6 +++--- .../java-plugin/workflowstep/build.gradle.template | 8 ++++---- 6 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/main/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGenerator.groovy b/src/main/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGenerator.groovy index 7887966..3ddd978 100644 --- a/src/main/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGenerator.groovy +++ b/src/main/groovy/com/rundeck/plugin/generator/JavaPluginTemplateGenerator.groovy @@ -36,6 +36,7 @@ class JavaPluginTemplateGenerator extends AbstractTemplateGenerator { templateProperties["currentDate"] = Instant.now().toString() templateProperties["pluginLang"] = "java" templateProperties["rundeckVersion"] = "5.0.2-20240212" + templateProperties["groovyVersion"] = "3.0.9" templateProperties["apiKeyPath"] = "\${apiKeyPath}" templateProperties["data"] = "\${data}" templateProperties["resourceInfo"] = "resourceInfo" diff --git a/src/main/resources/templates/java-plugin/nodeexecutor/build.gradle.template b/src/main/resources/templates/java-plugin/nodeexecutor/build.gradle.template index 4830534..56cae9d 100644 --- a/src/main/resources/templates/java-plugin/nodeexecutor/build.gradle.template +++ b/src/main/resources/templates/java-plugin/nodeexecutor/build.gradle.template @@ -31,12 +31,12 @@ configurations{ dependencies { implementation 'org.rundeck:rundeck-core:${rundeckVersion}' - implementation 'org.codehaus.groovy:groovy-all:2.4.21' + 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.13.2' - testImplementation 'org.codehaus.groovy:groovy-all:3.0.9' + 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' diff --git a/src/main/resources/templates/java-plugin/notification/build.gradle.template b/src/main/resources/templates/java-plugin/notification/build.gradle.template index ea340f8..722a66a 100644 --- a/src/main/resources/templates/java-plugin/notification/build.gradle.template +++ b/src/main/resources/templates/java-plugin/notification/build.gradle.template @@ -31,13 +31,13 @@ configurations{ dependencies { implementation 'org.rundeck:rundeck-core:${rundeckVersion}' - implementation 'org.codehaus.groovy:groovy-all:2.4.21' + implementation 'org.codehaus.groovy:groovy-all:${groovyVersion}' //use pluginLibs to add dependencies, example: 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/resourcemodelsource/build.gradle.template b/src/main/resources/templates/java-plugin/resourcemodelsource/build.gradle.template index 555b16d..6249856 100644 --- a/src/main/resources/templates/java-plugin/resourcemodelsource/build.gradle.template +++ b/src/main/resources/templates/java-plugin/resourcemodelsource/build.gradle.template @@ -31,13 +31,13 @@ configurations{ dependencies { implementation 'org.rundeck:rundeck-core:${rundeckVersion}' - implementation 'org.codehaus.groovy:groovy-all:2.4.21' + implementation 'org.codehaus.groovy:groovy-all:${groovyVersion}' - //use pluginLibs to add dependecies, example: + //use pluginLibs to add dependencies, example: 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/build.gradle.template b/src/main/resources/templates/java-plugin/workflownodestep/build.gradle.template index 261294e..7cde461 100644 --- a/src/main/resources/templates/java-plugin/workflownodestep/build.gradle.template +++ b/src/main/resources/templates/java-plugin/workflownodestep/build.gradle.template @@ -31,14 +31,14 @@ configurations{ dependencies { implementation 'org.rundeck:rundeck-core:${rundeckVersion}' - implementation 'org.codehaus.groovy:groovy-all:3.0.9' + implementation 'org.codehaus.groovy:groovy-all:${groovyVersion}' //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:2.4.15" - testImplementation "org.spockframework:spock-core:2.0-groovy-3.0" + 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/workflowstep/build.gradle.template b/src/main/resources/templates/java-plugin/workflowstep/build.gradle.template index 261294e..1a61b9e 100644 --- a/src/main/resources/templates/java-plugin/workflowstep/build.gradle.template +++ b/src/main/resources/templates/java-plugin/workflowstep/build.gradle.template @@ -31,14 +31,14 @@ configurations{ dependencies { implementation 'org.rundeck:rundeck-core:${rundeckVersion}' - implementation 'org.codehaus.groovy:groovy-all:3.0.9' + implementation 'org.codehaus.groovy:groovy-all:${groovyVersion}' - //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:2.0-groovy-3.0" + 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