Add ability to use defaultNamespace and defaultKind for scaffolder action catalog:fetch
Signed-off-by: Andreas Berger <andreas@berger-ecommerce.com> Signed-off-by: Christopher Diaz <cdiaz@rvohealth.com>
This commit is contained in:
committed by
Christopher Diaz
parent
9e4301e370
commit
67c1b53cdc
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': minor
|
||||
---
|
||||
|
||||
Add ability to use `defaultNamespace` and `defaultKind` for scaffolder action `catalog:fetch`
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user