Add defaultLocationConflictStrategy config option for catalog locations

Adds a catalog config option to set the default conflict strategy when
registering locations, so adopters can default to 'refresh' instead of
'reject' without requiring each caller to specify it explicitly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Fredrik Adelöw <freben@spotify.com>
This commit is contained in:
Fredrik Adelöw
2026-03-10 14:12:20 +01:00
parent d3796b6165
commit dfdb8e3a73
7 changed files with 24 additions and 4 deletions
+1 -1
View File
@@ -3,4 +3,4 @@
'@backstage/plugin-catalog-backend': minor
---
Add an `onConflict` option to location creation, that enables it to perform refreshes instead of throwing conflict errors when there's a pre-existing location already
Add an `onConflict` option to location creation that can refresh an existing location instead of throwing a conflict error.
+9
View File
@@ -191,6 +191,15 @@ export interface Config {
stitchTimeout?: HumanDuration | string;
};
/**
* The strategy to use when there is a conflict with a location being registered.
*
* The default value is "reject".
*
* The "refresh" strategy will refresh the existing location instead of throwing a conflict error.
*/
defaultLocationConflictStrategy?: 'refresh' | 'reject';
/**
* The interval at which the catalog should process its entities.
* @remarks
@@ -594,6 +594,10 @@ export class CatalogBuilder {
const locationService = new AuthorizedLocationService(
new DefaultLocationService(locationStore, orchestrator, {
allowedLocationTypes: this.allowedLocationType,
defaultLocationConflictStrategy:
(config.getOptionalString(
'catalog.defaultLocationConflictStrategy',
) as 'refresh' | 'reject') || 'reject',
}),
permissionsService,
);
@@ -282,6 +282,7 @@ describe('DefaultLocationServiceTest', () => {
orchestrator,
{
allowedLocationTypes: ['url', 'unknown'],
defaultLocationConflictStrategy: 'reject',
},
);
await expect(
@@ -33,6 +33,7 @@ import { BackstageCredentials } from '@backstage/backend-plugin-api';
export type DefaultLocationServiceOptions = {
allowedLocationTypes: string[];
defaultLocationConflictStrategy: 'refresh' | 'reject';
};
export class DefaultLocationService implements LocationService {
@@ -45,6 +46,7 @@ export class DefaultLocationService implements LocationService {
orchestrator: CatalogProcessingOrchestrator,
options: DefaultLocationServiceOptions = {
allowedLocationTypes: ['url'],
defaultLocationConflictStrategy: 'reject',
},
) {
this.store = store;
@@ -71,7 +73,8 @@ export class DefaultLocationService implements LocationService {
return this.dryRunCreateLocation(input);
}
const location = await this.store.createLocation(input, {
onConflict: options?.onConflict,
onConflict:
options?.onConflict ?? this.options.defaultLocationConflictStrategy,
});
return { location, entities: [] };
}
@@ -1685,7 +1685,10 @@ describe('POST /locations/by-query works end to end', () => {
const locationService = new DefaultLocationService(
store,
{ process: jest.fn() },
{ allowedLocationTypes: ['url'] },
{
allowedLocationTypes: ['url'],
defaultLocationConflictStrategy: 'reject',
},
);
const router = await createRouter({
+1 -1
View File
@@ -88,7 +88,7 @@ export interface LocationStore {
createLocation(
location: LocationInput,
options?: {
onConflict?: 'refresh' | 'reject';
onConflict: 'refresh' | 'reject';
},
): Promise<Location>;
listLocations(): Promise<Location[]>;