Add Jenkins Node (#2345)

* feat: initial jenkins setup

* feat: trigger job functionality

* feat: copy a job

* feat: basic Jenkins instance operations

* feat: create job from xml

* feat: trigger with params

* feat: basic build list

* feat: list build with params

* feat: basic credentials test

* chore: linting fixes

* feat: use baseUrl from credentials

* chore: naming fixes

* feat: filters collection for getall buils

* fix: better ui and credentials

* chore: alphabetize params and fix typos

*  Small changes

*  Improvements

*  Improvements

*  Improvements

*  Improvements

*  Improvements

*  Improvements

*  Some improvements

Co-authored-by: ricardo <ricardoespinoza105@gmail.com>
Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
Elvina Valieva 2022-01-15 13:19:37 +01:00 committed by GitHub
parent f788422364
commit add9c30e38
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 851 additions and 0 deletions

View file

@ -0,0 +1,30 @@
import {
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class JenkinsApi implements ICredentialType {
name = 'jenkinsApi';
displayName = 'Jenkins API';
documentationUrl = 'jenkins';
properties: INodeProperties[] = [
{
displayName: 'Jenking Username',
name: 'username',
type: 'string',
default: '',
},
{
displayName: 'Personal API Token',
name: 'apiKey',
type: 'string',
default: '',
},
{
displayName: 'Jenkins Instance URL',
name: 'baseUrl',
type: 'string',
default: '',
},
];
}

View file

@ -0,0 +1,45 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
IHookFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
NodeApiError,
} from 'n8n-workflow';
export async function jenkinsApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, uri: string, qs: IDataObject = {}, body: any = '', option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const credentials = await this.getCredentials('jenkinsApi') as IDataObject;
let options: OptionsWithUri = {
headers: {
'Accept': 'application/json',
},
method,
auth: {
username: credentials.username as string,
password: credentials.apiKey as string,
},
uri: `${tolerateTrailingSlash(credentials.baseUrl as string)}${uri}`,
json: true,
qs,
body,
};
options = Object.assign({}, options, option);
try {
return await this.helpers.request!(options);
} catch (error) {
throw new NodeApiError(this.getNode(), error);
}
}
export function tolerateTrailingSlash(baseUrl: string) {
return baseUrl.endsWith('/')
? baseUrl.substr(0, baseUrl.length - 1)
: baseUrl;
}

View file

@ -0,0 +1,20 @@
{
"node": "n8n-nodes-base.jenkins",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": [
"Development"
],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/credentials/jenkins"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.jenkins/"
}
]
}
}

View file

