From 54725d199e9131089e3ec6ff3d96a3158cc92a8c Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 17 Jun 2025 09:16:13 +0200 Subject: [PATCH] chore: added some basic docs for actions registry and actions service Signed-off-by: benjdlambert --- .../core-services/actions-registry.md | 176 ++++++++++++++++++ docs/backend-system/core-services/actions.md | 131 +++++++++++++ microsite/sidebars.ts | 2 + 3 files changed, 309 insertions(+) create mode 100644 docs/backend-system/core-services/actions-registry.md create mode 100644 docs/backend-system/core-services/actions.md diff --git a/docs/backend-system/core-services/actions-registry.md b/docs/backend-system/core-services/actions-registry.md new file mode 100644 index 0000000000..02ccfd1481 --- /dev/null +++ b/docs/backend-system/core-services/actions-registry.md @@ -0,0 +1,176 @@ +--- +id: actions-registry +title: Actions Registry (alpha) +sidebar_label: Actions Registry (alpha) +description: Documentation for the Actions Registry Service +--- + +## Overview + +The Actions Registry Service is a core service designed to provide a distributed registry for actions that can be executed within Backstage backend plugins. This service allows plugins to register reusable actions with well-defined schemas and execution logic, promoting consistency and reusability across the Backstage ecosystem. + +## Action Structure + +Each action registered with the service must conform to the `ActionsRegistryActionOptions` type, which includes: + +### Required Properties + +- **`name`:** A unique identifier for the action (string) +- **`title`:** A human-readable title for the action (string) +- **`description`:** A detailed description of what the action does (string) +- **`schema`:** Object containing input and output schema definitions + - **`input`:** Function that returns a Zod schema for validating input + - **`output`:** Function that returns a Zod schema for validating output +- **`action`:** The async function that executes the action logic + +### Optional Properties + +- **`attributes`:** Object containing behavioral flags: + - **`destructive`:** Boolean indicating if the action modifies or deletes data + - **`idempotent`:** Boolean indicating if running the action multiple times produces the same result + - **`readOnly`:** Boolean indicating if the action only reads data without modifications + +### Action Context + +When an action is executed, it receives a context object (`ActionsRegistryActionContext`) containing: + +- **`input`:** The validated input data matching the defined input schema +- **`logger`:** A LoggerService instance for logging within the action +- **`credentials`:** BackstageCredentials for authentication and authorization + +## Using the Service + +### Registering an Action + +Here's an example of how to register an action with the Actions Registry Service: + +```typescript +import { ActionsRegistryService } from '@backstage/backend-plugin-api'; + +export function registerMyActions(actionsRegistry: ActionsRegistryService) { + // Register a simple read-only action + actionsRegistry.register({ + name: 'fetch-user-info', + title: 'Fetch User Information', + description: 'Retrieves user information from the catalog', + schema: { + input: z => + z.object({ + userRef: z.string(), + includeGroups: z.boolean().optional(), + }), + output: z => + z.object({ + user: z.object({ + name: z.string(), + email: z.string(), + groups: z.array(z.string()).optional(), + }), + }), + }, + attributes: { + readOnly: true, + idempotent: true, + }, + action: async ({ input, logger, credentials }) => { + logger.info(`Fetching user info for ${input.userRef}`); + + // Perform the action logic here + const user = await fetchUserFromCatalog(input.userRef, credentials); + + return { + output: { + user: { + name: user.name, + email: user.email, + groups: input.includeGroups ? user.groups : undefined, + }, + }, + }; + }, + }); + + // Register a destructive action + actionsRegistry.register({ + name: 'delete-entity', + title: 'Delete Entity', + description: 'Removes an entity from the catalog', + schema: { + input: z => + z.object({ + entityRef: z.string(), + force: z.boolean().optional(), + }), + output: z => + z.object({ + deletedEntities: z.array(z.string()), + }), + }, + attributes: { + destructive: true, + idempotent: false, + }, + action: async ({ input, logger, credentials }) => { + logger.warn(`Deleting entity ${input.entityRef}`); + + // Perform the deletion logic here + const { deletedEntities } = await deleteEntityFromCatalog( + input.entityRef, + input.force, + credentials, + ); + + return { + output: deletedEntities, + }; + }, + }); +} +``` + +### Accessing the Service in a Plugin + +To use the Actions Registry Service in your plugin, access it through dependency injection: + +```typescript +import { + createBackendPlugin, + coreServices, +} from '@backstage/backend-plugin-api'; + +export const myPlugin = createBackendPlugin({ + pluginId: 'my-plugin', + register(env) { + env.registerInit({ + deps: { + actionsRegistry: coreServices.actionsRegistry, + logger: coreServices.logger, + }, + async init({ actionsRegistry, logger }) { + logger.info('Registering actions...'); + registerMyActions(actionsRegistry); + logger.info('Actions registered successfully'); + }, + }); + }, +}); +``` + +## Best Practices + +### Naming Conventions + +- **Use kebab-case:** Action names should be in kebab-case (e.g., `fetch-user-info`, `create-repository`) +- **Be Descriptive:** Choose names that clearly describe what the action does +- **Avoid Redundancy:** Don't include plugin names in action names since the plugin context is separate +- **Use Verbs:** Start action names with verbs that describe the operation (e.g., `fetch`, `create`, `delete`, `update`) + +## Action Attributes Reference + +| Attribute | Type | Default | Description | +| ------------- | ------- | ------- | ------------------------------------------------------------------- | +| `destructive` | boolean | `true` | Indicates the action modifies or deletes data. Use with caution. | +| `idempotent` | boolean | `false` | Indicates the action can be run multiple times with the same result | +| `readOnly` | boolean | `false` | Indicates the action only reads data without making modifications | + +These attributes help consumers of actions understand their behavior and implement appropriate safeguards, retries, or optimizations based on the action's characteristics. diff --git a/docs/backend-system/core-services/actions.md b/docs/backend-system/core-services/actions.md new file mode 100644 index 0000000000..ddb28b4da7 --- /dev/null +++ b/docs/backend-system/core-services/actions.md @@ -0,0 +1,131 @@ +--- +id: actions +title: Actions (alpha) +sidebar_label: Actions (alpha) +description: Documentation for the Actions Service +--- + +## Overview + +The Actions Service is a core service that provides a standardized interface for discovering and executing registered actions within Backstage backend plugins. This service acts as the consumer-facing API for actions that have been registered through the Actions Registry Service, allowing plugins to list available actions and invoke them with proper authentication and input validation. + +## How it Works + +The Actions Service implements the `ActionsService` interface, which provides two primary methods: + +- **`list()`:** Retrieves all available actions with their complete metadata +- **`invoke()`:** Executes a specific action by ID with provided input data + +The service works in conjunction with the [Actions Registry Service](./actions-registry.md), where actions are registered by plugins and then made available for discovery and execution through this service. + +## Action Identification + +Actions are identified using a unique ID that follows a specific format: + +- All action IDs are prefixed with the plugin ID that registered them, following the pattern `pluginId:actionName` +- An action named `fetch-user-info` registered by the `catalog` plugin would have the ID `catalog:fetch-user-info` +- When using the `actionsRegistryServiceMock`, the plugin ID prefix will be `test:` + +This naming convention ensures that action names are globally unique across all plugins and provides clear ownership identification. + +## Configuration + +The Actions Service can be configured to control which plugins' actions are available: + +```yaml +backend: + actions: + pluginSources: + - catalog +``` + +## Using the Service + +### Listing Available Actions + +Here's an example of how to list all available actions: + +```typescript +import { ActionsService } from '@backstage/backend-plugin-api'; + +export async function listAvailableActions( + actionsService: ActionsService, + credentials: BackstageCredentials, +) { + try { + const { actions } = await actionsService.list({ credentials }); + + console.log(`Found ${actions.length} available actions:`); + + actions.forEach(action => { + console.log(`- ${action.id}: ${action.title}`); + console.log(` Description: ${action.description}`); + console.log(` Attributes: ${JSON.stringify(action.attributes)}`); + + if (action.schema.input) { + console.log( + ` Input Schema: ${JSON.stringify(action.schema.input, null, 2)}`, + ); + } + }); + + return actions; + } catch (error) { + console.error('Failed to list actions:', error); + throw error; + } +} +``` + +### Invoking an Action + +Here's an example of how to execute a specific action: + +```typescript +import { ActionsService } from '@backstage/backend-plugin-api'; + +export async function executeAction( + actionsService: ActionsService, + actionId: string, + input: JsonObject, + credentials: BackstageCredentials, +) { + try { + const { output } = await actionsService.invoke({ + id: actionId, + input, + credentials, + }); + + console.log(`Action ${actionId} executed successfully`); + console.log('Output:', JSON.stringify(output, null, 2)); + + return output; + } catch (error) { + console.error(`Failed to execute action ${actionId}:`, error); + throw error; + } +} + +// Example usage +async function fetchUserInfo( + actionsService: ActionsService, + credentials: BackstageCredentials, +) { + const output = await executeAction( + actionsService, + 'catalog:fetch-user-info', // Note: Action ID includes plugin prefix + { + userRef: 'user:default/john.doe', + includeGroups: true, + }, + credentials, + ); + + return output; +} +``` + +## Best Practices + +For comprehensive guidance on action design, naming conventions, and schema design, see the [Actions Registry Best Practices](./actions-registry.md#best-practices) documentation. diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index 4bf0da08a8..eef7ef5ee9 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -462,6 +462,8 @@ export default { 'backend-system/core-services/token-manager', 'backend-system/core-services/url-reader', 'backend-system/core-services/user-info', + 'backend-system/core-services/actions-registry', + 'backend-system/core-services/actions', ], }, ],