badges-backend: refactor to use badge definitions in config. #4457
Signed-off-by: Andreas Stenius <git@astekk.se>
This commit is contained in:
committed by
Fredrik Adelöw
parent
61078ba14d
commit
798002ee03
@@ -401,3 +401,8 @@ homepage:
|
||||
timezone: 'Asia/Tokyo'
|
||||
pagerduty:
|
||||
eventsBaseUrl: 'https://events.pagerduty.com/v2'
|
||||
badges:
|
||||
pingback:
|
||||
kind: 'entity'
|
||||
label: 'View $${entity.metadata.name} in'
|
||||
message: '$${app.title}'
|
||||
|
||||
@@ -20,6 +20,7 @@ import { PluginEnvironment } from '../types';
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
config,
|
||||
discovery,
|
||||
}: PluginEnvironment) {
|
||||
return await createRouter({ logger, config });
|
||||
return await createRouter({ logger, config, discovery });
|
||||
}
|
||||
|
||||
Vendored
+44
-3
@@ -15,7 +15,48 @@
|
||||
*/
|
||||
|
||||
export interface Config {
|
||||
/** Configuration options for the badges-backend plugin */
|
||||
/* badges?: {
|
||||
};*/
|
||||
/**
|
||||
* Define all badges.
|
||||
*
|
||||
* The `label` and `message` fields may use templating to support
|
||||
* dynamic content, based on context. Use same syntax as for
|
||||
* javascript template literals, but with double `$$`.
|
||||
*
|
||||
*/
|
||||
badges?: {
|
||||
[badgeId: string]: {
|
||||
/**
|
||||
* (Optional) Badge kind.
|
||||
*
|
||||
* Restrict badge usage to a certain kind only.
|
||||
* Useful when using templating for label and/or message if they
|
||||
* use context data only available for a certain kind of badge.
|
||||
*
|
||||
* Available badge kinds:
|
||||
*
|
||||
* * `entity` The entity data is available as `entity` in the template.
|
||||
*
|
||||
* Default context for all badges:
|
||||
*
|
||||
* * `app.title` As configured or defaults to "Backstage".
|
||||
*
|
||||
*/
|
||||
kind?: 'entity';
|
||||
|
||||
/**
|
||||
* The badge label.
|
||||
*/
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* The badge color. Default: `#36BAA2`.
|
||||
*/
|
||||
color?: string;
|
||||
|
||||
/**
|
||||
* The badge message.
|
||||
*/
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"@types/express": "^4.17.6",
|
||||
"badge-maker": "^3.3.0",
|
||||
"cors": "^2.8.5",
|
||||
"cross-fetch": "^3.0.6",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^3.0.3",
|
||||
"winston": "^3.2.1",
|
||||
|
||||
@@ -17,16 +17,54 @@
|
||||
import { Logger } from 'winston';
|
||||
import { makeBadge, ValidationError } from 'badge-maker';
|
||||
import {} from './types';
|
||||
import { interpolate } from '../utils';
|
||||
|
||||
export interface BadgeConfig {
|
||||
kind?: 'entity';
|
||||
label: string;
|
||||
color: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export class BadgesApi {
|
||||
constructor(private readonly logger: Logger) {}
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly config: { [id: string]: BadgeConfig },
|
||||
) {}
|
||||
|
||||
public getBadge(badgeKind: string, badgeId: string, context: any) {
|
||||
const badge = this.config[badgeId] || this.config.default;
|
||||
|
||||
if (!badge) {
|
||||
return makeBadge({
|
||||
label: 'Unknown badge ID',
|
||||
message: badgeId,
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
|
||||
if (badge.kind && badge.kind !== badgeKind) {
|
||||
return makeBadge({
|
||||
label: 'Invalid badge kind',
|
||||
message: `${badgeId} is for ${badge.kind} not ${badgeKind}`,
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
|
||||
public getPoweredByBadge() {
|
||||
const svg = makeBadge({
|
||||
label: 'Powered By',
|
||||
message: 'Backstage',
|
||||
color: '#36BAA2',
|
||||
label: this.render(badge.label, context),
|
||||
message: this.render(badge.message, context),
|
||||
color: badge.color || '#36BAA2',
|
||||
});
|
||||
|
||||
return svg;
|
||||
}
|
||||
|
||||
private render(template, context) {
|
||||
try {
|
||||
return interpolate(template.replace('$$', '$'), context);
|
||||
} catch (err) {
|
||||
return `${err} [${template}]`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,13 @@
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import fetch from 'cross-fetch';
|
||||
import Router from 'express-promise-router';
|
||||
import { Logger } from 'winston';
|
||||
import { errorHandler } from '@backstage/backend-common';
|
||||
import {
|
||||
errorHandler,
|
||||
PluginEndpointDiscovery,
|
||||
} from '@backstage/backend-common';
|
||||
import { Config } from '@backstage/config';
|
||||
import { BadgesApi } from '../api';
|
||||
|
||||
@@ -25,6 +29,7 @@ export interface RouterOptions {
|
||||
badgesApi?: BadgesApi;
|
||||
logger: Logger;
|
||||
config: Config;
|
||||
discovery: PluginEndpointDiscovery;
|
||||
}
|
||||
|
||||
export async function createRouter(
|
||||
@@ -33,13 +38,22 @@ export async function createRouter(
|
||||
const router = Router();
|
||||
|
||||
const logger = options.logger.child({ plugin: 'badges' });
|
||||
// const config = options.config.getConfig('badges');
|
||||
const badgesConfig = options.config.getOptional('badges') ?? {};
|
||||
const title = options.config.getString('app.title') || 'Backstage';
|
||||
const badgesApi = options.badgesApi || new BadgesApi(logger, badgesConfig);
|
||||
|
||||
const badgesApi = options.badgesApi || new BadgesApi(logger);
|
||||
router.get('/entity/:namespace/:kind/:name/:badgeId', async (req, res) => {
|
||||
const entity = await getEntity(logger, options.discovery, req.params);
|
||||
if (!entity) {
|
||||
res.status(400).send(`Unknown entity`);
|
||||
return;
|
||||
}
|
||||
|
||||
router.get('/entity/:namespace/:kind/:name', async (req, res) => {
|
||||
const { namespace, kind, name } = req.params;
|
||||
const badge = badgesApi.getPoweredByBadge();
|
||||
const { badgeId } = req.params;
|
||||
const badge = badgesApi.getBadge('entity', badgeId, {
|
||||
app: { title },
|
||||
entity,
|
||||
});
|
||||
|
||||
res.setHeader('Content-Type', 'image/svg+xml');
|
||||
res.status(200).send(badge);
|
||||
@@ -49,3 +63,20 @@ export async function createRouter(
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
async function getEntity(logger, discovery, params) {
|
||||
const catalogUrl = await discovery.getBaseUrl('catalog');
|
||||
const { kind, namespace, name } = params;
|
||||
|
||||
try {
|
||||
const entity = (await (
|
||||
await fetch(`${catalogUrl}/entities/by-name/${kind}/${namespace}/${name}`)
|
||||
).json()) as Entity;
|
||||
|
||||
return entity;
|
||||
} catch (err) {
|
||||
const msg = `Unable to get entity ${kind}/${namespace}/${name}, error ${err}`;
|
||||
logger.info(msg);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Logger } from 'winston';
|
||||
import {
|
||||
createServiceBuilder,
|
||||
loadBackendConfig,
|
||||
SingleHostDiscovery,
|
||||
} from '@backstage/backend-common';
|
||||
import { createRouter } from './router';
|
||||
|
||||
@@ -33,10 +34,11 @@ export async function startStandaloneServer(
|
||||
): Promise<Server> {
|
||||
const logger = options.logger.child({ service: 'badges-backend' });
|
||||
const config = await loadBackendConfig({ logger, argv: process.argv });
|
||||
const discovery = SingleHostDiscovery.fromConfig(config);
|
||||
|
||||
logger.debug('Creating application...');
|
||||
|
||||
const router = await createRouter({ logger, config });
|
||||
const router = await createRouter({ logger, config, discovery });
|
||||
|
||||
const service = createServiceBuilder(module)
|
||||
.enableCors({ origin: 'http://localhost:3000' })
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* adapted from https://stackoverflow.com/a/41015840/444060
|
||||
*/
|
||||
export function interpolate(template, params) {
|
||||
const names = Object.keys(params);
|
||||
const vals = Object.values(params);
|
||||
// eslint-disable-next-line no-new-func
|
||||
return new Function(...names, `return \`${template}\`;`)(...vals);
|
||||
}
|
||||
Reference in New Issue
Block a user