@binhnt83/plugin-jenkins-backend
v0.10.0
Published
BinhNT83 Vingroup JSC custom Backstage backend plugin that integrates towards Jenkins
Downloads
299
Readme
Jenkins Plugin (Alpha)
Welcome to the Jenkins backend plugin! Website: https://jenkins.io/
This is the backend half of the 2 Jenkins plugins and is responsible for:
- finding an appropriate instance of Jenkins for an entity
- finding the appropriate job(s) on that instance for an entity
- connecting to Jenkins and gathering data to present to the frontend
New Backend System
The jenkins backend plugin has support for the new backend system, here's how you can set that up:
In your packages/backend/src/index.ts
make the following changes:
import { createBackend } from '@backstage/backend-defaults';
const backend = createBackend();
// ... other feature additions
backend.add(import('@backstage-community/plugin-jenkins-backend'));
backend.start();
Integrating into a backstage instance
This plugin needs to be added to an existing backstage instance.
# From your Backstage root directory
yarn --cwd packages/backend add @backstage-community/plugin-jenkins-backend
Typically, this means creating a src/plugins/jenkins.ts
file and adding a reference to it to src/index.ts
jenkins.ts
import {
createRouter,
DefaultJenkinsInfoProvider,
} from '@backstage-community/plugin-jenkins-backend';
import { CatalogClient } from '@backstage/catalog-client';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const catalog = new CatalogClient({
discoveryApi: env.discovery,
});
return await createRouter({
logger: env.logger,
jenkinsInfoProvider: DefaultJenkinsInfoProvider.fromConfig({
config: env.config,
catalog,
}),
permissions: env.permissions,
});
}
src/index.ts
diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
index f2b14b2..2c64f47 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -22,6 +22,7 @@ import { Config } from '@backstage/config';
import app from './plugins/app';
+import jenkins from './plugins/jenkins';
import scaffolder from './plugins/scaffolder';
@@ -56,6 +57,7 @@ async function main() {
const authEnv = useHotMemoize(module, () => createEnv('auth'));
+ const jenkinsEnv = useHotMemoize(module, () => createEnv('jenkins'));
const proxyEnv = useHotMemoize(module, () => createEnv('proxy'));
@@ -63,6 +65,7 @@ async function main() {
const apiRouter = Router();
apiRouter.use('/catalog', await catalog(catalogEnv));
+ apiRouter.use('/jenkins', await jenkins(jenkinsEnv));
apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv));
This plugin must be provided with a JenkinsInfoProvider, this is a strategy object for finding the Jenkins instance and job for an entity.
There is a standard one provided, but the Integrator is free to build their own.
DefaultJenkinsInfoProvider
Allows configuration of either a single or multiple global Jenkins instances and annotating entities with the job name on that instance (and optionally the name of the instance).
Example - Single global instance
The following will look for jobs for this entity at https://jenkins.example.com/job/teamA/job/artistLookup-build
Config
jenkins:
baseUrl: https://jenkins.example.com
username: backstage-bot
projectCountLimit: 100
apiKey: 123456789abcdef0123456789abcedf012
# optionally add extra headers
# extraRequestHeaders:
# extra-header: my-value
Catalog
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: artist-lookup
annotations:
'jenkins.io/job-full-name': teamA/artistLookup-build
The projectCountLimit
is optional and if not set, the default limit is 50.
The old annotation name of jenkins.io/github-folder
is equivalent to jenkins.io/job-full-name
Example - Multiple global instances
The following will look for jobs for this entity at https://jenkins-foo.example.com/job/teamA/job/artistLookup-build
Config
jenkins:
instances:
- name: default
baseUrl: https://jenkins.example.com
username: backstage-bot
projectCountLimit: 100
apiKey: 123456789abcdef0123456789abcedf012
- name: departmentFoo
baseUrl: https://jenkins-foo.example.com
username: backstage-bot
projectCountLimit: 100
apiKey: 123456789abcdef0123456789abcedf012
Catalog
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: artist-lookup
annotations:
'jenkins.io/job-full-name': departmentFoo:teamA/artistLookup-build
If the departmentFoo:
part is omitted, the default instance will be assumed.
The following config is an equivalent (but less clear) version of the above:
jenkins:
baseUrl: https://jenkins.example.com
username: backstage-bot
apiKey: 123456789abcdef0123456789abcedf012
instances:
- name: departmentFoo
baseUrl: https://jenkins-foo.example.com
username: backstage-bot
projectCountLimit: 100
apiKey: 123456789abcdef0123456789abcedf012
Example - Override Base Url from an Entity
The following will show you how to override a base url defined in the Config with a value from the Catalog. allowedBaseUrlOverrideRegex must be a regex string in the config, which will check if the sent in url matches that regex for override.
The check for the regex is to add a security check to make sure no malicious urls were sent to connect the plugin.
Config
jenkins:
instances:
- name: departmentFoo
baseUrl: https://departmentFoo.example.com
username: backstage-bot
projectCountLimit: 100
apiKey: 123456789abcdef0123456789abcedf012
allowedBaseUrlOverrideRegex: https://.*\.example\.com
Catalog
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: artist-lookup
annotations:
'jenkins.io/job-full-name': departmentFoo:teamA/artistLookup-build
'jenkins.io/override-base-url': 'https://other.example.com'
This will set the instance's base url to 'https://other.example.com' when loading the configuration. It will verify first if the url sent in is not null, along with the regex string list, and then compares the url to all regex strings to make sure one of them match.
This use case is for Jenkins systems where there are a lot of Jenkins instances configured from a base instance, which share the same API keys. Therefore a user does not have to define all of the instances here, but in the catalog for ease of use.
Custom JenkinsInfoProvider
An example of a bespoke JenkinsInfoProvider which uses an organisation specific annotation to look up the Jenkins info (including jobFullName):
class AcmeJenkinsInfoProvider implements JenkinsInfoProvider {
constructor(private readonly catalog: CatalogClient) {}
async getInstance(opt: {
entityRef: EntityName;
jobFullName?: string;
}): Promise<JenkinsInfo> {
const PAAS_ANNOTATION = 'acme.example.com/paas-project-name';
// lookup pass-project-name from entity annotation
const entity = await this.catalog.getEntityByRef(opt.entityRef);
if (!entity) {
throw new Error(
`Couldn't find entity with name: ${stringifyEntityRef(opt.entityRef)}`,
);
}
const paasProjectName = entity.metadata.annotations?.[PAAS_ANNOTATION];
if (!paasProjectName) {
throw new Error(
`Couldn't find paas annotation (${PAAS_ANNOTATION}) on entity with name: ${stringifyEntityRef(
opt.entityRef,
)}`,
);
}
// lookup department and team for paas project name
const { team, dept } = this.lookupPaasInfo(paasProjectName);
const baseUrl = `https://jenkins-${dept}.example.com/`;
const jobFullName = `${team}/${paasProjectName}`;
const username = 'backstage-bot';
const projectCountLimit = 100;
const apiKey = this.getJenkinsApiKey(paasProjectName);
const creds = btoa(`${username}:${apiKey}`);
return {
baseUrl,
headers: {
Authorization: `Basic ${creds}`,
},
jobFullName,
};
}
private lookupPaasInfo(_: string): { team: string; dept: string } {
// Mock implementation, this would get info from the paas system somehow in reality.
return {
team: 'teamA',
dept: 'DepartmentFoo',
};
}
private getJenkinsApiKey(_: string): string {
// Mock implementation, this would get info from the paas system somehow in reality.
return '123456789abcdef0123456789abcedf012';
}
}
No config would be needed if using this JenkinsInfoProvider
A Catalog entity of the following will look for jobs for this entity at https://jenkins-departmentFoo.example.com/job/teamA/job/artistLookupService
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: artist-lookup
annotations:
'acme.example.com/paas-project-name': artistLookupService
Jenkins' terminology notes
The domain model for Jenkins is not particularly clear but for the purposes of this plugin the following model has been assumed:
Jenkins contains a tree of jobs which have children of either; other jobs (making it a folder) or builds (making it a project). Concepts like pipeline and views are meaningless (pipelines are just jobs for our purposes, views are (as the name suggests) just views of subsets of jobs)
A job full name is a slash separated list of the names of the job, and the folders which contain it. For example teamA/artistLookupService/develop
, and the same way that a filesystem path has folders and file names.