catalog-backend: add validate-entity endpoint
Signed-off-by: Bret Hubbard <hubbard.bret@gmail.com>
This commit is contained in:
@@ -438,6 +438,7 @@ export class CatalogBuilder {
|
||||
entitiesCatalog,
|
||||
locationAnalyzer,
|
||||
locationService,
|
||||
orchestrator,
|
||||
refreshService,
|
||||
logger,
|
||||
config,
|
||||
|
||||
@@ -18,7 +18,11 @@ import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import type { Location } from '@backstage/catalog-client';
|
||||
import type { Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
ANNOTATION_ORIGIN_LOCATION,
|
||||
ANNOTATION_SOURCE_LOCATION,
|
||||
Entity,
|
||||
} from '@backstage/catalog-model';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
import { EntitiesCatalog } from '../catalog/types';
|
||||
@@ -31,10 +35,12 @@ import {
|
||||
createPermissionRule,
|
||||
} from '@backstage/plugin-permission-node';
|
||||
import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common';
|
||||
import { CatalogProcessingOrchestrator } from '../processing/types';
|
||||
|
||||
describe('createRouter readonly disabled', () => {
|
||||
let entitiesCatalog: jest.Mocked<EntitiesCatalog>;
|
||||
let locationService: jest.Mocked<LocationService>;
|
||||
let orchestrator: jest.Mocked<CatalogProcessingOrchestrator>;
|
||||
let app: express.Express;
|
||||
let refreshService: RefreshService;
|
||||
|
||||
@@ -52,9 +58,11 @@ describe('createRouter readonly disabled', () => {
|
||||
deleteLocation: jest.fn(),
|
||||
};
|
||||
refreshService = { refresh: jest.fn() };
|
||||
orchestrator = { process: jest.fn() };
|
||||
const router = await createRouter({
|
||||
entitiesCatalog,
|
||||
locationService,
|
||||
orchestrator,
|
||||
logger: getVoidLogger(),
|
||||
refreshService,
|
||||
config: new ConfigReader(undefined),
|
||||
@@ -386,6 +394,83 @@ describe('createRouter readonly disabled', () => {
|
||||
expect(response.status).toEqual(204);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /validate-entity', () => {
|
||||
describe('valid entity', () => {
|
||||
it('returns 200', async () => {
|
||||
const entity: Entity = {
|
||||
apiVersion: 'a',
|
||||
kind: 'b',
|
||||
metadata: { name: 'n' },
|
||||
};
|
||||
|
||||
orchestrator.process.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
state: {},
|
||||
completedEntity: entity,
|
||||
deferredEntities: [],
|
||||
relations: [],
|
||||
errors: [],
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post('/validate-entity')
|
||||
.send(entity);
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(orchestrator.process).toHaveBeenCalledTimes(1);
|
||||
expect(orchestrator.process).toHaveBeenCalledWith({
|
||||
entity: {
|
||||
apiVersion: 'a',
|
||||
kind: 'b',
|
||||
metadata: {
|
||||
name: 'n',
|
||||
annotations: {
|
||||
[ANNOTATION_SOURCE_LOCATION]: 'validate:entity',
|
||||
[ANNOTATION_ORIGIN_LOCATION]: 'validate:entity',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid entity', () => {
|
||||
it('returns 422', async () => {
|
||||
const entity: Entity = {
|
||||
apiVersion: 'a',
|
||||
kind: 'b',
|
||||
metadata: { name: 'invalid*name' },
|
||||
};
|
||||
|
||||
orchestrator.process.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
errors: [new Error('Invalid entity name')],
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post('/validate-entity')
|
||||
.send(entity);
|
||||
|
||||
expect(response.status).toEqual(422);
|
||||
expect(response.body).toEqual(['Invalid entity name']);
|
||||
expect(orchestrator.process).toHaveBeenCalledTimes(1);
|
||||
expect(orchestrator.process).toHaveBeenCalledWith({
|
||||
entity: {
|
||||
apiVersion: 'a',
|
||||
kind: 'b',
|
||||
metadata: {
|
||||
name: 'invalid*name',
|
||||
annotations: {
|
||||
[ANNOTATION_SOURCE_LOCATION]: 'validate:entity',
|
||||
[ANNOTATION_ORIGIN_LOCATION]: 'validate:entity',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createRouter readonly enabled', () => {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { errorHandler } from '@backstage/backend-common';
|
||||
import { stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import express from 'express';
|
||||
@@ -34,10 +34,13 @@ import {
|
||||
disallowReadonlyMode,
|
||||
locationInput,
|
||||
validateRequestBody,
|
||||
requireRequestBody,
|
||||
setRequiredEntityLocationAnnotations,
|
||||
} from './util';
|
||||
import { RefreshOptions, LocationService, RefreshService } from './types';
|
||||
import { z } from 'zod';
|
||||
import { parseEntityFacetParams } from './request/parseEntityFacetParams';
|
||||
import { RefreshOptions, LocationService, RefreshService } from './types';
|
||||
import { CatalogProcessingOrchestrator } from '../processing/types';
|
||||
|
||||
/**
|
||||
* Options used by {@link createRouter}.
|
||||
@@ -48,6 +51,7 @@ export interface RouterOptions {
|
||||
entitiesCatalog?: EntitiesCatalog;
|
||||
locationAnalyzer?: LocationAnalyzer;
|
||||
locationService: LocationService;
|
||||
orchestrator?: CatalogProcessingOrchestrator;
|
||||
refreshService?: RefreshService;
|
||||
logger: Logger;
|
||||
config: Config;
|
||||
@@ -66,12 +70,12 @@ export async function createRouter(
|
||||
entitiesCatalog,
|
||||
locationAnalyzer,
|
||||
locationService,
|
||||
orchestrator,
|
||||
refreshService,
|
||||
config,
|
||||
logger,
|
||||
permissionIntegrationRouter,
|
||||
} = options;
|
||||
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
|
||||
@@ -228,6 +232,21 @@ export async function createRouter(
|
||||
});
|
||||
}
|
||||
|
||||
if (orchestrator) {
|
||||
router.post('/validate-entity', async (req, res) => {
|
||||
const input = (await requireRequestBody(req)) as Entity;
|
||||
const entity = setRequiredEntityLocationAnnotations(input);
|
||||
const processingResult = await orchestrator.process({
|
||||
entity,
|
||||
});
|
||||
if (!processingResult.ok)
|
||||
return res
|
||||
.status(422)
|
||||
.json(processingResult.errors.map(e => e.message));
|
||||
return res.status(200).send();
|
||||
});
|
||||
}
|
||||
|
||||
router.use(errorHandler());
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Entity,
|
||||
ANNOTATION_SOURCE_LOCATION,
|
||||
ANNOTATION_ORIGIN_LOCATION,
|
||||
} from '@backstage/catalog-model';
|
||||
import { InputError, NotAllowedError } from '@backstage/errors';
|
||||
import { Request } from 'express';
|
||||
import lodash from 'lodash';
|
||||
@@ -48,6 +53,22 @@ export const locationInput = z
|
||||
})
|
||||
.strict(); // no unknown keys;
|
||||
|
||||
export function setRequiredEntityLocationAnnotations(entity: Entity): Entity {
|
||||
const clonedEntity = lodash.cloneDeep(entity);
|
||||
lodash.set(
|
||||
clonedEntity,
|
||||
`metadata.annotations.['${ANNOTATION_SOURCE_LOCATION}']`,
|
||||
'validate:entity',
|
||||
);
|
||||
lodash.set(
|
||||
clonedEntity,
|
||||
`metadata.annotations.['${ANNOTATION_ORIGIN_LOCATION}']`,
|
||||
'validate:entity',
|
||||
);
|
||||
|
||||
return clonedEntity;
|
||||
}
|
||||
|
||||
export async function validateRequestBody<T>(
|
||||
req: Request,
|
||||
schema: z.Schema<T>,
|
||||
|
||||
Reference in New Issue
Block a user