From 72611992911ab35f7d73ee91def4dda17c99564b Mon Sep 17 00:00:00 2001 From: Enrico Alvarenga Date: Thu, 24 Aug 2023 16:06:11 -0700 Subject: [PATCH 1/6] feat(scaffolder-backend): allow default kind and namespace args on parseEntityRef filter Signed-off-by: Enrico Alvarenga --- .../src/lib/templating/filters.ts | 10 +- .../tasks/NunjucksWorkflowRunner.test.ts | 131 +++++++++++++++--- 2 files changed, 119 insertions(+), 22 deletions(-) diff --git a/plugins/scaffolder-backend/src/lib/templating/filters.ts b/plugins/scaffolder-backend/src/lib/templating/filters.ts index 204d0015d8..0400adf159 100644 --- a/plugins/scaffolder-backend/src/lib/templating/filters.ts +++ b/plugins/scaffolder-backend/src/lib/templating/filters.ts @@ -27,7 +27,15 @@ export const createDefaultFilters = ({ }): Record => { return { parseRepoUrl: url => parseRepoUrl(url as string, integrations), - parseEntityRef: ref => parseEntityRef(ref as string), + parseEntityRef: ( + ref, + defaultKind?: JsonValue, + defaultNamespace?: JsonValue, + ) => + parseEntityRef(ref as string, { + defaultKind: defaultKind?.toString(), + defaultNamespace: defaultNamespace?.toString(), + }), pick: (obj: JsonValue, key: JsonValue) => get(obj, key as string), projectSlug: repoUrl => { const { owner, repo } = parseRepoUrl(repoUrl as string, integrations); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index 5d4f7b73cc..bf40357cfe 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -713,32 +713,121 @@ describe('DefaultWorkflowRunner', () => { }); }); - it('provides the parseEntityRef filter', async () => { - const task = createMockTaskWithSpec({ - apiVersion: 'scaffolder.backstage.io/v1beta3', - steps: [ - { - id: 'test', - name: 'name', - action: 'output-action', - input: {}, + describe('parseEntityRef', () => { + it('parses entity ref', async () => { + const task = createMockTaskWithSpec({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + steps: [ + { + id: 'test', + name: 'name', + action: 'output-action', + input: {}, + }, + ], + output: { + foo: '${{ parameters.entity | parseEntityRef }}', }, - ], - output: { - foo: '${{ parameters.entity | parseEntityRef }}', - }, - parameters: { - entity: 'component:default/ben', - }, + parameters: { + entity: 'component:default/ben', + }, + }); + + const { output } = await runner.execute(task); + + expect(output.foo).toEqual({ + kind: 'component', + namespace: 'default', + name: 'ben', + }); }); - const { output } = await runner.execute(task); + it('provides default kind for parsing entity ref', async () => { + const task = createMockTaskWithSpec({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + steps: [ + { + id: 'test', + name: 'name', + action: 'output-action', + input: {}, + }, + ], + output: { + foo: "${{ parameters.entity | parseEntityRef('user') }}", + }, + parameters: { + entity: 'ben', + }, + }); - expect(output.foo).toEqual({ - kind: 'component', - namespace: 'default', - name: 'ben', + const { output } = await runner.execute(task); + + expect(output.foo).toEqual({ + kind: 'user', + namespace: 'default', + name: 'ben', + }); }); + + it('provides default kind and namespace for parsing entity ref', async () => { + const task = createMockTaskWithSpec({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + steps: [ + { + id: 'test', + name: 'name', + action: 'output-action', + input: {}, + }, + ], + output: { + foo: "${{ parameters.entity | parseEntityRef('user', 'namespace-b') }}", + }, + parameters: { + entity: 'ben', + }, + }); + + const { output } = await runner.execute(task); + + expect(output.foo).toEqual({ + kind: 'user', + namespace: 'namespace-b', + name: 'ben', + }); + }); + + it.each(['undefined', 'null', 'None'])( + 'provides default namespace and kind as "%s" value for parsing entity ref', + async kind => { + const task = createMockTaskWithSpec({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + steps: [ + { + id: 'test', + name: 'name', + action: 'output-action', + input: {}, + }, + ], + output: { + foo: `\${{ parameters.entity | parseEntityRef(${kind}, 'namespace-b') }}`, + }, + parameters: { + entity: 'resource:infra-workspace', + }, + }); + + const { output } = await runner.execute(task); + + expect(output.foo).toEqual({ + kind: 'resource', + namespace: 'namespace-b', + name: 'infra-workspace', + }); + }, + ); }); it('provides the pick filter', async () => { From b5f239b50bcf4814e5190de0637a65f7b79029c5 Mon Sep 17 00:00:00 2001 From: Enrico Alvarenga Date: Thu, 24 Aug 2023 16:33:52 -0700 Subject: [PATCH 2/6] chore: add changeset Signed-off-by: Enrico Alvarenga --- .changeset/tame-jokes-do.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .changeset/tame-jokes-do.md diff --git a/.changeset/tame-jokes-do.md b/.changeset/tame-jokes-do.md new file mode 100644 index 0000000000..02e597e57b --- /dev/null +++ b/.changeset/tame-jokes-do.md @@ -0,0 +1,20 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Improved the `parseEntityRef` Scaffolder filter by introducing the ability for users to provide default kind and namespace values. The filter now takes +3 arguments: + +1. Entity reference +2. (Optional) Default kind +3. (Optional) Default namespace + +So you can now provide default `kind` and/or `namespace`. Check out the following examples: + +- Without default values: `${{ parameters.entity | parseEntityRef | pick('name') }}` +- With default kind: `${{ parameters.entity | parseEntityRef('group') | pick('name') }}` +- With default kind and namespace: `${{ parameters.entity | parseEntityRef('group', 'another-namespace') | pick('name') }}` +- With default namespace: + - `${{ parameters.entity | parseEntityRef(null, 'another-namespace') | pick('name') }}` + - `${{ parameters.entity | parseEntityRef(undefined, 'another-namespace') | pick('name') }}` + - `${{ parameters.entity | parseEntityRef('', 'another-namespace') | pick('name') }}` From bc799fbaebc03c2a0be8dacf99fbd55d9b95d25c Mon Sep 17 00:00:00 2001 From: Enrico Alvarenga Date: Mon, 28 Aug 2023 10:24:43 -0700 Subject: [PATCH 3/6] refactor(scaffolder-backend): parseEntityRef filter takes context object arg Signed-off-by: Enrico Alvarenga --- .changeset/tame-jokes-do.md | 19 ++--- .../src/lib/templating/filters.ts | 13 +--- .../tasks/NunjucksWorkflowRunner.test.ts | 72 ++++++++++++++++--- 3 files changed, 73 insertions(+), 31 deletions(-) diff --git a/.changeset/tame-jokes-do.md b/.changeset/tame-jokes-do.md index 02e597e57b..35a508e692 100644 --- a/.changeset/tame-jokes-do.md +++ b/.changeset/tame-jokes-do.md @@ -2,19 +2,14 @@ '@backstage/plugin-scaffolder-backend': minor --- -Improved the `parseEntityRef` Scaffolder filter by introducing the ability for users to provide default kind and namespace values. The filter now takes -3 arguments: +Improved the `parseEntityRef` Scaffolder filter by introducing the ability for users to provide default kind and/or namespace values. The filter now takes +2 arguments, similarly to the original [parseEntityRef](<(https://github.com/backstage/backstage/blob/v1.17.2/packages/catalog-model/src/entity/ref.ts#L77)>): 1. Entity reference -2. (Optional) Default kind -3. (Optional) Default namespace +2. [Context optional object](https://github.com/backstage/backstage/blob/v1.17.2/packages/catalog-model/src/entity/ref.ts#L77) -So you can now provide default `kind` and/or `namespace`. Check out the following examples: +Check out the following examples: -- Without default values: `${{ parameters.entity | parseEntityRef | pick('name') }}` -- With default kind: `${{ parameters.entity | parseEntityRef('group') | pick('name') }}` -- With default kind and namespace: `${{ parameters.entity | parseEntityRef('group', 'another-namespace') | pick('name') }}` -- With default namespace: - - `${{ parameters.entity | parseEntityRef(null, 'another-namespace') | pick('name') }}` - - `${{ parameters.entity | parseEntityRef(undefined, 'another-namespace') | pick('name') }}` - - `${{ parameters.entity | parseEntityRef('', 'another-namespace') | pick('name') }}` +- `${{ parameters.entityRef | parseEntityRef | pick('name') }}` +- `${{ parameters.entityRef | parseEntityRef({ defaultKind:"group", defaultNamespace:"default" }) | pick('name') }}` +- `${{ parameters.entityRef | parseEntityRef({ defaultKind:"group" }) | pick('name') }}` diff --git a/plugins/scaffolder-backend/src/lib/templating/filters.ts b/plugins/scaffolder-backend/src/lib/templating/filters.ts index 0400adf159..81f326b889 100644 --- a/plugins/scaffolder-backend/src/lib/templating/filters.ts +++ b/plugins/scaffolder-backend/src/lib/templating/filters.ts @@ -15,7 +15,7 @@ */ import { parseEntityRef } from '@backstage/catalog-model'; import { ScmIntegrations } from '@backstage/integration'; -import { JsonValue } from '@backstage/types'; +import type { JsonObject, JsonValue } from '@backstage/types'; import { TemplateFilter } from '..'; import { parseRepoUrl } from '../../scaffolder/actions/builtin/publish/util'; import get from 'lodash/get'; @@ -27,15 +27,8 @@ export const createDefaultFilters = ({ }): Record => { return { parseRepoUrl: url => parseRepoUrl(url as string, integrations), - parseEntityRef: ( - ref, - defaultKind?: JsonValue, - defaultNamespace?: JsonValue, - ) => - parseEntityRef(ref as string, { - defaultKind: defaultKind?.toString(), - defaultNamespace: defaultNamespace?.toString(), - }), + parseEntityRef: (ref, context?) => + parseEntityRef(ref as string, context as JsonObject), pick: (obj: JsonValue, key: JsonValue) => get(obj, key as string), projectSlug: repoUrl => { const { owner, repo } = parseRepoUrl(repoUrl as string, integrations); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index bf40357cfe..213270b109 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -754,7 +754,7 @@ describe('DefaultWorkflowRunner', () => { }, ], output: { - foo: "${{ parameters.entity | parseEntityRef('user') }}", + foo: `\${{ parameters.entity | parseEntityRef({ defaultKind:"user" }) }}`, }, parameters: { entity: 'ben', @@ -770,6 +770,34 @@ describe('DefaultWorkflowRunner', () => { }); }); + it('provides default namespace for parsing entity ref', async () => { + const task = createMockTaskWithSpec({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + steps: [ + { + id: 'test', + name: 'name', + action: 'output-action', + input: {}, + }, + ], + output: { + foo: `\${{ parameters.entity | parseEntityRef({ defaultNamespace:"namespace-b" }) }}`, + }, + parameters: { + entity: 'user:ben', + }, + }); + + const { output } = await runner.execute(task); + + expect(output.foo).toEqual({ + kind: 'user', + namespace: 'namespace-b', + name: 'ben', + }); + }); + it('provides default kind and namespace for parsing entity ref', async () => { const task = createMockTaskWithSpec({ apiVersion: 'scaffolder.backstage.io/v1beta3', @@ -782,7 +810,7 @@ describe('DefaultWorkflowRunner', () => { }, ], output: { - foo: "${{ parameters.entity | parseEntityRef('user', 'namespace-b') }}", + foo: `\${{ parameters.entity | parseEntityRef({ defaultKind:"user", defaultNamespace:"namespace-b" }) }}`, }, parameters: { entity: 'ben', @@ -798,8 +826,8 @@ describe('DefaultWorkflowRunner', () => { }); }); - it.each(['undefined', 'null', 'None'])( - 'provides default namespace and kind as "%s" value for parsing entity ref', + it.each(['undefined', 'null', 'None', 'group', 0, '{}', '[]'])( + 'ignores invalid context "%s" for parsing entity refF', async kind => { const task = createMockTaskWithSpec({ apiVersion: 'scaffolder.backstage.io/v1beta3', @@ -812,22 +840,48 @@ describe('DefaultWorkflowRunner', () => { }, ], output: { - foo: `\${{ parameters.entity | parseEntityRef(${kind}, 'namespace-b') }}`, + foo: `\${{ parameters.entity | parseEntityRef(${kind}) }}`, }, parameters: { - entity: 'resource:infra-workspace', + entity: 'user:default/ben', }, }); const { output } = await runner.execute(task); expect(output.foo).toEqual({ - kind: 'resource', - namespace: 'namespace-b', - name: 'infra-workspace', + kind: 'user', + namespace: 'default', + name: 'ben', }); }, ); + + it('fails when unable to parse entity ref', async () => { + const task = createMockTaskWithSpec({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + steps: [ + { + id: 'test', + name: 'name', + action: 'output-action', + input: {}, + }, + ], + output: { + foo: `\${{ parameters.entity | parseEntityRef({ defaultNamespace:"namespace-b" }) }}`, + }, + parameters: { + entity: 'ben', + }, + }); + + const { output } = await runner.execute(task); + + expect(output.foo).toEqual( + `\${{ parameters.entity | parseEntityRef({ defaultNamespace:"namespace-b" }) }}`, + ); + }); }); it('provides the pick filter', async () => { From b75655659d5f175df3e4d8c264bcc5ddcf32ec64 Mon Sep 17 00:00:00 2001 From: Enrico Alvarenga Date: Tue, 29 Aug 2023 09:21:23 -0700 Subject: [PATCH 4/6] fix: include explicit types to function args Co-authored-by: Ben Lambert Signed-off-by: Enrico Alvarenga --- plugins/scaffolder-backend/src/lib/templating/filters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/lib/templating/filters.ts b/plugins/scaffolder-backend/src/lib/templating/filters.ts index 81f326b889..7808d6d136 100644 --- a/plugins/scaffolder-backend/src/lib/templating/filters.ts +++ b/plugins/scaffolder-backend/src/lib/templating/filters.ts @@ -27,7 +27,7 @@ export const createDefaultFilters = ({ }): Record => { return { parseRepoUrl: url => parseRepoUrl(url as string, integrations), - parseEntityRef: (ref, context?) => + parseEntityRef: (ref: JsonValue, context?: JsonValue) => parseEntityRef(ref as string, context as JsonObject), pick: (obj: JsonValue, key: JsonValue) => get(obj, key as string), projectSlug: repoUrl => { From 9665ecee847ebcce4113b9b1346f4d70ff148258 Mon Sep 17 00:00:00 2001 From: Enrico Alvarenga Date: Tue, 29 Aug 2023 10:45:58 -0700 Subject: [PATCH 5/6] docs(writing-templates): add built in filters section Signed-off-by: Enrico Alvarenga --- .changeset/tame-jokes-do.md | 11 +-- .../software-templates/writing-templates.md | 95 +++++++++++++++++++ 2 files changed, 96 insertions(+), 10 deletions(-) diff --git a/.changeset/tame-jokes-do.md b/.changeset/tame-jokes-do.md index 35a508e692..8e9b5a6056 100644 --- a/.changeset/tame-jokes-do.md +++ b/.changeset/tame-jokes-do.md @@ -3,13 +3,4 @@ --- Improved the `parseEntityRef` Scaffolder filter by introducing the ability for users to provide default kind and/or namespace values. The filter now takes -2 arguments, similarly to the original [parseEntityRef](<(https://github.com/backstage/backstage/blob/v1.17.2/packages/catalog-model/src/entity/ref.ts#L77)>): - -1. Entity reference -2. [Context optional object](https://github.com/backstage/backstage/blob/v1.17.2/packages/catalog-model/src/entity/ref.ts#L77) - -Check out the following examples: - -- `${{ parameters.entityRef | parseEntityRef | pick('name') }}` -- `${{ parameters.entityRef | parseEntityRef({ defaultKind:"group", defaultNamespace:"default" }) | pick('name') }}` -- `${{ parameters.entityRef | parseEntityRef({ defaultKind:"group" }) | pick('name') }}` +2 arguments, similarly to the original [parseEntityRef](https://github.com/backstage/backstage/blob/v1.17.2/packages/catalog-model/src/entity/ref.ts#L77). diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 67bcb6c179..3594c395eb 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -601,3 +601,98 @@ output things. You can grab that output using `steps.$stepId.output.$property`. You can read more about all the `inputs` and `outputs` defined in the actions in code part of the `JSONSchema`, or you can read more about our [built in actions](./builtin-actions.md). + +## Built in Filters + +Template filters are functions that help you transform data, extract specific information, +and perform various operations in Scaffolder Templates. + +This section introduces the built-in filters provided by Backstage and offers examples of +how to use them in the Scaffolder templates. It's important to mention that Backstage also leverages the +native filters from the Nunjucks library. For a complete list of these native filters and their usage, +refer to the [Nunjucks documentation](https://mozilla.github.io/nunjucks/templating.html#builtin-filters). + +### parseRepoUrl + +The `parseRepoUrl` filter parse a repository URL into +its components, such as `owner`, repository `name`, and more. + +**Usage Example:** + +```yaml +- id: log + name: Parse Repo URL + action: debug:log + input: + extra: ${{ parameters.repoUrl | parseRepoUrl }} +``` + +- **Input**: `https://github.com/backstage/backstage` +- **Output**: [RepoSpec](https://github.com/backstage/backstage/blob/v1.17.2/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts#L39) + +### parseEntityRef + +The `parseEntityRef` filter allows you to extract different parts of +an entity reference, such as the `kind`, `namespace`, and `name`. + +**Usage example** + +1. Without context + +```yaml +- id: log + name: Parse Entity Reference + action: debug:log + input: + extra: ${{ parameters.owner | parseEntityRef }} +``` + +- **Input**: `group:techdocs` +- **Output**: [CompoundEntityRef](https://github.com/backstage/backstage/blob/v1.17.2/packages/catalog-model/src/types.ts#L23) + +2. With context + +```yaml +- id: log + name: Parse Entity Reference + action: debug:log + input: + extra: ${{ parameters.owner | parseEntityRef({ defaultKind:"group", defaultNamespace:"another-namespace" }) }} +``` + +- **Input**: `techdocs` +- **Output**: [CompoundEntityRef](https://github.com/backstage/backstage/blob/v1.17.2/packages/catalog-model/src/types.ts#L23) + +### pick + +This `pick` filter allows you to select specific properties from an object. + +**Usage Example** + +```yaml +- id: log + name: Pick + action: debug:log + input: + extra: ${{ parameters.owner | parseEntityRef | pick('name') }} +``` + +- **Input**: `group:techdocs` +- **Output**: `techndocs` + +### projectSlug + +The `projectSlug` filter generates a project slug from a repository URL + +**Usage Example** + +```yaml +- id: log + name: Project Slug + action: debug:log + input: + extra: ${{ parameters.repoUrl | projectSlug }} +``` + +- **Input**: `https://github.com/backstage/backstage` +- **Output**: `backstage/backstage` From 6ea9deab3d4966ed551ffe00618266184f5f4361 Mon Sep 17 00:00:00 2001 From: Enrico Alvarenga Date: Mon, 11 Sep 2023 08:34:42 -0700 Subject: [PATCH 6/6] docs(writing-templates): correct typos Signed-off-by: Enrico Alvarenga --- docs/features/software-templates/writing-templates.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 3594c395eb..95e9047bde 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -627,7 +627,7 @@ its components, such as `owner`, repository `name`, and more. extra: ${{ parameters.repoUrl | parseRepoUrl }} ``` -- **Input**: `https://github.com/backstage/backstage` +- **Input**: `github.com?repo=backstage&org=backstage` - **Output**: [RepoSpec](https://github.com/backstage/backstage/blob/v1.17.2/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts#L39) ### parseEntityRef @@ -677,8 +677,8 @@ This `pick` filter allows you to select specific properties from an object. extra: ${{ parameters.owner | parseEntityRef | pick('name') }} ``` -- **Input**: `group:techdocs` -- **Output**: `techndocs` +- **Input**: `{ kind: 'Group', namespace: 'default', name: 'techdocs' }` +- **Output**: `techdocs` ### projectSlug @@ -694,5 +694,5 @@ The `projectSlug` filter generates a project slug from a repository URL extra: ${{ parameters.repoUrl | projectSlug }} ``` -- **Input**: `https://github.com/backstage/backstage` +- **Input**: `github.com?repo=backstage&org=backstage` - **Output**: `backstage/backstage`