Allow unknown typed location from being registered via the location service by configuration settings

Signed-off-by: Stéphane MORI <stephane.mori@gmail.com>
This commit is contained in:
Stéphane MORI
2022-08-08 15:06:01 +02:00
parent 87608db69a
commit dd395335bc
5 changed files with 146 additions and 4 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Allow unknown typed location from being registered via the location service by configuration settings
+17
View File
@@ -54,6 +54,23 @@ export interface Config {
*/
readonly?: boolean;
/**
* Allow unknown type when create location.
*
* Setting 'locationService.create.allowUnknownType=false' forbids to create
* location with unknown type through location service.
* This is the default value.
*
* Setting 'locationService.create.allowUnknownType=true' allows to create
* location with unknown type through location service.
*
*/
locationService?: {
create?: {
allowUnknownType: boolean;
};
};
/**
* A set of static locations that the catalog shall always keep itself
* up-to-date with. This is commonly used for large, permanent integrations
@@ -465,7 +465,7 @@ export class CatalogBuilder {
const locationAnalyzer =
this.locationAnalyzer ?? new RepoLocationAnalyzer(logger, integrations);
const locationService = new AuthorizedLocationService(
new DefaultLocationService(locationStore, orchestrator),
new DefaultLocationService(locationStore, orchestrator, config),
permissionEvaluator,
);
const refreshService = new AuthorizedRefreshService(
@@ -18,6 +18,7 @@ import { DefaultLocationService } from './DefaultLocationService';
import { CatalogProcessingOrchestrator } from '../processing/types';
import { LocationStore } from './types';
import { InputError } from '@backstage/errors';
import { ConfigReader } from '@backstage/config';
describe('DefaultLocationServiceTest', () => {
const orchestrator: jest.Mocked<CatalogProcessingOrchestrator> = {
@@ -29,7 +30,17 @@ describe('DefaultLocationServiceTest', () => {
listLocations: jest.fn(),
getLocation: jest.fn(),
};
const locationService = new DefaultLocationService(store, orchestrator);
const mockConfig = (allowUnknownType: boolean) =>
new ConfigReader({
catalog: {
locationService: {
create: {
allowUnknownType,
},
},
},
});
beforeEach(() => {
jest.resetAllMocks();
@@ -81,6 +92,11 @@ describe('DefaultLocationServiceTest', () => {
errors: [],
});
const locationService = new DefaultLocationService(
store,
orchestrator,
mockConfig(false),
);
await locationService.createLocation(
{ type: 'url', target: 'https://backstage.io/catalog-info.yaml' },
true,
@@ -144,6 +160,12 @@ describe('DefaultLocationServiceTest', () => {
store.listLocations.mockResolvedValueOnce([
{ id: '137', ...locationSpec },
]);
const locationService = new DefaultLocationService(
store,
orchestrator,
mockConfig(false),
);
const result = await locationService.createLocation(
{ type: 'url', target: 'https://backstage.io/catalog-info.yaml' },
true,
@@ -197,6 +219,11 @@ describe('DefaultLocationServiceTest', () => {
errors: [],
});
const locationService = new DefaultLocationService(
store,
orchestrator,
mockConfig(false),
);
await expect(
locationService.createLocation(
{ type: 'url', target: 'https://backstage.io/catalog-info.yaml' },
@@ -225,6 +252,12 @@ describe('DefaultLocationServiceTest', () => {
store.listLocations.mockResolvedValueOnce([
{ id: '987', type: 'url', target: 'https://example.com' },
]);
const locationService = new DefaultLocationService(
store,
orchestrator,
mockConfig(false),
);
const result = await locationService.createLocation(
{ type: 'url', target: 'https://backstage.io/catalog-info.yaml' },
true,
@@ -243,6 +276,11 @@ describe('DefaultLocationServiceTest', () => {
id: '123',
});
const locationService = new DefaultLocationService(
store,
orchestrator,
mockConfig(false),
);
await expect(
locationService.createLocation(locationSpec, false),
).resolves.toEqual({
@@ -259,7 +297,61 @@ describe('DefaultLocationServiceTest', () => {
});
});
it('should not allow locations of unknown types', async () => {
it('should create location with unknown type if configuration allows it', async () => {
const locationSpec = {
type: 'unknown',
target: 'https://backstage.io/catalog-info.yaml',
};
store.createLocation.mockResolvedValue({
...locationSpec,
id: '123',
});
const locationService = new DefaultLocationService(
store,
orchestrator,
mockConfig(true),
);
await expect(
locationService.createLocation(locationSpec, false),
).resolves.toEqual({
entities: [],
location: {
id: '123',
target: 'https://backstage.io/catalog-info.yaml',
type: 'unknown',
},
});
expect(store.createLocation).toBeCalledWith({
target: 'https://backstage.io/catalog-info.yaml',
type: 'unknown',
});
});
it('should not allow locations of unknown types by default', async () => {
const locationService = new DefaultLocationService(
store,
orchestrator,
new ConfigReader({}),
);
await expect(
locationService.createLocation(
{
type: 'unknown',
target: 'https://backstage.io/catalog-info.yaml',
},
false,
),
).rejects.toThrow(InputError);
});
it('should not allow locations of unknown types if configuration forbids it', async () => {
const locationService = new DefaultLocationService(
store,
orchestrator,
mockConfig(false),
);
await expect(
locationService.createLocation(
{
@@ -279,6 +371,11 @@ describe('DefaultLocationServiceTest', () => {
errors: [new Error('Error: Unable to read url')],
});
const locationService = new DefaultLocationService(
store,
orchestrator,
mockConfig(false),
);
await expect(
locationService.createLocation(
{
@@ -293,6 +390,11 @@ describe('DefaultLocationServiceTest', () => {
describe('listLocations', () => {
it('should call locationStore.deleteLocation', async () => {
const locationService = new DefaultLocationService(
store,
orchestrator,
mockConfig(false),
);
await locationService.listLocations();
expect(store.listLocations).toBeCalled();
});
@@ -300,6 +402,11 @@ describe('DefaultLocationServiceTest', () => {
describe('deleteLocation', () => {
it('should call locationStore.deleteLocation', async () => {
const locationService = new DefaultLocationService(
store,
orchestrator,
mockConfig(false),
);
await locationService.deleteLocation('123');
expect(store.deleteLocation).toBeCalledWith('123');
});
@@ -307,6 +414,11 @@ describe('DefaultLocationServiceTest', () => {
describe('getLocation', () => {
it('should call locationStore.getLocation', async () => {
const locationService = new DefaultLocationService(
store,
orchestrator,
mockConfig(false),
);
await locationService.getLocation('123');
expect(store.getLocation).toBeCalledWith('123');
});
@@ -26,18 +26,26 @@ import { LocationInput, LocationService, LocationStore } from './types';
import { locationSpecToMetadataName } from '../util/conversion';
import { InputError } from '@backstage/errors';
import { DeferredEntity } from '@backstage/plugin-catalog-node';
import { Config } from '@backstage/config';
export class DefaultLocationService implements LocationService {
constructor(
private readonly store: LocationStore,
private readonly orchestrator: CatalogProcessingOrchestrator,
private readonly config: Config,
) {}
async createLocation(
input: LocationInput,
dryRun: boolean,
): Promise<{ location: Location; entities: Entity[]; exists?: boolean }> {
if (input.type !== 'url') {
const allowUnknownTypeConfigName =
'catalog.locationService.create.allowUnknownType';
const allowUnknownType =
this.config.has(allowUnknownTypeConfigName) &&
this.config.getBoolean(allowUnknownTypeConfigName);
if (!allowUnknownType && input.type !== 'url') {
throw new InputError(`Registered locations must be of type 'url'`);
}
if (dryRun) {