create a single DefaultJenkinsProvider

Signed-off-by: Andrew Shirley <andrew.shirley@sainsburys.co.uk>
Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
Andrew Shirley
2021-06-01 19:01:07 +01:00
committed by blam
parent 449b995261
commit e3675685a2
2 changed files with 175 additions and 250 deletions
+93 -49
View File
@@ -19,32 +19,39 @@ Typically, this means creating a `src/plugins/jenkins.ts` file and adding a refe
```typescript
import {
createRouter,
SingleJenkinsInfoProvider,
DefaultJenkinsInfoProvider,
} from '@backstage/plugin-jenkins-backend';
import { CatalogClient } from '@backstage/catalog-client';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
export default async function createPlugin({
logger,
config,
discovery,
}: PluginEnvironment): Promise<Router> {
const catalogClient = new CatalogClient({ discoveryApi: discovery });
return await createRouter({
logger,
jenkinsInfoProvider: new SingleJenkinsInfoProvider(),
jenkinsInfoProvider: new DefaultJenkinsInfoProvider(catalogClient, config),
});
}
```
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 selection of standard ones provided, but the Integrator is free to build their own.
There is a standard one provided, but the Integrator is free to build their own.
### SingleJenkinsInfoProvider
### DefaultJenkinsInfoProvider
Allows configuration of a single global jenkins instance and annotating entities with the job name on that instance.
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
Config
```yaml
jenkins:
@@ -53,7 +60,7 @@ jenkins:
apikey: 123456789abcdef0123456789abcedf012
```
#### Catalog
Catalog
```yaml
apiVersion: backstage.io/v1alpha1
@@ -61,16 +68,16 @@ kind: Component
metadata:
name: artist-lookup
annotations:
'jenkins.io/github-folder': teamA/artistLookup-build
'jenkins.io/job-slug': teamA/artistLookup-build
```
### PrefixedJenkinsInfoProvider
The old annotation name of `jenkins.io/github-folder` is equivalent to `jenkins.io/job-slug`
Allows configuration of multiple global jenkins instance and annotating entities with the name of the instance and job name on that instance.
#### 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
Config
```yaml
jenkins:
@@ -85,7 +92,7 @@ jenkins:
apikey: 123456789abcdef0123456789abcedf012
```
#### Catalog
Catalog
```yaml
apiVersion: backstage.io/v1alpha1
@@ -93,53 +100,92 @@ kind: Component
metadata:
name: artist-lookup
annotations:
'jenkins.io/github-folder': departmentFoo#teamA/artistLookup-build
'jenkins.io/job-slug': departmentFoo:teamA/artistLookup-build
```
If the `departmentFoo#` part is omitted, the default instance will be assumed.
If the `departmentFoo:` part is omitted, the default instance will be assumed.
### DefaultJenkinsInfoProvider
The default jenkins info provider makes it clear it is replaceable in the config but is otherwise the same as PrefixedJenkinsInfoProvider
The following will look for jobs for this entity at `https://jenkins-foo.example.com/job/teamA/job/artistLookup-build`
#### Config
The following config is an equivalent (but less clear) version of the above:
```yaml
jenkins:
DefaultJenkinsInfoProvider:
instances:
- name: default
baseUrl: https://jenkins.example.com
username: backstage-bot
apikey: 123456789abcdef0123456789abcedf012
- name: departmentFoo
baseUrl: https://jenkins-foo.example.com
username: backstage-bot
apikey: 123456789abcdef0123456789abcedf012
baseUrl: https://jenkins.example.com
username: backstage-bot
apikey: 123456789abcdef0123456789abcedf012
instances:
- name: departmentFoo
baseUrl: https://jenkins-foo.example.com
username: backstage-bot
apikey: 123456789abcdef0123456789abcedf012
```
#### Catalog
### Custom JenkinsInfoProvider
```yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: artist-lookup
annotations:
'jenkins.io/github-folder': departmentFoo#teamA/artistLookup-build
An example of a bespoke JenkinsInfoProvider which uses an organisation specific annotation to look up the jenkins info (including jobName):
```typescript
class AcmeJenkinsInfoProvider implements JenkinsInfoProvider {
constructor(private readonly catalog: CatalogClient) {}
async getInstance(opt: {
entityRef: EntityName;
jobName?: string;
}): Promise<JenkinsInfo> {
const PAAS_ANNOTATION = 'acme.example.com/paas-project-name';
// lookup pass-project-name from entity annotation
const entity = await this.catalog.getEntityByName(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 jobName = `${team}/${paasProjectName}`;
const username = 'backstage-bot';
const apiKey = this.getJenkinsApiKey(paasProjectName);
const creds = btoa(`${username}:${apiKey}`);
return {
baseUrl,
headers: {
Authorization: `Basic ${creds}`,
},
jobName,
};
}
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';
}
}
```
### AcmeJenkinsInfoProvider
No config would be needed if using this JenkinsInfoProvider
An example of a bespoke JenkinsInfoProvider which uses an organisation specific annotation to look up the jenkins info (including jobName)
#### Config
None needed
#### Catalog
A Catalog entity of the following will look for jobs for this entity at `https://jenkins-departmentFoo.example.com/job/teamA/job/artistLookupService`
```yaml
apiVersion: backstage.io/v1alpha1
@@ -150,8 +196,6 @@ metadata:
'acme.example.com/paas-project-name': artistLookupService
```
The following will look for jobs for this entity at `https://jenkins-departmentFoo.example.com/job/teamA/job/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:
@@ -13,7 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EntityName, stringifyEntityRef } from '@backstage/catalog-model';
import {
Entity,
EntityName,
stringifyEntityRef,
} from '@backstage/catalog-model';
import { CatalogClient } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
@@ -53,131 +57,14 @@ export class DummyJenkinsInfoProvider implements JenkinsInfoProvider {
}
/**
* Use the original annotation scheme and a simple config
*/
export class SingleJenkinsInfoProvider implements JenkinsInfoProvider {
constructor(
private readonly catalog: CatalogClient,
private readonly config: Config,
) {}
async getInstance(opt: {
entityRef: EntityName;
jobName?: string;
}): Promise<JenkinsInfo> {
const JENKINS_ANNOTATION = 'jenkins.io/github-folder';
// lookup jobName from entity annotation
const entity = await this.catalog.getEntityByName(opt.entityRef);
if (!entity) {
throw new Error(
`Couldn't find entity with name: ${stringifyEntityRef(opt.entityRef)}`,
);
}
const jobName = entity.metadata.annotations?.[JENKINS_ANNOTATION];
if (!jobName) {
throw new Error(
`Couldn't find jenkins annotation (${JENKINS_ANNOTATION}) on entity with name: ${stringifyEntityRef(
opt.entityRef,
)}`,
);
}
// lookup baseURL + creds from config
const baseUrl = this.config.getString('jenkins.baseUrl');
const username = this.config.getString('jenkins.username');
const apiKey = this.config.getString('jenkins.apiKey');
const creds = btoa(`${username}:${apiKey}`);
return {
baseUrl,
headers: {
Authorization: `Basic ${creds}`,
},
jobName,
};
}
}
/**
* Use a prefixed version of the original annotation scheme and a multiple instance config
*/
export class PrefixedJenkinsInfoProvider implements JenkinsInfoProvider {
constructor(
private readonly catalog: CatalogClient,
private readonly config: Config,
) {}
async getInstance(opt: {
entityRef: EntityName;
jobName?: string;
}): Promise<JenkinsInfo> {
const JENKINS_ANNOTATION = 'jenkins.io/github-folder';
const DEFAULT_JENKINS_NAME = 'default';
// lookup `[jenkinsName#]jobName` from entity annotation
const entity = await this.catalog.getEntityByName(opt.entityRef);
if (!entity) {
throw new Error(
`Couldn't find entity with name: ${stringifyEntityRef(opt.entityRef)}`,
);
}
const jenkinsAndJobName = entity.metadata.annotations?.[JENKINS_ANNOTATION];
if (!jenkinsAndJobName) {
throw new Error(
`Couldn't find jenkins annotation (${JENKINS_ANNOTATION}) on entity with name: ${stringifyEntityRef(
opt.entityRef,
)}`,
);
}
let jobName;
let jenkinsName: string;
const splitIndex = jenkinsAndJobName.indexOf('#');
if (splitIndex === -1) {
// no jenkinsName specified, use default
jenkinsName = DEFAULT_JENKINS_NAME;
jobName = jenkinsAndJobName;
} else {
// There is a jenkinsName specified
jenkinsName = jenkinsAndJobName.substring(0, splitIndex);
jobName = jenkinsAndJobName.substring(
splitIndex + 1,
jenkinsAndJobName.length,
);
}
// lookup baseURL + creds from config
const instanceConfig = this.config
.getConfigArray('jenkins.instances')
.filter(c => c.getString('name') === jenkinsName)[0];
if (!instanceConfig) {
throw new Error(
`Couldn't find a jenkins instance in the config with name ${jenkinsName}`,
);
}
const baseUrl = instanceConfig.getString('baseUrl');
const username = instanceConfig.getString('username');
const apiKey = instanceConfig.getString('apiKey');
const creds = btoa(`${username}:${apiKey}`);
return {
baseUrl,
headers: {
Authorization: `Basic ${creds}`,
},
jobName,
};
}
}
/**
* Use a prefixed version of the original annotation scheme and a multiple instance config with clear "default" name
* Use default config and annotations.
*
* This will fallback through various deprecated config and annotation schemes.
*/
export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider {
static readonly OLD_JENKINS_ANNOTATION = 'jenkins.io/github-folder';
static readonly NEW_JENKINS_ANNOTATION = 'jenkins.io/job-slug';
constructor(
private readonly catalog: CatalogClient,
private readonly config: Config,
@@ -187,10 +74,7 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider {
entityRef: EntityName;
jobName?: string;
}): Promise<JenkinsInfo> {
const JENKINS_ANNOTATION = 'jenkins.io/github-folder';
const DEFAULT_JENKINS_NAME = 'default';
// lookup `[jenkinsName#]jobName` from entity annotation
// load entity
const entity = await this.catalog.getEntityByName(opt.entityRef);
if (!entity) {
throw new Error(
@@ -198,21 +82,23 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider {
);
}
const jenkinsAndJobName = entity.metadata.annotations?.[JENKINS_ANNOTATION];
// lookup `[jenkinsName#]jobName` from entity annotation
const jenkinsAndJobName = DefaultJenkinsInfoProvider.getEntityAnnotationValue(
entity,
);
if (!jenkinsAndJobName) {
throw new Error(
`Couldn't find jenkins annotation (${JENKINS_ANNOTATION}) on entity with name: ${stringifyEntityRef(
opt.entityRef,
)}`,
`Couldn't find jenkins annotation (${
DefaultJenkinsInfoProvider.NEW_JENKINS_ANNOTATION
}) on entity with name: ${stringifyEntityRef(opt.entityRef)}`,
);
}
let jobName;
let jenkinsName: string;
const splitIndex = jenkinsAndJobName.indexOf('#');
let jenkinsName: string | undefined;
const splitIndex = jenkinsAndJobName.indexOf(':');
if (splitIndex === -1) {
// no jenkinsName specified, use default
jenkinsName = DEFAULT_JENKINS_NAME;
jobName = jenkinsAndJobName;
} else {
// There is a jenkinsName specified
@@ -224,67 +110,17 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider {
}
// lookup baseURL + creds from config
const instanceConfig = this.config
.getConfigArray('jenkins.DefaultJenkinsInfoProvider.instances')
.filter(c => c.getString('name') === jenkinsName)[0];
if (!instanceConfig) {
throw new Error(
`Couldn't find a jenkins instance in the config with name ${jenkinsName}`,
);
}
const instanceConfig = DefaultJenkinsInfoProvider.getInstanceConfig(
jenkinsName,
this.config,
);
const baseUrl = instanceConfig.getString('baseUrl');
const username = instanceConfig.getString('username');
const apiKey = instanceConfig.getString('apiKey');
const creds = btoa(`${username}:${apiKey}`);
return {
baseUrl,
headers: {
Authorization: `Basic ${creds}`,
},
jobName,
};
}
}
/**
* Use a bespoke annotation and no config
*/
export class AcmeJenkinsInfoProvider implements JenkinsInfoProvider {
constructor(private readonly catalog: CatalogClient) {}
async getInstance(opt: {
entityRef: EntityName;
jobName?: string;
}): Promise<JenkinsInfo> {
const PAAS_ANNOTATION = 'acme.example.com/paas-project-name';
// lookup pass-project-name from entity annotation
const entity = await this.catalog.getEntityByName(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 jobName = `${team}/${paasProjectName}`;
const username = 'backstage-bot';
const apiKey = this.getJenkinsApiKey(paasProjectName);
const creds = btoa(`${username}:${apiKey}`);
const creds = Buffer.from(`${username}:${apiKey}`, 'binary').toString(
'base64',
);
return {
baseUrl,
@@ -295,16 +131,61 @@ export class AcmeJenkinsInfoProvider implements JenkinsInfoProvider {
};
}
private lookupPaasInfo(_: string): { team: string; dept: string } {
// Mock implementation, this would get info from the paas system somehow in reality.
return {
team: 'teamA',
dept: 'DepartmentA',
};
private static getEntityAnnotationValue(entity: Entity) {
return (
entity.metadata.annotations?.[
DefaultJenkinsInfoProvider.OLD_JENKINS_ANNOTATION
] ||
entity.metadata.annotations?.[
DefaultJenkinsInfoProvider.NEW_JENKINS_ANNOTATION
]
);
}
private getJenkinsApiKey(_: string): string {
// Mock implementation, this would get info from the paas system somehow in reality.
return '123456789abcdef0123456789abcedf012';
private static getInstanceConfig(
jenkinsName: string | undefined,
rootConfig: Config,
): Config {
const DEFAULT_JENKINS_NAME = 'default';
const jenkinsConfig = rootConfig.getConfig('jenkins');
if (!jenkinsName || jenkinsName === DEFAULT_JENKINS_NAME) {
// no name provided, this could be
// (jenkins.baseUrl, jenkins.username, jenkins.apiKey) or
// the entry with default name in jenkins.instances
const namedInstanceConfig = jenkinsConfig
.getConfigArray('instances')
.filter(c => c.getString('name') === DEFAULT_JENKINS_NAME)[0];
if (namedInstanceConfig) {
return namedInstanceConfig;
}
// Get these as optional strings and check to give a better error message
const baseUrl = jenkinsConfig.getOptionalString('baseUrl');
const username = jenkinsConfig.getOptionalString('username');
const apiKey = jenkinsConfig.getOptionalString('apiKey');
if (!baseUrl || !username || !apiKey) {
throw new Error(
`Couldn't find a default jenkins instance in the config. Either configure an instance with name ${DEFAULT_JENKINS_NAME} or add a prefix to your annotation value.`,
);
}
return jenkinsConfig;
}
// A name is provided, look it up.
const namedInstanceConfig = jenkinsConfig
.getConfigArray('instances')
.filter(c => c.getString('name') === jenkinsName)[0];
if (!namedInstanceConfig) {
throw new Error(
`Couldn't find a jenkins instance in the config with name ${jenkinsName}`,
);
}
return namedInstanceConfig;
}
}