From 2384a499629320e59cc492958f5c2614074ec6bd Mon Sep 17 00:00:00 2001 From: Steven Billington Date: Wed, 24 Apr 2024 14:29:33 -0400 Subject: [PATCH 1/4] Making docs updates for software templates. Signed-off-by: Steven Billington --- .../software-templates/input-examples.md | 20 ++++++++++- .../writing-custom-actions.md | 34 +++++++++++++++++-- .../software-templates/writing-templates.md | 29 +++++++++++++++- 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/docs/features/software-templates/input-examples.md b/docs/features/software-templates/input-examples.md index a95cee24f4..50495b1784 100644 --- a/docs/features/software-templates/input-examples.md +++ b/docs/features/software-templates/input-examples.md @@ -181,9 +181,11 @@ parameters: ## Use parameters as condition in steps +Conditions use Javascript equality operators. + ```yaml - name: Only development environments - if: ${{ parameters.environment === "staging" and parameters.environment === "development" }} + if: ${{ parameters.environment === "staging" or parameters.environment === "development" }} action: debug:log input: message: 'development step' @@ -193,6 +195,12 @@ parameters: action: debug:log input: message: 'production step' + +- name: Non-production environments + if: ${{ parameters.environment !== "prod" and parameters.environment !== "production" }} + action: debug:log + input: + message: 'non-production step' ``` ## Use parameters as conditional for fields @@ -218,10 +226,15 @@ parameters: lastName: title: Last Name type: string + # You can use additional fields of parameters within conditional parameters such as required. + required: + - lastName ``` ## Conditionally set parameters +The `if` keyword within the parameter uses [nunjucks templating](https://mozilla.github.io/nunjucks/templating.html#if). The `not` keyword is unavailable; instead, use javascript equality. + ```yaml spec: parameters: @@ -237,4 +250,9 @@ spec: action: fetch:template input: url: ${{ parameters.path if parameters.path else '/root' }} + - id: fetch_not_example + name: Fetch template not example + action: fetch:template + input: + url: ${{ '/root' if parameters.path !== true else parameters.path }} ``` diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index 92ff092948..96b2540527 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -32,6 +32,7 @@ import { z } from 'zod'; export const createNewFileAction = () => { return createTemplateAction({ id: 'acme:file:create', + description: 'Create an Acme file.', schema: { input: z.object({ contents: z.string().describe('The contents of the file'), @@ -59,9 +60,10 @@ for reference. The `createTemplateAction` takes an object which specifies the following: -- `id` - a unique ID for your custom action. We encourage you to namespace these +- `id` - A unique ID for your custom action. We encourage you to namespace these in some way so that they won't collide with future built-in actions that we may ship with the `scaffolder-backend` plugin. +- `description` - An optional field to describe the purpose of the action. This will populate in the `/create/actions` endpoint. - `schema.input` - A `zod` or JSON schema object for input values to your function - `schema.output` - A `zod` or JSON schema object for values which are output from the function using `ctx.output` @@ -76,6 +78,7 @@ import { writeFile } from 'fs'; export const createNewFileAction = () => { return createTemplateAction<{ contents: string; filename: string }>({ id: 'acme:file:create', + description: 'Create an Acme file.', schema: { input: { required: ['contents', 'filename'], @@ -114,7 +117,7 @@ We follow `provider:entity:verb` or as close to this as possible for our built i Also feel free to use your company name to namespace them if you prefer too, for example `acme:file:create` like above. -Prefer to use `camelCase` over `snake-case` for these actions if possible, which leads to better reading and writing of template entity definitions. +Prefer to use `camelCase` over `snake_case` or `kebab-case` for these actions if possible, which leads to better reading and writing of template entity definitions. > We're aware that there are some exceptions to this, but try to follow as close as possible. We'll be working on migrating these in the repository over time too. @@ -139,6 +142,8 @@ argument. It looks like the following: ## Registering Custom Actions +### Register Action With Previous Backend System, Backstage Version < 1.25.x + Once you have your Custom Action ready for usage with the scaffolder, you'll need to pass this into the `scaffolder-backend` `createRouter` function. You should have something similar to the below in @@ -189,7 +194,7 @@ export default async function createPlugin( } ``` -### Register Action With New Backend System +### Register Action With New Backend System, Backstage Version >= 1.25.0 To register your new custom action in the New Backend System you will need to create a backend module. Here is a very simplified example of how to do that: @@ -225,6 +230,29 @@ backend.add(import('@backstage/plugin-scaffolder-backend/alpha')); backend.add(scaffolderModuleCustomExtensions()); ``` +If your custom action requires core services such as `config` or `cache` they can be imported in the dependencies and passed to the custom action function. + +```ts title="packages/backend/src/index.ts" +import { + coreServices, + createBackendModule, +} from '@backstage/backend-plugin-api'; + +... + + env.registerInit({ + deps: { + scaffolder: scaffolderActionsExtensionPoint, + cache: coreServices.cache, + config: coreServices.rootConfig, + }, + async init({ scaffolder, cache, config }) { + scaffolder.addActions( + customActionNeedingCacheAndConfig({ cache: cache, config: config }), + ); + }) +``` + ## List of custom action packages Here is a list of Open Source custom actions that you can add to your Backstage diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index ad542ba4a3..1e289d2409 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -311,10 +311,37 @@ spec: ``` If you have a feature flag `experimental-feature` active then -your first step would be shown. The same goes for the nested properties in the +your first set of parameter fields would be shown. The same goes for the nested properties in the spec. Make sure to use the key `backstage:featureFlag` in your templates if you want to use this functionality. +Feature Flags cannot be used in `spec.steps[].if`(the conditional on whether to execute an step/action). But you can use feature flags to display parameters that allow for skipping steps. + +```yaml +spec: + type: website + owner: team-a + parameters: + - name: Enter some stuff + description: Enter some stuff + backstage:featureFlag: experimental-feature + properties: + skipStep: + type: boolean + title: Whether or not to skip a step. + default: false + restOfParameters: + ... + steps: + - id: skipMe + name: A step to skip if the feature flag is turned on and the user selects true + action: debug:log + if: ${{ parameters.skipStep }} + input: + message: | + ... +``` + ### The Repository Picker In order to make working with repository providers easier, we've built a custom From 6a563f9e53a62f352b12c61fb73f7df938a17706 Mon Sep 17 00:00:00 2001 From: Steven Billington Date: Wed, 24 Apr 2024 15:43:57 -0400 Subject: [PATCH 2/4] Added additional documentation to the software templating section including unit tests. Signed-off-by: Steven Billington --- .../software-templates/writing-templates.md | 2 +- .../writing-tests-for-actions.md | 77 ++++++++++++++++--- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 1e289d2409..d8e6feb559 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -734,7 +734,7 @@ an entity reference, such as the `kind`, `namespace`, and `name`. ### pick -This `pick` filter allows you to select specific properties from an object. +This `pick` filter allows you to select specific properties (`kind`, `namespace`, `name`) from an object. **Usage Example** diff --git a/docs/features/software-templates/writing-tests-for-actions.md b/docs/features/software-templates/writing-tests-for-actions.md index 0c92811b6f..0bfd97e49f 100644 --- a/docs/features/software-templates/writing-tests-for-actions.md +++ b/docs/features/software-templates/writing-tests-for-actions.md @@ -4,15 +4,13 @@ title: Writing Tests For Actions description: How to write tests for actions --- -Once you created a new action, your own custom one, or you would like to contribute new actions, you have to cover it with -Unit tests to be sure that your actions do what they suppose to do. +# Unit Testing Custom Actions -Make sure that you cover the most of scenario's, which could happen with the action. -One of indispensable part of the test is to supply the context to a handler of action for the execution. -We encourage you to use a utility method for that, so your tests are immune to structural changes of context. -What is inevitably going to happen during the time. +Unit tests help prevent regressions in custom action functionality. The `createTemplateAction` function that is the core of a custom action can be difficult to mock. There are helper methods that can assist. -Example how to use it: +## Mocking the Context + +The `handler` property of the `createTemplateAction` input object expects a context. You can create a mock context using the code below: ```typescript import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; @@ -29,10 +27,11 @@ expect(mockContext.output).toHaveBeenCalledWith( ); ``` +### Mocking a Workspace within the Context object + One thing to be aware about: if you would like to call `createMockActionContext` inside `it`, you have to provide a `workspacePath`. By default, `createMockActionContext` uses -`import { createMockDirectory } from '@backstage/backend-test-utils';` to create it for you. -This implementation contains a hook inside which creates this limitation. So in this case you can do then: +`import { createMockDirectory } from '@backstage/backend-test-utils';` to create it for you. You can use the code below to customize the `workspacePath` without using the default workspace of the `createMockActionContext` function. ```typescript describe('github:autolinks:create', async () => { @@ -55,3 +54,63 @@ describe('github:autolinks:create', async () => { }); }); ``` + +## Mocking a Config Core Service + +If your custom Action requires the Config Core Service within execution of the `handler(ctx)` such as the custom action below, mocking the context object can be done by building a `mockContext` with the `ConfigReader` function within the `@backstage/config` package. + +```typescript +// custom-action.ts +import { Config } from '@backstage/config'; + +export const customActionRequiringConfigCoreService = (config: Config) => { + const fieldRequiringValueFromConfig = config.getString('app.service.url'); + return createTemplateAction({ + ... + async handler(ctx) { + // Some code requiring the config const + ctx.logger.info(fieldRequiringValueFromConfig); + } + }) +} +``` + +```typescript +// custom-action.test.ts +import { ConfigReader } from '@backstage/config'; +import { customActionRequiringConfigCoreService } from './custom-action.ts'; +... +const mockConfig = new ConfigReader({ + app: { + service: { + url: 'https://api.service.io/graphql', + apiKeyId: '123', + apiKeySecret: '123abc', + }, + }, +}); +... +const action = customActionRequiringConfigCoreService(mockConfig); +await action.handler({ + ...mockContext +}) +``` + +## Mocking a Cache Core Service + +Similar to the `Mocking a Config Core Service` section above, if your custom action expects a Cache Core Service Object as part of the function input, you can mock it out with the following: + +```typescript +import { CacheService } from '@backstage/backend-plugin-api'; + +const mockCacheServiceMethods = { + get: jest.fn(), + set: jest.fn(), + delete: jest.fn(), +}; + +const mockCacheService = mockCacheServiceMethods as unknown as CacheService; + +const action = customActionRequiringCacheCoreService(mockCacheService); +... +``` From 5f8ee599f5e9832e83ea6bcab791c1258ab683f6 Mon Sep 17 00:00:00 2001 From: Steven Billington Date: Thu, 25 Apr 2024 09:27:40 -0400 Subject: [PATCH 3/4] Minor fix on version numbering that I missed earlier in the documentation. Signed-off-by: Steven Billington --- docs/features/software-templates/writing-custom-actions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index 96b2540527..fa21c8f7a9 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -142,7 +142,7 @@ argument. It looks like the following: ## Registering Custom Actions -### Register Action With Previous Backend System, Backstage Version < 1.25.x +### Register Action With Previous Backend System, Backstage Version < 1.24.x Once you have your Custom Action ready for usage with the scaffolder, you'll need to pass this into the `scaffolder-backend` `createRouter` function. You @@ -194,7 +194,7 @@ export default async function createPlugin( } ``` -### Register Action With New Backend System, Backstage Version >= 1.25.0 +### Register Action With New Backend System, Backstage Version >= 1.24.0 To register your new custom action in the New Backend System you will need to create a backend module. Here is a very simplified example of how to do that: From 817e97606638bbf25bc504915f4090e3ae32a9ea Mon Sep 17 00:00:00 2001 From: Steven Billington Date: Fri, 31 May 2024 14:26:59 -0400 Subject: [PATCH 4/4] Changes made to the orger of registering a custom action as requested. Signed-off-by: Steven Billington --- .../writing-custom-actions.md | 108 +++++++++--------- 1 file changed, 53 insertions(+), 55 deletions(-) diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index fa21c8f7a9..c1386786c7 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -142,61 +142,7 @@ argument. It looks like the following: ## Registering Custom Actions -### Register Action With Previous Backend System, Backstage Version < 1.24.x - -Once you have your Custom Action ready for usage with the scaffolder, you'll -need to pass this into the `scaffolder-backend` `createRouter` function. You -should have something similar to the below in -`packages/backend/src/plugins/scaffolder.ts` - -```ts -return await createRouter({ - containerRunner, - catalogClient, - logger: env.logger, - config: env.config, - database: env.database, - reader: env.reader, -}); -``` - -There's another property you can pass here, which is an array of `actions` which -will set the available actions that the scaffolder has access to. - -```ts -import { createBuiltinActions } from '@backstage/plugin-scaffolder-backend'; -import { ScmIntegrations } from '@backstage/integration'; -import { createNewFileAction } from './scaffolder/actions/custom'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const catalogClient = new CatalogClient({ discoveryApi: env.discovery }); - const integrations = ScmIntegrations.fromConfig(env.config); - - const builtInActions = createBuiltinActions({ - integrations, - catalogClient, - config: env.config, - reader: env.reader, - }); - - const actions = [...builtInActions, createNewFileAction()]; - - return createRouter({ - actions, - catalogClient: catalogClient, - logger: env.logger, - config: env.config, - database: env.database, - reader: env.reader, - }); -} -``` - -### Register Action With New Backend System, Backstage Version >= 1.24.0 - -To register your new custom action in the New Backend System you will need to create a backend module. Here is a very simplified example of how to do that: +To register your new custom action in the Backend System you will need to create a backend module. Here is a very simplified example of how to do that: ```ts title="packages/backend/src/index.ts" /* highlight-add-start */ @@ -253,6 +199,58 @@ import { }) ``` +### Register Custom Actions with the Legacy Backend System + +Once you have your Custom Action ready for usage with the scaffolder, you'll +need to pass this into the `scaffolder-backend` `createRouter` function. You +should have something similar to the below in +`packages/backend/src/plugins/scaffolder.ts` + +```ts +return await createRouter({ + containerRunner, + catalogClient, + logger: env.logger, + config: env.config, + database: env.database, + reader: env.reader, +}); +``` + +There's another property you can pass here, which is an array of `actions` which +will set the available actions that the scaffolder has access to. + +```ts +import { createBuiltinActions } from '@backstage/plugin-scaffolder-backend'; +import { ScmIntegrations } from '@backstage/integration'; +import { createNewFileAction } from './scaffolder/actions/custom'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + const catalogClient = new CatalogClient({ discoveryApi: env.discovery }); + const integrations = ScmIntegrations.fromConfig(env.config); + + const builtInActions = createBuiltinActions({ + integrations, + catalogClient, + config: env.config, + reader: env.reader, + }); + + const actions = [...builtInActions, createNewFileAction()]; + + return createRouter({ + actions, + catalogClient: catalogClient, + logger: env.logger, + config: env.config, + database: env.database, + reader: env.reader, + }); +} +``` + ## List of custom action packages Here is a list of Open Source custom actions that you can add to your Backstage