diff --git a/.changeset/spicy-tomatoes-hammer.md b/.changeset/spicy-tomatoes-hammer.md index 20564b9a05..f70eafed51 100644 --- a/.changeset/spicy-tomatoes-hammer.md +++ b/.changeset/spicy-tomatoes-hammer.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-catalog-backend': minor +'@backstage/plugin-catalog-backend': patch --- -Removed the long-deprecated `DefaultCatalogCollatorFactory` and `DefaultCatalogCollatorFactoryOptions` exports, which now no longer exist in the search plugin's offerings. If you were using these, you want to migrate to [the new backend system](https://backstage.io/docs/backend-system/) and use the [catalog collator](https://backstage.io/docs/features/search/collators#catalog) directly. +Internalize the deprecated collator types since they were removed from the collator itself during new-backend-system migration. diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 216e720b07..82a38bc0ff 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -75,6 +75,7 @@ "@backstage/plugin-permission-common": "workspace:^", "@backstage/plugin-permission-node": "workspace:^", "@backstage/plugin-search-backend-module-catalog": "workspace:^", + "@backstage/plugin-search-common": "workspace:^", "@backstage/types": "workspace:^", "@opentelemetry/api": "^1.9.0", "@types/express": "^4.17.6", diff --git a/plugins/catalog-backend/report.api.md b/plugins/catalog-backend/report.api.md index ea7e11d0d7..7b1e173f82 100644 --- a/plugins/catalog-backend/report.api.md +++ b/plugins/catalog-backend/report.api.md @@ -30,6 +30,7 @@ import { Config } from '@backstage/config'; import { DatabaseService } from '@backstage/backend-plugin-api'; import { DeferredEntity as DeferredEntity_2 } from '@backstage/plugin-catalog-node'; import { DiscoveryService } from '@backstage/backend-plugin-api'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; import { EntitiesSearchFilter as EntitiesSearchFilter_2 } from '@backstage/plugin-catalog-node'; import { Entity } from '@backstage/catalog-model'; import { EntityFilter as EntityFilter_2 } from '@backstage/plugin-catalog-node'; @@ -56,6 +57,7 @@ import { PlaceholderResolver as PlaceholderResolver_2 } from '@backstage/plugin- import { PlaceholderResolverParams as PlaceholderResolverParams_2 } from '@backstage/plugin-catalog-node'; import { PlaceholderResolverRead as PlaceholderResolverRead_2 } from '@backstage/plugin-catalog-node'; import { PlaceholderResolverResolveUrl as PlaceholderResolverResolveUrl_2 } from '@backstage/plugin-catalog-node'; +import { Readable } from 'stream'; import { RootConfigService } from '@backstage/backend-plugin-api'; import { Router } from 'express'; import { SchedulerService } from '@backstage/backend-plugin-api'; @@ -323,6 +325,33 @@ export class DefaultCatalogCollator { // @public @deprecated (undocumented) export const defaultCatalogCollatorEntityTransformer: CatalogCollatorEntityTransformer_2; +// @public @deprecated (undocumented) +export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { + // (undocumented) + static fromConfig( + configRoot: Config, + options: DefaultCatalogCollatorFactoryOptions, + ): DefaultCatalogCollatorFactory; + // (undocumented) + getCollator(): Promise; + // (undocumented) + readonly type = 'software-catalog'; + // (undocumented) + readonly visibilityPermission: Permission; +} + +// @public @deprecated (undocumented) +export type DefaultCatalogCollatorFactoryOptions = { + auth?: AuthService; + discovery: DiscoveryService; + tokenManager?: TokenManager; + locationTemplate?: string; + filter?: GetEntitiesRequest['filter']; + batchSize?: number; + catalogClient?: CatalogApi; + entityTransformer?: CatalogCollatorEntityTransformer; +}; + // @public @deprecated (undocumented) export type DeferredEntity = DeferredEntity_2; diff --git a/plugins/catalog-backend/src/deprecated.ts b/plugins/catalog-backend/src/deprecated.ts index 3865852214..023e93e664 100644 --- a/plugins/catalog-backend/src/deprecated.ts +++ b/plugins/catalog-backend/src/deprecated.ts @@ -15,6 +15,25 @@ */ import { + TokenManager, + createLegacyAuthAdapters, +} from '@backstage/backend-common'; +import { AuthService, DiscoveryService } from '@backstage/backend-plugin-api'; +import { + CatalogApi, + CatalogClient, + EntityFilterQuery, + GetEntitiesRequest, +} from '@backstage/catalog-client'; +import { + Entity, + isGroupEntity, + isUserEntity, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { + CatalogEntityDocument, type AnalyzeLocationEntityField as _AnalyzeLocationEntityField, type AnalyzeLocationExistingEntity as _AnalyzeLocationExistingEntity, type AnalyzeLocationGenerateEntity as _AnalyzeLocationGenerateEntity, @@ -22,6 +41,7 @@ import { type AnalyzeLocationResponse as _AnalyzeLocationResponse, type LocationSpec as _LocationSpec, } from '@backstage/plugin-catalog-common'; +import { catalogEntityReadPermission } from '@backstage/plugin-catalog-common/alpha'; import { locationSpecToMetadataName as _locationSpecToMetadataName, locationSpecToLocationEntity as _locationSpecToLocationEntity, @@ -51,10 +71,13 @@ import { type LocationAnalyzer as _LocationAnalyzer, type ScmLocationAnalyzer as _ScmLocationAnalyzer, } from '@backstage/plugin-catalog-node'; +import { Permission } from '@backstage/plugin-permission-common'; import { defaultCatalogCollatorEntityTransformer as _defaultCatalogCollatorEntityTransformer, type CatalogCollatorEntityTransformer as _CatalogCollatorEntityTransformer, } from '@backstage/plugin-search-backend-module-catalog'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; +import { Readable } from 'stream'; /** * @public @@ -252,6 +275,69 @@ export type AnalyzeLocationGenerateEntity = _AnalyzeLocationGenerateEntity; */ export type AnalyzeLocationEntityField = _AnalyzeLocationEntityField; +namespace search { + const configKey = 'search.collators.catalog'; + + const defaults = { + schedule: { + frequency: { minutes: 10 }, + timeout: { minutes: 15 }, + initialDelay: { seconds: 3 }, + }, + collatorOptions: { + locationTemplate: '/catalog/:namespace/:kind/:name', + filter: undefined, + batchSize: 500, + }, + }; + + export const readCollatorConfigOptions = ( + configRoot: Config, + ): { + locationTemplate: string; + filter: EntityFilterQuery | undefined; + batchSize: number; + } => { + const config = configRoot.getOptionalConfig(configKey); + if (!config) { + return defaults.collatorOptions; + } + + return { + locationTemplate: + config.getOptionalString('locationTemplate') ?? + defaults.collatorOptions.locationTemplate, + filter: + config.getOptional('filter') ?? + defaults.collatorOptions.filter, + batchSize: + config.getOptionalNumber('batchSize') ?? + defaults.collatorOptions.batchSize, + }; + }; + + export const getDocumentText = (entity: Entity): string => { + const documentTexts: string[] = []; + if (entity.metadata.description) { + documentTexts.push(entity.metadata.description); + } + + if (isUserEntity(entity) || isGroupEntity(entity)) { + if (entity.spec?.profile?.displayName) { + documentTexts.push(entity.spec.profile.displayName); + } + } + + if (isUserEntity(entity)) { + if (entity.spec?.profile?.email) { + documentTexts.push(entity.spec.profile.email); + } + } + + return documentTexts.join(' : '); + }; +} + /** * @public * @deprecated import from `@backstage/plugin-search-backend-module-catalog` instead @@ -259,6 +345,155 @@ export type AnalyzeLocationEntityField = _AnalyzeLocationEntityField; export const defaultCatalogCollatorEntityTransformer = _defaultCatalogCollatorEntityTransformer; +/** + * @public + * @deprecated This is no longer supported since the new backend system migration + */ +export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { + public readonly type = 'software-catalog'; + public readonly visibilityPermission: Permission = + catalogEntityReadPermission; + + private locationTemplate: string; + private filter?: GetEntitiesRequest['filter']; + private batchSize: number; + private readonly catalogClient: CatalogApi; + private entityTransformer: CatalogCollatorEntityTransformer; + private auth: AuthService; + + static fromConfig( + configRoot: Config, + options: DefaultCatalogCollatorFactoryOptions, + ) { + const configOptions = search.readCollatorConfigOptions(configRoot); + const { auth: adaptedAuth } = createLegacyAuthAdapters({ + auth: options.auth, + discovery: options.discovery, + tokenManager: options.tokenManager, + }); + return new DefaultCatalogCollatorFactory({ + locationTemplate: + options.locationTemplate ?? configOptions.locationTemplate, + filter: options.filter ?? configOptions.filter, + batchSize: options.batchSize ?? configOptions.batchSize, + entityTransformer: options.entityTransformer, + auth: adaptedAuth, + discovery: options.discovery, + catalogClient: options.catalogClient, + }); + } + + private constructor(options: { + locationTemplate: string; + filter: GetEntitiesRequest['filter']; + batchSize: number; + entityTransformer?: CatalogCollatorEntityTransformer; + auth: AuthService; + discovery: DiscoveryService; + catalogClient?: CatalogApi; + }) { + const { + auth, + batchSize, + discovery, + locationTemplate, + filter, + catalogClient, + entityTransformer, + } = options; + + this.locationTemplate = locationTemplate; + this.filter = filter; + this.batchSize = batchSize; + this.catalogClient = + catalogClient || new CatalogClient({ discoveryApi: discovery }); + this.entityTransformer = + entityTransformer ?? defaultCatalogCollatorEntityTransformer; + this.auth = auth; + } + + async getCollator(): Promise { + return Readable.from(this.execute()); + } + + private async *execute(): AsyncGenerator { + let entitiesRetrieved = 0; + let cursor: string | undefined = undefined; + + do { + const { token } = await this.auth.getPluginRequestToken({ + onBehalfOf: await this.auth.getOwnServiceCredentials(), + targetPluginId: 'catalog', + }); + const response = await this.catalogClient.queryEntities( + { + filter: this.filter, + limit: this.batchSize, + ...(cursor ? { cursor } : {}), + }, + { token }, + ); + cursor = response.pageInfo.nextCursor; + entitiesRetrieved += response.items.length; + + for (const entity of response.items) { + yield { + ...this.entityTransformer(entity), + authorization: { + resourceRef: stringifyEntityRef(entity), + }, + location: this.applyArgsToFormat(this.locationTemplate, { + namespace: entity.metadata.namespace || 'default', + kind: entity.kind, + name: entity.metadata.name, + }), + }; + } + } while (cursor); + } + + private applyArgsToFormat( + format: string, + args: Record, + ): string { + let formatted = format; + + for (const [key, value] of Object.entries(args)) { + formatted = formatted.replace(`:${key}`, value); + } + + return formatted.toLowerCase(); + } +} + +/** + * @public + * @deprecated This is no longer supported since the new backend system migration + */ +export type DefaultCatalogCollatorFactoryOptions = { + auth?: AuthService; + discovery: DiscoveryService; + tokenManager?: TokenManager; + /** + * @deprecated Use the config key `search.collators.catalog.locationTemplate` instead. + */ + locationTemplate?: string; + /** + * @deprecated Use the config key `search.collators.catalog.filter` instead. + */ + filter?: GetEntitiesRequest['filter']; + /** + * @deprecated Use the config key `search.collators.catalog.batchSize` instead. + */ + batchSize?: number; + // TODO(freben): Change to required CatalogService instead when fully migrated to the new backend system. + catalogClient?: CatalogApi; + /** + * Allows you to customize how entities are shaped into documents. + */ + entityTransformer?: CatalogCollatorEntityTransformer; +}; + /** * @public * @deprecated import from `@backstage/plugin-search-backend-module-catalog` instead diff --git a/yarn.lock b/yarn.lock index d40f8114b7..b763d8501d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6194,6 +6194,7 @@ __metadata: "@backstage/plugin-permission-common": "workspace:^" "@backstage/plugin-permission-node": "workspace:^" "@backstage/plugin-search-backend-module-catalog": "workspace:^" + "@backstage/plugin-search-common": "workspace:^" "@backstage/repo-tools": "workspace:^" "@backstage/types": "workspace:^" "@opentelemetry/api": ^1.9.0