[Search] Improvements to DefaultCatalogCollator (#5491)
* Add owner/lifecycle to default catalog document and allow location to be configured by integrators. Signed-off-by: Eric Peterson <ericpeterson@spotify.com> * Test the DefaultCatalogCollator Signed-off-by: Eric Peterson <ericpeterson@spotify.com> * Changeset. Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Entity lifecycle and owner are now indexed by the `DefaultCatalogCollator`. A `locationTemplate` may now optionally be provided to its constructor to reflect a custom catalog entity path in the Backstage frontend.
|
||||
@@ -32,7 +32,7 @@ export default async function createPlugin({
|
||||
indexBuilder.addCollator({
|
||||
type: 'software-catalog',
|
||||
defaultRefreshIntervalSeconds: 600,
|
||||
collator: new DefaultCatalogCollator(discovery),
|
||||
collator: new DefaultCatalogCollator({ discovery }),
|
||||
});
|
||||
|
||||
const { scheduler } = await indexBuilder.build();
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { DefaultCatalogCollator } from './DefaultCatalogCollator';
|
||||
|
||||
const expectedEntities: Entity[] = [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'test-entity',
|
||||
description: 'The expected description',
|
||||
},
|
||||
spec: {
|
||||
type: 'some-type',
|
||||
lifecycle: 'experimental',
|
||||
owner: 'someone',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
jest.mock('cross-fetch', () => ({
|
||||
__esModule: true,
|
||||
default: async () => {
|
||||
return {
|
||||
json: async () => {
|
||||
return expectedEntities;
|
||||
},
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
describe('DefaultCatalogCollator', () => {
|
||||
let mockDiscoveryApi: jest.Mocked<PluginEndpointDiscovery>;
|
||||
let collator: DefaultCatalogCollator;
|
||||
|
||||
beforeEach(() => {
|
||||
mockDiscoveryApi = {
|
||||
getBaseUrl: jest.fn().mockResolvedValueOnce('http://localhost:7000'),
|
||||
getExternalBaseUrl: jest.fn(),
|
||||
};
|
||||
collator = new DefaultCatalogCollator({ discovery: mockDiscoveryApi });
|
||||
});
|
||||
|
||||
it('fetches from the configured catalog service', async () => {
|
||||
const documents = await collator.execute();
|
||||
expect(mockDiscoveryApi.getBaseUrl).toHaveBeenCalledWith('catalog');
|
||||
expect(documents).toHaveLength(expectedEntities.length);
|
||||
});
|
||||
|
||||
it('maps a returned entity to an expected CatalogEntityDocument', async () => {
|
||||
const documents = await collator.execute();
|
||||
expect(documents[0]).toMatchObject({
|
||||
title: expectedEntities[0].metadata.name,
|
||||
location: '/catalog/default/component/test-entity',
|
||||
text: expectedEntities[0].metadata.description,
|
||||
namespace: 'default',
|
||||
componentType: expectedEntities[0]!.spec!.type,
|
||||
lifecycle: expectedEntities[0]!.spec!.lifecycle,
|
||||
owner: expectedEntities[0]!.spec!.owner,
|
||||
});
|
||||
});
|
||||
|
||||
it('maps a returned entity with a custom locationTemplate', async () => {
|
||||
// Provide an alternate location template.
|
||||
collator = new DefaultCatalogCollator({
|
||||
discovery: mockDiscoveryApi,
|
||||
locationTemplate: '/software/:name',
|
||||
});
|
||||
|
||||
const documents = await collator.execute();
|
||||
expect(documents[0]).toMatchObject({
|
||||
location: '/software/test-entity',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -23,13 +23,35 @@ export interface CatalogEntityDocument extends IndexableDocument {
|
||||
componentType: string;
|
||||
namespace: string;
|
||||
kind: string;
|
||||
lifecycle: string;
|
||||
owner: string;
|
||||
}
|
||||
|
||||
export class DefaultCatalogCollator implements DocumentCollator {
|
||||
protected discovery: PluginEndpointDiscovery;
|
||||
protected locationTemplate: string;
|
||||
|
||||
constructor(discovery: PluginEndpointDiscovery) {
|
||||
constructor({
|
||||
discovery,
|
||||
locationTemplate,
|
||||
}: {
|
||||
discovery: PluginEndpointDiscovery;
|
||||
locationTemplate?: string;
|
||||
}) {
|
||||
this.discovery = discovery;
|
||||
this.locationTemplate =
|
||||
locationTemplate || '/catalog/:namespace/:kind/:name';
|
||||
}
|
||||
|
||||
protected applyArgsToFormat(
|
||||
format: string,
|
||||
args: Record<string, string>,
|
||||
): string {
|
||||
let formatted = format;
|
||||
for (const [key, value] of Object.entries(args)) {
|
||||
formatted = formatted.replace(`:${key}`, value);
|
||||
}
|
||||
return formatted.toLowerCase();
|
||||
}
|
||||
|
||||
async execute() {
|
||||
@@ -37,17 +59,20 @@ export class DefaultCatalogCollator implements DocumentCollator {
|
||||
const res = await fetch(`${baseUrl}/entities`);
|
||||
const entities: Entity[] = await res.json();
|
||||
return entities.map(
|
||||
(entity): CatalogEntityDocument => {
|
||||
(entity: Entity): CatalogEntityDocument => {
|
||||
return {
|
||||
title: entity.metadata.name,
|
||||
// TODO: Use a config-based template approach for entity location.
|
||||
location: `/catalog/${
|
||||
entity.metadata.namespace || 'default'
|
||||
}/component/${entity.metadata.name}`,
|
||||
location: this.applyArgsToFormat(this.locationTemplate, {
|
||||
namespace: entity.metadata.namespace || 'default',
|
||||
kind: entity.kind,
|
||||
name: entity.metadata.name,
|
||||
}),
|
||||
text: entity.metadata.description || '',
|
||||
componentType: entity.spec?.type?.toString() || 'other',
|
||||
namespace: entity.metadata.namespace || 'default',
|
||||
kind: entity.kind,
|
||||
lifecycle: (entity.spec?.lifecycle as string) || '',
|
||||
owner: (entity.spec?.owner as string) || '',
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user