From b0d711e7184bbd9e2c6608ea6930c9be8c7637bf Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Thu, 6 May 2021 15:42:49 +0200 Subject: [PATCH] Catalog/next: Add support for dryRun which is used by the catalog-import plugin. Co-authored-by: Ben Lambert Signed-off-by: Johan Haals --- packages/catalog-client/src/CatalogClient.ts | 1 + .../src/next/DefaultLocationService.test.ts | 159 ++++++++++++++++++ .../src/next/DefaultLocationService.ts | 42 +++-- plugins/catalog-backend/src/next/types.ts | 1 - 4 files changed, 189 insertions(+), 14 deletions(-) create mode 100644 plugins/catalog-backend/src/next/DefaultLocationService.test.ts diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index a246b3abab..063db5a6a3 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -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}`, diff --git a/plugins/catalog-backend/src/next/DefaultLocationService.test.ts b/plugins/catalog-backend/src/next/DefaultLocationService.test.ts new file mode 100644 index 0000000000..662be5d3a6 --- /dev/null +++ b/plugins/catalog-backend/src/next/DefaultLocationService.test.ts @@ -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 = { + process: jest.fn(), + }; + const store: jest.Mocked = { + 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'); + }); + }); +}); diff --git a/plugins/catalog-backend/src/next/DefaultLocationService.ts b/plugins/catalog-backend/src/next/DefaultLocationService.ts index 3935f6e4d0..884410c2f0 100644 --- a/plugins/catalog-backend/src/next/DefaultLocationService.ts +++ b/plugins/catalog-backend/src/next/DefaultLocationService.ts @@ -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); diff --git a/plugins/catalog-backend/src/next/types.ts b/plugins/catalog-backend/src/next/types.ts index 410066eafd..fbc243ce69 100644 --- a/plugins/catalog-backend/src/next/types.ts +++ b/plugins/catalog-backend/src/next/types.ts @@ -59,7 +59,6 @@ export interface EntityProvider { export type EntityProcessingRequest = { entity: Entity; - eager?: boolean; state: Map; // Versions for multiple deployments etc };