From 7aae8e302e54a6e0eb5a019be2ac4ba4c97cc46b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 17 Oct 2024 17:38:47 +0200 Subject: [PATCH 1/6] backend-test-utils: fix discovery mock factory Signed-off-by: Patrik Oldsberg --- .changeset/selfish-ads-count.md | 5 +++++ .../src/next/services/mockServices.ts | 12 +++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 .changeset/selfish-ads-count.md diff --git a/.changeset/selfish-ads-count.md b/.changeset/selfish-ads-count.md new file mode 100644 index 0000000000..2f2f0597dd --- /dev/null +++ b/.changeset/selfish-ads-count.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +The `mockServices.discovery.factory()` factory now uses the mocked discovery service as its implementation, avoid the need for configuration. diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index 789b75de9d..55632f4734 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -16,10 +16,7 @@ import { cacheServiceFactory } from '@backstage/backend-defaults/cache'; import { databaseServiceFactory } from '@backstage/backend-defaults/database'; -import { - HostDiscovery, - discoveryServiceFactory, -} from '@backstage/backend-defaults/discovery'; +import { HostDiscovery } from '@backstage/backend-defaults/discovery'; import { httpRouterServiceFactory } from '@backstage/backend-defaults/httpRouter'; import { lifecycleServiceFactory } from '@backstage/backend-defaults/lifecycle'; import { loggerServiceFactory } from '@backstage/backend-defaults/logger'; @@ -273,7 +270,12 @@ export namespace mockServices { ); } export namespace discovery { - export const factory = () => discoveryServiceFactory; + export const factory = () => + createServiceFactory({ + service: coreServices.discovery, + deps: {}, + factory: () => discovery(), + }); export const mock = simpleMock(coreServices.discovery, () => ({ getBaseUrl: jest.fn(), getExternalBaseUrl: jest.fn(), From bc13b42a49a6defdc515c8cb79398fc3317c4406 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 17 Oct 2024 17:48:22 +0200 Subject: [PATCH 2/6] catalog-node: add new CatalogService with credentials support Signed-off-by: Patrik Oldsberg --- .changeset/rotten-needles-relax.md | 5 + plugins/catalog-node/package.json | 3 +- plugins/catalog-node/report-alpha.api.md | 25 +- .../catalog-node/src/catalogService.test.ts | 134 ++++++++- plugins/catalog-node/src/catalogService.ts | 256 +++++++++++++++++- yarn.lock | 1 + 6 files changed, 415 insertions(+), 9 deletions(-) create mode 100644 .changeset/rotten-needles-relax.md diff --git a/.changeset/rotten-needles-relax.md b/.changeset/rotten-needles-relax.md new file mode 100644 index 0000000000..403e06146a --- /dev/null +++ b/.changeset/rotten-needles-relax.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-node': minor +--- + +The `catalogServiceRef` now have its own accompanying `CatalogService` interface, which also supports passing Backstage `credentials` objects in addition to tokens. diff --git a/plugins/catalog-node/package.json b/plugins/catalog-node/package.json index 351b0cd58b..526bacefe4 100644 --- a/plugins/catalog-node/package.json +++ b/plugins/catalog-node/package.json @@ -71,6 +71,7 @@ }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", - "@backstage/cli": "workspace:^" + "@backstage/cli": "workspace:^", + "msw": "^1.0.0" } } diff --git a/plugins/catalog-node/report-alpha.api.md b/plugins/catalog-node/report-alpha.api.md index ba02079ceb..de5bea0811 100644 --- a/plugins/catalog-node/report-alpha.api.md +++ b/plugins/catalog-node/report-alpha.api.md @@ -3,20 +3,37 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AddLocationRequest } from '@backstage/catalog-client'; +import { AddLocationResponse } from '@backstage/catalog-client'; +import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { CatalogProcessor } from '@backstage/plugin-catalog-node'; import { CatalogProcessorParser } from '@backstage/plugin-catalog-node'; +import { CatalogRequestOptions } from '@backstage/catalog-client'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { EntitiesSearchFilter } from '@backstage/plugin-catalog-node'; import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-node'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; +import { GetEntitiesByRefsRequest } from '@backstage/catalog-client'; +import { GetEntitiesByRefsResponse } from '@backstage/catalog-client'; +import { GetEntitiesRequest } from '@backstage/catalog-client'; +import { GetEntitiesResponse } from '@backstage/catalog-client'; +import { GetEntityAncestorsRequest } from '@backstage/catalog-client'; +import { GetEntityAncestorsResponse } from '@backstage/catalog-client'; +import { GetEntityFacetsRequest } from '@backstage/catalog-client'; +import { GetEntityFacetsResponse } from '@backstage/catalog-client'; +import { Location as Location_2 } from '@backstage/catalog-client'; import { LocationAnalyzer } from '@backstage/plugin-catalog-node'; import { Permission } from '@backstage/plugin-permission-common'; import { PermissionRule } from '@backstage/plugin-permission-node'; import { PermissionRuleParams } from '@backstage/plugin-permission-common'; import { PlaceholderResolver } from '@backstage/plugin-catalog-node'; +import { QueryEntitiesRequest } from '@backstage/catalog-client'; +import { QueryEntitiesResponse } from '@backstage/catalog-client'; import { ScmLocationAnalyzer } from '@backstage/plugin-catalog-node'; import { ServiceRef } from '@backstage/backend-plugin-api'; +import { ValidateEntityResponse } from '@backstage/catalog-client'; import { Validators } from '@backstage/catalog-model'; // @alpha (undocumented) @@ -95,8 +112,14 @@ export interface CatalogProcessingExtensionPoint { // @alpha (undocumented) export const catalogProcessingExtensionPoint: ExtensionPoint; +// Warning: (ae-forgotten-export) The symbol "CatalogService" needs to be exported by the entry point alpha.d.ts +// // @alpha -export const catalogServiceRef: ServiceRef; +export const catalogServiceRef: ServiceRef< + CatalogService, + 'plugin', + 'singleton' +>; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/catalog-node/src/catalogService.test.ts b/plugins/catalog-node/src/catalogService.test.ts index cf7b10009c..387817df98 100644 --- a/plugins/catalog-node/src/catalogService.test.ts +++ b/plugins/catalog-node/src/catalogService.test.ts @@ -14,12 +14,27 @@ * limitations under the License. */ -import { createBackendModule } from '@backstage/backend-plugin-api'; -import { startTestBackend } from '@backstage/backend-test-utils'; +import { + createBackendModule, + createServiceFactory, + createServiceRef, +} from '@backstage/backend-plugin-api'; +import { + ServiceFactoryTester, + mockCredentials, + mockServices, + registerMswTestHooks, + startTestBackend, +} from '@backstage/backend-test-utils'; import { CatalogClient } from '@backstage/catalog-client'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; import { catalogServiceRef } from './catalogService'; describe('catalogServiceRef', () => { + const server = setupServer(); + registerMswTestHooks(server); + it('should return a catalogClient', async () => { expect.assertions(1); const testModule = createBackendModule({ @@ -41,4 +56,119 @@ describe('catalogServiceRef', () => { features: [testModule], }); }); + + it('should inject token from user credentials', async () => { + expect.assertions(1); + + server.use( + rest.get('http://localhost/api/catalog/entities', (req, res, ctx) => { + expect(req.headers.get('authorization')).toBe( + mockCredentials.service.header({ + onBehalfOf: mockCredentials.user(), + targetPluginId: 'catalog', + }), + ); + return res(ctx.json({})); + }), + ); + const tester = ServiceFactoryTester.from( + createServiceFactory({ + service: createServiceRef({ id: 'unused-dummy' }), + deps: {}, + factory() {}, + }), + { dependencies: [mockServices.discovery.factory()] }, + ); + + const catalogService = await tester.getService(catalogServiceRef); + + await catalogService.getEntities( + {}, + { credentials: mockCredentials.user() }, + ); + }); + + it('should inject token from service credentials', async () => { + expect.assertions(1); + + server.use( + rest.get('http://localhost/api/catalog/entities', (req, res, ctx) => { + expect(req.headers.get('authorization')).toBe( + mockCredentials.service.header({ + onBehalfOf: mockCredentials.service(), + targetPluginId: 'catalog', + }), + ); + return res(ctx.json({})); + }), + ); + const tester = ServiceFactoryTester.from( + createServiceFactory({ + service: createServiceRef({ id: 'unused-dummy' }), + deps: {}, + factory() {}, + }), + { dependencies: [mockServices.discovery.factory()] }, + ); + + const catalogService = await tester.getService(catalogServiceRef); + + await catalogService.getEntities( + {}, + { credentials: mockCredentials.service() }, + ); + }); + + it('should call with token', async () => { + expect.assertions(1); + + server.use( + rest.get('http://localhost/api/catalog/entities', (req, res, ctx) => { + expect(req.headers.get('authorization')).toBe( + mockCredentials.user.header(), + ); + return res(ctx.json({})); + }), + ); + const tester = ServiceFactoryTester.from( + createServiceFactory({ + service: createServiceRef({ id: 'unused-dummy' }), + deps: {}, + factory() {}, + }), + { dependencies: [mockServices.discovery.factory()] }, + ); + + const catalogService = await tester.getService(catalogServiceRef); + + await catalogService.getEntities( + {}, + { + token: mockCredentials.user.token(), + }, + ); + }); + + it('should call without credentials', async () => { + expect.assertions(1); + + server.use( + rest.get('http://localhost/api/catalog/entities', (req, res, ctx) => { + expect(req.headers.get('authorization')).toBeFalsy(); + return res(ctx.json({})); + }), + ); + const tester = ServiceFactoryTester.from( + createServiceFactory({ + service: createServiceRef({ id: 'unused-dummy' }), + deps: {}, + factory() {}, + }), + { dependencies: [mockServices.discovery.factory()] }, + ); + + const catalogService = await tester.getService(catalogServiceRef); + + await catalogService.getEntities(); + }); }); diff --git a/plugins/catalog-node/src/catalogService.ts b/plugins/catalog-node/src/catalogService.ts index 9a8ff55e1a..a018483ca4 100644 --- a/plugins/catalog-node/src/catalogService.ts +++ b/plugins/catalog-node/src/catalogService.ts @@ -18,23 +18,269 @@ import { createServiceFactory, createServiceRef, coreServices, + BackstageCredentials, + DiscoveryService, + AuthService, } from '@backstage/backend-plugin-api'; -import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; +import { + AddLocationRequest, + AddLocationResponse, + CatalogApi, + CatalogClient, + CatalogRequestOptions, + GetEntitiesByRefsRequest, + GetEntitiesByRefsResponse, + GetEntitiesRequest, + GetEntitiesResponse, + GetEntityAncestorsRequest, + GetEntityAncestorsResponse, + GetEntityFacetsRequest, + GetEntityFacetsResponse, + Location, + QueryEntitiesRequest, + QueryEntitiesResponse, + ValidateEntityResponse, +} from '@backstage/catalog-client'; +import { CompoundEntityRef, Entity } from '@backstage/catalog-model'; + +/** + * @public + */ +export interface CatalogServiceRequestOptions extends CatalogRequestOptions { + credentials?: BackstageCredentials; +} + +/** + * A version of the {@link CatalogApi | CatalogApi} that + * accepts backend credentials in addition to a token. + * + * @public + */ +export interface CatalogService extends CatalogApi { + getEntities( + request?: GetEntitiesRequest, + options?: CatalogServiceRequestOptions, + ): Promise; + + getEntitiesByRefs( + request: GetEntitiesByRefsRequest, + options?: CatalogServiceRequestOptions, + ): Promise; + + queryEntities( + request?: QueryEntitiesRequest, + options?: CatalogServiceRequestOptions, + ): Promise; + + getEntityAncestors( + request: GetEntityAncestorsRequest, + options?: CatalogServiceRequestOptions, + ): Promise; + + getEntityByRef( + entityRef: string | CompoundEntityRef, + options?: CatalogServiceRequestOptions, + ): Promise; + + removeEntityByUid( + uid: string, + options?: CatalogServiceRequestOptions, + ): Promise; + + refreshEntity( + entityRef: string, + options?: CatalogServiceRequestOptions, + ): Promise; + + getEntityFacets( + request: GetEntityFacetsRequest, + options?: CatalogServiceRequestOptions, + ): Promise; + + getLocationById( + id: string, + options?: CatalogServiceRequestOptions, + ): Promise; + + getLocationByRef( + locationRef: string, + options?: CatalogServiceRequestOptions, + ): Promise; + + addLocation( + location: AddLocationRequest, + options?: CatalogServiceRequestOptions, + ): Promise; + + removeLocationById( + id: string, + options?: CatalogServiceRequestOptions, + ): Promise; + + getLocationByEntity( + entityRef: string | CompoundEntityRef, + options?: CatalogServiceRequestOptions, + ): Promise; + + validateEntity( + entity: Entity, + locationRef: string, + options?: CatalogServiceRequestOptions, + ): Promise; +} + +class DefaultCatalogService extends CatalogClient { + readonly #auth: AuthService; + + constructor({ + discoveryApi, + auth, + }: { + discoveryApi: DiscoveryService; + auth: AuthService; + }) { + super({ discoveryApi }); + this.#auth = auth; + } + + async getEntities( + request?: GetEntitiesRequest, + options?: CatalogServiceRequestOptions, + ): Promise { + return super.getEntities(request, await this.#getOptions(options)); + } + + async getEntitiesByRefs( + request: GetEntitiesByRefsRequest, + options?: CatalogServiceRequestOptions, + ): Promise { + return super.getEntitiesByRefs(request, await this.#getOptions(options)); + } + + async queryEntities( + request?: QueryEntitiesRequest, + options?: CatalogServiceRequestOptions, + ): Promise { + return super.queryEntities(request, await this.#getOptions(options)); + } + + async getEntityAncestors( + request: GetEntityAncestorsRequest, + options?: CatalogServiceRequestOptions, + ): Promise { + return super.getEntityAncestors(request, await this.#getOptions(options)); + } + + async getEntityByRef( + entityRef: string | CompoundEntityRef, + options?: CatalogServiceRequestOptions, + ): Promise { + return super.getEntityByRef(entityRef, await this.#getOptions(options)); + } + + async removeEntityByUid( + uid: string, + options?: CatalogServiceRequestOptions, + ): Promise { + return super.removeEntityByUid(uid, await this.#getOptions(options)); + } + + async refreshEntity( + entityRef: string, + options?: CatalogServiceRequestOptions, + ): Promise { + return super.refreshEntity(entityRef, await this.#getOptions(options)); + } + + async getEntityFacets( + request: GetEntityFacetsRequest, + options?: CatalogServiceRequestOptions, + ): Promise { + return super.getEntityFacets(request, await this.#getOptions(options)); + } + + async getLocationById( + id: string, + options?: CatalogServiceRequestOptions, + ): Promise { + return super.getLocationById(id, await this.#getOptions(options)); + } + + async getLocationByRef( + locationRef: string, + options?: CatalogServiceRequestOptions, + ): Promise { + return super.getLocationByRef(locationRef, await this.#getOptions(options)); + } + + async addLocation( + location: AddLocationRequest, + options?: CatalogServiceRequestOptions, + ): Promise { + return super.addLocation(location, await this.#getOptions(options)); + } + + async removeLocationById( + id: string, + options?: CatalogServiceRequestOptions, + ): Promise { + return super.removeLocationById(id, await this.#getOptions(options)); + } + + async getLocationByEntity( + entityRef: string | CompoundEntityRef, + options?: CatalogServiceRequestOptions, + ): Promise { + return super.getLocationByEntity( + entityRef, + await this.#getOptions(options), + ); + } + + async validateEntity( + entity: Entity, + locationRef: string, + options?: CatalogServiceRequestOptions, + ): Promise { + return super.validateEntity( + entity, + locationRef, + await this.#getOptions(options), + ); + } + + async #getOptions( + options?: CatalogServiceRequestOptions, + ): Promise { + if (options?.token) { + return options; + } + if (options?.credentials) { + return this.#auth.getPluginRequestToken({ + onBehalfOf: options.credentials, + targetPluginId: 'catalog', + }); + } + return options; + } +} /** * The catalogService provides the catalog API. - * @alpha + * + * @public */ -export const catalogServiceRef = createServiceRef({ +export const catalogServiceRef = createServiceRef({ id: 'catalog-client', defaultFactory: async service => createServiceFactory({ service, deps: { + auth: coreServices.auth, discoveryApi: coreServices.discovery, }, - async factory({ discoveryApi }) { - return new CatalogClient({ discoveryApi }); + async factory(deps) { + return new DefaultCatalogService(deps); }, }), }); diff --git a/yarn.lock b/yarn.lock index af909aa8c1..f25ccf3289 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5929,6 +5929,7 @@ __metadata: "@backstage/plugin-permission-common": "workspace:^" "@backstage/plugin-permission-node": "workspace:^" "@backstage/types": "workspace:^" + msw: ^1.0.0 languageName: unknown linkType: soft From 33b189263351ec25075b1e4004b72c737931f746 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 17 Oct 2024 19:14:33 +0200 Subject: [PATCH 3/6] catalog-node: promote catalogServiceRef to main entry point Signed-off-by: Patrik Oldsberg --- .changeset/rotten-needles-relax.md | 2 +- .../08-migrating.md | 2 +- plugins/catalog-node/report-alpha.api.md | 27 +---- plugins/catalog-node/report.api.md | 105 ++++++++++++++++++ plugins/catalog-node/src/alpha.ts | 15 ++- plugins/catalog-node/src/catalogService.ts | 2 +- plugins/catalog-node/src/index.ts | 5 + 7 files changed, 129 insertions(+), 29 deletions(-) diff --git a/.changeset/rotten-needles-relax.md b/.changeset/rotten-needles-relax.md index 403e06146a..a34d0a7cb4 100644 --- a/.changeset/rotten-needles-relax.md +++ b/.changeset/rotten-needles-relax.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-node': minor --- -The `catalogServiceRef` now have its own accompanying `CatalogService` interface, which also supports passing Backstage `credentials` objects in addition to tokens. +The `catalogServiceRef` now have its own accompanying `CatalogService` interface, which also supports passing Backstage `credentials` objects in addition to tokens. In addition, it has been promoted from the `/alpha` export and is now available from the main `@backstage/plugin-catalog-node` entry point. diff --git a/docs/backend-system/building-plugins-and-modules/08-migrating.md b/docs/backend-system/building-plugins-and-modules/08-migrating.md index 8b2ea17de8..ea86854b18 100644 --- a/docs/backend-system/building-plugins-and-modules/08-migrating.md +++ b/docs/backend-system/building-plugins-and-modules/08-migrating.md @@ -39,7 +39,7 @@ import { coreServices, createBackendPlugin, } from '@backstage/backend-plugin-api'; -import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; +import { catalogServiceRef } from '@backstage/plugin-catalog-node'; import { Router } from 'express'; import { KubernetesBuilder } from './KubernetesBuilder'; diff --git a/plugins/catalog-node/report-alpha.api.md b/plugins/catalog-node/report-alpha.api.md index de5bea0811..044bc14de3 100644 --- a/plugins/catalog-node/report-alpha.api.md +++ b/plugins/catalog-node/report-alpha.api.md @@ -3,37 +3,20 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { AddLocationRequest } from '@backstage/catalog-client'; -import { AddLocationResponse } from '@backstage/catalog-client'; -import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { CatalogProcessor } from '@backstage/plugin-catalog-node'; import { CatalogProcessorParser } from '@backstage/plugin-catalog-node'; -import { CatalogRequestOptions } from '@backstage/catalog-client'; -import { CompoundEntityRef } from '@backstage/catalog-model'; import { EntitiesSearchFilter } from '@backstage/plugin-catalog-node'; import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-node'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; -import { GetEntitiesByRefsRequest } from '@backstage/catalog-client'; -import { GetEntitiesByRefsResponse } from '@backstage/catalog-client'; -import { GetEntitiesRequest } from '@backstage/catalog-client'; -import { GetEntitiesResponse } from '@backstage/catalog-client'; -import { GetEntityAncestorsRequest } from '@backstage/catalog-client'; -import { GetEntityAncestorsResponse } from '@backstage/catalog-client'; -import { GetEntityFacetsRequest } from '@backstage/catalog-client'; -import { GetEntityFacetsResponse } from '@backstage/catalog-client'; -import { Location as Location_2 } from '@backstage/catalog-client'; import { LocationAnalyzer } from '@backstage/plugin-catalog-node'; import { Permission } from '@backstage/plugin-permission-common'; import { PermissionRule } from '@backstage/plugin-permission-node'; import { PermissionRuleParams } from '@backstage/plugin-permission-common'; import { PlaceholderResolver } from '@backstage/plugin-catalog-node'; -import { QueryEntitiesRequest } from '@backstage/catalog-client'; -import { QueryEntitiesResponse } from '@backstage/catalog-client'; import { ScmLocationAnalyzer } from '@backstage/plugin-catalog-node'; import { ServiceRef } from '@backstage/backend-plugin-api'; -import { ValidateEntityResponse } from '@backstage/catalog-client'; import { Validators } from '@backstage/catalog-model'; // @alpha (undocumented) @@ -112,14 +95,8 @@ export interface CatalogProcessingExtensionPoint { // @alpha (undocumented) export const catalogProcessingExtensionPoint: ExtensionPoint; -// Warning: (ae-forgotten-export) The symbol "CatalogService" needs to be exported by the entry point alpha.d.ts -// -// @alpha -export const catalogServiceRef: ServiceRef< - CatalogService, - 'plugin', - 'singleton' ->; +// @alpha @deprecated (undocumented) +export const catalogServiceRef: ServiceRef; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/catalog-node/report.api.md b/plugins/catalog-node/report.api.md index 43209aa862..f063e7944b 100644 --- a/plugins/catalog-node/report.api.md +++ b/plugins/catalog-node/report.api.md @@ -5,15 +5,32 @@ ```ts /// +import { AddLocationRequest } from '@backstage/catalog-client'; +import { AddLocationResponse } from '@backstage/catalog-client'; import { AnalyzeLocationExistingEntity } from '@backstage/plugin-catalog-common'; import { AnalyzeLocationRequest } from '@backstage/plugin-catalog-common'; import { AnalyzeLocationResponse } from '@backstage/plugin-catalog-common'; import { BackstageCredentials } from '@backstage/backend-plugin-api'; +import { CatalogApi } from '@backstage/catalog-client'; +import { CatalogRequestOptions } from '@backstage/catalog-client'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { Entity } from '@backstage/catalog-model'; +import { GetEntitiesByRefsRequest } from '@backstage/catalog-client'; +import { GetEntitiesByRefsResponse } from '@backstage/catalog-client'; +import { GetEntitiesRequest } from '@backstage/catalog-client'; +import { GetEntitiesResponse } from '@backstage/catalog-client'; +import { GetEntityAncestorsRequest } from '@backstage/catalog-client'; +import { GetEntityAncestorsResponse } from '@backstage/catalog-client'; +import { GetEntityFacetsRequest } from '@backstage/catalog-client'; +import { GetEntityFacetsResponse } from '@backstage/catalog-client'; import { JsonValue } from '@backstage/types'; +import { Location as Location_2 } from '@backstage/catalog-client'; import { LocationEntityV1alpha1 } from '@backstage/catalog-model'; import { LocationSpec as LocationSpec_2 } from '@backstage/plugin-catalog-common'; +import { QueryEntitiesRequest } from '@backstage/catalog-client'; +import { QueryEntitiesResponse } from '@backstage/catalog-client'; +import { ServiceRef } from '@backstage/backend-plugin-api'; +import { ValidateEntityResponse } from '@backstage/catalog-client'; // @public (undocumented) export type AnalyzeOptions = { @@ -102,6 +119,94 @@ export type CatalogProcessorResult = | CatalogProcessorErrorResult | CatalogProcessorRefreshKeysResult; +// @public +export interface CatalogService extends CatalogApi { + // (undocumented) + addLocation( + location: AddLocationRequest, + options?: CatalogServiceRequestOptions, + ): Promise; + // (undocumented) + getEntities( + request?: GetEntitiesRequest, + options?: CatalogServiceRequestOptions, + ): Promise; + // (undocumented) + getEntitiesByRefs( + request: GetEntitiesByRefsRequest, + options?: CatalogServiceRequestOptions, + ): Promise; + // (undocumented) + getEntityAncestors( + request: GetEntityAncestorsRequest, + options?: CatalogServiceRequestOptions, + ): Promise; + // (undocumented) + getEntityByRef( + entityRef: string | CompoundEntityRef, + options?: CatalogServiceRequestOptions, + ): Promise; + // (undocumented) + getEntityFacets( + request: GetEntityFacetsRequest, + options?: CatalogServiceRequestOptions, + ): Promise; + // (undocumented) + getLocationByEntity( + entityRef: string | CompoundEntityRef, + options?: CatalogServiceRequestOptions, + ): Promise; + // (undocumented) + getLocationById( + id: string, + options?: CatalogServiceRequestOptions, + ): Promise; + // (undocumented) + getLocationByRef( + locationRef: string, + options?: CatalogServiceRequestOptions, + ): Promise; + // (undocumented) + queryEntities( + request?: QueryEntitiesRequest, + options?: CatalogServiceRequestOptions, + ): Promise; + // (undocumented) + refreshEntity( + entityRef: string, + options?: CatalogServiceRequestOptions, + ): Promise; + // (undocumented) + removeEntityByUid( + uid: string, + options?: CatalogServiceRequestOptions, + ): Promise; + // (undocumented) + removeLocationById( + id: string, + options?: CatalogServiceRequestOptions, + ): Promise; + // (undocumented) + validateEntity( + entity: Entity, + locationRef: string, + options?: CatalogServiceRequestOptions, + ): Promise; +} + +// @public +export const catalogServiceRef: ServiceRef< + CatalogService, + 'plugin', + 'singleton' +>; + +// @public (undocumented) +export interface CatalogServiceRequestOptions extends CatalogRequestOptions { + // (undocumented) + credentials?: BackstageCredentials; +} + // @public export type DeferredEntity = { entity: Entity; diff --git a/plugins/catalog-node/src/alpha.ts b/plugins/catalog-node/src/alpha.ts index b75ec48a7f..159b642e52 100644 --- a/plugins/catalog-node/src/alpha.ts +++ b/plugins/catalog-node/src/alpha.ts @@ -14,7 +14,20 @@ * limitations under the License. */ -export { catalogServiceRef } from './catalogService'; +import { ServiceRef } from '@backstage/backend-plugin-api'; +import { catalogServiceRef as _catalogServiceRef } from './catalogService'; +import { CatalogApi } from '@backstage/catalog-client'; + +/** + * @alpha + * @deprecated Use {@link @backstage/plugin-catalog-node#catalogServiceRef} instead + */ +export const catalogServiceRef = _catalogServiceRef as ServiceRef< + CatalogApi, + 'plugin', + 'singleton' +>; + export type { CatalogLocationsExtensionPoint } from './extensions'; export { catalogLocationsExtensionPoint } from './extensions'; export type { CatalogProcessingExtensionPoint } from './extensions'; diff --git a/plugins/catalog-node/src/catalogService.ts b/plugins/catalog-node/src/catalogService.ts index a018483ca4..8b81bc68fd 100644 --- a/plugins/catalog-node/src/catalogService.ts +++ b/plugins/catalog-node/src/catalogService.ts @@ -51,7 +51,7 @@ export interface CatalogServiceRequestOptions extends CatalogRequestOptions { } /** - * A version of the {@link CatalogApi | CatalogApi} that + * A version of the {@link @backstage/catalog-client#CatalogApi | CatalogApi} that * accepts backend credentials in addition to a token. * * @public diff --git a/plugins/catalog-node/src/index.ts b/plugins/catalog-node/src/index.ts index 2dd8b5fd5a..9a95b708ba 100644 --- a/plugins/catalog-node/src/index.ts +++ b/plugins/catalog-node/src/index.ts @@ -23,3 +23,8 @@ export * from './api'; export * from './conversion'; export * from './processing'; +export { + catalogServiceRef, + type CatalogService, + type CatalogServiceRequestOptions, +} from './catalogService'; From 3fb2f7c69077d75cc4dcf37e9c9e255b53264a15 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 22 Oct 2024 13:09:46 +0200 Subject: [PATCH 4/6] catalog-node: refactor catalog service to always use credentials Signed-off-by: Patrik Oldsberg --- .changeset/rotten-needles-relax.md | 4 +- plugins/catalog-node/report.api.md | 40 ++-- .../catalog-node/src/catalogService.test.ts | 53 ------ plugins/catalog-node/src/catalogService.ts | 171 +++++++++++------- 4 files changed, 124 insertions(+), 144 deletions(-) diff --git a/.changeset/rotten-needles-relax.md b/.changeset/rotten-needles-relax.md index a34d0a7cb4..ef389402aa 100644 --- a/.changeset/rotten-needles-relax.md +++ b/.changeset/rotten-needles-relax.md @@ -2,4 +2,6 @@ '@backstage/plugin-catalog-node': minor --- -The `catalogServiceRef` now have its own accompanying `CatalogService` interface, which also supports passing Backstage `credentials` objects in addition to tokens. In addition, it has been promoted from the `/alpha` export and is now available from the main `@backstage/plugin-catalog-node` entry point. +The `catalogServiceRef` now has its own accompanying `CatalogService` interface that requires Backstage `credentials` objects to be passed. This new version of the `catalogServiceRef` has been promoted and is now available via the main `@backstage/plugin-catalog-node` entry point. + +The old `catalogServiceRef` with the old `CatalogApi` type is still available from the `/alpha` entry point. diff --git a/plugins/catalog-node/report.api.md b/plugins/catalog-node/report.api.md index f063e7944b..e196fff230 100644 --- a/plugins/catalog-node/report.api.md +++ b/plugins/catalog-node/report.api.md @@ -11,8 +11,6 @@ import { AnalyzeLocationExistingEntity } from '@backstage/plugin-catalog-common' import { AnalyzeLocationRequest } from '@backstage/plugin-catalog-common'; import { AnalyzeLocationResponse } from '@backstage/plugin-catalog-common'; import { BackstageCredentials } from '@backstage/backend-plugin-api'; -import { CatalogApi } from '@backstage/catalog-client'; -import { CatalogRequestOptions } from '@backstage/catalog-client'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { Entity } from '@backstage/catalog-model'; import { GetEntitiesByRefsRequest } from '@backstage/catalog-client'; @@ -120,77 +118,77 @@ export type CatalogProcessorResult = | CatalogProcessorRefreshKeysResult; // @public -export interface CatalogService extends CatalogApi { +export interface CatalogService { // (undocumented) addLocation( location: AddLocationRequest, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise; // (undocumented) getEntities( - request?: GetEntitiesRequest, - options?: CatalogServiceRequestOptions, + request: GetEntitiesRequest | undefined, + options: CatalogServiceRequestOptions, ): Promise; // (undocumented) getEntitiesByRefs( request: GetEntitiesByRefsRequest, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise; // (undocumented) getEntityAncestors( request: GetEntityAncestorsRequest, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise; // (undocumented) getEntityByRef( entityRef: string | CompoundEntityRef, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise; // (undocumented) getEntityFacets( request: GetEntityFacetsRequest, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise; // (undocumented) getLocationByEntity( entityRef: string | CompoundEntityRef, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise; // (undocumented) getLocationById( id: string, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise; // (undocumented) getLocationByRef( locationRef: string, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise; // (undocumented) queryEntities( - request?: QueryEntitiesRequest, - options?: CatalogServiceRequestOptions, + request: QueryEntitiesRequest | undefined, + options: CatalogServiceRequestOptions, ): Promise; // (undocumented) refreshEntity( entityRef: string, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise; // (undocumented) removeEntityByUid( uid: string, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise; // (undocumented) removeLocationById( id: string, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise; // (undocumented) validateEntity( entity: Entity, locationRef: string, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise; } @@ -202,9 +200,9 @@ export const catalogServiceRef: ServiceRef< >; // @public (undocumented) -export interface CatalogServiceRequestOptions extends CatalogRequestOptions { +export interface CatalogServiceRequestOptions { // (undocumented) - credentials?: BackstageCredentials; + credentials: BackstageCredentials; } // @public diff --git a/plugins/catalog-node/src/catalogService.test.ts b/plugins/catalog-node/src/catalogService.test.ts index 387817df98..e6f2668d70 100644 --- a/plugins/catalog-node/src/catalogService.test.ts +++ b/plugins/catalog-node/src/catalogService.test.ts @@ -118,57 +118,4 @@ describe('catalogServiceRef', () => { { credentials: mockCredentials.service() }, ); }); - - it('should call with token', async () => { - expect.assertions(1); - - server.use( - rest.get('http://localhost/api/catalog/entities', (req, res, ctx) => { - expect(req.headers.get('authorization')).toBe( - mockCredentials.user.header(), - ); - return res(ctx.json({})); - }), - ); - const tester = ServiceFactoryTester.from( - createServiceFactory({ - service: createServiceRef({ id: 'unused-dummy' }), - deps: {}, - factory() {}, - }), - { dependencies: [mockServices.discovery.factory()] }, - ); - - const catalogService = await tester.getService(catalogServiceRef); - - await catalogService.getEntities( - {}, - { - token: mockCredentials.user.token(), - }, - ); - }); - - it('should call without credentials', async () => { - expect.assertions(1); - - server.use( - rest.get('http://localhost/api/catalog/entities', (req, res, ctx) => { - expect(req.headers.get('authorization')).toBeFalsy(); - return res(ctx.json({})); - }), - ); - const tester = ServiceFactoryTester.from( - createServiceFactory({ - service: createServiceRef({ id: 'unused-dummy' }), - deps: {}, - factory() {}, - }), - { dependencies: [mockServices.discovery.factory()] }, - ); - - const catalogService = await tester.getService(catalogServiceRef); - - await catalogService.getEntities(); - }); }); diff --git a/plugins/catalog-node/src/catalogService.ts b/plugins/catalog-node/src/catalogService.ts index 8b81bc68fd..5e8c920dad 100644 --- a/plugins/catalog-node/src/catalogService.ts +++ b/plugins/catalog-node/src/catalogService.ts @@ -19,7 +19,6 @@ import { createServiceRef, coreServices, BackstageCredentials, - DiscoveryService, AuthService, } from '@backstage/backend-plugin-api'; import { @@ -46,192 +45,229 @@ import { CompoundEntityRef, Entity } from '@backstage/catalog-model'; /** * @public */ -export interface CatalogServiceRequestOptions extends CatalogRequestOptions { - credentials?: BackstageCredentials; +export interface CatalogServiceRequestOptions { + credentials: BackstageCredentials; } /** * A version of the {@link @backstage/catalog-client#CatalogApi | CatalogApi} that - * accepts backend credentials in addition to a token. + * requires backend credentials to be passed instead of a token. * * @public */ -export interface CatalogService extends CatalogApi { +export interface CatalogService { getEntities( - request?: GetEntitiesRequest, - options?: CatalogServiceRequestOptions, + request: GetEntitiesRequest | undefined, + options: CatalogServiceRequestOptions, ): Promise; getEntitiesByRefs( request: GetEntitiesByRefsRequest, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise; queryEntities( - request?: QueryEntitiesRequest, - options?: CatalogServiceRequestOptions, + request: QueryEntitiesRequest | undefined, + options: CatalogServiceRequestOptions, ): Promise; getEntityAncestors( request: GetEntityAncestorsRequest, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise; getEntityByRef( entityRef: string | CompoundEntityRef, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise; removeEntityByUid( uid: string, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise; refreshEntity( entityRef: string, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise; getEntityFacets( request: GetEntityFacetsRequest, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise; getLocationById( id: string, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise; getLocationByRef( locationRef: string, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise; addLocation( location: AddLocationRequest, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise; removeLocationById( id: string, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise; getLocationByEntity( entityRef: string | CompoundEntityRef, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise; validateEntity( entity: Entity, locationRef: string, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise; } -class DefaultCatalogService extends CatalogClient { +class DefaultCatalogService implements CatalogService { readonly #auth: AuthService; + readonly #catalogApi: CatalogApi; constructor({ - discoveryApi, + catalogApi, auth, }: { - discoveryApi: DiscoveryService; + catalogApi: CatalogApi; auth: AuthService; }) { - super({ discoveryApi }); + this.#catalogApi = catalogApi; this.#auth = auth; } async getEntities( - request?: GetEntitiesRequest, - options?: CatalogServiceRequestOptions, + request: GetEntitiesRequest | undefined, + options: CatalogServiceRequestOptions, ): Promise { - return super.getEntities(request, await this.#getOptions(options)); + return this.#catalogApi.getEntities( + request, + await this.#getOptions(options), + ); } async getEntitiesByRefs( request: GetEntitiesByRefsRequest, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise { - return super.getEntitiesByRefs(request, await this.#getOptions(options)); + return this.#catalogApi.getEntitiesByRefs( + request, + await this.#getOptions(options), + ); } async queryEntities( - request?: QueryEntitiesRequest, - options?: CatalogServiceRequestOptions, + request: QueryEntitiesRequest | undefined, + options: CatalogServiceRequestOptions, ): Promise { - return super.queryEntities(request, await this.#getOptions(options)); + return this.#catalogApi.queryEntities( + request, + await this.#getOptions(options), + ); } async getEntityAncestors( request: GetEntityAncestorsRequest, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise { - return super.getEntityAncestors(request, await this.#getOptions(options)); + return this.#catalogApi.getEntityAncestors( + request, + await this.#getOptions(options), + ); } async getEntityByRef( entityRef: string | CompoundEntityRef, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise { - return super.getEntityByRef(entityRef, await this.#getOptions(options)); + return this.#catalogApi.getEntityByRef( + entityRef, + await this.#getOptions(options), + ); } async removeEntityByUid( uid: string, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise { - return super.removeEntityByUid(uid, await this.#getOptions(options)); + return this.#catalogApi.removeEntityByUid( + uid, + await this.#getOptions(options), + ); } async refreshEntity( entityRef: string, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise { - return super.refreshEntity(entityRef, await this.#getOptions(options)); + return this.#catalogApi.refreshEntity( + entityRef, + await this.#getOptions(options), + ); } async getEntityFacets( request: GetEntityFacetsRequest, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise { - return super.getEntityFacets(request, await this.#getOptions(options)); + return this.#catalogApi.getEntityFacets( + request, + await this.#getOptions(options), + ); } async getLocationById( id: string, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise { - return super.getLocationById(id, await this.#getOptions(options)); + return this.#catalogApi.getLocationById( + id, + await this.#getOptions(options), + ); } async getLocationByRef( locationRef: string, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise { - return super.getLocationByRef(locationRef, await this.#getOptions(options)); + return this.#catalogApi.getLocationByRef( + locationRef, + await this.#getOptions(options), + ); } async addLocation( location: AddLocationRequest, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise { - return super.addLocation(location, await this.#getOptions(options)); + return this.#catalogApi.addLocation( + location, + await this.#getOptions(options), + ); } async removeLocationById( id: string, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise { - return super.removeLocationById(id, await this.#getOptions(options)); + return this.#catalogApi.removeLocationById( + id, + await this.#getOptions(options), + ); } async getLocationByEntity( entityRef: string | CompoundEntityRef, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise { - return super.getLocationByEntity( + return this.#catalogApi.getLocationByEntity( entityRef, await this.#getOptions(options), ); @@ -240,9 +276,9 @@ class DefaultCatalogService extends CatalogClient { async validateEntity( entity: Entity, locationRef: string, - options?: CatalogServiceRequestOptions, + options: CatalogServiceRequestOptions, ): Promise { - return super.validateEntity( + return this.#catalogApi.validateEntity( entity, locationRef, await this.#getOptions(options), @@ -250,18 +286,12 @@ class DefaultCatalogService extends CatalogClient { } async #getOptions( - options?: CatalogServiceRequestOptions, - ): Promise { - if (options?.token) { - return options; - } - if (options?.credentials) { - return this.#auth.getPluginRequestToken({ - onBehalfOf: options.credentials, - targetPluginId: 'catalog', - }); - } - return options; + options: CatalogServiceRequestOptions, + ): Promise { + return this.#auth.getPluginRequestToken({ + onBehalfOf: options.credentials, + targetPluginId: 'catalog', + }); } } @@ -279,8 +309,11 @@ export const catalogServiceRef = createServiceRef({ auth: coreServices.auth, discoveryApi: coreServices.discovery, }, - async factory(deps) { - return new DefaultCatalogService(deps); + async factory({ auth, discoveryApi }) { + return new DefaultCatalogService({ + auth, + catalogApi: new CatalogClient({ discoveryApi }), + }); }, }), }); From 419768feae2ed7f66e871ffea77452031d818cf2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 22 Oct 2024 14:18:41 +0200 Subject: [PATCH 5/6] catalog-node: fix test assertion Signed-off-by: Patrik Oldsberg --- plugins/catalog-node/src/catalogService.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/catalog-node/src/catalogService.test.ts b/plugins/catalog-node/src/catalogService.test.ts index e6f2668d70..b34de6c17c 100644 --- a/plugins/catalog-node/src/catalogService.test.ts +++ b/plugins/catalog-node/src/catalogService.test.ts @@ -26,7 +26,6 @@ import { registerMswTestHooks, startTestBackend, } from '@backstage/backend-test-utils'; -import { CatalogClient } from '@backstage/catalog-client'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { catalogServiceRef } from './catalogService'; @@ -46,7 +45,7 @@ describe('catalogServiceRef', () => { catalog: catalogServiceRef, }, async init({ catalog }) { - expect(catalog).toBeInstanceOf(CatalogClient); + expect(catalog.getEntities).toBeDefined(); }, }); }, From 10b7b62359081424951d45c91028938138602f19 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 22 Oct 2024 14:30:57 +0200 Subject: [PATCH 6/6] catalog-node: restore old alpha client Signed-off-by: Patrik Oldsberg --- plugins/catalog-node/src/alpha.ts | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/plugins/catalog-node/src/alpha.ts b/plugins/catalog-node/src/alpha.ts index 159b642e52..63b817fbb3 100644 --- a/plugins/catalog-node/src/alpha.ts +++ b/plugins/catalog-node/src/alpha.ts @@ -14,19 +14,31 @@ * limitations under the License. */ -import { ServiceRef } from '@backstage/backend-plugin-api'; +import { + coreServices, + createServiceFactory, + createServiceRef, +} from '@backstage/backend-plugin-api'; import { catalogServiceRef as _catalogServiceRef } from './catalogService'; -import { CatalogApi } from '@backstage/catalog-client'; +import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; /** * @alpha * @deprecated Use {@link @backstage/plugin-catalog-node#catalogServiceRef} instead */ -export const catalogServiceRef = _catalogServiceRef as ServiceRef< - CatalogApi, - 'plugin', - 'singleton' ->; +export const catalogServiceRef = createServiceRef({ + id: 'catalog-client', + defaultFactory: async service => + createServiceFactory({ + service, + deps: { + discoveryApi: coreServices.discovery, + }, + async factory({ discoveryApi }) { + return new CatalogClient({ discoveryApi }); + }, + }), +}); export type { CatalogLocationsExtensionPoint } from './extensions'; export { catalogLocationsExtensionPoint } from './extensions';