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:
@@ -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
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user