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:
Oliver Sand
2021-08-13 12:05:49 +02:00
committed by GitHub
5 changed files with 330 additions and 142 deletions
+25
View File
@@ -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)
+10 -3
View File
@@ -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
]
);
}
}