diff --git a/packages/nodes-base/credentials/RundeckApi.credentials.ts b/packages/nodes-base/credentials/RundeckApi.credentials.ts new file mode 100644 index 000000000..5f6c7422f --- /dev/null +++ b/packages/nodes-base/credentials/RundeckApi.credentials.ts @@ -0,0 +1,25 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class RundeckApi implements ICredentialType { + name = 'rundeckApi'; + displayName = 'Rundeck API'; + properties = [ + { + displayName: 'Url', + name: 'url', + type: 'string' as NodePropertyTypes, + default: '', + placeholder: 'http://127.0.0.1:4440', + }, + { + displayName: 'Token', + name: 'token', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Rundeck/Rundeck.node.ts b/packages/nodes-base/nodes/Rundeck/Rundeck.node.ts new file mode 100644 index 000000000..bae6b9a07 --- /dev/null +++ b/packages/nodes-base/nodes/Rundeck/Rundeck.node.ts @@ -0,0 +1,198 @@ +import { IExecuteFunctions } from 'n8n-core'; +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; +import { RundeckApi } from './RundeckApi'; + +export class Rundeck implements INodeType { + description: INodeTypeDescription = { + displayName: 'Rundeck', + name: 'rundeck', + icon: 'file:rundeck.png', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Manage Rundeck API', + defaults: { + name: 'Rundeck', + color: '#F73F39', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'rundeckApi', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Job', + value: 'job', + }, + ], + default: 'job', + description: 'The resource to operate on.', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Execute', + value: 'execute', + description: 'Executes job', + }, + { + name: 'Get Metadata', + value: 'getMetadata', + description: 'Get metadata of a job', + }, + ], + default: 'execute', + description: 'The operation to perform.', + }, + + // ---------------------------------- + // job:execute + // ---------------------------------- + { + displayName: 'Job Id', + name: 'jobid', + type: 'string', + displayOptions: { + show: { + operation: [ + 'execute', + ], + resource: [ + 'job', + ], + }, + }, + default: '', + placeholder: 'Rundeck Job Id', + required: true, + description: 'The job Id to execute.', + }, + { + displayName: 'Arguments', + name: 'arguments', + placeholder: 'Add Argument', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + operation: [ + 'execute', + ], + resource: [ + 'job', + ], + }, + }, + default: {}, + options: [ + { + name: 'arguments', + displayName: 'Arguments', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + }, + ] + }, + ], + }, + + + // ---------------------------------- + // job:getMetadata + // ---------------------------------- + { + displayName: 'Job Id', + name: 'jobid', + type: 'string', + displayOptions: { + show: { + operation: [ + 'getMetadata', + ], + resource: [ + 'job', + ], + }, + }, + default: '', + placeholder: 'Rundeck Job Id', + required: true, + description: 'The job Id to get metadata off.', + }, + ], + + }; + + + async execute(this: IExecuteFunctions): Promise { + + // Input data + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + + const operation = this.getNodeParameter('operation', 0) as string; + const resource = this.getNodeParameter('resource', 0) as string; + + for (let i = 0; i < length; i++) { + const rundeckApi = new RundeckApi(this); + + if (resource === 'job') { + if (operation === 'execute') { + // ---------------------------------- + // job: execute + // ---------------------------------- + const jobid = this.getNodeParameter('jobid', i) as string; + const rundeckArguments = (this.getNodeParameter('arguments', i) as IDataObject).arguments as IDataObject[]; + const response = await rundeckApi.executeJob(jobid, rundeckArguments); + + returnData.push(response); + } else if (operation === 'getMetadata') { + // ---------------------------------- + // job: getMetadata + // ---------------------------------- + const jobid = this.getNodeParameter('jobid', i) as string; + const response = await rundeckApi.getJobMetadata(jobid); + + returnData.push(response); + } else { + throw new Error(`The operation "${operation}" is not supported!`); + } + } else { + throw new Error(`The resource "${resource}" is not supported!`); + } + } + + return [this.helpers.returnJsonArray(returnData)]; + + } +} diff --git a/packages/nodes-base/nodes/Rundeck/RundeckApi.ts b/packages/nodes-base/nodes/Rundeck/RundeckApi.ts new file mode 100644 index 000000000..6b5fdfb20 --- /dev/null +++ b/packages/nodes-base/nodes/Rundeck/RundeckApi.ts @@ -0,0 +1,77 @@ +import { OptionsWithUri } from 'request'; +import { IExecuteFunctions } from 'n8n-core'; +import { IDataObject } from 'n8n-workflow'; + +export interface RundeckCredentials { + url: string; + token: string; +} + +export class RundeckApi { + private credentials: RundeckCredentials; + private executeFunctions: IExecuteFunctions; + + + constructor(executeFunctions: IExecuteFunctions) { + + const credentials = executeFunctions.getCredentials('rundeckApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + this.credentials = credentials as unknown as RundeckCredentials; + this.executeFunctions = executeFunctions; + } + + + protected async request(method: string, endpoint: string, body: IDataObject, query: object) { + + const options: OptionsWithUri = { + headers: { + 'user-agent': 'n8n', + 'X-Rundeck-Auth-Token': this.credentials.token, + }, + rejectUnauthorized: false, + method, + qs: query, + uri: this.credentials.url + endpoint, + body, + json: true + }; + + try { + return await this.executeFunctions.helpers.request!(options); + } catch (error) { + let errorMessage = error.message; + if (error.response && error.response.body && error.response.body.message) { + errorMessage = error.response.body.message.replace('\n', ''); + } + + throw Error(`Rundeck Error [${error.statusCode}]: ${errorMessage}`); + } + } + + + executeJob(jobId: string, args: IDataObject[]): Promise { + + let params = ''; + + if(args) { + for(const arg of args) { + params += "-" + arg.name + " " + arg.value + " "; + } + } + + const body = { + argString: params + }; + + return this.request('POST', `/api/14/job/${jobId}/run`, body, {}); + } + + getJobMetadata(jobId: string): Promise { + return this.request('GET', `/api/18/job/${jobId}/info`, {}, {}); + } + +} diff --git a/packages/nodes-base/nodes/Rundeck/rundeck.png b/packages/nodes-base/nodes/Rundeck/rundeck.png new file mode 100644 index 000000000..45084aea6 Binary files /dev/null and b/packages/nodes-base/nodes/Rundeck/rundeck.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 28bb58ed8..f60f63849 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -83,6 +83,7 @@ "dist/credentials/Postgres.credentials.js", "dist/credentials/Redis.credentials.js", "dist/credentials/RocketchatApi.credentials.js", + "dist/credentials/RundeckApi.credentials.js", "dist/credentials/ShopifyApi.credentials.js", "dist/credentials/SlackApi.credentials.js", "dist/credentials/Smtp.credentials.js", @@ -197,6 +198,7 @@ "dist/nodes/RenameKeys.node.js", "dist/nodes/Rocketchat/Rocketchat.node.js", "dist/nodes/RssFeedRead.node.js", + "dist/nodes/Rundeck/Rundeck.node.js", "dist/nodes/Set.node.js", "dist/nodes/Shopify/ShopifyTrigger.node.js", "dist/nodes/Slack/Slack.node.js",