Merge pull request #17747 from Andy2003/feature/improve-catalog-fetch

Add ability to use `defaultNamespace` and `defaultKind` for scaffolder action `catalog:fetch`
This commit is contained in:
Ben Lambert
2023-05-16 11:28:53 +02:00
committed by GitHub
4 changed files with 242 additions and 147 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': minor
---
Add ability to use `defaultNamespace` and `defaultKind` for scaffolder action `catalog:fetch`
+2
View File
@@ -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;
@@ -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,
]);
});
});
});
@@ -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,
},