Add the ability to view the catalog's spec from the Backstage interface.
Signed-off-by: Aramis <sennyeyaramis@gmail.com> Signed-off-by: Aramis Sennyey <aramiss@spotify.com>
This commit is contained in:
committed by
Aramis Sennyey
parent
d8b27e89ba
commit
785fb1ea75
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-openapi-spec': patch
|
||||
---
|
||||
|
||||
Adds a new catalog module for ingesting Backstage plugin OpenAPI specs into the catalog for display as an API entity.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-openapi-utils': patch
|
||||
---
|
||||
|
||||
Adds a new route, `/openapi.json` to validated routers for displaying their full OpenAPI spec in a standard endpoint.
|
||||
@@ -50,6 +50,7 @@ backend:
|
||||
allow:
|
||||
- host: example.com
|
||||
- host: '*.mozilla.org'
|
||||
- host: localhost:7007
|
||||
# workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir
|
||||
|
||||
# See README.md in the proxy-backend plugin for information on the configuration format
|
||||
@@ -464,3 +465,9 @@ stackstorm:
|
||||
|
||||
permission:
|
||||
enabled: true
|
||||
|
||||
openapi:
|
||||
plugins:
|
||||
- catalog
|
||||
- search
|
||||
- todo
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
"@backstage/plugin-azure-devops-backend": "workspace:^",
|
||||
"@backstage/plugin-badges-backend": "workspace:^",
|
||||
"@backstage/plugin-catalog-backend": "workspace:^",
|
||||
"@backstage/plugin-catalog-backend-module-openapi": "workspace:^",
|
||||
"@backstage/plugin-catalog-backend-module-openapi-spec": "workspace:^",
|
||||
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "workspace:^",
|
||||
"@backstage/plugin-catalog-backend-module-unprocessed": "workspace:^",
|
||||
"@backstage/plugin-devtools-backend": "workspace:^",
|
||||
|
||||
@@ -43,6 +43,7 @@ backend.add(import('@backstage/plugin-scaffolder-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend-module-catalog/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend-module-explore/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-openapi-spec'));
|
||||
backend.add(import('@backstage/plugin-search-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-techdocs-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-todo-backend'));
|
||||
|
||||
@@ -248,6 +248,9 @@ type FullMap<
|
||||
},
|
||||
> = RequiredMap<T> & OptionalMap<T>;
|
||||
|
||||
// @public
|
||||
export function getOpenApiSpecRoute(baseUrl: string): string;
|
||||
|
||||
// @public (undocumented)
|
||||
interface HeaderObject extends ParameterObject {
|
||||
// (undocumented)
|
||||
@@ -442,6 +445,9 @@ type ObjectWithContentSchema<
|
||||
? SchemaRef<Doc, Object['content']['application/json']['schema']>
|
||||
: never;
|
||||
|
||||
// @public
|
||||
export const OPENAPI_SPEC_ROUTE = '/openapi.json';
|
||||
|
||||
// @public (undocumented)
|
||||
type OptionalMap<
|
||||
T extends {
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/express-serve-static-core": "^4.17.5",
|
||||
@@ -45,6 +47,7 @@
|
||||
"express-promise-router": "^4.1.0",
|
||||
"json-schema-to-ts": "^2.6.2",
|
||||
"lodash": "^4.17.21",
|
||||
"openapi-merge": "^1.3.2",
|
||||
"openapi3-ts": "^3.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The route that all OpenAPI specs should be served from.
|
||||
* @public
|
||||
*/
|
||||
export const OPENAPI_SPEC_ROUTE = '/openapi.json';
|
||||
@@ -31,4 +31,5 @@ export type {
|
||||
PathParameters,
|
||||
} from './utility';
|
||||
export type { ApiRouter } from './router';
|
||||
export { createValidatedOpenApiRouter } from './stub';
|
||||
export { createValidatedOpenApiRouter, getOpenApiSpecRoute } from './stub';
|
||||
export * from './constants';
|
||||
|
||||
@@ -14,11 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createValidatedOpenApiRouter } from './stub';
|
||||
import { createValidatedOpenApiRouter, getOpenApiSpecRoute } from './stub';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
import singlePathSpec from './___fixtures__/single-path';
|
||||
import { Response } from './utility';
|
||||
import { OPENAPI_SPEC_ROUTE } from './constants';
|
||||
|
||||
describe('createRouter', () => {
|
||||
const pet: Response<typeof singlePathSpec, '/pet/:petId', 'get'> = {
|
||||
@@ -28,6 +29,33 @@ describe('createRouter', () => {
|
||||
photoUrls: [],
|
||||
};
|
||||
|
||||
const specs = [singlePathSpec];
|
||||
const ONCE_NESTED_ROUTER_PREFIX = '/pet-store';
|
||||
const TWICE_NESTED_ROUTER_PREFIX = `/api`;
|
||||
|
||||
const routers = specs.flatMap(spec => {
|
||||
const router = createValidatedOpenApiRouter(spec);
|
||||
const unnestedApp = express();
|
||||
unnestedApp.use('/', router);
|
||||
|
||||
const onceNestedRouter = express.Router();
|
||||
onceNestedRouter.use(`${ONCE_NESTED_ROUTER_PREFIX}`, router);
|
||||
const onceNestedApp = express();
|
||||
onceNestedApp.use(`/`, onceNestedRouter);
|
||||
|
||||
const twiceNestedApp = express();
|
||||
twiceNestedApp.use(`${TWICE_NESTED_ROUTER_PREFIX}`, onceNestedRouter);
|
||||
return [
|
||||
['', unnestedApp, router],
|
||||
[ONCE_NESTED_ROUTER_PREFIX, onceNestedApp, router],
|
||||
[
|
||||
`${TWICE_NESTED_ROUTER_PREFIX}${ONCE_NESTED_ROUTER_PREFIX}`,
|
||||
twiceNestedApp,
|
||||
router,
|
||||
],
|
||||
] as const;
|
||||
});
|
||||
|
||||
it('does NOT override originalUrl and basePath after execution', async () => {
|
||||
expect.assertions(2);
|
||||
const router = createValidatedOpenApiRouter(singlePathSpec);
|
||||
@@ -61,19 +89,40 @@ describe('createRouter', () => {
|
||||
expect(routerGetFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('handles coercing parameters correctly', async () => {
|
||||
expect.assertions(1);
|
||||
const router = createValidatedOpenApiRouter(singlePathSpec);
|
||||
router.get('/pet/:petId', (req, res) => {
|
||||
expect(typeof req.params.petId).toBe('integer');
|
||||
res.json(pet);
|
||||
});
|
||||
it.each(routers)(
|
||||
'%s handles coercing parameters correctly',
|
||||
async (prefix, app, router) => {
|
||||
expect.assertions(1);
|
||||
router.get('/pet/:petId', (req, res) => {
|
||||
expect(typeof req.params.petId).toBe('integer');
|
||||
res.send(pet);
|
||||
});
|
||||
|
||||
const apiRouter = express.Router();
|
||||
apiRouter.use('/pet-store', router);
|
||||
const appRouter = express();
|
||||
appRouter.use('/api', apiRouter);
|
||||
await request(app).get(`${prefix}/pet/1`);
|
||||
},
|
||||
);
|
||||
|
||||
await request(appRouter).get('/api/pet-store/pet/1');
|
||||
it.each(routers)(
|
||||
'%s adds the openapi spec to the router',
|
||||
async (prefix, app) => {
|
||||
const response = await request(app)
|
||||
.get(`${prefix}${OPENAPI_SPEC_ROUTE}`)
|
||||
.expect(200);
|
||||
expect(response.body).toHaveProperty('paths');
|
||||
Object.keys(response.body.paths).forEach(key => {
|
||||
const specKey = key.replace(prefix, '');
|
||||
expect(singlePathSpec.paths).toHaveProperty(specKey);
|
||||
expect(response.body.paths[key]).toEqual(
|
||||
(singlePathSpec.paths as any)[specKey],
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('getOpenApiSpecRoute', () => {
|
||||
it('handles expected values', () => {
|
||||
expect(getOpenApiSpecRoute('/api/test')).toEqual('/api/test/openapi.json');
|
||||
expect(getOpenApiSpecRoute('api/test')).toEqual('api/test/openapi.json');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
} from 'express';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { middleware as OpenApiValidator } from 'express-openapi-validator';
|
||||
import { OPENAPI_SPEC_ROUTE } from './constants';
|
||||
import { isErrorResult, merge } from 'openapi-merge';
|
||||
|
||||
type PropertyOverrideRequest = Request & {
|
||||
[key: symbol]: string;
|
||||
@@ -45,6 +47,16 @@ export function getDefaultRouterMiddleware() {
|
||||
return [json()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a base url for a plugin, find the given OpenAPI spec for that plugin.
|
||||
* @param baseUrl - Plugin base url.
|
||||
* @returns OpenAPI spec route for the base url.
|
||||
* @public
|
||||
*/
|
||||
export function getOpenApiSpecRoute(baseUrl: string) {
|
||||
return `${baseUrl}${OPENAPI_SPEC_ROUTE}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new OpenAPI router with some default middleware.
|
||||
* @param spec - Your OpenAPI spec imported as a JSON object.
|
||||
@@ -59,7 +71,7 @@ export function createValidatedOpenApiRouter<T extends RequiredDoc>(
|
||||
middleware?: RequestHandler[];
|
||||
},
|
||||
) {
|
||||
const router = PromiseRouter() as ApiRouter<typeof spec>;
|
||||
const router = PromiseRouter();
|
||||
router.use(options?.middleware || getDefaultRouterMiddleware());
|
||||
|
||||
/**
|
||||
@@ -116,5 +128,30 @@ export function createValidatedOpenApiRouter<T extends RequiredDoc>(
|
||||
// Any errors from the middleware get through here.
|
||||
router.use(validatorErrorTransformer());
|
||||
|
||||
return router;
|
||||
router.get(OPENAPI_SPEC_ROUTE, async (req, res) => {
|
||||
const mergeOutput = merge([
|
||||
{
|
||||
oas: spec as any,
|
||||
pathModification: {
|
||||
/**
|
||||
* Get the route that this OpenAPI spec is hosted on. The other
|
||||
* approach of using the discovery API increases the router constructor
|
||||
* significantly and since we're just looking for path and not full URL,
|
||||
* this works.
|
||||
*
|
||||
* If we wanted to add a list of servers, there may be a case for adding
|
||||
* discovery API to get an exhaustive list of upstream servers, but that's
|
||||
* also not currently supported.
|
||||
*/
|
||||
prepend: req.originalUrl.replace(OPENAPI_SPEC_ROUTE, ''),
|
||||
},
|
||||
},
|
||||
]);
|
||||
if (isErrorResult(mergeOutput)) {
|
||||
throw new InputError('Invalid spec defined');
|
||||
}
|
||||
res.json(mergeOutput.output);
|
||||
});
|
||||
|
||||
return router as ApiRouter<typeof spec>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,30 @@
|
||||
# catalog-backend-module-openapi-spec
|
||||
|
||||
## Summary
|
||||
|
||||
This module installs an entity provider that exports a single entity, your Backstage instance documentation, which merges as many backend plugins as you have defined in the config value `openapi.plugins`.
|
||||
|
||||
## Notes
|
||||
|
||||
- This only works with the new backend system.
|
||||
- This requires populating a new config value `openapi.plugins` with an array of plugin IDs. The entity processor expo
|
||||
|
||||
## Installation
|
||||
|
||||
To your new backend file, add
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-openapi-spec'));
|
||||
```
|
||||
|
||||
Add a list of plugins to your config like,
|
||||
|
||||
```yaml title="app-config.yaml"
|
||||
openapi:
|
||||
plugins:
|
||||
- catalog
|
||||
- todo
|
||||
- search
|
||||
```
|
||||
|
||||
We will attempt to load each plugin's OpenAPI spec hosted at `${pluginRoute}/openapi.json`. These are automatically added if you are using `@backstage/backend-openapi-utils`'s `createValidatedOpenApiRouter`.
|
||||
@@ -0,0 +1,20 @@
|
||||
## API Report File for "@backstage/plugin-catalog-backend-module-openapi-spec"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { BackendFeature } from '@backstage/backend-plugin-api';
|
||||
|
||||
// @public (undocumented)
|
||||
export const catalogModuleInternalOpenApiSpec: () => BackendFeature;
|
||||
|
||||
// @public (undocumented)
|
||||
export type MetaApiDocsPluginOptions = {
|
||||
exampleOption: boolean;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export const metaOpenApiDocsPluginId = 'meta-api-docs';
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "@backstage/plugin-catalog-backend-module-openapi-spec",
|
||||
"version": "0.0.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "backend-plugin-module"
|
||||
},
|
||||
"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/backend-openapi-utils": "workspace:^",
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/backend-tasks": "workspace:^",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/plugin-catalog-node": "workspace:^",
|
||||
"@types/express": "*",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"openapi-merge": "^1.3.2",
|
||||
"uuid": "^9.0.0",
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/catalog-model": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/test-utils": "workspace:^",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"msw": "^1.0.0",
|
||||
"openapi3-ts": "^3.1.2",
|
||||
"supertest": "^6.2.4"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
* 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 type { ApiEntity } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { ForwardedError } from '@backstage/errors';
|
||||
import {
|
||||
EntityProvider,
|
||||
EntityProviderConnection,
|
||||
} from '@backstage/plugin-catalog-node';
|
||||
import { merge, isErrorResult } from 'openapi-merge';
|
||||
import { getOpenApiSpecRoute } from '@backstage/backend-openapi-utils';
|
||||
import type {
|
||||
OpenAPIObject,
|
||||
OperationObject,
|
||||
PathItemObject,
|
||||
} from 'openapi3-ts';
|
||||
import fetch from 'cross-fetch';
|
||||
import { DiscoveryService, LoggerService } from '@backstage/backend-plugin-api';
|
||||
import * as uuid from 'uuid';
|
||||
import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks';
|
||||
|
||||
const HTTP_VERBS: (keyof PathItemObject)[] = [
|
||||
'get',
|
||||
'post',
|
||||
'put',
|
||||
'delete',
|
||||
'patch',
|
||||
'trace',
|
||||
'options',
|
||||
'head',
|
||||
];
|
||||
|
||||
const addTagsToSpec = (spec: OpenAPIObject, tag: string) => {
|
||||
Object.values(spec?.paths).forEach((path: PathItemObject) => {
|
||||
HTTP_VERBS.forEach(verb => {
|
||||
if (verb in path) {
|
||||
if (!('tags' in path[verb])) {
|
||||
(path[verb] as OperationObject).tags = [];
|
||||
}
|
||||
if (!(path[verb] as OperationObject).tags?.includes(tag)) {
|
||||
(path[verb] as OperationObject).tags?.push(tag);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const mergeSpecs = async ({
|
||||
baseUrl,
|
||||
specs,
|
||||
}: {
|
||||
baseUrl: string;
|
||||
specs: OpenAPIObject[];
|
||||
}) => {
|
||||
const mergeResult = merge([
|
||||
// Add the full API information as the first item for other items to merge against it with.
|
||||
{
|
||||
oas: {
|
||||
openapi: '3.0.3',
|
||||
info: {
|
||||
title: 'Backstage API',
|
||||
version: '1',
|
||||
},
|
||||
servers: [{ url: baseUrl }],
|
||||
paths: {},
|
||||
},
|
||||
},
|
||||
// For each plugin, load its spec and the known endpoint that it sits under.
|
||||
...specs.map(
|
||||
spec =>
|
||||
({
|
||||
oas: spec,
|
||||
// Weird typing differences between this package and the client package's openapi 3.
|
||||
} as any),
|
||||
),
|
||||
]);
|
||||
|
||||
if (isErrorResult(mergeResult)) {
|
||||
throw new ForwardedError(
|
||||
`${mergeResult.message} (${mergeResult.type})`,
|
||||
mergeResult,
|
||||
);
|
||||
} else {
|
||||
return mergeResult.output;
|
||||
}
|
||||
};
|
||||
|
||||
const loadSpecs = async ({
|
||||
baseUrl,
|
||||
discovery,
|
||||
plugins,
|
||||
logger,
|
||||
}: {
|
||||
baseUrl: string;
|
||||
plugins: string[];
|
||||
discovery: DiscoveryService;
|
||||
logger: LoggerService;
|
||||
}) => {
|
||||
const specs: OpenAPIObject[] = [];
|
||||
for (const pluginId of plugins) {
|
||||
const url = await discovery.getExternalBaseUrl(pluginId);
|
||||
const openApiUrl = getOpenApiSpecRoute(url);
|
||||
const response = await fetch(openApiUrl);
|
||||
if (response.ok) {
|
||||
const spec = await response.json();
|
||||
addTagsToSpec(spec, pluginId);
|
||||
specs.push(spec);
|
||||
} else if (response.status === 404) {
|
||||
logger.error(
|
||||
`Plugin=${pluginId} does not have an OpenAPI spec at '${openApiUrl}'.`,
|
||||
);
|
||||
} else {
|
||||
logger.error(
|
||||
`Failed to load spec for plugin=${pluginId} at ${openApiUrl}. Error (${
|
||||
response.status
|
||||
}): ${response.body ? await response.text() : response.statusText}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return mergeSpecs({ baseUrl, specs });
|
||||
};
|
||||
|
||||
export class InternalOpenApiDocumentationProvider implements EntityProvider {
|
||||
private connection?: EntityProviderConnection;
|
||||
private readonly scheduleFn: () => Promise<void>;
|
||||
constructor(
|
||||
public readonly config: Config,
|
||||
public readonly discovery: DiscoveryService,
|
||||
public readonly logger: LoggerService,
|
||||
taskRunner: TaskRunner,
|
||||
) {
|
||||
this.scheduleFn = this.createScheduleFn(taskRunner);
|
||||
}
|
||||
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
options: {
|
||||
discovery: DiscoveryService;
|
||||
logger: LoggerService;
|
||||
schedule: PluginTaskScheduler;
|
||||
},
|
||||
) {
|
||||
const taskRunner = options.schedule.createScheduledTaskRunner({
|
||||
frequency: {
|
||||
minutes: 1,
|
||||
},
|
||||
timeout: {
|
||||
minutes: 1,
|
||||
},
|
||||
});
|
||||
return new InternalOpenApiDocumentationProvider(
|
||||
config,
|
||||
options.discovery,
|
||||
options.logger,
|
||||
taskRunner,
|
||||
);
|
||||
}
|
||||
/** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.getProviderName} */
|
||||
getProviderName() {
|
||||
return `InternalOpenApiDocumentationProvider`;
|
||||
}
|
||||
|
||||
/** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.connect} */
|
||||
async connect(connection: EntityProviderConnection) {
|
||||
this.connection = connection;
|
||||
return await this.scheduleFn();
|
||||
}
|
||||
|
||||
private createScheduleFn(taskRunner: TaskRunner): () => Promise<void> {
|
||||
return async () => {
|
||||
const taskId = `${this.getProviderName()}:refresh`;
|
||||
return taskRunner.run({
|
||||
id: taskId,
|
||||
fn: async () => {
|
||||
const logger = this.logger.child({
|
||||
class:
|
||||
InternalOpenApiDocumentationProvider.prototype.constructor.name,
|
||||
taskId,
|
||||
taskInstanceId: uuid.v4(),
|
||||
});
|
||||
try {
|
||||
await this.refresh(logger);
|
||||
} catch (error) {
|
||||
logger.error(`${this.getProviderName()} refresh failed`, error);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
async refresh(logger: LoggerService) {
|
||||
const pluginsToMerge = this.config.getStringArray('openapi.plugins');
|
||||
logger.info(`Loading specs from from ${pluginsToMerge}.`);
|
||||
const documentationEntity: ApiEntity = {
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'API',
|
||||
metadata: {
|
||||
name: 'INTERNAL_instance_openapi_doc',
|
||||
title: 'Your Backstage Instance documentation',
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'internal-package:@backstage/plugin-catalog-backend-module-openapi-spec',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'internal-package:@backstage/plugin-catalog-backend-module-openapi-spec',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
type: 'openapi',
|
||||
lifecycle: 'production',
|
||||
owner: 'backstage',
|
||||
definition: JSON.stringify(
|
||||
await loadSpecs({
|
||||
baseUrl: this.config.getString('backend.baseUrl'),
|
||||
discovery: this.discovery,
|
||||
plugins: pluginsToMerge,
|
||||
logger,
|
||||
}),
|
||||
),
|
||||
},
|
||||
};
|
||||
await this.connection?.applyMutation({
|
||||
type: 'full',
|
||||
entities: [
|
||||
{
|
||||
entity: documentationEntity,
|
||||
locationKey: 'internal-api-doc',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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 {
|
||||
coreServices,
|
||||
createBackendModule,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
|
||||
import { InternalOpenApiDocumentationProvider } from './InternalOpenApiDocumentationProvider';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type MetaApiDocsPluginOptions = { exampleOption: boolean };
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const metaOpenApiDocsPluginId = 'meta-api-docs';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const catalogModuleInternalOpenApiSpec = createBackendModule({
|
||||
moduleId: metaOpenApiDocsPluginId,
|
||||
pluginId: 'catalog',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
catalog: catalogProcessingExtensionPoint,
|
||||
config: coreServices.rootConfig,
|
||||
discovery: coreServices.discovery,
|
||||
scheduler: coreServices.scheduler,
|
||||
logger: coreServices.logger,
|
||||
},
|
||||
async init({ catalog, config, discovery, scheduler, logger }) {
|
||||
console.log('testing');
|
||||
catalog.addEntityProvider(
|
||||
InternalOpenApiDocumentationProvider.fromConfig(config, {
|
||||
discovery,
|
||||
schedule: scheduler,
|
||||
logger,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default catalogModuleInternalOpenApiSpec;
|
||||
@@ -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 {};
|
||||
@@ -3544,7 +3544,9 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/backend-openapi-utils@workspace:packages/backend-openapi-utils"
|
||||
dependencies:
|
||||
"@backstage/backend-plugin-api": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/config": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@types/express": ^4.17.6
|
||||
"@types/express-serve-static-core": ^4.17.5
|
||||
@@ -3553,6 +3555,7 @@ __metadata:
|
||||
express-promise-router: ^4.1.0
|
||||
json-schema-to-ts: ^2.6.2
|
||||
lodash: ^4.17.21
|
||||
openapi-merge: ^1.3.2
|
||||
openapi3-ts: ^3.1.2
|
||||
supertest: ^6.1.3
|
||||
languageName: unknown
|
||||
@@ -5633,7 +5636,36 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/plugin-catalog-backend-module-openapi@workspace:plugins/catalog-backend-module-openapi":
|
||||
"@backstage/plugin-catalog-backend-module-openapi-spec@workspace:^, @backstage/plugin-catalog-backend-module-openapi-spec@workspace:plugins/catalog-backend-module-openapi-spec":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-catalog-backend-module-openapi-spec@workspace:plugins/catalog-backend-module-openapi-spec"
|
||||
dependencies:
|
||||
"@backstage/backend-common": "workspace:^"
|
||||
"@backstage/backend-openapi-utils": "workspace:^"
|
||||
"@backstage/backend-plugin-api": "workspace:^"
|
||||
"@backstage/backend-tasks": "workspace:^"
|
||||
"@backstage/catalog-model": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/config": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@backstage/plugin-catalog-node": "workspace:^"
|
||||
"@backstage/test-utils": "workspace:^"
|
||||
"@types/express": "*"
|
||||
"@types/supertest": ^2.0.8
|
||||
cross-fetch: ^3.1.5
|
||||
express: ^4.17.1
|
||||
express-promise-router: ^4.1.0
|
||||
lodash: ^4.17.21
|
||||
msw: ^1.0.0
|
||||
openapi-merge: ^1.3.2
|
||||
openapi3-ts: ^3.1.2
|
||||
supertest: ^6.2.4
|
||||
uuid: ^9.0.0
|
||||
yn: ^4.0.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/plugin-catalog-backend-module-openapi@workspace:^, @backstage/plugin-catalog-backend-module-openapi@workspace:plugins/catalog-backend-module-openapi":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-catalog-backend-module-openapi@workspace:plugins/catalog-backend-module-openapi"
|
||||
dependencies:
|
||||
@@ -20154,6 +20186,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"atlassian-openapi@npm:^1.0.8":
|
||||
version: 1.0.17
|
||||
resolution: "atlassian-openapi@npm:1.0.17"
|
||||
dependencies:
|
||||
jsonpointer: ^5.0.0
|
||||
urijs: ^1.19.10
|
||||
checksum: 372a454e8f5e000e016e261b2151019f0b3dabfad908593c71aae23ebd5b9998c374ae3c0bf0d85dd55dbcca94d5d93e38edf02346d0bd2cb34d3fd20968ddaf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"atomic-sleep@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "atomic-sleep@npm:1.0.0"
|
||||
@@ -25645,6 +25687,8 @@ __metadata:
|
||||
"@backstage/plugin-azure-devops-backend": "workspace:^"
|
||||
"@backstage/plugin-badges-backend": "workspace:^"
|
||||
"@backstage/plugin-catalog-backend": "workspace:^"
|
||||
"@backstage/plugin-catalog-backend-module-openapi": "workspace:^"
|
||||
"@backstage/plugin-catalog-backend-module-openapi-spec": "workspace:^"
|
||||
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "workspace:^"
|
||||
"@backstage/plugin-catalog-backend-module-unprocessed": "workspace:^"
|
||||
"@backstage/plugin-devtools-backend": "workspace:^"
|
||||
@@ -34240,6 +34284,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"openapi-merge@npm:^1.3.2":
|
||||
version: 1.3.2
|
||||
resolution: "openapi-merge@npm:1.3.2"
|
||||
dependencies:
|
||||
atlassian-openapi: ^1.0.8
|
||||
lodash: ^4.17.15
|
||||
ts-is-present: ^1.1.1
|
||||
checksum: 53284a563270177422db8c7536544913c133dfc5cc7058a1043f3092b5aa997b8224a83c59569d18620f94ccf0a014fcb735e22941a9259b2c60861002f01638
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"openapi-sampler@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "openapi-sampler@npm:1.2.1"
|
||||
@@ -40922,6 +40977,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ts-is-present@npm:^1.1.1":
|
||||
version: 1.2.2
|
||||
resolution: "ts-is-present@npm:1.2.2"
|
||||
checksum: 3620ecf48219d0dd108e493260a207f4733d8e39a18dffec23c7ed2b1ef2aba7158d0dfafe36f3f27d0092472535a5e474ce04ade54e972e64b2b6329d20ab0b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ts-log@npm:^2.2.3":
|
||||
version: 2.2.5
|
||||
resolution: "ts-log@npm:2.2.5"
|
||||
@@ -41674,7 +41736,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"urijs@npm:^1.19.11":
|
||||
"urijs@npm:^1.19.10, urijs@npm:^1.19.11":
|
||||
version: 1.19.11
|
||||
resolution: "urijs@npm:1.19.11"
|
||||
checksum: f9b95004560754d30fd7dbee44b47414d662dc9863f1cf5632a7c7983648df11d23c0be73b9b4f9554463b61d5b0a520b70df9e1ee963ebb4af02e6da2cc80f3
|
||||
|
||||
Reference in New Issue
Block a user