From b373ade3445dc32fc846391cb671f7714890bb05 Mon Sep 17 00:00:00 2001 From: josh Date: Sun, 4 Jun 2023 15:33:41 -0400 Subject: [PATCH] Add Nomad Backend plugin Signed-off-by: josh --- .github/CODEOWNERS | 1 + packages/backend/package.json | 1 + packages/backend/src/index.ts | 3 + packages/backend/src/plugins/nomad.ts | 32 ++++++++ plugins/nomad-backend/.eslintrc.js | 1 + plugins/nomad-backend/README.md | 14 ++++ plugins/nomad-backend/package.json | 69 ++++++++++++++++ plugins/nomad-backend/src/index.ts | 16 ++++ plugins/nomad-backend/src/run.ts | 32 ++++++++ .../nomad-backend/src/service/router.test.ts | 44 ++++++++++ plugins/nomad-backend/src/service/router.ts | 80 +++++++++++++++++++ .../src/service/standaloneServer.ts | 52 ++++++++++++ plugins/nomad-backend/src/setupTests.ts | 16 ++++ plugins/nomad/package.json | 24 +----- plugins/nomad/src/api.ts | 31 +++---- .../GroupList/GroupListForEntity.tsx | 3 +- plugins/nomad/src/plugin.ts | 8 +- yarn.lock | 21 +++++ 18 files changed, 402 insertions(+), 46 deletions(-) create mode 100644 packages/backend/src/plugins/nomad.ts create mode 100644 plugins/nomad-backend/.eslintrc.js create mode 100644 plugins/nomad-backend/README.md create mode 100644 plugins/nomad-backend/package.json create mode 100644 plugins/nomad-backend/src/index.ts create mode 100644 plugins/nomad-backend/src/run.ts create mode 100644 plugins/nomad-backend/src/service/router.test.ts create mode 100644 plugins/nomad-backend/src/service/router.ts create mode 100644 plugins/nomad-backend/src/service/standaloneServer.ts create mode 100644 plugins/nomad-backend/src/setupTests.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f7a7f99250..a3756e437c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -54,6 +54,7 @@ /plugins/gcalendar @backstage/maintainers @szubster @ptychu @kielosz @alexrybch /plugins/git-release-manager @backstage/maintainers @erikengervall /plugins/home @backstage/discoverability-maintainers +/plugins/home-* @backstage/discoverability-maintainers /plugins/ilert @backstage/maintainers @yacut /plugins/jenkins @backstage/maintainers @timja /plugins/jenkins-backend @backstage/maintainers @timja diff --git a/packages/backend/package.json b/packages/backend/package.json index 88937a9e6c..375f86ce00 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -54,6 +54,7 @@ "@backstage/plugin-kubernetes-backend": "workspace:^", "@backstage/plugin-lighthouse-backend": "workspace:^", "@backstage/plugin-linguist-backend": "workspace:^", + "@backstage/plugin-nomad-backend": "workspace:^", "@backstage/plugin-permission-backend": "workspace:^", "@backstage/plugin-permission-common": "workspace:^", "@backstage/plugin-permission-node": "workspace:^", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 5d22ec92e8..54f0f98e41 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -65,6 +65,7 @@ import adr from './plugins/adr'; import lighthouse from './plugins/lighthouse'; import linguist from './plugins/linguist'; import devTools from './plugins/devtools'; +import nomad from './plugins/nomad'; import { PluginEnvironment } from './types'; import { ServerPermissionClient } from '@backstage/plugin-permission-node'; import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; @@ -172,6 +173,7 @@ async function main() { const lighthouseEnv = useHotMemoize(module, () => createEnv('lighthouse')); const linguistEnv = useHotMemoize(module, () => createEnv('linguist')); const devToolsEnv = useHotMemoize(module, () => createEnv('devtools')); + const nomadEnv = useHotMemoize(module, () => createEnv('nomad')); const apiRouter = Router(); apiRouter.use('/catalog', await catalog(catalogEnv)); @@ -198,6 +200,7 @@ async function main() { apiRouter.use('/adr', await adr(adrEnv)); apiRouter.use('/linguist', await linguist(linguistEnv)); apiRouter.use('/devtools', await devTools(devToolsEnv)); + apiRouter.use('/nomad', await nomad(nomadEnv)); apiRouter.use(notFoundHandler()); await lighthouse(lighthouseEnv); diff --git a/packages/backend/src/plugins/nomad.ts b/packages/backend/src/plugins/nomad.ts new file mode 100644 index 0000000000..68e4344f98 --- /dev/null +++ b/packages/backend/src/plugins/nomad.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2023 The Backstage Authors + * + * 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. + */ +import { createRouter } from '@backstage/plugin-nomad-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + // Here is where you will add all of the required initialization code that + // your backend plugin needs to be able to start! + + // The env contains a lot of goodies, but our router currently only + // needs a logger + return await createRouter({ + logger: env.logger, + config: env.config, + }); +} diff --git a/plugins/nomad-backend/.eslintrc.js b/plugins/nomad-backend/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/nomad-backend/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/nomad-backend/README.md b/plugins/nomad-backend/README.md new file mode 100644 index 0000000000..1a670bd9a8 --- /dev/null +++ b/plugins/nomad-backend/README.md @@ -0,0 +1,14 @@ +# nomad + +Welcome to the nomad backend plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn +start` in the root directory, and then navigating to [/nomad-backend](http://localhost:3000/nomad-backend). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory. diff --git a/plugins/nomad-backend/package.json b/plugins/nomad-backend/package.json new file mode 100644 index 0000000000..0967c8ab65 --- /dev/null +++ b/plugins/nomad-backend/package.json @@ -0,0 +1,69 @@ +{ + "name": "@backstage/plugin-nomad-backend", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/errors": "workspace:^", + "@types/express": "*", + "express": "^4.17.1", + "express-promise-router": "^4.1.0", + "node-fetch": "^2.6.7", + "winston": "^3.2.1", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@types/supertest": "^2.0.8", + "msw": "^1.0.0", + "supertest": "^6.2.4" + }, + "files": [ + "dist" + ], + "configSchema": { + "$schema": "https://backstage.io/schema/config-v1", + "title": "@backstage/nomad", + "type": "object", + "properties": { + "nomad": { + "type": "object", + "properties": { + "addr": { + "type": "string", + "description": "The address of the Nomad API. See: https://developer.hashicorp.com/nomad/api-docs#addressing-and-ports", + "visibility": "backend" + }, + "token": { + "type": "string", + "description": "The token to call the Nomad API with. See: https://developer.hashicorp.com/nomad/api-docs#authentication", + "visibility": "secret" + } + }, + "required": [ + "addr" + ] + } + } + } +} diff --git a/plugins/nomad-backend/src/index.ts b/plugins/nomad-backend/src/index.ts new file mode 100644 index 0000000000..d2e8d61bad --- /dev/null +++ b/plugins/nomad-backend/src/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * 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. + */ +export * from './service/router'; diff --git a/plugins/nomad-backend/src/run.ts b/plugins/nomad-backend/src/run.ts new file mode 100644 index 0000000000..d299ed23e9 --- /dev/null +++ b/plugins/nomad-backend/src/run.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2023 The Backstage Authors + * + * 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. + */ +import { getRootLogger } from '@backstage/backend-common'; +import yn from 'yn'; +import { startStandaloneServer } from './service/standaloneServer'; + +const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007; +const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); +const logger = getRootLogger(); + +startStandaloneServer({ port, enableCors, logger }).catch(err => { + logger.error(err); + process.exit(1); +}); + +process.on('SIGINT', () => { + logger.info('CTRL+C pressed; exiting.'); + process.exit(0); +}); diff --git a/plugins/nomad-backend/src/service/router.test.ts b/plugins/nomad-backend/src/service/router.test.ts new file mode 100644 index 0000000000..c10dc8ba0e --- /dev/null +++ b/plugins/nomad-backend/src/service/router.test.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2023 The Backstage Authors + * + * 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. + */ +import { getVoidLogger } from '@backstage/backend-common'; +import express from 'express'; +import request from 'supertest'; + +import { createRouter } from './router'; + +describe('createRouter', () => { + let app: express.Express; + + beforeAll(async () => { + const router = await createRouter({ + logger: getVoidLogger(), + }); + app = express().use(router); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('GET /health', () => { + it('returns ok', async () => { + const response = await request(app).get('/health'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ status: 'ok' }); + }); + }); +}); diff --git a/plugins/nomad-backend/src/service/router.ts b/plugins/nomad-backend/src/service/router.ts new file mode 100644 index 0000000000..332fd004e1 --- /dev/null +++ b/plugins/nomad-backend/src/service/router.ts @@ -0,0 +1,80 @@ +/* + * Copyright 2023 The Backstage Authors + * + * 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. + */ +import { errorHandler, requestLoggingHandler } from '@backstage/backend-common'; +import { Config } from '@backstage/config'; +import { InputError } from '@backstage/errors'; +import express from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; + +export interface RouterOptions { + logger: Logger; + config: Config; +} + +export async function createRouter( + options: RouterOptions, +): Promise { + const { config, logger } = options; + + const router = Router(); + router.use(express.json()); + + router.get('/health', (_, response) => { + logger.info('PONG!'); + response.json({ status: 'ok' }); + }); + + router.get('/v1/allocations', async (req, resp) => { + const addr = config.getString('nomad.addr'); + const token = config.getOptionalString('nomad.token'); + + const namespace = (req.query.namespace as string) ?? ''; + if (!namespace || namespace === '') { + throw new InputError(`Missing "namespace" query parameter`); + } + + const filter = (req.query.filter as string) ?? ''; + if (!filter || filter === '') { + throw new InputError(`Missing "filter" query parameter`); + } + + logger.debug( + `/v1/allocations request headers: namespace=${namespace} filter=${filter}`, + ); + const allocationsResp = await fetch( + `${addr}/v1/allocations?namespace=${encodeURIComponent( + namespace, + )}&filter=${encodeURIComponent(filter)}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'X-Nomad-Token': token || '', + }, + }, + ); + + const allocationsBody = await allocationsResp.json(); + logger.debug(`/v1/allocations response: ${allocationsBody}`); + + resp.json(allocationsBody); + }); + + router.use(requestLoggingHandler()); + router.use(errorHandler()); + return router; +} diff --git a/plugins/nomad-backend/src/service/standaloneServer.ts b/plugins/nomad-backend/src/service/standaloneServer.ts new file mode 100644 index 0000000000..133577ffc2 --- /dev/null +++ b/plugins/nomad-backend/src/service/standaloneServer.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2023 The Backstage Authors + * + * 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. + */ +import { createServiceBuilder } from '@backstage/backend-common'; +import { Server } from 'http'; +import { Logger } from 'winston'; +import { createRouter } from './router'; +import { Config } from '@backstage/config'; + +export interface ServerOptions { + config: Config; + enableCors: boolean; + logger: Logger; + port: number; +} + +export async function startStandaloneServer( + options: ServerOptions, +): Promise { + const logger = options.logger.child({ service: 'nomad' }); + logger.debug('Starting application server...'); + const router = await createRouter({ + logger, + config: options.config, + }); + + let service = createServiceBuilder(module) + .setPort(options.port) + .addRouter('/nomad', router); + if (options.enableCors) { + service = service.enableCors({ origin: 'http://localhost:3000' }); + } + + return await service.start().catch(err => { + logger.error(err); + process.exit(1); + }); +} + +module.hot?.accept(); diff --git a/plugins/nomad-backend/src/setupTests.ts b/plugins/nomad-backend/src/setupTests.ts new file mode 100644 index 0000000000..4b9026cde5 --- /dev/null +++ b/plugins/nomad-backend/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * 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. + */ +export {}; diff --git a/plugins/nomad/package.json b/plugins/nomad/package.json index 18a343ebd2..79a0fb0607 100644 --- a/plugins/nomad/package.json +++ b/plugins/nomad/package.json @@ -51,27 +51,5 @@ }, "files": [ "dist" - ], - "configSchema": { - "$schema": "https://backstage.io/schema/config-v1", - "title": "@backstage/nomad", - "type": "object", - "properties": { - "nomad": { - "type": "object", - "properties": { - "addr": { - "type": "string", - "description": "The address of the Nomad API. See: https://developer.hashicorp.com/nomad/api-docs#addressing-and-ports", - "visibility": "frontend" - }, - "token": { - "type": "string", - "description": "The token to call the Nomad API with. See: https://developer.hashicorp.com/nomad/api-docs#authentication", - "visibility": "secret" - } - } - } - } - } + ] } diff --git a/plugins/nomad/src/api.ts b/plugins/nomad/src/api.ts index e9a967e2ae..6280bcbeda 100644 --- a/plugins/nomad/src/api.ts +++ b/plugins/nomad/src/api.ts @@ -14,8 +14,11 @@ * limitations under the License. */ -import { Config } from '@backstage/config'; -import { createApiRef } from '@backstage/core-plugin-api'; +import { + DiscoveryApi, + FetchApi, + createApiRef, +} from '@backstage/core-plugin-api'; /** @public */ export const nomadApiRef = createApiRef({ @@ -81,32 +84,22 @@ export class FetchError extends Error { /** @public */ export class NomadHttpApi implements NomadApi { - static fromConfig(config: Config) { - return new NomadHttpApi( - config.getOptionalString('nomad.addr'), - config.getOptionalString('nomad.token'), - ); + static new(discoveryApi: DiscoveryApi, fetchApi: FetchApi) { + return new NomadHttpApi(discoveryApi, fetchApi); } - constructor( - private addr: string = 'http://127.0.0.1:4646', - private token?: string, - ) {} + constructor(private discoveryApi: DiscoveryApi, private fetchApi: FetchApi) {} // TODO: pagination async listAllocations( options: ListAllocationsRequest, ): Promise { - const resp = await fetch( - `${this.addr}/v1/allocations?namespace=${encodeURIComponent( + const apiUrl = await this.discoveryApi.getBaseUrl('nomad'); + + const resp = await this.fetchApi.fetch( + `${apiUrl}/v1/allocations?namespace=${encodeURIComponent( options.namespace, )}&filter=${encodeURIComponent(options.filter)}`, - { - method: 'GET', - headers: { - 'X-Nomad-Token': this.token || '', - }, - }, ); if (!resp.ok) throw await FetchError.forResponse(resp); diff --git a/plugins/nomad/src/components/GroupList/GroupListForEntity.tsx b/plugins/nomad/src/components/GroupList/GroupListForEntity.tsx index d8af936569..ac11cb2665 100644 --- a/plugins/nomad/src/components/GroupList/GroupListForEntity.tsx +++ b/plugins/nomad/src/components/GroupList/GroupListForEntity.tsx @@ -44,6 +44,7 @@ export const GroupListForEntity = () => { return; } + // Issue call to nomad-backend try { const resp = await nomadApi.listAllocations({ namespace, @@ -58,7 +59,7 @@ export const GroupListForEntity = () => { if (response.loading) { return ; } - if (response.error || !response.value) { + if (response.error) { return null; } diff --git a/plugins/nomad/src/plugin.ts b/plugins/nomad/src/plugin.ts index 535b898723..1b45e803b4 100644 --- a/plugins/nomad/src/plugin.ts +++ b/plugins/nomad/src/plugin.ts @@ -14,7 +14,8 @@ * limitations under the License. */ import { - configApiRef, + discoveryApiRef, + fetchApiRef, createApiFactory, createPlugin, createRoutableExtension, @@ -33,8 +34,9 @@ export const nomadPlugin = createPlugin({ apis: [ createApiFactory({ api: nomadApiRef, - deps: { configApi: configApiRef }, - factory: ({ configApi }) => NomadHttpApi.fromConfig(configApi), + deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef }, + factory: ({ discoveryApi, fetchApi }) => + NomadHttpApi.new(discoveryApi, fetchApi), }), ], routes: { diff --git a/yarn.lock b/yarn.lock index 34eba288b3..b6183636aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7926,6 +7926,26 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-nomad-backend@workspace:^, @backstage/plugin-nomad-backend@workspace:plugins/nomad-backend": + version: 0.0.0-use.local + resolution: "@backstage/plugin-nomad-backend@workspace:plugins/nomad-backend" + dependencies: + "@backstage/backend-common": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/errors": "workspace:^" + "@types/express": "*" + "@types/supertest": ^2.0.8 + express: ^4.17.1 + express-promise-router: ^4.1.0 + msw: ^1.0.0 + node-fetch: ^2.6.7 + supertest: ^6.2.4 + winston: ^3.2.1 + yn: ^4.0.0 + languageName: unknown + linkType: soft + "@backstage/plugin-nomad@workspace:^, @backstage/plugin-nomad@workspace:plugins/nomad": version: 0.0.0-use.local resolution: "@backstage/plugin-nomad@workspace:plugins/nomad" @@ -24670,6 +24690,7 @@ __metadata: "@backstage/plugin-kubernetes-backend": "workspace:^" "@backstage/plugin-lighthouse-backend": "workspace:^" "@backstage/plugin-linguist-backend": "workspace:^" + "@backstage/plugin-nomad-backend": "workspace:^" "@backstage/plugin-permission-backend": "workspace:^" "@backstage/plugin-permission-common": "workspace:^" "@backstage/plugin-permission-node": "workspace:^"