From 5e32f77884825ba6cb166fc8ea5b7d58861b38d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 3 Apr 2026 22:53:27 +0200 Subject: [PATCH] Migrate techdocs plugins from alpha catalogServiceRef to stable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrated `@backstage/plugin-techdocs-backend` and `@backstage/plugin-search-backend-module-techdocs` to use the stable `catalogServiceRef` from `@backstage/plugin-catalog-node` instead of the deprecated one from `@backstage/plugin-catalog-node/alpha`. This also updates `CachedEntityLoader`, `DefaultTechDocsCollatorFactory`, and the TechDocs router to use `CatalogService` (credentials-based) instead of `CatalogApi` (token-based). Signed-off-by: Fredrik Adelöw Made-with: Cursor --- .../migrate-techdocs-catalog-service-ref.md | 6 +++ .../DefaultTechDocsCollatorFactory.test.ts | 30 ++---------- .../DefaultTechDocsCollatorFactory.ts | 21 ++++----- .../src/module.ts | 2 +- plugins/techdocs-backend/src/plugin.ts | 2 +- .../src/service/CachedEntityLoader.test.ts | 20 ++++---- .../src/service/CachedEntityLoader.ts | 9 ++-- .../src/service/router.test.ts | 5 +- .../techdocs-backend/src/service/router.ts | 46 ++++++------------- 9 files changed, 52 insertions(+), 89 deletions(-) create mode 100644 .changeset/migrate-techdocs-catalog-service-ref.md diff --git a/.changeset/migrate-techdocs-catalog-service-ref.md b/.changeset/migrate-techdocs-catalog-service-ref.md new file mode 100644 index 0000000000..c8db66d42a --- /dev/null +++ b/.changeset/migrate-techdocs-catalog-service-ref.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-search-backend-module-techdocs': patch +'@backstage/plugin-techdocs-backend': patch +--- + +Migrated internal usage of the deprecated `catalogServiceRef` from `@backstage/plugin-catalog-node/alpha` to the stable `catalogServiceRef` from `@backstage/plugin-catalog-node`. diff --git a/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.test.ts b/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.test.ts index c3793917e5..d4f3e33d60 100644 --- a/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.test.ts +++ b/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.test.ts @@ -21,6 +21,7 @@ import { mockServices, registerMswTestHooks, } from '@backstage/backend-test-utils'; +import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { Readable } from 'node:stream'; @@ -85,10 +86,12 @@ describe('DefaultTechDocsCollatorFactory', () => { const mockDiscoveryApi = mockServices.discovery.mock({ getBaseUrl: async () => 'http://test-backend', }); + const mockCatalog = catalogServiceMock({ entities: expectedEntities }); const options = { logger, discovery: mockDiscoveryApi, auth: mockServices.auth(), + catalogClient: mockCatalog, }; it('has expected type', () => { @@ -112,31 +115,6 @@ describe('DefaultTechDocsCollatorFactory', () => { 'http://test-backend/static/docs/default/Component/test-entity-with-docs/search/search_index.json', (_, res, ctx) => res(ctx.status(200), ctx.json(mockSearchDocIndex)), ), - rest.get('http://test-backend/entities', (req, res, ctx) => { - // Imitate offset/limit pagination. - const offset = parseInt( - req.url.searchParams.get('offset') || '0', - 10, - ); - const limit = parseInt( - req.url.searchParams.get('limit') || '500', - 10, - ); - - // Limit 50 corresponds to a case testing pagination. - if (limit === 50) { - // Return 50 copies of invalid entities on the first request. - if (offset === 0) { - return res(ctx.status(200), ctx.json(Array(50).fill({}))); - } - // Then just the regular 2 on the second. - return res(ctx.status(200), ctx.json(expectedEntities)); - } - return res( - ctx.status(200), - ctx.json(expectedEntities.slice(offset, limit + offset)), - ); - }), ); }); @@ -147,7 +125,6 @@ describe('DefaultTechDocsCollatorFactory', () => { it('fetches from the configured catalog and tech docs services', async () => { const pipeline = TestPipeline.fromCollator(collator); const { documents } = await pipeline.execute(); - expect(mockDiscoveryApi.getBaseUrl).toHaveBeenCalledWith('catalog'); expect(mockDiscoveryApi.getBaseUrl).toHaveBeenCalledWith('techdocs'); expect(documents).toHaveLength(mockSearchDocIndex.docs.length); }); @@ -184,6 +161,7 @@ describe('DefaultTechDocsCollatorFactory', () => { discovery: mockDiscoveryApi, logger, auth: mockServices.auth(), + catalogClient: mockCatalog, }); collator = await factory.getCollator(); diff --git a/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.ts b/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.ts index 53957b08c4..1a2e4af099 100644 --- a/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.ts +++ b/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.ts @@ -16,8 +16,6 @@ import { CATALOG_FILTER_EXISTS, - CatalogApi, - CatalogClient, EntityFilterQuery, } from '@backstage/catalog-client'; import { @@ -45,6 +43,7 @@ import { DiscoveryService, LoggerService, } from '@backstage/backend-plugin-api'; +import { CatalogService } from '@backstage/plugin-catalog-node'; /** * Options to configure the TechDocs collator factory @@ -56,7 +55,7 @@ export type TechDocsCollatorFactoryOptions = { logger: LoggerService; auth: AuthService; locationTemplate?: string; - catalogClient?: CatalogApi; + catalogClient?: CatalogService; parallelismLimit?: number; legacyPathCasing?: boolean; entityTransformer?: TechDocsCollatorEntityTransformer; @@ -86,7 +85,7 @@ export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { private locationTemplate: string; private readonly logger: LoggerService; private readonly auth: AuthService; - private readonly catalogClient: CatalogApi; + private readonly catalogClient: CatalogService; private readonly parallelismLimit: number; private readonly legacyPathCasing: boolean; private entityTransformer: TechDocsCollatorEntityTransformer; @@ -99,9 +98,10 @@ export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { this.locationTemplate = options.locationTemplate || '/docs/:namespace/:kind/:name/:path'; this.logger = options.logger.child({ documentType: this.type }); - this.catalogClient = - options.catalogClient || - new CatalogClient({ discoveryApi: options.discovery }); + if (!options.catalogClient) { + throw new Error('catalogClient is required'); + } + this.catalogClient = options.catalogClient; this.parallelismLimit = options.parallelismLimit ?? 10; this.legacyPathCasing = options.legacyPathCasing ?? false; this.entityTransformer = options.entityTransformer ?? (() => ({})); @@ -147,10 +147,7 @@ export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { // parallelism limit to simplify configuration. const batchSize = this.parallelismLimit * 50; while (moreEntitiesToGet) { - const { token: catalogToken } = await this.auth.getPluginRequestToken({ - onBehalfOf: await this.auth.getOwnServiceCredentials(), - targetPluginId: 'catalog', - }); + const credentials = await this.auth.getOwnServiceCredentials(); const entities = ( await this.catalogClient.getEntities( @@ -163,7 +160,7 @@ export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { limit: batchSize, offset: entitiesRetrieved, }, - { token: catalogToken }, + { credentials }, ) ).items; diff --git a/plugins/search-backend-module-techdocs/src/module.ts b/plugins/search-backend-module-techdocs/src/module.ts index db419169f9..050c075ea2 100644 --- a/plugins/search-backend-module-techdocs/src/module.ts +++ b/plugins/search-backend-module-techdocs/src/module.ts @@ -27,7 +27,7 @@ import { } from '@backstage/backend-plugin-api'; import { EntityFilterQuery } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; -import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; +import { catalogServiceRef } from '@backstage/plugin-catalog-node'; import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha'; import { DefaultTechDocsCollatorFactory } from './collators/DefaultTechDocsCollatorFactory'; import { diff --git a/plugins/techdocs-backend/src/plugin.ts b/plugins/techdocs-backend/src/plugin.ts index ac16e43dab..543d017ae1 100644 --- a/plugins/techdocs-backend/src/plugin.ts +++ b/plugins/techdocs-backend/src/plugin.ts @@ -34,7 +34,7 @@ import { techdocsPreparerExtensionPoint, techdocsPublisherExtensionPoint, } from '@backstage/plugin-techdocs-node'; -import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; +import { catalogServiceRef } from '@backstage/plugin-catalog-node'; import * as winston from 'winston'; import { createRouter } from './service/router'; diff --git a/plugins/techdocs-backend/src/service/CachedEntityLoader.test.ts b/plugins/techdocs-backend/src/service/CachedEntityLoader.test.ts index df5c038dce..82bbfd27d0 100644 --- a/plugins/techdocs-backend/src/service/CachedEntityLoader.test.ts +++ b/plugins/techdocs-backend/src/service/CachedEntityLoader.test.ts @@ -39,8 +39,6 @@ describe('CachedEntityLoader', () => { }, }; - const token = 'test-token'; - const userCredentials: BackstageCredentials = { $$type: '@backstage/BackstageCredentials', principal: { @@ -67,7 +65,7 @@ describe('CachedEntityLoader', () => { auth.isPrincipal.mockReturnValue(true); const loader = new CachedEntityLoader({ auth, catalog, cache }); - const result = await loader.load(userCredentials, entityName, token); + const result = await loader.load(userCredentials, entityName); expect(result).toEqual(entity); expect(cache.set).toHaveBeenCalledWith( @@ -84,7 +82,7 @@ describe('CachedEntityLoader', () => { auth.isPrincipal.mockReturnValue(true); const loader = new CachedEntityLoader({ auth, catalog, cache }); - const result = await loader.load(userCredentials, entityName, token); + const result = await loader.load(userCredentials, entityName); expect(result).toEqual(entity); expect(catalog.getEntityByRef).not.toHaveBeenCalled(); @@ -96,7 +94,7 @@ describe('CachedEntityLoader', () => { auth.isPrincipal.mockReturnValue(true); const loader = new CachedEntityLoader({ auth, catalog, cache }); - const result = await loader.load(userCredentials, entityName, token); + const result = await loader.load(userCredentials, entityName); expect(result).toBeUndefined(); expect(cache.set).not.toHaveBeenCalled(); @@ -108,7 +106,7 @@ describe('CachedEntityLoader', () => { auth.isPrincipal.mockReturnValueOnce(false).mockReturnValueOnce(true); const loader = new CachedEntityLoader({ auth, catalog, cache }); - const result = await loader.load(pluginCredentials, entityName, undefined); + const result = await loader.load(pluginCredentials, entityName); expect(result).toEqual(entity); expect(cache.set).toHaveBeenCalledWith( @@ -131,7 +129,7 @@ describe('CachedEntityLoader', () => { auth.isPrincipal.mockReturnValue(true); const loader = new CachedEntityLoader({ auth, catalog, cache }); - const result = await loader.load(userCredentials, entityName, token); + const result = await loader.load(userCredentials, entityName); expect(result).toEqual(entity); }); @@ -151,8 +149,8 @@ describe('CachedEntityLoader', () => { }, }; - await loader.load(userCredentials, entityName, token); - await loader.load(anotherUserCredentials, entityName, token); + await loader.load(userCredentials, entityName); + await loader.load(anotherUserCredentials, entityName); expect(cache.set).toHaveBeenCalledWith( 'catalog:component:default/test:user:default/test-user', @@ -172,7 +170,7 @@ describe('CachedEntityLoader', () => { auth.isPrincipal.mockReturnValueOnce(false).mockReturnValueOnce(true); const loader = new CachedEntityLoader({ auth, catalog, cache }); - const result = await loader.load(pluginCredentials, entityName, token); + const result = await loader.load(pluginCredentials, entityName); expect(result).toEqual(entity); expect(cache.set).toHaveBeenCalledWith( @@ -197,7 +195,7 @@ describe('CachedEntityLoader', () => { }; const loader = new CachedEntityLoader({ auth, catalog, cache }); - const result = await loader.load(unknownCredentials, entityName, token); + const result = await loader.load(unknownCredentials, entityName); expect(result).toEqual(entity); expect(cache.set).toHaveBeenCalledWith( diff --git a/plugins/techdocs-backend/src/service/CachedEntityLoader.ts b/plugins/techdocs-backend/src/service/CachedEntityLoader.ts index 9f97ed2cd4..8e19bb2411 100644 --- a/plugins/techdocs-backend/src/service/CachedEntityLoader.ts +++ b/plugins/techdocs-backend/src/service/CachedEntityLoader.ts @@ -19,22 +19,22 @@ import { BackstageCredentials, CacheService, } from '@backstage/backend-plugin-api'; -import { CatalogApi } from '@backstage/catalog-client'; import { Entity, CompoundEntityRef, stringifyEntityRef, } from '@backstage/catalog-model'; +import { CatalogService } from '@backstage/plugin-catalog-node'; export type CachedEntityLoaderOptions = { auth: AuthService; - catalog: CatalogApi; + catalog: CatalogService; cache: CacheService; }; export class CachedEntityLoader { private readonly auth: AuthService; - private readonly catalog: CatalogApi; + private readonly catalog: CatalogService; private readonly cache: CacheService; private readonly readTimeout = 1000; @@ -47,7 +47,6 @@ export class CachedEntityLoader { async load( credentials: BackstageCredentials, entityRef: CompoundEntityRef, - token: string | undefined, ): Promise { const cacheKey = this.getCacheKey(entityRef, credentials); let result = await this.getFromCache(cacheKey); @@ -56,7 +55,7 @@ export class CachedEntityLoader { return result; } - result = await this.catalog.getEntityByRef(entityRef, { token }); + result = await this.catalog.getEntityByRef(entityRef, { credentials }); if (result) { this.cache.set(cacheKey, result, { ttl: 5000 }); diff --git a/plugins/techdocs-backend/src/service/router.test.ts b/plugins/techdocs-backend/src/service/router.test.ts index 89c1009c3f..5b409960d0 100644 --- a/plugins/techdocs-backend/src/service/router.test.ts +++ b/plugins/techdocs-backend/src/service/router.test.ts @@ -28,8 +28,8 @@ import { CachedEntityLoader } from './CachedEntityLoader'; import { createEventStream, createRouter, RouterOptions } from './router'; import { TechDocsCache } from '../cache'; import { mockErrorHandler, mockServices } from '@backstage/backend-test-utils'; +import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; -jest.mock('@backstage/catalog-client'); jest.mock('./CachedEntityLoader'); jest.mock('./DocsSynchronizer'); jest.mock('../cache/TechDocsCache'); @@ -107,6 +107,7 @@ describe('createRouter', () => { const docsBuildStrategy: jest.Mocked = { shouldBuild: jest.fn(), }; + const mockCatalogService = catalogServiceMock(); const outOfTheBoxOptions = { preparers, generators, @@ -124,6 +125,7 @@ describe('createRouter', () => { docsBuildStrategy, auth: mockServices.auth(), httpAuth: mockServices.httpAuth(), + catalogClient: mockCatalogService, }; const recommendedOptions = { publisher, @@ -134,6 +136,7 @@ describe('createRouter', () => { docsBuildStrategy, auth: mockServices.auth(), httpAuth: mockServices.httpAuth(), + catalogClient: mockCatalogService, }; beforeEach(() => { diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index 4312e1a5af..b05eefff79 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; import { stringifyEntityRef } from '@backstage/catalog-model'; import { Config, readDurationFromConfig } from '@backstage/config'; import { NotFoundError } from '@backstage/errors'; @@ -41,6 +40,7 @@ import { HttpAuthService, LoggerService, } from '@backstage/backend-plugin-api'; +import { CatalogService } from '@backstage/plugin-catalog-node'; import { durationToMilliseconds } from '@backstage/types'; /** @@ -60,7 +60,7 @@ export type OutOfTheBoxDeploymentOptions = { cache: CacheService; docsBuildStrategy?: DocsBuildStrategy; buildLogTransport?: winston.transport; - catalogClient?: CatalogApi; + catalogClient?: CatalogService; httpAuth: HttpAuthService; auth: AuthService; }; @@ -79,7 +79,7 @@ export type RecommendedDeploymentOptions = { cache: CacheService; docsBuildStrategy?: DocsBuildStrategy; buildLogTransport?: winston.transport; - catalogClient?: CatalogApi; + catalogClient?: CatalogService; httpAuth: HttpAuthService; auth: AuthService; }; @@ -116,8 +116,10 @@ export async function createRouter( const router = Router(); const { publisher, config, logger, discovery, httpAuth, auth } = options; - const catalogClient = - options.catalogClient ?? new CatalogClient({ discoveryApi: discovery }); + if (!options.catalogClient) { + throw new Error('catalogClient is required'); + } + const catalogClient = options.catalogClient; const docsBuildStrategy = options.docsBuildStrategy ?? DefaultDocsBuildStrategy.fromConfig(config); const buildLogTransport = options.buildLogTransport; @@ -163,13 +165,8 @@ export async function createRouter( const credentials = await httpAuth.credentials(req); - const { token } = await auth.getPluginRequestToken({ - onBehalfOf: credentials, - targetPluginId: 'catalog', - }); - // Verify that the related entity exists and the current user has permission to view it. - const entity = await entityLoader.load(credentials, entityName, token); + const entity = await entityLoader.load(credentials, entityName); if (!entity) { throw new NotFoundError( @@ -202,12 +199,7 @@ export async function createRouter( const credentials = await httpAuth.credentials(req); - const { token } = await auth.getPluginRequestToken({ - onBehalfOf: credentials, - targetPluginId: 'catalog', - }); - - const entity = await entityLoader.load(credentials, entityName, token); + const entity = await entityLoader.load(credentials, entityName); if (!entity) { throw new NotFoundError( @@ -240,17 +232,12 @@ export async function createRouter( const credentials = await httpAuth.credentials(req); - const { token } = await auth.getPluginRequestToken({ - onBehalfOf: credentials, - targetPluginId: 'catalog', + const entity = await entityLoader.load(credentials, { + kind, + namespace, + name, }); - const entity = await entityLoader.load( - credentials, - { kind, namespace, name }, - token, - ); - if (!entity?.metadata?.uid) { throw new NotFoundError('Entity metadata UID missing'); } @@ -315,12 +302,7 @@ export async function createRouter( allowLimitedAccess: true, }); - const { token } = await auth.getPluginRequestToken({ - onBehalfOf: credentials, - targetPluginId: 'catalog', - }); - - const entity = await entityLoader.load(credentials, entityName, token); + const entity = await entityLoader.load(credentials, entityName); if (!entity) { throw new NotFoundError(