badges-backend: refactor to use badge definitions in config. #4457

Signed-off-by: Andreas Stenius <git@astekk.se>
This commit is contained in:
Andreas Stenius
2021-02-19 17:33:32 +01:00
committed by Fredrik Adelöw
parent 61078ba14d
commit 798002ee03
8 changed files with 160 additions and 16 deletions
+44 -3
View File
@@ -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;
};
};
}
+1
View File
@@ -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",
+43 -5
View File
@@ -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}]`;
}
}
}
+37 -6
View File
@@ -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' })
+25
View File
@@ -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);
}