catalog-backend: include parent refs for deferred entities in cache key

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-01-28 12:11:48 +01:00
parent 9b9737c107
commit 4ae6884a32
5 changed files with 103 additions and 3 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Fixed an bug in the entity processing caching that would prevent entities that were emitted during processing to be restored after being overridden.
@@ -272,7 +272,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase {
const rows = await tx<DbRefreshStateReferencesRow>(
'refresh_state_references',
)
.where({ target_entity_ref: options.entityRef })
.whereIn('target_entity_ref', options.entityRefs)
.select();
const entityRefs = rows.map(r => r.source_entity_ref!).filter(Boolean);
@@ -101,7 +101,7 @@ export type ListAncestorsResult = {
};
export type ListParentsOptions = {
entityRef: string;
entityRefs: string[];
};
export type ListParentsResult = {
@@ -215,7 +215,12 @@ export class DefaultCatalogProcessingEngine {
const { entityRefs: parents } =
await this.processingDatabase.transaction(tx =>
this.processingDatabase.listParents(tx, {
entityRef,
entityRefs: [
entityRef,
...result.deferredEntities.map(e =>
stringifyEntityRef(e.entity),
),
],
}),
);
@@ -1223,4 +1223,94 @@ describe('Catalog Backend Integration', () => {
'component:default/b': withOutputFields(entityBOverride),
});
});
it('should resolve handle location key conflicts across processed and provided entities', async () => {
const baseEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
annotations: {
'backstage.io/managed-by-location': 'url:.',
'backstage.io/managed-by-origin-location': 'url:.',
},
},
};
const entityA = merge({ metadata: { name: 'a' } }, baseEntity);
const entityB = merge({ metadata: { name: 'b' } }, baseEntity);
const entityBOverride = merge({ metadata: { override: true } }, entityB);
const processEntity = jest.fn(
async (
entity: Entity,
_location: LocationSpec,
_emit: CatalogProcessorEmit,
) => entity,
);
const harness = await TestHarness.create({ processEntity });
processEntity.mockImplementation(async (entity, location, emit) => {
if (entity.metadata.name === entityA.metadata.name) {
emit(
processingResult.entity(location, entityB, {
locationKey: 'my-key',
}),
);
}
return entity;
});
// Start with just A, which outputs B via processing
await harness.setInputEntities([entityA]);
await expect(harness.process()).resolves.toEqual({});
await expect(harness.getOutputEntities()).resolves.toEqual({
'component:default/a': withOutputFields(entityA),
'component:default/b': withOutputFields(entityB),
});
// Now apply A and B', but since B already has a different location key, B' should not be used
await harness.setInputEntities([entityA, entityBOverride]);
await expect(harness.process()).resolves.toEqual({});
await expect(harness.getOutputEntities()).resolves.toEqual({
'component:default/a': withOutputFields(entityA),
'component:default/b': withOutputFields(entityB),
});
// Stop emitting B from A
processEntity.mockImplementation(async e => e);
// Apply A and B', but since B still has a location key set it should remain, but now orphaned
await harness.setInputEntities([entityA, entityBOverride]);
await expect(harness.process()).resolves.toEqual({});
await expect(harness.getOutputEntities()).resolves.toEqual({
'component:default/a': withOutputFields(entityA),
'component:default/b': withOutputFields(
merge(entityB, {
metadata: { annotations: { 'backstage.io/orphan': 'true' } },
}),
),
});
// Apply A and B' but this time with the matching location key, causing B' to override B
await harness.setInputEntities([
entityA,
{ ...entityBOverride, locationKey: 'my-key' },
]);
await expect(harness.process()).resolves.toEqual({});
await expect(harness.getOutputEntities()).resolves.toEqual({
'component:default/a': withOutputFields(entityA),
'component:default/b': withOutputFields(entityBOverride),
});
// Finally, remove B', leaving just A
await harness.setInputEntities([entityA]);
await expect(harness.process()).resolves.toEqual({});
await expect(harness.getOutputEntities()).resolves.toEqual({
'component:default/a': withOutputFields(entityA),
});
});
});