Merge pull request #30293 from backstage/blam/catalog-action
`catalog`: Implement simple `get-catalog-entity` action
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': minor
|
||||
---
|
||||
|
||||
Implement the action `get-catalog-entity` with the `ActionsRegistry`
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* 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 { createGetCatalogEntityAction } from './createGetCatalogEntityAction';
|
||||
import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils';
|
||||
import { actionsRegistryServiceMock } from '@backstage/backend-test-utils/alpha';
|
||||
|
||||
describe('createGetCatalogEntityAction', () => {
|
||||
it('should throw an error if the entity is not found', async () => {
|
||||
const mockActionsRegistry = actionsRegistryServiceMock();
|
||||
const mockCatalog = catalogServiceMock();
|
||||
|
||||
createGetCatalogEntityAction({
|
||||
catalog: mockCatalog,
|
||||
actionsRegistry: mockActionsRegistry,
|
||||
});
|
||||
|
||||
await expect(
|
||||
mockActionsRegistry.invoke({
|
||||
id: 'test:get-catalog-entity',
|
||||
input: { name: 'test' },
|
||||
}),
|
||||
).rejects.toThrow(`No entity found with name "test"`);
|
||||
});
|
||||
|
||||
it('should throw an error if theres multiple entities found', async () => {
|
||||
const mockActionsRegistry = actionsRegistryServiceMock();
|
||||
const mockCatalog = catalogServiceMock({
|
||||
entities: [
|
||||
{
|
||||
kind: 'Component',
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
metadata: {
|
||||
name: 'test',
|
||||
namespace: 'default',
|
||||
},
|
||||
spec: {
|
||||
type: 'component',
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'API',
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
metadata: {
|
||||
name: 'test',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
createGetCatalogEntityAction({
|
||||
catalog: mockCatalog,
|
||||
actionsRegistry: mockActionsRegistry,
|
||||
});
|
||||
|
||||
await expect(
|
||||
mockActionsRegistry.invoke({
|
||||
id: 'test:get-catalog-entity',
|
||||
input: { name: 'test' },
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
`Multiple entities found with name "test", please provide more specific filters. Entities found: "component:default/test", "api:default/test"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* 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 { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';
|
||||
import { stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { CatalogService } from '@backstage/plugin-catalog-node';
|
||||
|
||||
export const createGetCatalogEntityAction = ({
|
||||
catalog,
|
||||
actionsRegistry,
|
||||
}: {
|
||||
catalog: CatalogService;
|
||||
actionsRegistry: ActionsRegistryService;
|
||||
}) => {
|
||||
actionsRegistry.register({
|
||||
name: 'get-catalog-entity',
|
||||
title: 'Get Catalog Entity',
|
||||
description: `
|
||||
This allows you to get a single entity from the software catalog.
|
||||
Each entity in the software catalog has a unique name, kind, and namespace. The default namespace is "default".
|
||||
Each entity is identified by a unique entity reference, which is a string of the form "kind:namespace/name".
|
||||
`,
|
||||
schema: {
|
||||
input: z =>
|
||||
z.object({
|
||||
kind: z
|
||||
.string()
|
||||
.describe(
|
||||
'The kind of the entity to query. If the kind is unknown it can be omitted.',
|
||||
)
|
||||
.optional(),
|
||||
namespace: z
|
||||
.string()
|
||||
.describe(
|
||||
'The namespace of the entity to query. If the namespace is unknown it can be omitted.',
|
||||
)
|
||||
.optional(),
|
||||
name: z.string().describe('The name of the entity to query'),
|
||||
}),
|
||||
// TODO: is there a better way to do this?
|
||||
output: z => z.object({}).passthrough(),
|
||||
},
|
||||
action: async ({ input, credentials }) => {
|
||||
const filter: Record<string, string> = { 'metadata.name': input.name };
|
||||
|
||||
if (input.kind) {
|
||||
filter.kind = input.kind;
|
||||
}
|
||||
|
||||
if (input.namespace) {
|
||||
filter['metadata.namespace'] = input.namespace;
|
||||
}
|
||||
|
||||
const { items } = await catalog.queryEntities(
|
||||
{ filter },
|
||||
{
|
||||
credentials,
|
||||
},
|
||||
);
|
||||
|
||||
if (items.length === 0) {
|
||||
throw new InputError(`No entity found with name "${input.name}"`);
|
||||
}
|
||||
|
||||
if (items.length > 1) {
|
||||
throw new Error(
|
||||
`Multiple entities found with name "${
|
||||
input.name
|
||||
}", please provide more specific filters. Entities found: ${items
|
||||
.map(item => `"${stringifyEntityRef(item)}"`)
|
||||
.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
const [entity] = items;
|
||||
|
||||
return {
|
||||
output: entity,
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -22,6 +22,7 @@ import { ForwardedError } from '@backstage/errors';
|
||||
import {
|
||||
CatalogProcessor,
|
||||
CatalogProcessorParser,
|
||||
catalogServiceRef,
|
||||
EntityProvider,
|
||||
LocationAnalyzer,
|
||||
PlaceholderResolver,
|
||||
@@ -43,6 +44,8 @@ import { eventsServiceRef } from '@backstage/plugin-events-node';
|
||||
import { Permission } from '@backstage/plugin-permission-common';
|
||||
import { merge } from 'lodash';
|
||||
import { CatalogBuilder } from './CatalogBuilder';
|
||||
import { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha';
|
||||
import { createGetCatalogEntityAction } from '../actions/createGetCatalogEntityAction';
|
||||
|
||||
class CatalogLocationsExtensionPointImpl
|
||||
implements CatalogLocationsExtensionPoint
|
||||
@@ -237,6 +240,8 @@ export const catalogPlugin = createBackendPlugin({
|
||||
httpAuth: coreServices.httpAuth,
|
||||
auditor: coreServices.auditor,
|
||||
events: eventsServiceRef,
|
||||
catalog: catalogServiceRef,
|
||||
actionsRegistry: actionsRegistryServiceRef,
|
||||
},
|
||||
async init({
|
||||
logger,
|
||||
@@ -250,6 +255,8 @@ export const catalogPlugin = createBackendPlugin({
|
||||
scheduler,
|
||||
auth,
|
||||
httpAuth,
|
||||
catalog,
|
||||
actionsRegistry,
|
||||
auditor,
|
||||
events,
|
||||
}) {
|
||||
@@ -313,6 +320,11 @@ export const catalogPlugin = createBackendPlugin({
|
||||
}
|
||||
|
||||
httpRouter.use(router);
|
||||
|
||||
createGetCatalogEntityAction({
|
||||
catalog,
|
||||
actionsRegistry,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user