diff --git a/.changeset/lemon-foxes-breathe.md b/.changeset/lemon-foxes-breathe.md new file mode 100644 index 0000000000..368c620f2a --- /dev/null +++ b/.changeset/lemon-foxes-breathe.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Add Scaffolder action `catalog:fetch` to get entity by entity reference from catalog diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index f3abd574e6..1a3c1db03f 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -96,6 +96,14 @@ export function createDebugLogAction(): TemplateAction<{ listWorkspace?: boolean | undefined; }>; +// @public +export function createFetchCatalogEntityAction(options: { + catalogClient: CatalogApi; +}): TemplateAction<{ + entityRef: string; + optional?: boolean | undefined; +}>; + // @public export function createFetchPlainAction(options: { reader: UrlReader; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts new file mode 100644 index 0000000000..f8bdf2bdcb --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts @@ -0,0 +1,112 @@ +/* + * Copyright 2021 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 { PassThrough } from 'stream'; +import os from 'os'; +import { getVoidLogger } from '@backstage/backend-common'; +import { CatalogApi } from '@backstage/catalog-client'; +import { Entity } from '@backstage/catalog-model'; +import { createFetchCatalogEntityAction } from './fetch'; + +describe('catalog:fetch', () => { + const getEntityByRef = jest.fn(); + const catalogClient = { + getEntityByRef: getEntityByRef, + }; + + const action = createFetchCatalogEntityAction({ + catalogClient: catalogClient as unknown as CatalogApi, + }); + + const mockContext = { + workspacePath: os.tmpdir(), + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + secrets: { backstageToken: 'secret' }, + }; + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should return entity from catalog', async () => { + getEntityByRef.mockReturnValueOnce({ + metadata: { + namespace: 'default', + name: 'test', + }, + kind: 'Component', + } as Entity); + + await action.handler({ + ...mockContext, + input: { + entityRef: 'component:default/test', + }, + }); + + expect(getEntityByRef).toHaveBeenCalledWith('component:default/test', { + token: 'secret', + }); + expect(mockContext.output).toHaveBeenCalledWith('entity', { + metadata: { + namespace: 'default', + name: 'test', + }, + kind: 'Component', + }); + }); + + it('should return null if entity not in catalog and optional is true', async () => { + getEntityByRef.mockImplementationOnce(() => { + throw new Error('Not found'); + }); + + await action.handler({ + ...mockContext, + input: { + entityRef: 'component:default/test', + optional: true, + }, + }); + + expect(getEntityByRef).toHaveBeenCalledWith('component:default/test', { + token: 'secret', + }); + expect(mockContext.output).toHaveBeenCalledWith('entity', null); + }); + + it('should throw error if entity not in catalog and optional is false', async () => { + getEntityByRef.mockImplementationOnce(() => { + throw new Error('Not found'); + }); + + await expect( + action.handler({ + ...mockContext, + input: { + entityRef: 'component:default/test', + }, + }), + ).rejects.toThrow('Not found'); + + expect(getEntityByRef).toHaveBeenCalledWith('component:default/test', { + token: 'secret', + }); + expect(mockContext.output).not.toHaveBeenCalled(); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.ts new file mode 100644 index 0000000000..c45eb62fce --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.ts @@ -0,0 +1,99 @@ +/* + * Copyright 2021 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 { CatalogApi } from '@backstage/catalog-client'; +import { createTemplateAction } from '../../createTemplateAction'; +import yaml from 'yaml'; + +const id = 'catalog:fetch'; + +const examples = [ + { + description: 'Fetch entity by reference', + example: yaml.stringify({ + steps: [ + { + action: id, + id: 'fetch', + name: 'Fetch catalog entity', + input: { + entityRef: 'component:default/name', + }, + }, + ], + }), + }, +]; + +/** + * Returns entity from the catalog by entity reference. + * @public + */ +export function createFetchCatalogEntityAction(options: { + catalogClient: CatalogApi; +}) { + const { catalogClient } = options; + + return createTemplateAction<{ entityRef: string; optional?: boolean }>({ + id, + description: 'Returns entity from the catalog by entity reference', + examples, + schema: { + input: { + required: ['entityRef'], + type: 'object', + properties: { + entityRef: { + type: 'string', + title: 'Entity reference', + description: 'Entity reference of the entity to get', + }, + optional: { + title: 'Optional', + description: + 'Permit the entity to optionally exist. Default: false', + type: 'boolean', + }, + }, + }, + output: { + type: 'object', + properties: { + entity: { + title: 'Entity found by the entity reference', + type: 'object', + description: + 'Object containing same values used in the Entity schema.', + }, + }, + }, + }, + async handler(ctx) { + const { entityRef, optional } = ctx.input; + let entity; + try { + entity = await catalogClient.getEntityByRef(entityRef, { + token: ctx.secrets?.backstageToken, + }); + } catch (e) { + if (!optional) { + throw e; + } + } + ctx.output('entity', entity ?? null); + }, + }); +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/index.ts index 1cd683dcf5..2aeb3f4a32 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/index.ts @@ -16,3 +16,4 @@ export { createCatalogRegisterAction } from './register'; export { createCatalogWriteAction } from './write'; +export { createFetchCatalogEntityAction } from './fetch'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index e388a70f00..7e954295a1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -26,6 +26,7 @@ import { JsonObject } from '@backstage/types'; import { createCatalogRegisterAction, createCatalogWriteAction, + createFetchCatalogEntityAction, } from './catalog'; import { TemplateFilter, TemplateGlobal } from '../../../lib'; @@ -160,6 +161,7 @@ export const createBuiltinActions = ( }), createDebugLogAction(), createCatalogRegisterAction({ catalogClient, integrations }), + createFetchCatalogEntityAction({ catalogClient }), createCatalogWriteAction(), createFilesystemDeleteAction(), createFilesystemRenameAction(),