@ -0,0 +1,682 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
ICredentialsDecrypted,
ICredentialTestFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
NodeApiError,
NodeCredentialTestResult,
} from 'n8n-workflow';
import {
jenkinsApiRequest,
tolerateTrailingSlash
} from './GenericFunctions';
export type JenkinsApiCredentials = {
username: string;
apiKey: string;
baseUrl: string;
};
export class Jenkins implements INodeType {
description: INodeTypeDescription = {
displayName: 'Jenkins',
name: 'jenkins',
icon: 'file:jenkins.svg',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Jenkins API',
defaults: {
name: 'Jenkins',
color: '#04AA51',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'jenkinsApi',
required: true,
testedBy: 'jenkinApiCredentialTest',
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Build',
value: 'build',
},
{
name: 'Instance',
value: 'instance',
},
{
name: 'Job',
value: 'job',
},
],
default: 'job',
noDataExpression: true,
},
// --------------------------------------------------------------------------------------------------------
// Job Operations
// --------------------------------------------------------------------------------------------------------
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'job',
],
},
},
options: [
{
name: 'Copy',
value: 'copy',
description: 'Copy a specific job',
},
{
name: 'Create',
value: 'create',
description: 'Create a new job',
},
{
name: 'Trigger',
value: 'trigger',
description: 'Trigger a specific job',
},
{
name: 'Trigger with Parameters',
value: 'triggerParams',
description: 'Trigger a specific job',
},
],
default: 'trigger',
description: 'Possible operations',
noDataExpression: true,
},
{
displayName: 'Make sure the job is setup to support triggering with parameters. <a href="https://wiki.jenkins.io/display/JENKINS/Parameterized+Build" target="_blank">More info</a>',
name: 'triggerParamsNotice',
type: 'notice',
displayOptions: {
show: {
resource: [
'job',
],
operation: [
'triggerParams',
],
},
},
default: '',
},
{
displayName: 'Job Name',
name: 'job',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getJobs',
},
displayOptions: {
show: {
resource: [
'job',
],
operation: [
'trigger',
'triggerParams',
'copy',
],
},
},
required: true,
default: '',
description: 'Name of the job',
},
// --------------------------------------------------------------------------------------------------------
// Trigger a Job
// --------------------------------------------------------------------------------------------------------
{
displayName: 'Parameters',
name: 'param',
type: 'fixedCollection',
placeholder: 'Add Parameter',
displayOptions: {
show: {
resource: [
'job',
],
operation: [
'triggerParams',
],
},
},
required: true,
default: '',
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'params',
displayName: 'Parameters',
values: [
{
displayName: 'Name',
name: 'name',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getJobParameters',
loadOptionsDependsOn: [
'job',
],
},
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
},
],
description: 'Parameters for Jenkins job',
},
// --------------------------------------------------------------------------------------------------------
// Copy or Create a Job
// --------------------------------------------------------------------------------------------------------
{
displayName: 'New Job Name',
name: 'newJob',
type: 'string',
displayOptions: {
show: {
resource: [
'job',
],
operation: [
'copy',
'create',
],
},
},
required: true,
default: '',
description: 'Name of the new Jenkins job',
},
{
displayName: 'XML',
name: 'xml',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
displayOptions: {
show: {
resource: [
'job',
],
operation: [
'create',
],
},
},
required: true,
default: '',
description: 'XML of Jenkins config',
},
{
displayName: 'To get the XML of an existing job, add config.xml to the end of the job URL',
name: 'createNotice',
type: 'notice',
default: '',
displayOptions: {
show: {
resource: [
'job',
],
operation: [
'create',
],
},
},
},
// --------------------------------------------------------------------------------------------------------
// Jenkins operations
// --------------------------------------------------------------------------------------------------------
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'instance',
],
},
},
options: [
{
name: 'Cancel Quiet Down',
value: 'cancelQuietDown',
description: 'Cancel quiet down state',
},
{
name: 'Quiet Down',
value: 'quietDown',
description: 'Put Jenkins in quiet mode, no builds can be started, Jenkins is ready for shutdown',
},
{
name: 'Restart',
value: 'restart',
description: 'Restart Jenkins immediately on environments where it is possible',
},
{
name: 'Safely Restart',
value: 'safeRestart',
description: 'Restart Jenkins once no jobs are running on environments where it is possible',
},
{
name: 'Safely Shutdown',
value: 'safeExit',
description: 'Shutdown once no jobs are running',
},
{
name: 'Shutdown',
value: 'exit',
description: 'Shutdown Jenkins immediately',
},
],
default: 'safeRestart',
description: 'Jenkins instance operations',
noDataExpression: true,
},
{
displayName: 'Reason',
name: 'reason',
type: 'string',
displayOptions: {
show: {
resource: [
'instance',
],
operation: [
'quietDown',
],
},
},
required: false,
default: '',
description: 'Freeform reason for quiet down mode',
},
{
displayName: 'Instance operation can shutdown Jenkins instance and make it unresponsive. Some commands may not be available depending on instance implementation.',
name: 'instanceNotice',
type: 'notice',
default: '',
displayOptions: {
show: {
resource: [
'instance',
],
},
},
},
// --------------------------------------------------------------------------------------------------------
// Builds operations
// --------------------------------------------------------------------------------------------------------
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'build',
],
},
},
options: [
{
name: 'Get All',
value: 'getAll',
description: 'List Builds',
},
],
default: 'getAll',
noDataExpression: true,
},
{
displayName: 'Job Name',
name: 'job',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getJobs',
},
displayOptions: {
show: {
resource: [
'build',
],
operation: [
'getAll',
],
},
},
required: true,
default: '',
description: 'Name of the job',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: [
'build',
],
operation: [
'getAll',
],
},
},
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 50,
typeOptions: {
minValue: 1,
},
displayOptions: {
show: {
resource: [
'build',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
description: 'Max number of results to return',
},
],
};
methods = {
credentialTest: {
async jenkinApiCredentialTest(
this: ICredentialTestFunctions,
credential: ICredentialsDecrypted,
): Promise<NodeCredentialTestResult> {
const { baseUrl, username, apiKey } = credential.data as JenkinsApiCredentials;
const url = tolerateTrailingSlash(baseUrl);
const endpoint = '/api/json';
const options = {
auth: {
username: username,
password: apiKey,
},
method: 'GET',
body: {},
qs: {},
uri: `${url}${endpoint}`,
json: true,
};
try {
await this.helpers.request(options);
return {
status: 'OK',
message: 'Authentication successful',
};
} catch (error) {
return {
status: 'Error',
message: error.message,
};
}
},
},
loadOptions: {
async getJobs(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const endpoint = `/api/json`;
const { jobs } = await jenkinsApiRequest.call(this, 'GET', endpoint);
for (const job of jobs) {
returnData.push({
name: job.name,
value: job.name,
});
}
returnData.sort((a, b) => {
if (a.name < b.name) { return -1; }
if (a.name > b.name) { return 1; }
return 0;
});
return returnData;
},
async getJobParameters(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const job = this.getCurrentNodeParameter('job') as string;
const returnData: INodePropertyOptions[] = [];
const endpoint = `/job/${job}/api/json?tree=actions[parameterDefinitions[*]]`;
const { actions } = await jenkinsApiRequest.call(this, 'GET', endpoint);
for (const { _class, parameterDefinitions } of actions) {
if (_class?.includes('ParametersDefinitionProperty')) {
for (const { name, type } of parameterDefinitions) {
returnData.push({
name: `${name} - (${type})`,
value: name,
});
}
}
}
returnData.sort((a, b) => {
if (a.name < b.name) { return -1; }
if (a.name > b.name) { return 1; }
return 0;
});
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = items.length as unknown as number;
let responseData;
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < length; i++) {
try {
if (resource === 'job') {
if (operation === 'trigger') {
const job = this.getNodeParameter('job', i) as string;
const endpoint = `/job/${job}/build`;
await jenkinsApiRequest.call(this, 'POST', endpoint);
responseData = { success: true };
}
if (operation === 'triggerParams') {
const job = this.getNodeParameter('job', i) as string;
const params = this.getNodeParameter('param.params', i, []) as [];
let body = {};
if (params.length) {
body = params.reduce((body: IDataObject, param: { name: string; value: string }) => {
body[param.name] = param.value;
return body;
}, {});
}
const endpoint = `/job/${job}/buildWithParameters`;
await jenkinsApiRequest.call(this, 'POST', endpoint, {}, {},
{
form: body,
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
},
);
responseData = { success: true };
}
if (operation === 'copy') {
const job = this.getNodeParameter('job', i) as string;
const name = this.getNodeParameter('newJob', i) as string;
const queryParams = {
name,
mode: 'copy',
from: job,
};
const endpoint = `/createItem`;
try {
await jenkinsApiRequest.call(this, 'POST', endpoint, queryParams);
responseData = { success: true };
}
catch (error) {
if (error.httpCode === '302') {
responseData = { success: true };
} else {
throw new NodeApiError(this.getNode(), error);
}
}
}
if (operation === 'create') {
const name = this.getNodeParameter('newJob', i) as string;
const queryParams = {
name,
};
const headers = {
'content-type': 'application/xml',
};
const body = this.getNodeParameter('xml', i) as string;
const endpoint = `/createItem`;
await jenkinsApiRequest.call(this, 'POST', endpoint, queryParams, body, { headers, json: false });
responseData = { success: true };
}
}
if (resource === 'instance') {
if (operation === 'quietDown') {
const reason = this.getNodeParameter('reason', i) as string;
let queryParams;
if (reason) {
queryParams = {
reason,
};
}
const endpoint = `/quietDown`;
await jenkinsApiRequest.call(this, 'POST', endpoint, queryParams);
responseData = { success: true };
}
if (operation === 'cancelQuietDown') {
const endpoint = `/cancelQuietDown`;
await jenkinsApiRequest.call(this, 'POST', endpoint);
responseData = { success: true };
}
if (operation === 'restart') {
const endpoint = `/restart`;
try {
await jenkinsApiRequest.call(this, 'POST', endpoint);
} catch (error) {
if (error.httpCode === '503') {
responseData = { success: true };
} else {
throw new NodeApiError(this.getNode(), error);
}
}
}
if (operation === 'safeRestart') {
const endpoint = `/safeRestart`;
try {
await jenkinsApiRequest.call(this, 'POST', endpoint);
} catch (error) {
if (error.httpCode === '503') {
responseData = { success: true };
} else {
throw new NodeApiError(this.getNode(), error);
}
}
}
if (operation === 'exit') {
const endpoint = `/exit`;
await jenkinsApiRequest.call(this, 'POST', endpoint);
responseData = { success: true };
}
if (operation === 'safeExit') {
const endpoint = `/safeExit`;
await jenkinsApiRequest.call(this, 'POST', endpoint);
responseData = { success: true };
}
}
if (resource === 'build') {
if (operation === 'getAll') {
const job = this.getNodeParameter('job', i) as string;
let endpoint = `/job/${job}/api/json?tree=builds[*]`;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (!returnAll) {
const limit = this.getNodeParameter('limit', i) as number;
endpoint += `{0,${limit}}`;
}
responseData = await jenkinsApiRequest.call(this, 'GET', endpoint);
responseData = responseData.builds;
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else {
returnData.push(responseData as IDataObject);
}
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ error: error.message });
continue;
}
throw error;
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 47 KiB

View file

@ -149,6 +149,7 @@
"dist/credentials/IntercomApi.credentials.js",
"dist/credentials/InvoiceNinjaApi.credentials.js",
"dist/credentials/IterableApi.credentials.js",
"dist/credentials/JenkinsApi.credentials.js",
"dist/credentials/JiraSoftwareCloudApi.credentials.js",
"dist/credentials/JiraSoftwareServerApi.credentials.js",
"dist/credentials/JotFormApi.credentials.js",
@ -472,6 +473,7 @@
"dist/nodes/InvoiceNinja/InvoiceNinjaTrigger.node.js",
"dist/nodes/ItemLists/ItemLists.node.js",
"dist/nodes/Iterable/Iterable.node.js",
"dist/nodes/Jenkins/Jenkins.node.js",
"dist/nodes/Jira/Jira.node.js",
"dist/nodes/Jira/JiraTrigger.node.js",
"dist/nodes/JotForm/JotFormTrigger.node.js",