Merge pull request #6803 from SDA-SE/feat/jenkins-provider-config
Extract `JenkinsConfig` to make writing a custom `JenkinsInfoProvider` easier
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-jenkins-backend': patch
|
||||
---
|
||||
|
||||
Extract `JenkinsConfig` to make writing a custom `JenkinsInfoProvider` easier.
|
||||
@@ -34,6 +34,17 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider {
|
||||
static readonly OLD_JENKINS_ANNOTATION = 'jenkins.io/github-folder';
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "JenkinsConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export class JenkinsConfig {
|
||||
constructor(instances: JenkinsInstanceConfig[]);
|
||||
static fromConfig(config: Config): JenkinsConfig;
|
||||
getInstanceConfig(jenkinsName?: string): JenkinsInstanceConfig;
|
||||
// (undocumented)
|
||||
readonly instances: JenkinsInstanceConfig[];
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "JenkinsInfo" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
@@ -57,6 +68,20 @@ export interface JenkinsInfoProvider {
|
||||
}): Promise<JenkinsInfo>;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "JenkinsInstanceConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export interface JenkinsInstanceConfig {
|
||||
// (undocumented)
|
||||
apiKey: string;
|
||||
// (undocumented)
|
||||
baseUrl: string;
|
||||
// (undocumented)
|
||||
name: string;
|
||||
// (undocumented)
|
||||
username: string;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "RouterOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -14,7 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type { RouterOptions } from './router';
|
||||
export {
|
||||
DefaultJenkinsInfoProvider,
|
||||
JenkinsConfig,
|
||||
} from './jenkinsInfoProvider';
|
||||
export type {
|
||||
JenkinsInfo,
|
||||
JenkinsInfoProvider,
|
||||
JenkinsInstanceConfig,
|
||||
} from './jenkinsInfoProvider';
|
||||
export { createRouter } from './router';
|
||||
export type { JenkinsInfo, JenkinsInfoProvider } from './jenkinsInfoProvider';
|
||||
export { DefaultJenkinsInfoProvider } from './jenkinsInfoProvider';
|
||||
export type { RouterOptions } from './router';
|
||||
|
||||
@@ -14,10 +14,149 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DefaultJenkinsInfoProvider, JenkinsInfo } from './jenkinsInfoProvider';
|
||||
import { CatalogClient } from '@backstage/catalog-client';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { Entity, EntityName } from '@backstage/catalog-model';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import {
|
||||
DefaultJenkinsInfoProvider,
|
||||
JenkinsConfig,
|
||||
JenkinsInfo,
|
||||
} from './jenkinsInfoProvider';
|
||||
|
||||
describe('JenkinsConfig', () => {
|
||||
it('Reads simple config and annotation', async () => {
|
||||
const config = JenkinsConfig.fromConfig(
|
||||
new ConfigReader({
|
||||
jenkins: {
|
||||
baseUrl: 'https://jenkins.example.com',
|
||||
username: 'backstage - bot',
|
||||
apiKey: '123456789abcdef0123456789abcedf012',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(config.instances).toEqual([
|
||||
{
|
||||
name: 'default',
|
||||
baseUrl: 'https://jenkins.example.com',
|
||||
username: 'backstage - bot',
|
||||
apiKey: '123456789abcdef0123456789abcedf012',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('Reads named default config and annotation', async () => {
|
||||
const config = JenkinsConfig.fromConfig(
|
||||
new ConfigReader({
|
||||
jenkins: {
|
||||
instances: [
|
||||
{
|
||||
name: 'default',
|
||||
baseUrl: 'https://jenkins.example.com',
|
||||
username: 'backstage - bot',
|
||||
apiKey: '123456789abcdef0123456789abcedf012',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(config.instances).toEqual([
|
||||
{
|
||||
name: 'default',
|
||||
baseUrl: 'https://jenkins.example.com',
|
||||
username: 'backstage - bot',
|
||||
apiKey: '123456789abcdef0123456789abcedf012',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('Parses named default config (amongst named other configs)', async () => {
|
||||
const config = JenkinsConfig.fromConfig(
|
||||
new ConfigReader({
|
||||
jenkins: {
|
||||
instances: [
|
||||
{
|
||||
name: 'default',
|
||||
baseUrl: 'https://jenkins.example.com',
|
||||
username: 'backstage - bot',
|
||||
apiKey: '123456789abcdef0123456789abcedf012',
|
||||
},
|
||||
{
|
||||
name: 'other',
|
||||
baseUrl: 'https://jenkins-other.example.com',
|
||||
username: 'backstage - bot',
|
||||
apiKey: '123456789abcdef0123456789abcedf012',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(config.instances).toEqual([
|
||||
{
|
||||
name: 'default',
|
||||
baseUrl: 'https://jenkins.example.com',
|
||||
username: 'backstage - bot',
|
||||
apiKey: '123456789abcdef0123456789abcedf012',
|
||||
},
|
||||
{
|
||||
name: 'other',
|
||||
baseUrl: 'https://jenkins-other.example.com',
|
||||
username: 'backstage - bot',
|
||||
apiKey: '123456789abcdef0123456789abcedf012',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('Gets default Jenkins instance', async () => {
|
||||
const config = new JenkinsConfig([
|
||||
{
|
||||
name: 'default',
|
||||
baseUrl: 'https://jenkins.example.com',
|
||||
username: 'backstage - bot',
|
||||
apiKey: '123456789abcdef0123456789abcedf012',
|
||||
},
|
||||
{
|
||||
name: 'other',
|
||||
baseUrl: 'https://jenkins-other.example.com',
|
||||
username: 'backstage - bot',
|
||||
apiKey: '123456789abcdef0123456789abcedf012',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(config.getInstanceConfig()).toEqual({
|
||||
name: 'default',
|
||||
baseUrl: 'https://jenkins.example.com',
|
||||
username: 'backstage - bot',
|
||||
apiKey: '123456789abcdef0123456789abcedf012',
|
||||
});
|
||||
});
|
||||
|
||||
it('Gets named Jenkins instance', async () => {
|
||||
const config = new JenkinsConfig([
|
||||
{
|
||||
name: 'default',
|
||||
baseUrl: 'https://jenkins.example.com',
|
||||
username: 'backstage - bot',
|
||||
apiKey: '123456789abcdef0123456789abcedf012',
|
||||
},
|
||||
{
|
||||
name: 'other',
|
||||
baseUrl: 'https://jenkins-other.example.com',
|
||||
username: 'backstage - bot',
|
||||
apiKey: '123456789abcdef0123456789abcedf012',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(config.getInstanceConfig('other')).toEqual({
|
||||
name: 'other',
|
||||
baseUrl: 'https://jenkins-other.example.com',
|
||||
username: 'backstage - bot',
|
||||
apiKey: '123456789abcdef0123456789abcedf012',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DefaultJenkinsInfoProvider', () => {
|
||||
const mockCatalog: jest.Mocked<CatalogClient> = {
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { CatalogClient } from '@backstage/catalog-client';
|
||||
import {
|
||||
Entity,
|
||||
EntityName,
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import { CatalogClient } from '@backstage/catalog-client';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
export interface JenkinsInfoProvider {
|
||||
@@ -40,145 +40,28 @@ export interface JenkinsInfo {
|
||||
jobFullName: string; // TODO: make this an array
|
||||
}
|
||||
|
||||
export interface JenkinsInstanceConfig {
|
||||
name: string;
|
||||
baseUrl: string;
|
||||
username: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use default config and annotations, build using fromConfig static function.
|
||||
*
|
||||
* This will fallback through various deprecated config and annotation schemes.
|
||||
* Holds multiple Jenkins configurations.
|
||||
*/
|
||||
export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider {
|
||||
static readonly OLD_JENKINS_ANNOTATION = 'jenkins.io/github-folder';
|
||||
static readonly NEW_JENKINS_ANNOTATION = 'jenkins.io/job-full-name';
|
||||
export class JenkinsConfig {
|
||||
constructor(public readonly instances: JenkinsInstanceConfig[]) {}
|
||||
|
||||
private constructor(
|
||||
private readonly config: {
|
||||
name: string;
|
||||
baseUrl: string;
|
||||
username: string;
|
||||
apiKey: string;
|
||||
}[],
|
||||
private readonly catalog: CatalogClient,
|
||||
) {}
|
||||
|
||||
static fromConfig(options: {
|
||||
config: Config;
|
||||
catalog: CatalogClient;
|
||||
}): DefaultJenkinsInfoProvider {
|
||||
return new DefaultJenkinsInfoProvider(
|
||||
this.loadConfig(options.config),
|
||||
options.catalog,
|
||||
);
|
||||
}
|
||||
|
||||
async getInstance(opt: {
|
||||
entityRef: EntityName;
|
||||
jobFullName?: string;
|
||||
}): Promise<JenkinsInfo> {
|
||||
// load entity
|
||||
const entity = await this.catalog.getEntityByName(opt.entityRef);
|
||||
if (!entity) {
|
||||
throw new Error(
|
||||
`Couldn't find entity with name: ${stringifyEntityRef(opt.entityRef)}`,
|
||||
);
|
||||
}
|
||||
|
||||
// lookup `[jenkinsName#]jobFullName` from entity annotation
|
||||
const jenkinsAndJobName =
|
||||
DefaultJenkinsInfoProvider.getEntityAnnotationValue(entity);
|
||||
if (!jenkinsAndJobName) {
|
||||
throw new Error(
|
||||
`Couldn't find jenkins annotation (${
|
||||
DefaultJenkinsInfoProvider.NEW_JENKINS_ANNOTATION
|
||||
}) on entity with name: ${stringifyEntityRef(opt.entityRef)}`,
|
||||
);
|
||||
}
|
||||
|
||||
let jobFullName;
|
||||
let jenkinsName: string | undefined;
|
||||
const splitIndex = jenkinsAndJobName.indexOf(':');
|
||||
if (splitIndex === -1) {
|
||||
// no jenkinsName specified, use default
|
||||
jobFullName = jenkinsAndJobName;
|
||||
} else {
|
||||
// There is a jenkinsName specified
|
||||
jenkinsName = jenkinsAndJobName.substring(0, splitIndex);
|
||||
jobFullName = jenkinsAndJobName.substring(
|
||||
splitIndex + 1,
|
||||
jenkinsAndJobName.length,
|
||||
);
|
||||
}
|
||||
|
||||
// lookup baseURL + creds from config
|
||||
const instanceConfig = DefaultJenkinsInfoProvider.getInstanceConfig(
|
||||
jenkinsName,
|
||||
this.config,
|
||||
);
|
||||
|
||||
const creds = Buffer.from(
|
||||
`${instanceConfig.username}:${instanceConfig.apiKey}`,
|
||||
'binary',
|
||||
).toString('base64');
|
||||
|
||||
return {
|
||||
baseUrl: instanceConfig.baseUrl,
|
||||
headers: {
|
||||
Authorization: `Basic ${creds}`,
|
||||
},
|
||||
jobFullName,
|
||||
};
|
||||
}
|
||||
|
||||
private static getEntityAnnotationValue(entity: Entity) {
|
||||
return (
|
||||
entity.metadata.annotations?.[
|
||||
DefaultJenkinsInfoProvider.OLD_JENKINS_ANNOTATION
|
||||
] ||
|
||||
entity.metadata.annotations?.[
|
||||
DefaultJenkinsInfoProvider.NEW_JENKINS_ANNOTATION
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private static getInstanceConfig(
|
||||
jenkinsName: string | undefined,
|
||||
config: {
|
||||
name: string;
|
||||
baseUrl: string;
|
||||
username: string;
|
||||
apiKey: string;
|
||||
}[],
|
||||
): { name: string; baseUrl: string; username: string; apiKey: string } {
|
||||
/**
|
||||
* Read all Jenkins instance configurations.
|
||||
* @param config - Root configuration
|
||||
* @returns A JenkinsConfig that contains all configured Jenkins instances.
|
||||
*/
|
||||
static fromConfig(config: Config): JenkinsConfig {
|
||||
const DEFAULT_JENKINS_NAME = 'default';
|
||||
|
||||
if (!jenkinsName || jenkinsName === DEFAULT_JENKINS_NAME) {
|
||||
// no name provided, use default
|
||||
const instanceConfig = config.find(c => c.name === DEFAULT_JENKINS_NAME);
|
||||
|
||||
if (!instanceConfig) {
|
||||
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 instanceConfig;
|
||||
}
|
||||
|
||||
// A name is provided, look it up.
|
||||
const instanceConfig = config.find(c => c.name === jenkinsName);
|
||||
|
||||
if (!instanceConfig) {
|
||||
throw new Error(
|
||||
`Couldn't find a jenkins instance in the config with name ${jenkinsName}`,
|
||||
);
|
||||
}
|
||||
return instanceConfig;
|
||||
}
|
||||
|
||||
private static loadConfig(
|
||||
rootConfig: Config,
|
||||
): { name: string; baseUrl: string; username: string; apiKey: string }[] {
|
||||
const DEFAULT_JENKINS_NAME = 'default';
|
||||
|
||||
const jenkinsConfig = rootConfig.getConfig('jenkins');
|
||||
const jenkinsConfig = config.getConfig('jenkins');
|
||||
|
||||
// load all named instance config
|
||||
const namedInstanceConfig =
|
||||
@@ -223,9 +106,138 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider {
|
||||
apiKey: string;
|
||||
}[];
|
||||
|
||||
return [...namedInstanceConfig, ...unnamedInstanceConfig];
|
||||
return new JenkinsConfig([
|
||||
...namedInstanceConfig,
|
||||
...unnamedInstanceConfig,
|
||||
]);
|
||||
}
|
||||
|
||||
return namedInstanceConfig;
|
||||
return new JenkinsConfig(namedInstanceConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a Jenkins instance configuration by name, or the default one if no
|
||||
* name is provided.
|
||||
* @param jenkinsName - Optional name of the Jenkins instance.
|
||||
* @returns The requested Jenkins instance.
|
||||
*/
|
||||
getInstanceConfig(jenkinsName?: string): JenkinsInstanceConfig {
|
||||
const DEFAULT_JENKINS_NAME = 'default';
|
||||
|
||||
if (!jenkinsName || jenkinsName === DEFAULT_JENKINS_NAME) {
|
||||
// no name provided, use default
|
||||
const instanceConfig = this.instances.find(
|
||||
c => c.name === DEFAULT_JENKINS_NAME,
|
||||
);
|
||||
|
||||
if (!instanceConfig) {
|
||||
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 instanceConfig;
|
||||
}
|
||||
|
||||
// A name is provided, look it up.
|
||||
const instanceConfig = this.instances.find(c => c.name === jenkinsName);
|
||||
|
||||
if (!instanceConfig) {
|
||||
throw new Error(
|
||||
`Couldn't find a jenkins instance in the config with name ${jenkinsName}`,
|
||||
);
|
||||
}
|
||||
return instanceConfig;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use default config and annotations, build using fromConfig static function.
|
||||
*
|
||||
* 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-full-name';
|
||||
|
||||
private constructor(
|
||||
private readonly config: JenkinsConfig,
|
||||
private readonly catalog: CatalogClient,
|
||||
) {}
|
||||
|
||||
static fromConfig(options: {
|
||||
config: Config;
|
||||
catalog: CatalogClient;
|
||||
}): DefaultJenkinsInfoProvider {
|
||||
return new DefaultJenkinsInfoProvider(
|
||||
JenkinsConfig.fromConfig(options.config),
|
||||
options.catalog,
|
||||
);
|
||||
}
|
||||
|
||||
async getInstance(opt: {
|
||||
entityRef: EntityName;
|
||||
jobFullName?: string;
|
||||
}): Promise<JenkinsInfo> {
|
||||
// load entity
|
||||
const entity = await this.catalog.getEntityByName(opt.entityRef);
|
||||
if (!entity) {
|
||||
throw new Error(
|
||||
`Couldn't find entity with name: ${stringifyEntityRef(opt.entityRef)}`,
|
||||
);
|
||||
}
|
||||
|
||||
// lookup `[jenkinsName#]jobFullName` from entity annotation
|
||||
const jenkinsAndJobName =
|
||||
DefaultJenkinsInfoProvider.getEntityAnnotationValue(entity);
|
||||
if (!jenkinsAndJobName) {
|
||||
throw new Error(
|
||||
`Couldn't find jenkins annotation (${
|
||||
DefaultJenkinsInfoProvider.NEW_JENKINS_ANNOTATION
|
||||
}) on entity with name: ${stringifyEntityRef(opt.entityRef)}`,
|
||||
);
|
||||
}
|
||||
|
||||
let jobFullName;
|
||||
let jenkinsName: string | undefined;
|
||||
const splitIndex = jenkinsAndJobName.indexOf(':');
|
||||
if (splitIndex === -1) {
|
||||
// no jenkinsName specified, use default
|
||||
jobFullName = jenkinsAndJobName;
|
||||
} else {
|
||||
// There is a jenkinsName specified
|
||||
jenkinsName = jenkinsAndJobName.substring(0, splitIndex);
|
||||
jobFullName = jenkinsAndJobName.substring(
|
||||
splitIndex + 1,
|
||||
jenkinsAndJobName.length,
|
||||
);
|
||||
}
|
||||
|
||||
// lookup baseURL + creds from config
|
||||
const instanceConfig = this.config.getInstanceConfig(jenkinsName);
|
||||
|
||||
const creds = Buffer.from(
|
||||
`${instanceConfig.username}:${instanceConfig.apiKey}`,
|
||||
'binary',
|
||||
).toString('base64');
|
||||
|
||||
return {
|
||||
baseUrl: instanceConfig.baseUrl,
|
||||
headers: {
|
||||
Authorization: `Basic ${creds}`,
|
||||
},
|
||||
jobFullName,
|
||||
};
|
||||
}
|
||||
|
||||
private static getEntityAnnotationValue(entity: Entity) {
|
||||
return (
|
||||
entity.metadata.annotations?.[
|
||||
DefaultJenkinsInfoProvider.OLD_JENKINS_ANNOTATION
|
||||
] ||
|
||||
entity.metadata.annotations?.[
|
||||
DefaultJenkinsInfoProvider.NEW_JENKINS_ANNOTATION
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user