catalog-backend: refactor entityProviders connect

Signed-off-by: Johan Haals <johan.haals@gmail.com>
This commit is contained in:
Johan Haals
2021-11-17 14:55:13 +01:00
parent d701630972
commit 5d2a7303bd
4 changed files with 100 additions and 68 deletions
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/plugin-catalog-backend': patch
---
This fixes a bug where locations couldn't be added unless the processing engine is started.
It's now possible to run the catalog backend without starting the processing engine and still allowing locations registrations.
This is done by refactor the `EntityProvider.connect` to happen outside the engine.
@@ -14,11 +14,7 @@
* limitations under the License.
*/
import {
Entity,
entityEnvelopeSchemaValidator,
stringifyEntityRef,
} from '@backstage/catalog-model';
import { stringifyEntityRef } from '@backstage/catalog-model';
import { assertError, serializeError } from '@backstage/errors';
import { Hash } from 'crypto';
import stableStringify from 'fast-json-stable-stringify';
@@ -32,68 +28,15 @@ import {
} from '../processing/types';
import { Stitcher } from '../stitching/Stitcher';
import { startTaskPipeline } from './TaskPipeline';
import {
EntityProvider,
EntityProviderConnection,
EntityProviderMutation,
} from '../providers/types';
const CACHE_TTL = 5;
class Connection implements EntityProviderConnection {
readonly validateEntityEnvelope = entityEnvelopeSchemaValidator();
constructor(
private readonly config: {
id: string;
processingDatabase: ProcessingDatabase;
},
) {}
async applyMutation(mutation: EntityProviderMutation): Promise<void> {
const db = this.config.processingDatabase;
if (mutation.type === 'full') {
this.check(mutation.entities.map(e => e.entity));
await db.transaction(async tx => {
await db.replaceUnprocessedEntities(tx, {
sourceKey: this.config.id,
type: 'full',
items: mutation.entities,
});
});
} else if (mutation.type === 'delta') {
this.check(mutation.added.map(e => e.entity));
this.check(mutation.removed.map(e => e.entity));
await db.transaction(async tx => {
await db.replaceUnprocessedEntities(tx, {
sourceKey: this.config.id,
type: 'delta',
added: mutation.added,
removed: mutation.removed,
});
});
}
}
private check(entities: Entity[]) {
for (const entity of entities) {
try {
this.validateEntityEnvelope(entity);
} catch (e) {
throw new TypeError(`Malformed entity envelope, ${e}`);
}
}
}
}
export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine {
private readonly tracker = progressTracker();
private stopFunc?: () => void;
constructor(
private readonly logger: Logger,
private readonly entityProviders: EntityProvider[],
private readonly processingDatabase: ProcessingDatabase,
private readonly orchestrator: CatalogProcessingOrchestrator,
private readonly stitcher: Stitcher,
@@ -106,15 +49,6 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine {
throw new Error('Processing engine is already started');
}
for (const provider of this.entityProviders) {
await provider.connect(
new Connection({
id: provider.getProviderName(),
processingDatabase: this.processingDatabase,
}),
);
}
this.stopFunc = startTaskPipeline<RefreshStateItem>({
lowWatermark: 5,
highWatermark: 10,
@@ -0,0 +1,88 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 {
Entity,
entityEnvelopeSchemaValidator,
} from '@backstage/catalog-model';
import { ProcessingDatabase } from '../database/types';
import {
EntityProvider,
EntityProviderConnection,
EntityProviderMutation,
} from '../providers/types';
class Connection implements EntityProviderConnection {
readonly validateEntityEnvelope = entityEnvelopeSchemaValidator();
constructor(
private readonly config: {
id: string;
processingDatabase: ProcessingDatabase;
},
) {}
async applyMutation(mutation: EntityProviderMutation): Promise<void> {
const db = this.config.processingDatabase;
if (mutation.type === 'full') {
this.check(mutation.entities.map(e => e.entity));
await db.transaction(async tx => {
await db.replaceUnprocessedEntities(tx, {
sourceKey: this.config.id,
type: 'full',
items: mutation.entities,
});
});
} else if (mutation.type === 'delta') {
this.check(mutation.added.map(e => e.entity));
this.check(mutation.removed.map(e => e.entity));
await db.transaction(async tx => {
await db.replaceUnprocessedEntities(tx, {
sourceKey: this.config.id,
type: 'delta',
added: mutation.added,
removed: mutation.removed,
});
});
}
}
private check(entities: Entity[]) {
for (const entity of entities) {
try {
this.validateEntityEnvelope(entity);
} catch (e) {
throw new TypeError(`Malformed entity envelope, ${e}`);
}
}
}
}
export async function connectEntityProviders(
db: ProcessingDatabase,
providers: EntityProvider[],
) {
await Promise.all(
providers.map(async provider => {
const connection = new Connection({
id: provider.getProviderName(),
processingDatabase: db,
});
return provider.connect(connection);
}),
);
}
@@ -80,6 +80,7 @@ import { DefaultCatalogRulesEnforcer } from '../ingestion/CatalogRules';
import { Config } from '@backstage/config';
import { Logger } from 'winston';
import { LocationService } from './types';
import { connectEntityProviders } from '../processing/connectEntityProviders';
export type CatalogEnvironment = {
logger: Logger;
@@ -364,7 +365,6 @@ export class NextCatalogBuilder {
const processingEngine = new DefaultCatalogProcessingEngine(
logger,
entityProviders,
processingDatabase,
orchestrator,
stitcher,
@@ -390,6 +390,8 @@ export class NextCatalogBuilder {
config,
});
await connectEntityProviders(processingDatabase, entityProviders);
return {
entitiesCatalog,
locationsCatalog,