From a73b3c0b097ded0d3a0b843b3d42a704b4e8f22d Mon Sep 17 00:00:00 2001 From: Andreas Berger Date: Thu, 11 May 2023 10:40:04 +0200 Subject: [PATCH] Add ability to use `defaultNamespace` and `defaultKind` for scaffolder action `catalog:fetch` Signed-off-by: Andreas Berger --- .changeset/giant-students-lie.md | 5 + plugins/scaffolder-backend/api-report.md | 2 + .../actions/builtin/catalog/fetch.test.ts | 354 +++++++++++------- .../actions/builtin/catalog/fetch.ts | 28 +- 4 files changed, 242 insertions(+), 147 deletions(-) create mode 100644 .changeset/giant-students-lie.md diff --git a/.changeset/giant-students-lie.md b/.changeset/giant-students-lie.md new file mode 100644 index 0000000000..fdcb406296 --- /dev/null +++ b/.changeset/giant-students-lie.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Add ability to use `defaultNamespace` and `defaultKind` for scaffolder action `catalog:fetch` diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index c87521151b..07dd5eaf1d 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -119,6 +119,8 @@ export function createFetchCatalogEntityAction(options: { entityRef?: string | undefined; entityRefs?: string[] | undefined; optional?: boolean | undefined; + defaultKind?: string | undefined; + defaultNamespace?: string | undefined; }, { entity?: any; 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 index 1cce7d66cb..bed15f2e39 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts @@ -46,179 +46,249 @@ describe('catalog:fetch', () => { 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 throw error if entity fetch fails from 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(); - }); - - it('should throw error if entity not in catalog and optional is false', async () => { - getEntityByRef.mockReturnValueOnce(null); - - await expect( - action.handler({ - ...mockContext, - input: { - entityRef: 'component:default/test', - }, - }), - ).rejects.toThrow('Entity component:default/test not found'); - - expect(getEntityByRef).toHaveBeenCalledWith('component:default/test', { - token: 'secret', - }); - expect(mockContext.output).not.toHaveBeenCalled(); - }); - - it('should return entities from catalog', async () => { - getEntitiesByRefs.mockReturnValueOnce({ - items: [ - { - metadata: { - namespace: 'default', - name: 'test', - }, - kind: 'Component', - } as Entity, - ], - }); - - await action.handler({ - ...mockContext, - input: { - entityRefs: ['component:default/test'], - }, - }); - - expect(getEntitiesByRefs).toHaveBeenCalledWith( - { entityRefs: ['component:default/test'] }, - { - token: 'secret', - }, - ); - expect(mockContext.output).toHaveBeenCalledWith('entities', [ - { + describe('fetch single entity', () => { + 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 throw error if entity fetch fails from 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(); + }); + + it('should throw error if entity not in catalog and optional is false', async () => { + getEntityByRef.mockReturnValueOnce(null); + + await expect( + action.handler({ + ...mockContext, + input: { + entityRef: 'component:default/test', + }, + }), + ).rejects.toThrow('Entity component:default/test not found'); + + expect(getEntityByRef).toHaveBeenCalledWith('component:default/test', { + token: 'secret', + }); + expect(mockContext.output).not.toHaveBeenCalled(); + }); + + it('should use defaultKind and defaultNamespace if provided', async () => { + const entity: Entity = { + metadata: { + namespace: 'ns', + name: 'test', + }, + kind: 'Group', + } as Entity; + getEntityByRef.mockReturnValueOnce(entity); + + await action.handler({ + ...mockContext, + input: { + entityRef: 'test', + defaultKind: 'Group', + defaultNamespace: 'ns', + }, + }); + + expect(getEntityByRef).toHaveBeenCalledWith('group:ns/test', { + token: 'secret', + }); + expect(mockContext.output).toHaveBeenCalledWith('entity', entity); + }); }); - it('should throw error if undefined is returned for some entity', async () => { - getEntitiesByRefs.mockReturnValueOnce({ - items: [ + describe('fetch multiple entities', () => { + it('should return entities from catalog', async () => { + getEntitiesByRefs.mockReturnValueOnce({ + items: [ + { + metadata: { + namespace: 'default', + name: 'test', + }, + kind: 'Component', + } as Entity, + ], + }); + + await action.handler({ + ...mockContext, + input: { + entityRefs: ['component:default/test'], + }, + }); + + expect(getEntitiesByRefs).toHaveBeenCalledWith( + { entityRefs: ['component:default/test'] }, + { + token: 'secret', + }, + ); + expect(mockContext.output).toHaveBeenCalledWith('entities', [ { metadata: { namespace: 'default', name: 'test', }, kind: 'Component', - } as Entity, - undefined, - ], + }, + ]); }); - await expect( - action.handler({ + it('should throw error if undefined is returned for some entity', async () => { + getEntitiesByRefs.mockReturnValueOnce({ + items: [ + { + metadata: { + namespace: 'default', + name: 'test', + }, + kind: 'Component', + } as Entity, + undefined, + ], + }); + + await expect( + action.handler({ + ...mockContext, + input: { + entityRefs: ['component:default/test', 'component:default/test2'], + optional: false, + }, + }), + ).rejects.toThrow('Entity component:default/test2 not found'); + + expect(getEntitiesByRefs).toHaveBeenCalledWith( + { entityRefs: ['component:default/test', 'component:default/test2'] }, + { + token: 'secret', + }, + ); + expect(mockContext.output).not.toHaveBeenCalled(); + }); + + it('should return null in case some of the entities not found and optional is true', async () => { + getEntitiesByRefs.mockReturnValueOnce({ + items: [ + { + metadata: { + namespace: 'default', + name: 'test', + }, + kind: 'Component', + } as Entity, + undefined, + ], + }); + + await action.handler({ ...mockContext, input: { entityRefs: ['component:default/test', 'component:default/test2'], - optional: false, + optional: true, }, - }), - ).rejects.toThrow('Entity component:default/test2 not found'); + }); - expect(getEntitiesByRefs).toHaveBeenCalledWith( - { entityRefs: ['component:default/test', 'component:default/test2'] }, - { - token: 'secret', - }, - ); - expect(mockContext.output).not.toHaveBeenCalled(); - }); - - it('should return null in case some of the entities not found and optional is true', async () => { - getEntitiesByRefs.mockReturnValueOnce({ - items: [ + expect(getEntitiesByRefs).toHaveBeenCalledWith( + { entityRefs: ['component:default/test', 'component:default/test2'] }, + { + token: 'secret', + }, + ); + expect(mockContext.output).toHaveBeenCalledWith('entities', [ { metadata: { namespace: 'default', name: 'test', }, kind: 'Component', - } as Entity, - undefined, - ], + }, + null, + ]); }); - await action.handler({ - ...mockContext, - input: { - entityRefs: ['component:default/test', 'component:default/test2'], - optional: true, - }, - }); - - expect(getEntitiesByRefs).toHaveBeenCalledWith( - { entityRefs: ['component:default/test', 'component:default/test2'] }, - { - token: 'secret', - }, - ); - expect(mockContext.output).toHaveBeenCalledWith('entities', [ - { + it('should use defaultKind and defaultNamespace if provided', async () => { + const entity1: Entity = { + metadata: { + namespace: 'ns', + name: 'test', + }, + kind: 'Group', + } as Entity; + const entity2: Entity = { metadata: { namespace: 'default', name: 'test', }, - kind: 'Component', - }, - null, - ]); + kind: 'User', + } as Entity; + getEntitiesByRefs.mockReturnValueOnce({ + items: [entity1, entity2], + }); + + await action.handler({ + ...mockContext, + input: { + entityRefs: ['test', 'user:default/test'], + defaultKind: 'Group', + defaultNamespace: 'ns', + }, + }); + + expect(getEntitiesByRefs).toHaveBeenCalledWith( + { entityRefs: ['group:ns/test', 'user:default/test'] }, + { + token: 'secret', + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('entities', [ + entity1, + entity2, + ]); + }); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.ts index d6abeee792..437da8ae73 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.ts @@ -18,6 +18,7 @@ import { CatalogApi } from '@backstage/catalog-client'; import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import yaml from 'yaml'; import { z } from 'zod'; +import { parseEntityRef, stringifyEntityRef } from '@backstage/catalog-model'; const id = 'catalog:fetch'; @@ -69,6 +70,7 @@ export function createFetchCatalogEntityAction(options: { description: 'Returns entity or entities from the catalog by entity reference(s)', examples, + supportsDryRun: true, schema: { input: z.object({ entityRef: z @@ -87,6 +89,10 @@ export function createFetchCatalogEntityAction(options: { 'Allow the entity or entities to optionally exist. Default: false', }) .optional(), + defaultKind: z.string({ description: 'The default kind' }).optional(), + defaultNamespace: z + .string({ description: 'The default namespace' }) + .optional(), }), output: z.object({ entity: z @@ -106,7 +112,8 @@ export function createFetchCatalogEntityAction(options: { }), }, async handler(ctx) { - const { entityRef, entityRefs, optional } = ctx.input; + const { entityRef, entityRefs, optional, defaultKind, defaultNamespace } = + ctx.input; if (!entityRef && !entityRefs) { if (optional) { return; @@ -115,9 +122,14 @@ export function createFetchCatalogEntityAction(options: { } if (entityRef) { - const entity = await catalogClient.getEntityByRef(entityRef, { - token: ctx.secrets?.backstageToken, - }); + const entity = await catalogClient.getEntityByRef( + stringifyEntityRef( + parseEntityRef(entityRef, { defaultKind, defaultNamespace }), + ), + { + token: ctx.secrets?.backstageToken, + }, + ); if (!entity && !optional) { throw new Error(`Entity ${entityRef} not found`); @@ -127,7 +139,13 @@ export function createFetchCatalogEntityAction(options: { if (entityRefs) { const entities = await catalogClient.getEntitiesByRefs( - { entityRefs }, + { + entityRefs: entityRefs.map(ref => + stringifyEntityRef( + parseEntityRef(ref, { defaultKind, defaultNamespace }), + ), + ), + }, { token: ctx.secrets?.backstageToken, },