[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:
Eric Peterson
2021-04-29 11:38:21 +02:00
committed by GitHub
parent 12cbc6a4af
commit a99e0bc42c
4 changed files with 128 additions and 7 deletions
+5
View File
@@ -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.
+1 -1
View File
@@ -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) || '',
};
},
);