diff --git a/.changeset/short-pots-remember.md b/.changeset/short-pots-remember.md new file mode 100644 index 0000000000..9a30bc222c --- /dev/null +++ b/.changeset/short-pots-remember.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fixed an issue where entities would not be marked for restitching if only the target of a relationship changed. diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.test.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.test.ts index 3f283a5b38..de7c3600c2 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.test.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.test.ts @@ -464,6 +464,111 @@ describe('DefaultCatalogProcessingEngine', () => { await engine.stop(); }); + it('should stitch both the previous and new sources when relation target changes', async () => { + const engine = new DefaultCatalogProcessingEngine({ + config: new ConfigReader({}), + logger: mockServices.logger.mock(), + processingDatabase: db, + knex: {} as any, + orchestrator: orchestrator, + stitcher: stitcher, + createHash: () => hash, + pollingIntervalMs: 100, + }); + + db.transaction.mockImplementation(cb => cb((() => {}) as any)); + + const entity = { + apiVersion: '1', + kind: 'k', + metadata: { name: 'me', namespace: 'ns' }, + }; + const processableEntity = { + entityRef: 'foo', + id: '1', + unprocessedEntity: entity, + resultHash: '', + state: [] as any, + nextUpdateAt: DateTime.now(), + lastDiscoveryAt: DateTime.now(), + }; + + db.listParents.mockResolvedValue({ entityRefs: [] }); + db.getProcessableEntities + .mockResolvedValueOnce({ + items: [processableEntity], + }) + .mockResolvedValueOnce({ + items: [processableEntity], + }); + db.updateProcessedEntity + .mockImplementationOnce(async () => ({ + previous: { relations: [] }, + })) + .mockImplementationOnce(async () => ({ + previous: { + relations: [ + { + originating_entity_id: '', + type: 't', + source_entity_ref: 'k:ns/other1', + target_entity_ref: 'k:ns/me', + }, + ], + }, + })); + + orchestrator.process + .mockResolvedValueOnce({ + ok: true, + completedEntity: entity, + relations: [ + { + type: 't', + source: { kind: 'k', namespace: 'ns', name: 'other1' }, + target: { kind: 'k', namespace: 'ns', name: 'me' }, + }, + ], + errors: [], + deferredEntities: [], + state: {}, + refreshKeys: [], + }) + .mockResolvedValueOnce({ + ok: true, + completedEntity: entity, + // change just the target of the relationship to a new entity, + // leaving the source and relation type the same. + // see: https://github.com/backstage/backstage/issues/27325 + relations: [ + { + type: 't', + source: { kind: 'k', namespace: 'ns', name: 'other1' }, + target: { kind: 'k', namespace: 'ns', name: 'newtarget' }, + }, + ], + errors: [], + deferredEntities: [], + state: {}, + refreshKeys: [], + }); + + await engine.start(); + await waitForExpect(() => { + expect(stitcher.stitch).toHaveBeenCalledTimes(2); + }); + expect([...stitcher.stitch.mock.calls[0][0].entityRefs!]).toEqual( + expect.arrayContaining(['k:ns/me', 'k:ns/other1']), + ); + // As a result of switching the relationship for source other1 to + // a new target entity, the other1 relationship source must be + // restitched. + expect([...stitcher.stitch.mock.calls[1][0].entityRefs!]).toEqual( + expect.arrayContaining(['k:ns/me', 'k:ns/other1']), + ); + await engine.stop(); + }); + it('should not stitch sources entities when relations are the same', async () => { const engine = new DefaultCatalogProcessingEngine({ config: new ConfigReader({}), diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts index e42847bafe..0f7acba652 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts @@ -295,7 +295,7 @@ export class DefaultCatalogProcessingEngine { }); oldRelationSources = new Map( previous.relations.map(r => [ - `${r.source_entity_ref}:${r.type}`, + `${r.source_entity_ref}:${r.type}->${r.target_entity_ref}`, r.source_entity_ref, ]), ); @@ -304,7 +304,11 @@ export class DefaultCatalogProcessingEngine { const newRelationSources = new Map( result.relations.map(relation => { const sourceEntityRef = stringifyEntityRef(relation.source); - return [`${sourceEntityRef}:${relation.type}`, sourceEntityRef]; + const targetEntityRef = stringifyEntityRef(relation.target); + return [ + `${sourceEntityRef}:${relation.type}->${targetEntityRef}`, + sourceEntityRef, + ]; }), );