Merge pull request #27234 from backstage/rugvip/catalog-credentials

plugins/catalog-node: add credentials support for catalog service + promote
This commit is contained in:
Patrik Oldsberg
2024-10-22 14:50:40 +02:00
committed by GitHub
12 changed files with 522 additions and 18 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-catalog-node': minor
---
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.
+5
View File
@@ -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.
@@ -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';
@@ -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(),
+2 -1
View File
@@ -71,6 +71,7 @@
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^"
"@backstage/cli": "workspace:^",
"msw": "^1.0.0"
}
}
+1 -1
View File
@@ -95,7 +95,7 @@ export interface CatalogProcessingExtensionPoint {
// @alpha (undocumented)
export const catalogProcessingExtensionPoint: ExtensionPoint<CatalogProcessingExtensionPoint>;
// @alpha
// @alpha @deprecated (undocumented)
export const catalogServiceRef: ServiceRef<CatalogApi, 'plugin', 'singleton'>;
// (No @packageDocumentation comment for this package)
+103
View File
@@ -5,15 +5,30 @@
```ts
/// <reference types="node" />
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 { 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 +117,94 @@ export type CatalogProcessorResult =
| CatalogProcessorErrorResult
| CatalogProcessorRefreshKeysResult;
// @public
export interface CatalogService {
// (undocumented)
addLocation(
location: AddLocationRequest,
options: CatalogServiceRequestOptions,
): Promise<AddLocationResponse>;
// (undocumented)
getEntities(
request: GetEntitiesRequest | undefined,
options: CatalogServiceRequestOptions,
): Promise<GetEntitiesResponse>;
// (undocumented)
getEntitiesByRefs(
request: GetEntitiesByRefsRequest,
options: CatalogServiceRequestOptions,
): Promise<GetEntitiesByRefsResponse>;
// (undocumented)
getEntityAncestors(
request: GetEntityAncestorsRequest,
options: CatalogServiceRequestOptions,
): Promise<GetEntityAncestorsResponse>;
// (undocumented)
getEntityByRef(
entityRef: string | CompoundEntityRef,
options: CatalogServiceRequestOptions,
): Promise<Entity | undefined>;
// (undocumented)
getEntityFacets(
request: GetEntityFacetsRequest,
options: CatalogServiceRequestOptions,
): Promise<GetEntityFacetsResponse>;
// (undocumented)
getLocationByEntity(
entityRef: string | CompoundEntityRef,
options: CatalogServiceRequestOptions,
): Promise<Location_2 | undefined>;
// (undocumented)
getLocationById(
id: string,
options: CatalogServiceRequestOptions,
): Promise<Location_2 | undefined>;
// (undocumented)
getLocationByRef(
locationRef: string,
options: CatalogServiceRequestOptions,
): Promise<Location_2 | undefined>;
// (undocumented)
queryEntities(
request: QueryEntitiesRequest | undefined,
options: CatalogServiceRequestOptions,
): Promise<QueryEntitiesResponse>;
// (undocumented)
refreshEntity(
entityRef: string,
options: CatalogServiceRequestOptions,
): Promise<void>;
// (undocumented)
removeEntityByUid(
uid: string,
options: CatalogServiceRequestOptions,
): Promise<void>;
// (undocumented)
removeLocationById(
id: string,
options: CatalogServiceRequestOptions,
): Promise<void>;
// (undocumented)
validateEntity(
entity: Entity,
locationRef: string,
options: CatalogServiceRequestOptions,
): Promise<ValidateEntityResponse>;
}
// @public
export const catalogServiceRef: ServiceRef<
CatalogService,
'plugin',
'singleton'
>;
// @public (undocumented)
export interface CatalogServiceRequestOptions {
// (undocumented)
credentials: BackstageCredentials;
}
// @public
export type DeferredEntity = {
entity: Entity;
+26 -1
View File
@@ -14,7 +14,32 @@
* limitations under the License.
*/
export { catalogServiceRef } from './catalogService';
import {
coreServices,
createServiceFactory,
createServiceRef,
} from '@backstage/backend-plugin-api';
import { catalogServiceRef as _catalogServiceRef } from './catalogService';
import { CatalogApi, CatalogClient } from '@backstage/catalog-client';
/**
* @alpha
* @deprecated Use {@link @backstage/plugin-catalog-node#catalogServiceRef} instead
*/
export const catalogServiceRef = createServiceRef<CatalogApi>({
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';
export type { CatalogProcessingExtensionPoint } from './extensions';
@@ -14,12 +14,26 @@
* limitations under the License.
*/
import { createBackendModule } from '@backstage/backend-plugin-api';
import { startTestBackend } from '@backstage/backend-test-utils';
import { CatalogClient } from '@backstage/catalog-client';
import {
createBackendModule,
createServiceFactory,
createServiceRef,
} from '@backstage/backend-plugin-api';
import {
ServiceFactoryTester,
mockCredentials,
mockServices,
registerMswTestHooks,
startTestBackend,
} from '@backstage/backend-test-utils';
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({
@@ -31,7 +45,7 @@ describe('catalogServiceRef', () => {
catalog: catalogServiceRef,
},
async init({ catalog }) {
expect(catalog).toBeInstanceOf(CatalogClient);
expect(catalog.getEntities).toBeDefined();
},
});
},
@@ -41,4 +55,66 @@ 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<void>({ 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<void>({ id: 'unused-dummy' }),
deps: {},
factory() {},
}),
{ dependencies: [mockServices.discovery.factory()] },
);
const catalogService = await tester.getService(catalogServiceRef);
await catalogService.getEntities(
{},
{ credentials: mockCredentials.service() },
);
});
});
+284 -5
View File
@@ -18,23 +18,302 @@ import {
createServiceFactory,
createServiceRef,
coreServices,
BackstageCredentials,
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 {
credentials: BackstageCredentials;
}
/**
* A version of the {@link @backstage/catalog-client#CatalogApi | CatalogApi} that
* requires backend credentials to be passed instead of a token.
*
* @public
*/
export interface CatalogService {
getEntities(
request: GetEntitiesRequest | undefined,
options: CatalogServiceRequestOptions,
): Promise<GetEntitiesResponse>;
getEntitiesByRefs(
request: GetEntitiesByRefsRequest,
options: CatalogServiceRequestOptions,
): Promise<GetEntitiesByRefsResponse>;
queryEntities(
request: QueryEntitiesRequest | undefined,
options: CatalogServiceRequestOptions,
): Promise<QueryEntitiesResponse>;
getEntityAncestors(
request: GetEntityAncestorsRequest,
options: CatalogServiceRequestOptions,
): Promise<GetEntityAncestorsResponse>;
getEntityByRef(
entityRef: string | CompoundEntityRef,
options: CatalogServiceRequestOptions,
): Promise<Entity | undefined>;
removeEntityByUid(
uid: string,
options: CatalogServiceRequestOptions,
): Promise<void>;
refreshEntity(
entityRef: string,
options: CatalogServiceRequestOptions,
): Promise<void>;
getEntityFacets(
request: GetEntityFacetsRequest,
options: CatalogServiceRequestOptions,
): Promise<GetEntityFacetsResponse>;
getLocationById(
id: string,
options: CatalogServiceRequestOptions,
): Promise<Location | undefined>;
getLocationByRef(
locationRef: string,
options: CatalogServiceRequestOptions,
): Promise<Location | undefined>;
addLocation(
location: AddLocationRequest,
options: CatalogServiceRequestOptions,
): Promise<AddLocationResponse>;
removeLocationById(
id: string,
options: CatalogServiceRequestOptions,
): Promise<void>;
getLocationByEntity(
entityRef: string | CompoundEntityRef,
options: CatalogServiceRequestOptions,
): Promise<Location | undefined>;
validateEntity(
entity: Entity,
locationRef: string,
options: CatalogServiceRequestOptions,
): Promise<ValidateEntityResponse>;
}
class DefaultCatalogService implements CatalogService {
readonly #auth: AuthService;
readonly #catalogApi: CatalogApi;
constructor({
catalogApi,
auth,
}: {
catalogApi: CatalogApi;
auth: AuthService;
}) {
this.#catalogApi = catalogApi;
this.#auth = auth;
}
async getEntities(
request: GetEntitiesRequest | undefined,
options: CatalogServiceRequestOptions,
): Promise<GetEntitiesResponse> {
return this.#catalogApi.getEntities(
request,
await this.#getOptions(options),
);
}
async getEntitiesByRefs(
request: GetEntitiesByRefsRequest,
options: CatalogServiceRequestOptions,
): Promise<GetEntitiesByRefsResponse> {
return this.#catalogApi.getEntitiesByRefs(
request,
await this.#getOptions(options),
);
}
async queryEntities(
request: QueryEntitiesRequest | undefined,
options: CatalogServiceRequestOptions,
): Promise<QueryEntitiesResponse> {
return this.#catalogApi.queryEntities(
request,
await this.#getOptions(options),
);
}
async getEntityAncestors(
request: GetEntityAncestorsRequest,
options: CatalogServiceRequestOptions,
): Promise<GetEntityAncestorsResponse> {
return this.#catalogApi.getEntityAncestors(
request,
await this.#getOptions(options),
);
}
async getEntityByRef(
entityRef: string | CompoundEntityRef,
options: CatalogServiceRequestOptions,
): Promise<Entity | undefined> {
return this.#catalogApi.getEntityByRef(
entityRef,
await this.#getOptions(options),
);
}
async removeEntityByUid(
uid: string,
options: CatalogServiceRequestOptions,
): Promise<void> {
return this.#catalogApi.removeEntityByUid(
uid,
await this.#getOptions(options),
);
}
async refreshEntity(
entityRef: string,
options: CatalogServiceRequestOptions,
): Promise<void> {
return this.#catalogApi.refreshEntity(
entityRef,
await this.#getOptions(options),
);
}
async getEntityFacets(
request: GetEntityFacetsRequest,
options: CatalogServiceRequestOptions,
): Promise<GetEntityFacetsResponse> {
return this.#catalogApi.getEntityFacets(
request,
await this.#getOptions(options),
);
}
async getLocationById(
id: string,
options: CatalogServiceRequestOptions,
): Promise<Location | undefined> {
return this.#catalogApi.getLocationById(
id,
await this.#getOptions(options),
);
}
async getLocationByRef(
locationRef: string,
options: CatalogServiceRequestOptions,
): Promise<Location | undefined> {
return this.#catalogApi.getLocationByRef(
locationRef,
await this.#getOptions(options),
);
}
async addLocation(
location: AddLocationRequest,
options: CatalogServiceRequestOptions,
): Promise<AddLocationResponse> {
return this.#catalogApi.addLocation(
location,
await this.#getOptions(options),
);
}
async removeLocationById(
id: string,
options: CatalogServiceRequestOptions,
): Promise<void> {
return this.#catalogApi.removeLocationById(
id,
await this.#getOptions(options),
);
}
async getLocationByEntity(
entityRef: string | CompoundEntityRef,
options: CatalogServiceRequestOptions,
): Promise<Location | undefined> {
return this.#catalogApi.getLocationByEntity(
entityRef,
await this.#getOptions(options),
);
}
async validateEntity(
entity: Entity,
locationRef: string,
options: CatalogServiceRequestOptions,
): Promise<ValidateEntityResponse> {
return this.#catalogApi.validateEntity(
entity,
locationRef,
await this.#getOptions(options),
);
}
async #getOptions(
options: CatalogServiceRequestOptions,
): Promise<CatalogRequestOptions> {
return this.#auth.getPluginRequestToken({
onBehalfOf: options.credentials,
targetPluginId: 'catalog',
});
}
}
/**
* The catalogService provides the catalog API.
* @alpha
*
* @public
*/
export const catalogServiceRef = createServiceRef<CatalogApi>({
export const catalogServiceRef = createServiceRef<CatalogService>({
id: 'catalog-client',
defaultFactory: async service =>
createServiceFactory({
service,
deps: {
auth: coreServices.auth,
discoveryApi: coreServices.discovery,
},
async factory({ discoveryApi }) {
return new CatalogClient({ discoveryApi });
async factory({ auth, discoveryApi }) {
return new DefaultCatalogService({
auth,
catalogApi: new CatalogClient({ discoveryApi }),
});
},
}),
});
+5
View File
@@ -23,3 +23,8 @@
export * from './api';
export * from './conversion';
export * from './processing';
export {
catalogServiceRef,
type CatalogService,
type CatalogServiceRequestOptions,
} from './catalogService';
+1
View File
@@ -5928,6 +5928,7 @@ __metadata:
"@backstage/plugin-permission-common": "workspace:^"
"@backstage/plugin-permission-node": "workspace:^"
"@backstage/types": "workspace:^"
msw: ^1.0.0
languageName: unknown
linkType: soft