Merge pull request #31126 from backstage/blam/scaffolder-actions-registry
`scaffolder`: Allow executing `ActionsRegistry` actions from the Scaffolder
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Added support for executing actions from the `ActionsRegistry` in the `scaffolder-backend`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Updating `catalog:get-catalog-entity` action to be `readOnly` and non destructive
|
||||
@@ -28,6 +28,11 @@ export const createGetCatalogEntityAction = ({
|
||||
actionsRegistry.register({
|
||||
name: 'get-catalog-entity',
|
||||
title: 'Get Catalog Entity',
|
||||
attributes: {
|
||||
destructive: false,
|
||||
readOnly: true,
|
||||
idempotent: true,
|
||||
},
|
||||
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".
|
||||
|
||||
@@ -56,6 +56,7 @@ import {
|
||||
convertFiltersToRecord,
|
||||
convertGlobalsToRecord,
|
||||
} from './util/templating';
|
||||
import { actionsServiceRef } from '@backstage/backend-plugin-api/alpha';
|
||||
|
||||
/**
|
||||
* Scaffolder plugin
|
||||
@@ -139,6 +140,7 @@ export const scaffolderPlugin = createBackendPlugin({
|
||||
auditor: coreServices.auditor,
|
||||
catalog: catalogServiceRef,
|
||||
events: eventsServiceRef,
|
||||
actionsRegistry: actionsServiceRef,
|
||||
},
|
||||
async init({
|
||||
logger,
|
||||
@@ -153,6 +155,7 @@ export const scaffolderPlugin = createBackendPlugin({
|
||||
permissions,
|
||||
events,
|
||||
auditor,
|
||||
actionsRegistry,
|
||||
}) {
|
||||
const log = loggerToWinstonLogger(logger);
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
@@ -222,6 +225,7 @@ export const scaffolderPlugin = createBackendPlugin({
|
||||
additionalWorkspaceProviders,
|
||||
events,
|
||||
auditor,
|
||||
actionsRegistry,
|
||||
});
|
||||
httpRouter.use(router);
|
||||
},
|
||||
|
||||
@@ -68,6 +68,8 @@ import {
|
||||
import { createDefaultFilters } from '../lib/templating/filters/createDefaultFilters';
|
||||
import { createRouter } from './router';
|
||||
import { DatabaseTaskStore } from '../scaffolder/tasks/DatabaseTaskStore';
|
||||
import { actionsRegistryServiceMock } from '@backstage/backend-test-utils/alpha';
|
||||
import { ActionsService } from '@backstage/backend-plugin-api/alpha';
|
||||
|
||||
function createDatabase(): DatabaseService {
|
||||
return DatabaseManager.fromConfig(
|
||||
@@ -178,6 +180,7 @@ const createTestRouter = async (
|
||||
| Record<string, TemplateGlobal>
|
||||
| CreatedTemplateGlobal[];
|
||||
autocompleteHandlers?: Record<string, AutocompleteHandler>;
|
||||
actionsRegistry?: ActionsService;
|
||||
} = {},
|
||||
) => {
|
||||
const logger = mockServices.logger.mock({
|
||||
@@ -246,6 +249,7 @@ const createTestRouter = async (
|
||||
}),
|
||||
createDebugLogAction(),
|
||||
],
|
||||
actionsRegistry: overrides.actionsRegistry ?? actionsRegistryServiceMock(),
|
||||
});
|
||||
|
||||
router.use(mockErrorHandler());
|
||||
@@ -275,6 +279,49 @@ describe('scaffolder router', () => {
|
||||
expect(response.body[0].id).toBeDefined();
|
||||
expect(response.body.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should include actions from the remote actions registry', async () => {
|
||||
const mockActionsRegistry = actionsRegistryServiceMock();
|
||||
mockActionsRegistry.register({
|
||||
name: 'my-demo-action',
|
||||
title: 'Test',
|
||||
description: 'Test',
|
||||
schema: {
|
||||
input: z => z.object({ name: z.string() }),
|
||||
output: z => z.object({ name: z.string() }),
|
||||
},
|
||||
action: async () => ({ output: { name: 'test' } }),
|
||||
});
|
||||
const { router } = await createTestRouter({
|
||||
actionsRegistry: mockActionsRegistry,
|
||||
});
|
||||
const response = await request(router).get('/v2/actions').send();
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body.length).toBe(3);
|
||||
|
||||
expect(response.body).toContainEqual({
|
||||
description: 'Test',
|
||||
examples: [],
|
||||
id: 'test:my-demo-action',
|
||||
schema: {
|
||||
input: {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
additionalProperties: false,
|
||||
properties: { name: { type: 'string' } },
|
||||
required: ['name'],
|
||||
type: 'object',
|
||||
},
|
||||
output: {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
additionalProperties: false,
|
||||
properties: { name: { type: 'string' } },
|
||||
required: ['name'],
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /v2/templating-extensions', () => {
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
resolveSafeChildPath,
|
||||
SchedulerService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { Schema } from 'jsonschema';
|
||||
import {
|
||||
CompoundEntityRef,
|
||||
Entity,
|
||||
@@ -131,6 +132,8 @@ import {
|
||||
} from './rules';
|
||||
|
||||
import { TaskFilters } from '@backstage/plugin-scaffolder-node';
|
||||
import { ActionsService } from '@backstage/backend-plugin-api/alpha';
|
||||
import { isPlainObject } from 'lodash';
|
||||
|
||||
/**
|
||||
* RouterOptions
|
||||
@@ -163,6 +166,7 @@ export interface RouterOptions {
|
||||
events?: EventsService;
|
||||
auditor?: AuditorService;
|
||||
autocompleteHandlers?: Record<string, AutocompleteHandler>;
|
||||
actionsRegistry: ActionsService;
|
||||
}
|
||||
|
||||
function isSupportedTemplate(entity: TemplateEntityV1beta3) {
|
||||
@@ -198,7 +202,7 @@ export async function createRouter(
|
||||
config,
|
||||
database,
|
||||
catalog,
|
||||
actions,
|
||||
actions = [],
|
||||
scheduler,
|
||||
additionalTemplateFilters,
|
||||
additionalTemplateGlobals,
|
||||
@@ -210,6 +214,7 @@ export async function createRouter(
|
||||
auth,
|
||||
httpAuth,
|
||||
auditor,
|
||||
actionsRegistry,
|
||||
} = options;
|
||||
|
||||
const concurrentTasksLimit =
|
||||
@@ -301,7 +306,47 @@ export async function createRouter(
|
||||
workers.push(worker);
|
||||
}
|
||||
|
||||
actions?.forEach(action => actionRegistry.register(action));
|
||||
// TODO(blam): it's a little unfortunate that you have to restart the scaffolder
|
||||
// backend in order to pick these up. We should really just make `ActionsRegistry.get()` async
|
||||
// and then we can move this logic into the there instead.
|
||||
// But we can't make those changes until next major.
|
||||
// Alternatively, we could look at setting up a periodic task that refreshes the actions registry, but
|
||||
// not feeling that it's worth the complexity.
|
||||
const { actions: distributedActions } = await actionsRegistry.list({
|
||||
credentials: await auth.getOwnServiceCredentials(),
|
||||
});
|
||||
|
||||
for (const action of actions) {
|
||||
actionRegistry.register(action);
|
||||
}
|
||||
|
||||
for (const action of distributedActions) {
|
||||
actionRegistry.register({
|
||||
id: action.id,
|
||||
description: action.description,
|
||||
examples: [],
|
||||
supportsDryRun:
|
||||
action.attributes?.readOnly === true &&
|
||||
action.attributes?.destructive === false,
|
||||
handler: async ctx => {
|
||||
const { output } = await actionsRegistry.invoke({
|
||||
id: action.id,
|
||||
input: ctx.input,
|
||||
credentials: await ctx.getInitiatorCredentials(),
|
||||
});
|
||||
|
||||
if (isPlainObject(output)) {
|
||||
for (const [key, value] of Object.entries(output as JsonObject)) {
|
||||
ctx.output(key as keyof typeof output, value);
|
||||
}
|
||||
}
|
||||
},
|
||||
schema: {
|
||||
input: action.schema.input as Schema,
|
||||
output: action.schema.output as Schema,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const launchWorkers = () => workers.forEach(worker => worker.start());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user