Catalog/next: Add support for dryRun

which is used by the catalog-import plugin.

Co-authored-by: Ben Lambert <ben@blam.sh>
Signed-off-by: Johan Haals <johan.haals@gmail.com>
This commit is contained in:
Johan Haals
2021-05-06 15:42:49 +02:00
parent d36f262904
commit b0d711e718
4 changed files with 189 additions and 14 deletions
@@ -134,6 +134,7 @@ export class CatalogClient implements CatalogApi {
throw new Error(`Location wasn't added: ${target}`);
}
// TODO(jhaals): This will throw using the experimental catalog since all discovered entities are deferred.
if (entities.length === 0) {
throw new Error(
`Location was added but has no entities specified yet: ${target}`,
@@ -0,0 +1,159 @@
/*
* 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.
*/
import { DefaultLocationService } from './DefaultLocationService';
import { CatalogProcessingOrchestrator, LocationStore } from './types';
describe('DefaultLocationServiceTest', () => {
const orchestrator: jest.Mocked<CatalogProcessingOrchestrator> = {
process: jest.fn(),
};
const store: jest.Mocked<LocationStore> = {
deleteLocation: jest.fn(),
createLocation: jest.fn(),
listLocations: jest.fn(),
getLocation: jest.fn(),
};
beforeEach(() => jest.resetAllMocks());
const locationService = new DefaultLocationService(store, orchestrator);
describe('createLocation', () => {
it('should support dry run', async () => {
orchestrator.process.mockResolvedValueOnce({
ok: true,
state: new Map(),
completedEntity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Location',
metadata: {
name: 'foo',
},
},
deferredEntities: [
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'bar',
},
},
],
relations: [],
errors: [],
});
orchestrator.process.mockResolvedValueOnce({
ok: true,
state: new Map(),
completedEntity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'bar',
},
},
deferredEntities: [],
relations: [],
errors: [],
});
await locationService.createLocation(
{ type: 'url', target: 'https://backstage.io/catalog-info.yaml' },
true,
);
expect(orchestrator.process).toBeCalledWith({
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Location',
metadata: {
annotations: {
'backstage.io/managed-by-location':
'url:https://backstage.io/catalog-info.yaml',
'backstage.io/managed-by-origin-location':
'url:https://backstage.io/catalog-info.yaml',
},
name: 'generated-bbad4f61e08f24e25d5c5e68e13e164f760aff06',
namespace: 'default',
},
spec: {
target: 'https://backstage.io/catalog-info.yaml',
type: 'url',
},
},
state: expect.anything(),
});
expect(orchestrator.process).toBeCalledWith({
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: { name: 'bar' },
},
state: expect.anything(),
});
expect(orchestrator.process).toBeCalledTimes(2);
expect(store.createLocation).not.toBeCalled();
});
it('should create location', async () => {
const locationSpec = {
type: 'url',
target: 'https://backstage.io/catalog-info.yaml',
};
store.createLocation.mockResolvedValue({
...locationSpec,
id: '123',
});
await expect(
locationService.createLocation(locationSpec, false),
).resolves.toEqual({
entities: [],
location: {
id: '123',
target: 'https://backstage.io/catalog-info.yaml',
type: 'url',
},
});
expect(store.createLocation).toBeCalledWith({
target: 'https://backstage.io/catalog-info.yaml',
type: 'url',
});
});
});
describe('listLocations', () => {
it('should call locationStore.deleteLocation', async () => {
await locationService.listLocations();
expect(store.listLocations).toBeCalled();
});
});
describe('deleteLocation', () => {
it('should call locationStore.deleteLocation', async () => {
await locationService.deleteLocation('123');
expect(store.deleteLocation).toBeCalledWith('123');
});
});
describe('getLocation', () => {
it('should call locationStore.getLocation', async () => {
await locationService.getLocation('123');
expect(store.getLocation).toBeCalledWith('123');
});
});
});
@@ -25,6 +25,7 @@ import {
LocationStore,
CatalogProcessingOrchestrator,
} from './types';
import { locationSpecToMetadataName } from './util';
export class DefaultLocationService implements LocationService {
constructor(
@@ -41,7 +42,10 @@ export class DefaultLocationService implements LocationService {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Location',
metadata: {
name: `${spec.type}:${spec.target}`,
name: locationSpecToMetadataName({
type: spec.type,
target: spec.target,
}),
namespace: 'default',
annotations: {
[LOCATION_ANNOTATION]: `${spec.type}:${spec.target}`,
@@ -49,22 +53,34 @@ export class DefaultLocationService implements LocationService {
},
},
spec: {
location: { type: spec.type, target: spec.target },
type: spec.type,
target: spec.target,
},
};
const processed = await this.orchestrator.process({
entity,
eager: true,
state: new Map(),
});
if (processed.ok) {
return {
location: { ...spec, id: `${spec.type}:${spec.target}` },
entities: [processed.completedEntity],
};
const unprocessedEntities: Entity[] = [entity];
const entities: Entity[] = [];
const state = new Map(); // ignored
while (unprocessedEntities.length) {
const currentEntity = unprocessedEntities.pop();
if (!currentEntity) {
continue;
}
const processed = await this.orchestrator.process({
entity: currentEntity,
state,
});
if (processed.ok) {
unprocessedEntities.push(...processed.deferredEntities);
entities.push(processed.completedEntity);
} else {
throw Error(processed.errors.join(', '));
}
}
throw Error('error handling not implemented.');
return {
location: { ...spec, id: `${spec.type}:${spec.target}` },
entities,
};
}
const location = await this.store.createLocation(spec);
@@ -59,7 +59,6 @@ export interface EntityProvider {
export type EntityProcessingRequest = {
entity: Entity;
eager?: boolean;
state: Map<string, JsonObject>; // Versions for multiple deployments etc
};