From b6de0abf85cfc7c1c75626466f1242e665f12cd7 Mon Sep 17 00:00:00 2001 From: Taras Date: Thu, 15 Sep 2022 09:15:57 -0400 Subject: [PATCH 1/6] Add failing test Signed-off-by: Taras --- .../modules/core/PlaceholderProcessor.test.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts index 8fd8114760..e0f7d81968 100644 --- a/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts @@ -387,6 +387,44 @@ describe('PlaceholderProcessor', () => { key: 'url:http://example.com/path-to-file.json', }); }); + + it('accepts arbitrary object as value', async () => { + read.mockResolvedValue( + Buffer.from(JSON.stringify({ a: ['b', 7] }), 'utf-8'), + ); + + const processor = new PlaceholderProcessor({ + resolvers: { + merge: async ({ value }) => { + console.log({ value }); + if (value instanceof Object) { + return { merged: 'value', ...value }; + } + return value; + }, + }, + reader, + integrations, + }); + + const result = await processor.preProcessEntity( + { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'n' }, + spec: { a: { $merge: { passed: 'value' } } }, + }, + { type: 'fake', target: 'http://example.com' }, + () => {}, + ); + + expect(result).toMatchObject({ + apiVersion: 'a', + kind: 'k', + metadata: { name: 'n' }, + spec: { a: { passed: 'value', merged: 'value' } }, + }); + }); }); describe('yamlPlaceholderResolver', () => { From f2f2df9a731491a55bdf530244f21e706af88866 Mon Sep 17 00:00:00 2001 From: Taras Date: Thu, 15 Sep 2022 09:23:07 -0400 Subject: [PATCH 2/6] Allow value to be non-string Signed-off-by: Taras --- .../modules/core/PlaceholderProcessor.test.ts | 1 - .../src/modules/core/PlaceholderProcessor.ts | 16 ++++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts index e0f7d81968..aa0d6c4766 100644 --- a/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts @@ -396,7 +396,6 @@ describe('PlaceholderProcessor', () => { const processor = new PlaceholderProcessor({ resolvers: { merge: async ({ value }) => { - console.log({ value }); if (value instanceof Object) { return { merged: 'value', ...value }; } diff --git a/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts index 39c9dc59ec..cf5679058d 100644 --- a/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts @@ -111,15 +111,19 @@ export class PlaceholderProcessor implements CatalogProcessor { const resolverValue = data[keys[0]]; const resolver = this.options.resolvers[resolverKey]; - if (!resolver || typeof resolverValue !== 'string') { - // If there was no such placeholder resolver or if the value was not a - // string, we err on the side of safety and assume that this is - // something that's best left alone. For example, if the input contains - // JSONSchema, there may be "$ref": "#/definitions/node" nodes in the - // document. + if (!resolver) { + // If there was no such placeholder resolver, we err on the side of safety + // and assume that this is something that's best left alone. For example, if + // the input contains JSONSchema, there may be "$ref": "#/definitions/node" + // nodes in the document. return [data, false]; } + // if (typeof resolverValue !== 'string') { + // treat it as an argument to resolver function + // TODO: make this recursive, but it should resolve from bottom up + // } + const read = async (url: string): Promise => { if (this.options.reader.readUrl) { const response = await this.options.reader.readUrl(url); From 63296ebcd4191c58d11fb168c953440c1871bfd5 Mon Sep 17 00:00:00 2001 From: Taras Date: Thu, 15 Sep 2022 09:44:14 -0400 Subject: [PATCH 3/6] Added changeset Signed-off-by: Taras --- .changeset/rude-bulldogs-sleep.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/rude-bulldogs-sleep.md diff --git a/.changeset/rude-bulldogs-sleep.md b/.changeset/rude-bulldogs-sleep.md new file mode 100644 index 0000000000..8a2a0590b3 --- /dev/null +++ b/.changeset/rude-bulldogs-sleep.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Allow Placeholder value to be any value, not only string. From 95376dd679e66df3c2b464e8b62cb018a8d859a5 Mon Sep 17 00:00:00 2001 From: Taras Date: Thu, 15 Sep 2022 14:50:43 -0400 Subject: [PATCH 4/6] Remove unnecessary mock Signed-off-by: Taras --- .../src/modules/core/PlaceholderProcessor.test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts index aa0d6c4766..105ab03d90 100644 --- a/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts @@ -389,10 +389,6 @@ describe('PlaceholderProcessor', () => { }); it('accepts arbitrary object as value', async () => { - read.mockResolvedValue( - Buffer.from(JSON.stringify({ a: ['b', 7] }), 'utf-8'), - ); - const processor = new PlaceholderProcessor({ resolvers: { merge: async ({ value }) => { From 06bd9581778ad435e9f39c20ed54af9d9be4a8ec Mon Sep 17 00:00:00 2001 From: Taras Date: Thu, 15 Sep 2022 15:08:56 -0400 Subject: [PATCH 5/6] Add some minimal documentation on how to use the PlaceholderProcessor Signed-off-by: Taras --- .../software-templates/writing-templates.md | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index a1cd50f271..2ce7c27bc1 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -581,3 +581,96 @@ 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). + +## Creating reusable templates + +We can use the PlaceholderProcessor to create reusable portions of a template. A placeholder is a property on an entity object that starts with `$`. Backstage has some built in placeholders including `$text`, `$json` and `$yaml`. Each placeholder has a resolver. A resolver is an asyncronous function that receives the value assigned to the placeholder. A resolver is expected to return a promise that resolves to a value. The resolved value will replace the object where the placeholder was defined. + +Let's say we want to reuse a portion of the template that asks user to specify a host name and we want to be able to add other fields to that form step. + +```yaml +apiVersion: scaffolder.backstage.io/v1beta3 +kind: Template +metadata: + name: v1beta3-demo + title: Microservice example + description: scaffolder v1beta3 template demo +spec: + owner: backstage/techdocs-core + type: service + parameters: + - title: Specify a host + parameters: + name: + title: Hostname + type: string + description: Specify host name +``` + +Our placeholder is going to be called `$specifyHostname`. We'd use it like this, + +```yaml +apiVersion: scaffolder.backstage.io/v1beta3 +kind: Template +metadata: + name: v1beta3-demo + title: Microservice example + description: scaffolder v1beta3 template demo +spec: + owner: backstage/techdocs-core + type: service + parameters: + - $specifyHostname: + domainExt: + name: Domain extension + type: string + description: Specify domain extension like .com, .ca, .co.uk or something else. +``` + +The result after the placeholder is applied will look like this, + +```yaml +apiVersion: scaffolder.backstage.io/v1beta3 +kind: Template +metadata: + name: v1beta3-demo + title: Microservice example + description: scaffolder v1beta3 template demo +spec: + owner: backstage/techdocs-core + type: service + parameters: + - title: Specify a host + parameters: + name: + title: Hostname + type: string + description: Specify host name + domainExt: + name: Domain extension + type: string + description: Specify domain extension like .com, .ca, .co.uk or something else. +``` + +To implement this, you have to modify the catalog plugin to create a `specifyHostname` resolver. In `/packages/backend/src/plugins/catalog.ts`. Add the following code, + +```ts +const builder = await CatalogBuilder.create(env); + +builder.setPlaceholderResolver( + 'specifyHostname', + async specifyHostnameResolver({ value }) => { + return { + "title": "Specify a host", + "parameters": { + "name": { + "title": "Hostname", + "type": "string", + "description": "Specify host name" + }, + ...value + } + } + }, + );` +``` From 1f8d4ae7d806f844a66bbd7002f85481510cf6e2 Mon Sep 17 00:00:00 2001 From: Taras Date: Tue, 20 Sep 2022 10:26:10 -0400 Subject: [PATCH 6/6] Remove unnecessary content Signed-off-by: Taras --- .../software-templates/writing-templates.md | 93 ------------------- .../src/modules/core/PlaceholderProcessor.ts | 5 - 2 files changed, 98 deletions(-) diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 2ce7c27bc1..a1cd50f271 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -581,96 +581,3 @@ 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). - -## Creating reusable templates - -We can use the PlaceholderProcessor to create reusable portions of a template. A placeholder is a property on an entity object that starts with `$`. Backstage has some built in placeholders including `$text`, `$json` and `$yaml`. Each placeholder has a resolver. A resolver is an asyncronous function that receives the value assigned to the placeholder. A resolver is expected to return a promise that resolves to a value. The resolved value will replace the object where the placeholder was defined. - -Let's say we want to reuse a portion of the template that asks user to specify a host name and we want to be able to add other fields to that form step. - -```yaml -apiVersion: scaffolder.backstage.io/v1beta3 -kind: Template -metadata: - name: v1beta3-demo - title: Microservice example - description: scaffolder v1beta3 template demo -spec: - owner: backstage/techdocs-core - type: service - parameters: - - title: Specify a host - parameters: - name: - title: Hostname - type: string - description: Specify host name -``` - -Our placeholder is going to be called `$specifyHostname`. We'd use it like this, - -```yaml -apiVersion: scaffolder.backstage.io/v1beta3 -kind: Template -metadata: - name: v1beta3-demo - title: Microservice example - description: scaffolder v1beta3 template demo -spec: - owner: backstage/techdocs-core - type: service - parameters: - - $specifyHostname: - domainExt: - name: Domain extension - type: string - description: Specify domain extension like .com, .ca, .co.uk or something else. -``` - -The result after the placeholder is applied will look like this, - -```yaml -apiVersion: scaffolder.backstage.io/v1beta3 -kind: Template -metadata: - name: v1beta3-demo - title: Microservice example - description: scaffolder v1beta3 template demo -spec: - owner: backstage/techdocs-core - type: service - parameters: - - title: Specify a host - parameters: - name: - title: Hostname - type: string - description: Specify host name - domainExt: - name: Domain extension - type: string - description: Specify domain extension like .com, .ca, .co.uk or something else. -``` - -To implement this, you have to modify the catalog plugin to create a `specifyHostname` resolver. In `/packages/backend/src/plugins/catalog.ts`. Add the following code, - -```ts -const builder = await CatalogBuilder.create(env); - -builder.setPlaceholderResolver( - 'specifyHostname', - async specifyHostnameResolver({ value }) => { - return { - "title": "Specify a host", - "parameters": { - "name": { - "title": "Hostname", - "type": "string", - "description": "Specify host name" - }, - ...value - } - } - }, - );` -``` diff --git a/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts index cf5679058d..5e49c67915 100644 --- a/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts @@ -119,11 +119,6 @@ export class PlaceholderProcessor implements CatalogProcessor { return [data, false]; } - // if (typeof resolverValue !== 'string') { - // treat it as an argument to resolver function - // TODO: make this recursive, but it should resolve from bottom up - // } - const read = async (url: string): Promise => { if (this.options.reader.readUrl) { const response = await this.options.reader.readUrl(url);