feat: Add option to evict entities belonging to an orphaned entity provider

Signed-off-by: Jack Palmer <jackpalmer@spotify.com>
This commit is contained in:
Jack Palmer
2025-02-25 17:02:04 +00:00
parent 2bbb493280
commit 259b22efdd
5 changed files with 105 additions and 0 deletions
+8
View File
@@ -216,5 +216,13 @@ export interface Config {
* This flag is temporary and will be enabled by default in future releases.
*/
useUrlReadersSearch?: boolean;
/**
* Evicts entities from the catalog that are no longer referenced by any
* added entity providers.
*
* Defaults to false.
*/
evictOrphanedEntityProviders?: boolean;
};
}
@@ -199,6 +199,20 @@ export class DefaultProviderDatabase implements ProviderDatabase {
}
}
async listEntityProviderNames(txOpaque: Transaction): Promise<string[]> {
const tx = txOpaque as Knex | Knex.Transaction;
const rows = await tx<DbRefreshStateReferencesRow>(
'refresh_state_references',
)
.distinct('source_key')
.whereNotNull('source_key');
return rows
.map(row => row.source_key)
.filter((key): key is string => !!key);
}
async refreshByRefreshKeys(
txOpaque: Transaction,
options: RefreshByKeyOptions,
@@ -173,6 +173,11 @@ export interface ProviderDatabase {
txOpaque: Transaction,
options: RefreshByKeyOptions,
): Promise<void>;
/**
* List the names of all the entity providers that have references in the provider database.
*/
listEntityProviderNames(txOpaque: Transaction): Promise<string[]>;
}
// TODO(Rugvip): This is only partial for now
@@ -0,0 +1,69 @@
/*
* Copyright 2025 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 { EntityProvider } from '@backstage/plugin-catalog-node';
import { ProviderDatabase } from '../database/types';
import { LoggerService } from '@backstage/backend-plugin-api';
async function getOrphanedEntityProviderNames(
db: ProviderDatabase,
providers: EntityProvider[],
): Promise<string[]> {
const dbProviderNames = await db.transaction(async tx =>
db.listEntityProviderNames(tx),
);
const providerNames = providers.map(p => p.getProviderName());
return dbProviderNames.filter(
dbProviderName => !providerNames.includes(dbProviderName),
);
}
async function removeEntitiesForProvider(
db: ProviderDatabase,
providerName: string,
logger: LoggerService,
) {
try {
await db.transaction(async tx => {
await db.replaceUnprocessedEntities(tx, {
sourceKey: providerName,
type: 'full',
items: [],
});
});
logger.info(`Removed entities for orphaned provider ${providerName}`);
} catch (e) {
logger.error(
`Failed to remove entities for orphaned provider ${providerName}`,
e,
);
}
}
export async function evictOrphanedEntityProviders(
db: ProviderDatabase,
providers: EntityProvider[],
logger: LoggerService,
) {
for (const providerName of await getOrphanedEntityProviderNames(
db,
providers,
)) {
await removeEntitiesForProvider(db, providerName, logger);
}
}
@@ -87,6 +87,7 @@ import {
ProcessingIntervalFunction,
} from '../processing';
import { connectEntityProviders } from '../processing/connectEntityProviders';
import { evictOrphanedEntityProviders } from '../processing/evictOrphanedEntityProviders';
import { DefaultCatalogProcessingEngine } from '../processing/DefaultCatalogProcessingEngine';
import { DefaultCatalogProcessingOrchestrator } from '../processing/DefaultCatalogProcessingOrchestrator';
import {
@@ -645,6 +646,14 @@ export class CatalogBuilder {
await connectEntityProviders(providerDatabase, entityProviders);
if (config.getOptionalBoolean('catalog.evictOrphanedEntityProviders')) {
await evictOrphanedEntityProviders(
providerDatabase,
entityProviders,
logger,
);
}
return {
processingEngine: {
async start() {