From 2380506364c6642dddac6032801e144d1f52e4a3 Mon Sep 17 00:00:00 2001 From: Ilya Savich Date: Fri, 9 Dec 2022 15:58:53 +0100 Subject: [PATCH 001/118] Refactor catalog collator, extracted abstract class and entity processor to make it possible to re-use functionality Signed-off-by: Ilya Savich --- .changeset/clean-queens-judge.md | 5 + plugins/catalog-backend/api-report.md | 58 ++++-- ...l.ts => CatalogCollatorEntityProcessor.ts} | 15 +- .../src/search/CatalogCollatorFactory.ts | 132 ++++++++++++ ...aultCatalogCollatorEntityProcessor.test.ts | 193 ++++++++++++++++++ .../DefaultCatalogCollatorEntityProcessor.ts | 78 +++++++ .../search/DefaultCatalogCollatorFactory.ts | 130 ++---------- plugins/catalog-backend/src/search/index.ts | 5 + .../catalog-backend/src/search/util.test.ts | 146 ------------- 9 files changed, 472 insertions(+), 290 deletions(-) create mode 100644 .changeset/clean-queens-judge.md rename plugins/catalog-backend/src/search/{util.ts => CatalogCollatorEntityProcessor.ts} (58%) create mode 100644 plugins/catalog-backend/src/search/CatalogCollatorFactory.ts create mode 100644 plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityProcessor.test.ts create mode 100644 plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityProcessor.ts delete mode 100644 plugins/catalog-backend/src/search/util.test.ts diff --git a/.changeset/clean-queens-judge.md b/.changeset/clean-queens-judge.md new file mode 100644 index 0000000000..22f87e6959 --- /dev/null +++ b/.changeset/clean-queens-judge.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Refactored catalog collator, extracted abstract class and entity processor to make it possible to re-use functionality diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index a46abda5d3..8432092796 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -167,6 +167,42 @@ export class CatalogBuilder { useLegacySingleProcessorValidation(): this; } +// @public (undocumented) +export abstract class CatalogCollatorFactory + implements DocumentCollatorFactory +{ + protected constructor(options: CatalogCollatorFactoryOptions); + // (undocumented) + static fromConfig( + _config: Config, + _options: CatalogCollatorFactoryCreateOptions, + ): CatalogCollatorFactory; + // (undocumented) + getCollator(): Promise; + // (undocumented) + readonly type: string; + // (undocumented) + readonly visibilityPermission: Permission; +} + +// @public (undocumented) +export type CatalogCollatorFactoryCreateOptions = Omit< + CatalogCollatorFactoryOptions, + 'entityProcessor' | 'type' +>; + +// @public (undocumented) +export type CatalogCollatorFactoryOptions = { + type: string; + discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; + entityProcessor: CatalogCollatorEntityProcessor; + locationTemplate?: string; + filter?: GetEntitiesRequest['filter']; + batchSize?: number; + catalogClient?: CatalogApi; +}; + // @alpha export const catalogConditions: Conditions<{ hasAnnotation: PermissionRule< @@ -351,29 +387,17 @@ export class DefaultCatalogCollator { } // @public (undocumented) -export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { +export class DefaultCatalogCollatorFactory extends CatalogCollatorFactory { // (undocumented) static fromConfig( _config: Config, options: DefaultCatalogCollatorFactoryOptions, ): DefaultCatalogCollatorFactory; - // (undocumented) - getCollator(): Promise; - // (undocumented) - readonly type: string; - // (undocumented) - readonly visibilityPermission: Permission; } // @public (undocumented) -export type DefaultCatalogCollatorFactoryOptions = { - discovery: PluginEndpointDiscovery; - tokenManager: TokenManager; - locationTemplate?: string; - filter?: GetEntitiesRequest['filter']; - batchSize?: number; - catalogClient?: CatalogApi; -}; +export type DefaultCatalogCollatorFactoryOptions = + CatalogCollatorFactoryCreateOptions; export { DeferredEntity }; @@ -583,4 +607,8 @@ export class UrlReaderProcessor implements CatalogProcessor { cache: CatalogProcessorCache, ): Promise; } + +// Warnings were encountered during analysis: +// +// src/search/CatalogCollatorFactory.d.ts:14:5 - (ae-forgotten-export) The symbol "CatalogCollatorEntityProcessor" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/catalog-backend/src/search/util.ts b/plugins/catalog-backend/src/search/CatalogCollatorEntityProcessor.ts similarity index 58% rename from plugins/catalog-backend/src/search/util.ts rename to plugins/catalog-backend/src/search/CatalogCollatorEntityProcessor.ts index fd54d397c9..0288cbec24 100644 --- a/plugins/catalog-backend/src/search/util.ts +++ b/plugins/catalog-backend/src/search/CatalogCollatorEntityProcessor.ts @@ -14,16 +14,9 @@ * limitations under the License. */ -import { Entity, isUserEntity, isGroupEntity } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; +import { CatalogEntityDocument } from '@backstage/plugin-catalog-common'; -export function getDocumentText(entity: Entity): string { - const documentTexts: string[] = []; - documentTexts.push(entity.metadata.description || ''); - - if (isUserEntity(entity) || isGroupEntity(entity)) { - if (entity.spec?.profile?.displayName) { - documentTexts.push(entity.spec.profile.displayName); - } - } - return documentTexts.join(' : '); +export interface CatalogCollatorEntityProcessor { + process(entity: Entity, locationTemplate: string): CatalogEntityDocument; } diff --git a/plugins/catalog-backend/src/search/CatalogCollatorFactory.ts b/plugins/catalog-backend/src/search/CatalogCollatorFactory.ts new file mode 100644 index 0000000000..8b9a60b264 --- /dev/null +++ b/plugins/catalog-backend/src/search/CatalogCollatorFactory.ts @@ -0,0 +1,132 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; +import { + CatalogApi, + CatalogClient, + GetEntitiesRequest, +} from '@backstage/catalog-client'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; +import { + catalogEntityReadPermission, + CatalogEntityDocument, +} from '@backstage/plugin-catalog-common'; +import { Permission } from '@backstage/plugin-permission-common'; +import { Readable } from 'stream'; +import { CatalogCollatorEntityProcessor } from './CatalogCollatorEntityProcessor'; +import { Config } from '@backstage/config'; + +/** @public */ +export type CatalogCollatorFactoryOptions = { + type: string; + discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; + entityProcessor: CatalogCollatorEntityProcessor; + locationTemplate?: string; + filter?: GetEntitiesRequest['filter']; + batchSize?: number; + catalogClient?: CatalogApi; +}; + +/** @public */ +export type CatalogCollatorFactoryCreateOptions = Omit< + CatalogCollatorFactoryOptions, + 'entityProcessor' | 'type' +>; + +/** @public */ +export abstract class CatalogCollatorFactory + implements DocumentCollatorFactory +{ + public readonly type: string; + public readonly visibilityPermission: Permission = + catalogEntityReadPermission; + + private locationTemplate: string; + private filter?: GetEntitiesRequest['filter']; + private batchSize: number; + private readonly catalogClient: CatalogApi; + private tokenManager: TokenManager; + private entityProcessor: CatalogCollatorEntityProcessor; + + static fromConfig( + _config: Config, + _options: CatalogCollatorFactoryCreateOptions, + ): CatalogCollatorFactory { + throw new Error('Method should be implemented'); + } + + protected constructor(options: CatalogCollatorFactoryOptions) { + const { + type, + batchSize, + discovery, + locationTemplate, + filter, + catalogClient, + tokenManager, + entityProcessor, + } = options; + + this.type = type; + this.locationTemplate = + locationTemplate || '/catalog/:namespace/:kind/:name'; + this.filter = filter; + this.batchSize = batchSize || 500; + this.catalogClient = + catalogClient || new CatalogClient({ discoveryApi: discovery }); + this.tokenManager = tokenManager; + this.entityProcessor = entityProcessor; + } + + async getCollator(): Promise { + return Readable.from(this.execute()); + } + + private async *execute(): AsyncGenerator { + const { token } = await this.tokenManager.getToken(); + let entitiesRetrieved = 0; + let moreEntitiesToGet = true; + + // Offset/limit pagination is used on the Catalog Client in order to + // limit (and allow some control over) memory used by the search backend + // at index-time. + while (moreEntitiesToGet) { + const entities = ( + await this.catalogClient.getEntities( + { + filter: this.filter, + limit: this.batchSize, + offset: entitiesRetrieved, + }, + { token }, + ) + ).items; + + // Control looping through entity batches. + moreEntitiesToGet = entities.length === this.batchSize; + entitiesRetrieved += entities.length; + + for (const entity of entities) { + yield this.entityProcessor.process(entity, this.locationTemplate); + } + } + } +} diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityProcessor.test.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityProcessor.test.ts new file mode 100644 index 0000000000..1183d5be97 --- /dev/null +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityProcessor.test.ts @@ -0,0 +1,193 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DefaultCatalogCollatorEntityProcessor } from './DefaultCatalogCollatorEntityProcessor'; + +const entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + title: 'Test Entity', + name: 'test-entity', + description: 'The expected description', + namespace: 'namespace', + }, + spec: { + type: 'some-type', + lifecycle: 'experimental', + owner: 'someone', + }, +}; + +const userEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'test-user-entity', + description: 'The expected user description', + }, + spec: { + profile: { + displayName: 'User 1', + }, + }, +}; + +const locationTemplate = '/catalog/:namespace/:kind/:name'; + +describe('DefaultCatalogCollatorEntityProcessor', () => { + const entityProcessor = new DefaultCatalogCollatorEntityProcessor(); + + describe('process', () => { + it('maps a returned entity', async () => { + const document = entityProcessor.process(entity, locationTemplate); + + expect(document).toMatchObject({ + title: entity.metadata.title, + location: '/catalog/namespace/component/test-entity', + text: entity.metadata.description, + namespace: entity.metadata.namespace, + componentType: entity.spec.type, + lifecycle: entity.spec.lifecycle, + owner: entity.spec.owner, + authorization: { + resourceRef: 'component:namespace/test-entity', + }, + }); + }); + + it('maps a returned entity with default fallback', async () => { + const entityWithoutTitle = { + ...entity, + metadata: { + ...entity.metadata, + title: undefined, + namespace: undefined, + }, + spec: { + type: undefined, + lifecycle: undefined, + owner: undefined, + }, + }; + + const document = entityProcessor.process( + entityWithoutTitle, + locationTemplate, + ); + + expect(document).toMatchObject({ + title: entity.metadata.name, + location: '/catalog/default/component/test-entity', + text: entity.metadata.description, + namespace: 'default', + componentType: 'other', + lifecycle: '', + owner: '', + authorization: { + resourceRef: 'component:default/test-entity', + }, + }); + }); + + it('maps a returned entity with custom locationTemplate', async () => { + const document = entityProcessor.process(entity, '/catalog/:name'); + + expect(document).toMatchObject({ + title: entity.metadata.title, + location: '/catalog/test-entity', + text: entity.metadata.description, + namespace: entity.metadata.namespace, + componentType: entity.spec.type, + lifecycle: entity.spec.lifecycle, + owner: entity.spec.owner, + authorization: { + resourceRef: 'component:namespace/test-entity', + }, + }); + }); + + it('maps a returned user entity', async () => { + const document = entityProcessor.process(userEntity, locationTemplate); + + expect(document).toMatchObject({ + title: userEntity.metadata.name, + location: '/catalog/default/user/test-user-entity', + text: `${userEntity.metadata.description} : ${userEntity.spec.profile.displayName}`, + namespace: 'default', + componentType: 'other', + lifecycle: '', + owner: '', + authorization: { + resourceRef: 'user:default/test-user-entity', + }, + }); + }); + + it('maps a returned user entity without display name', async () => { + const testEntity = { + ...userEntity, + spec: undefined, + }; + + const document = entityProcessor.process(testEntity, locationTemplate); + + expect(document).toMatchObject({ + title: userEntity.metadata.name, + location: '/catalog/default/user/test-user-entity', + text: userEntity.metadata.description, + namespace: 'default', + componentType: 'other', + lifecycle: '', + owner: '', + authorization: { + resourceRef: 'user:default/test-user-entity', + }, + }); + }); + + it('maps a returned group entity', async () => { + const groupEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'test-group-entity', + description: 'The expected group description', + }, + spec: { + profile: { + displayName: 'Group 1', + }, + }, + }; + + const document = entityProcessor.process(groupEntity, locationTemplate); + + expect(document).toMatchObject({ + title: groupEntity.metadata.name, + location: '/catalog/default/group/test-group-entity', + text: `${groupEntity.metadata.description} : ${groupEntity.spec.profile.displayName}`, + namespace: 'default', + componentType: 'other', + lifecycle: '', + owner: '', + authorization: { + resourceRef: 'group:default/test-group-entity', + }, + }); + }); + }); +}); diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityProcessor.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityProcessor.ts new file mode 100644 index 0000000000..3078ed4310 --- /dev/null +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityProcessor.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Entity, + isGroupEntity, + isUserEntity, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { CatalogEntityDocument } from '@backstage/plugin-catalog-common'; +import { CatalogCollatorEntityProcessor } from './CatalogCollatorEntityProcessor'; + +export class DefaultCatalogCollatorEntityProcessor + implements CatalogCollatorEntityProcessor +{ + public process( + entity: Entity, + locationTemplate: string, + ): CatalogEntityDocument { + return { + title: entity.metadata.title ?? entity.metadata.name, + location: this.applyArgsToFormat(locationTemplate, { + namespace: entity.metadata.namespace || 'default', + kind: entity.kind, + name: entity.metadata.name, + }), + text: this.getDocumentText(entity), + componentType: entity.spec?.type?.toString() || 'other', + type: entity.spec?.type?.toString() || 'other', + namespace: entity.metadata.namespace || 'default', + kind: entity.kind, + lifecycle: (entity.spec?.lifecycle as string) || '', + owner: (entity.spec?.owner as string) || '', + authorization: { + resourceRef: stringifyEntityRef(entity), + }, + }; + } + + private applyArgsToFormat( + format: string, + args: Record, + ): string { + let formatted = format; + + for (const [key, value] of Object.entries(args)) { + formatted = formatted.replace(`:${key}`, value); + } + + return formatted.toLowerCase(); + } + + private getDocumentText(entity: Entity): string { + const documentTexts: string[] = []; + documentTexts.push(entity.metadata.description || ''); + + if (isUserEntity(entity) || isGroupEntity(entity)) { + if (entity.spec?.profile?.displayName) { + documentTexts.push(entity.spec.profile.displayName); + } + } + + return documentTexts.join(' : '); + } +} diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts index cf082c271c..8fbc2bef60 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts @@ -14,133 +14,27 @@ * limitations under the License. */ -import { - PluginEndpointDiscovery, - TokenManager, -} from '@backstage/backend-common'; -import { - CatalogApi, - CatalogClient, - GetEntitiesRequest, -} from '@backstage/catalog-client'; -import { stringifyEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; import { - catalogEntityReadPermission, - CatalogEntityDocument, -} from '@backstage/plugin-catalog-common'; -import { Permission } from '@backstage/plugin-permission-common'; -import { Readable } from 'stream'; -import { getDocumentText } from './util'; + CatalogCollatorFactory, + CatalogCollatorFactoryCreateOptions, +} from './CatalogCollatorFactory'; +import { DefaultCatalogCollatorEntityProcessor } from './DefaultCatalogCollatorEntityProcessor'; /** @public */ -export type DefaultCatalogCollatorFactoryOptions = { - discovery: PluginEndpointDiscovery; - tokenManager: TokenManager; - locationTemplate?: string; - filter?: GetEntitiesRequest['filter']; - batchSize?: number; - catalogClient?: CatalogApi; -}; +export type DefaultCatalogCollatorFactoryOptions = + CatalogCollatorFactoryCreateOptions; /** @public */ -export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { - public readonly type: string = 'software-catalog'; - public readonly visibilityPermission: Permission = - catalogEntityReadPermission; - - private locationTemplate: string; - private filter?: GetEntitiesRequest['filter']; - private batchSize: number; - private readonly catalogClient: CatalogApi; - private tokenManager: TokenManager; - +export class DefaultCatalogCollatorFactory extends CatalogCollatorFactory { static fromConfig( _config: Config, options: DefaultCatalogCollatorFactoryOptions, ) { - return new DefaultCatalogCollatorFactory(options); - } - - private constructor(options: DefaultCatalogCollatorFactoryOptions) { - const { - batchSize, - discovery, - locationTemplate, - filter, - catalogClient, - tokenManager, - } = options; - - this.locationTemplate = - locationTemplate || '/catalog/:namespace/:kind/:name'; - this.filter = filter; - this.batchSize = batchSize || 500; - this.catalogClient = - catalogClient || new CatalogClient({ discoveryApi: discovery }); - this.tokenManager = tokenManager; - } - - async getCollator(): Promise { - return Readable.from(this.execute()); - } - - private applyArgsToFormat( - format: string, - args: Record, - ): string { - let formatted = format; - for (const [key, value] of Object.entries(args)) { - formatted = formatted.replace(`:${key}`, value); - } - return formatted.toLowerCase(); - } - - private async *execute(): AsyncGenerator { - const { token } = await this.tokenManager.getToken(); - let entitiesRetrieved = 0; - let moreEntitiesToGet = true; - - // Offset/limit pagination is used on the Catalog Client in order to - // limit (and allow some control over) memory used by the search backend - // at index-time. - while (moreEntitiesToGet) { - const entities = ( - await this.catalogClient.getEntities( - { - filter: this.filter, - limit: this.batchSize, - offset: entitiesRetrieved, - }, - { token }, - ) - ).items; - - // Control looping through entity batches. - moreEntitiesToGet = entities.length === this.batchSize; - entitiesRetrieved += entities.length; - - for (const entity of entities) { - yield { - title: entity.metadata.title ?? entity.metadata.name, - location: this.applyArgsToFormat(this.locationTemplate, { - namespace: entity.metadata.namespace || 'default', - kind: entity.kind, - name: entity.metadata.name, - }), - text: getDocumentText(entity), - componentType: entity.spec?.type?.toString() || 'other', - type: entity.spec?.type?.toString() || 'other', - namespace: entity.metadata.namespace || 'default', - kind: entity.kind, - lifecycle: (entity.spec?.lifecycle as string) || '', - owner: (entity.spec?.owner as string) || '', - authorization: { - resourceRef: stringifyEntityRef(entity), - }, - }; - } - } + return new DefaultCatalogCollatorFactory({ + ...options, + type: 'software-catalog', + entityProcessor: new DefaultCatalogCollatorEntityProcessor(), + }); } } diff --git a/plugins/catalog-backend/src/search/index.ts b/plugins/catalog-backend/src/search/index.ts index 2602f0fada..fd025ddca4 100644 --- a/plugins/catalog-backend/src/search/index.ts +++ b/plugins/catalog-backend/src/search/index.ts @@ -16,6 +16,11 @@ export { DefaultCatalogCollatorFactory } from './DefaultCatalogCollatorFactory'; export type { DefaultCatalogCollatorFactoryOptions } from './DefaultCatalogCollatorFactory'; +export { CatalogCollatorFactory } from './CatalogCollatorFactory'; +export type { + CatalogCollatorFactoryOptions, + CatalogCollatorFactoryCreateOptions, +} from './CatalogCollatorFactory'; /** * todo(backstage/techdocs-core): stop exporting this in a future release. diff --git a/plugins/catalog-backend/src/search/util.test.ts b/plugins/catalog-backend/src/search/util.test.ts deleted file mode 100644 index d2d1776338..0000000000 --- a/plugins/catalog-backend/src/search/util.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - ComponentEntity, - GroupEntity, - UserEntity, -} from '@backstage/catalog-model'; -import { getDocumentText } from './util'; - -describe('getDocumentText', () => { - describe('kind is not User or Group', () => { - test('contains description if set', () => { - const entity = createComponent(); - entity.metadata.description = 'The expected description'; - const actual = getDocumentText(entity); - expect(actual).toContain(entity.metadata.description); - }); - - test('is empty if description is not set', () => { - const entity = createComponent(); - const actual = getDocumentText(entity); - expect(actual).toEqual(''); - }); - }); - - describe('kind is User', () => { - test('contains display name if set', () => { - const entity = createUser(); - const actual = getDocumentText(entity); - expect(actual).toContain(entity.spec.profile?.displayName); - }); - - test('contains description if set', () => { - const entity = createUser(); - const actual = getDocumentText(entity); - expect(actual).toContain(entity.metadata.description); - }); - - test('contains both description and display name if both are set', () => { - const entity = createUser(); - const actual = getDocumentText(entity); - expect(actual).toContain(entity.spec.profile?.displayName); - expect(actual).toContain(entity.metadata.description); - }); - - test('is empty if description and display name are not set', () => { - const entity = createUser(); - delete entity.metadata.description; - delete entity.spec.profile?.displayName; - const actual = getDocumentText(entity); - expect(actual).toEqual(''); - }); - }); - - describe('kind is Group', () => { - test('contains display name if set', () => { - const entity = createGroup(); - const actual = getDocumentText(entity); - expect(actual).toContain(entity.spec.profile?.displayName); - }); - - test('contains description if set', () => { - const entity = createGroup(); - const actual = getDocumentText(entity); - expect(actual).toContain(entity.metadata.description); - }); - - test('contains both description and display name if both are set', () => { - const entity = createGroup(); - const actual = getDocumentText(entity); - expect(actual).toContain(entity.spec.profile?.displayName); - expect(actual).toContain(entity.metadata.description); - }); - - test('is empty if description and display name are not set', () => { - const entity = createGroup(); - delete entity.metadata.description; - delete entity.spec.profile?.displayName; - const actual = getDocumentText(entity); - expect(actual).toEqual(''); - }); - }); -}); - -function createGroup(): GroupEntity { - return { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Group', - metadata: { - name: 'group-1', - description: 'The expected description', - }, - spec: { - type: 'team', - profile: { - displayName: 'Group 1', - }, - children: [], - }, - }; -} - -function createUser(): UserEntity { - return { - apiVersion: 'backstage.io/v1alpha1', - kind: 'User', - metadata: { - name: 'user-1', - description: 'The expected description', - }, - spec: { - profile: { - displayName: 'User 1', - }, - }, - }; -} - -function createComponent(): ComponentEntity { - return { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'component-1', - }, - spec: { - lifecycle: 'experimental', - owner: 'someone', - type: 'service', - }, - }; -} From 8ca614f9e34ce250e3f71ec2b65a636cda475ef1 Mon Sep 17 00:00:00 2001 From: Ilya Savich Date: Mon, 12 Dec 2022 14:08:02 +0100 Subject: [PATCH 002/118] draft Signed-off-by: Ilya Savich --- plugins/catalog-backend/api-report.md | 58 +++----- ...ts => CatalogCollatorEntityTransformer.ts} | 5 +- .../src/search/CatalogCollatorFactory.ts | 132 ------------------ ...tCatalogCollatorEntityTransformer.test.ts} | 29 ++-- ...efaultCatalogCollatorEntityTransformer.ts} | 8 +- .../DefaultCatalogCollatorFactory.test.ts | 46 +++++- .../search/DefaultCatalogCollatorFactory.ts | 110 +++++++++++++-- plugins/catalog-backend/src/search/index.ts | 6 +- 8 files changed, 187 insertions(+), 207 deletions(-) rename plugins/catalog-backend/src/search/{CatalogCollatorEntityProcessor.ts => CatalogCollatorEntityTransformer.ts} (83%) delete mode 100644 plugins/catalog-backend/src/search/CatalogCollatorFactory.ts rename plugins/catalog-backend/src/search/{DefaultCatalogCollatorEntityProcessor.test.ts => DefaultCatalogCollatorEntityTransformer.test.ts} (86%) rename plugins/catalog-backend/src/search/{DefaultCatalogCollatorEntityProcessor.ts => DefaultCatalogCollatorEntityTransformer.ts} (91%) diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 8432092796..dfb3f94dc1 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -168,41 +168,11 @@ export class CatalogBuilder { } // @public (undocumented) -export abstract class CatalogCollatorFactory - implements DocumentCollatorFactory -{ - protected constructor(options: CatalogCollatorFactoryOptions); +export interface CatalogCollatorEntityTransformer { // (undocumented) - static fromConfig( - _config: Config, - _options: CatalogCollatorFactoryCreateOptions, - ): CatalogCollatorFactory; - // (undocumented) - getCollator(): Promise; - // (undocumented) - readonly type: string; - // (undocumented) - readonly visibilityPermission: Permission; + transform(entity: Entity, locationTemplate: string): CatalogEntityDocument; } -// @public (undocumented) -export type CatalogCollatorFactoryCreateOptions = Omit< - CatalogCollatorFactoryOptions, - 'entityProcessor' | 'type' ->; - -// @public (undocumented) -export type CatalogCollatorFactoryOptions = { - type: string; - discovery: PluginEndpointDiscovery; - tokenManager: TokenManager; - entityProcessor: CatalogCollatorEntityProcessor; - locationTemplate?: string; - filter?: GetEntitiesRequest['filter']; - batchSize?: number; - catalogClient?: CatalogApi; -}; - // @alpha export const catalogConditions: Conditions<{ hasAnnotation: PermissionRule< @@ -387,17 +357,31 @@ export class DefaultCatalogCollator { } // @public (undocumented) -export class DefaultCatalogCollatorFactory extends CatalogCollatorFactory { +export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { // (undocumented) static fromConfig( _config: Config, options: DefaultCatalogCollatorFactoryOptions, ): DefaultCatalogCollatorFactory; + // (undocumented) + getCollator(): Promise; + // (undocumented) + readonly type: string; + // (undocumented) + readonly visibilityPermission: Permission; } // @public (undocumented) -export type DefaultCatalogCollatorFactoryOptions = - CatalogCollatorFactoryCreateOptions; +export type DefaultCatalogCollatorFactoryOptions = { + discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; + type?: string; + locationTemplate?: string; + filter?: GetEntitiesRequest['filter']; + batchSize?: number; + catalogClient?: CatalogApi; + entityTransformer?: CatalogCollatorEntityTransformer; +}; export { DeferredEntity }; @@ -607,8 +591,4 @@ export class UrlReaderProcessor implements CatalogProcessor { cache: CatalogProcessorCache, ): Promise; } - -// Warnings were encountered during analysis: -// -// src/search/CatalogCollatorFactory.d.ts:14:5 - (ae-forgotten-export) The symbol "CatalogCollatorEntityProcessor" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/catalog-backend/src/search/CatalogCollatorEntityProcessor.ts b/plugins/catalog-backend/src/search/CatalogCollatorEntityTransformer.ts similarity index 83% rename from plugins/catalog-backend/src/search/CatalogCollatorEntityProcessor.ts rename to plugins/catalog-backend/src/search/CatalogCollatorEntityTransformer.ts index 0288cbec24..74615f40e9 100644 --- a/plugins/catalog-backend/src/search/CatalogCollatorEntityProcessor.ts +++ b/plugins/catalog-backend/src/search/CatalogCollatorEntityTransformer.ts @@ -17,6 +17,7 @@ import { Entity } from '@backstage/catalog-model'; import { CatalogEntityDocument } from '@backstage/plugin-catalog-common'; -export interface CatalogCollatorEntityProcessor { - process(entity: Entity, locationTemplate: string): CatalogEntityDocument; +/** @public */ +export interface CatalogCollatorEntityTransformer { + transform(entity: Entity, locationTemplate: string): CatalogEntityDocument; } diff --git a/plugins/catalog-backend/src/search/CatalogCollatorFactory.ts b/plugins/catalog-backend/src/search/CatalogCollatorFactory.ts deleted file mode 100644 index 8b9a60b264..0000000000 --- a/plugins/catalog-backend/src/search/CatalogCollatorFactory.ts +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - PluginEndpointDiscovery, - TokenManager, -} from '@backstage/backend-common'; -import { - CatalogApi, - CatalogClient, - GetEntitiesRequest, -} from '@backstage/catalog-client'; -import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; -import { - catalogEntityReadPermission, - CatalogEntityDocument, -} from '@backstage/plugin-catalog-common'; -import { Permission } from '@backstage/plugin-permission-common'; -import { Readable } from 'stream'; -import { CatalogCollatorEntityProcessor } from './CatalogCollatorEntityProcessor'; -import { Config } from '@backstage/config'; - -/** @public */ -export type CatalogCollatorFactoryOptions = { - type: string; - discovery: PluginEndpointDiscovery; - tokenManager: TokenManager; - entityProcessor: CatalogCollatorEntityProcessor; - locationTemplate?: string; - filter?: GetEntitiesRequest['filter']; - batchSize?: number; - catalogClient?: CatalogApi; -}; - -/** @public */ -export type CatalogCollatorFactoryCreateOptions = Omit< - CatalogCollatorFactoryOptions, - 'entityProcessor' | 'type' ->; - -/** @public */ -export abstract class CatalogCollatorFactory - implements DocumentCollatorFactory -{ - public readonly type: string; - public readonly visibilityPermission: Permission = - catalogEntityReadPermission; - - private locationTemplate: string; - private filter?: GetEntitiesRequest['filter']; - private batchSize: number; - private readonly catalogClient: CatalogApi; - private tokenManager: TokenManager; - private entityProcessor: CatalogCollatorEntityProcessor; - - static fromConfig( - _config: Config, - _options: CatalogCollatorFactoryCreateOptions, - ): CatalogCollatorFactory { - throw new Error('Method should be implemented'); - } - - protected constructor(options: CatalogCollatorFactoryOptions) { - const { - type, - batchSize, - discovery, - locationTemplate, - filter, - catalogClient, - tokenManager, - entityProcessor, - } = options; - - this.type = type; - this.locationTemplate = - locationTemplate || '/catalog/:namespace/:kind/:name'; - this.filter = filter; - this.batchSize = batchSize || 500; - this.catalogClient = - catalogClient || new CatalogClient({ discoveryApi: discovery }); - this.tokenManager = tokenManager; - this.entityProcessor = entityProcessor; - } - - async getCollator(): Promise { - return Readable.from(this.execute()); - } - - private async *execute(): AsyncGenerator { - const { token } = await this.tokenManager.getToken(); - let entitiesRetrieved = 0; - let moreEntitiesToGet = true; - - // Offset/limit pagination is used on the Catalog Client in order to - // limit (and allow some control over) memory used by the search backend - // at index-time. - while (moreEntitiesToGet) { - const entities = ( - await this.catalogClient.getEntities( - { - filter: this.filter, - limit: this.batchSize, - offset: entitiesRetrieved, - }, - { token }, - ) - ).items; - - // Control looping through entity batches. - moreEntitiesToGet = entities.length === this.batchSize; - entitiesRetrieved += entities.length; - - for (const entity of entities) { - yield this.entityProcessor.process(entity, this.locationTemplate); - } - } - } -} diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityProcessor.test.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityTransformer.test.ts similarity index 86% rename from plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityProcessor.test.ts rename to plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityTransformer.test.ts index 1183d5be97..648d287cdd 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityProcessor.test.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityTransformer.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { DefaultCatalogCollatorEntityProcessor } from './DefaultCatalogCollatorEntityProcessor'; +import { DefaultCatalogCollatorEntityTransformer } from './DefaultCatalogCollatorEntityTransformer'; const entity = { apiVersion: 'backstage.io/v1alpha1', @@ -48,12 +48,12 @@ const userEntity = { const locationTemplate = '/catalog/:namespace/:kind/:name'; -describe('DefaultCatalogCollatorEntityProcessor', () => { - const entityProcessor = new DefaultCatalogCollatorEntityProcessor(); +describe('DefaultCatalogCollatorEntityTransformer', () => { + const entityTransformer = new DefaultCatalogCollatorEntityTransformer(); - describe('process', () => { + describe('transform', () => { it('maps a returned entity', async () => { - const document = entityProcessor.process(entity, locationTemplate); + const document = entityTransformer.transform(entity, locationTemplate); expect(document).toMatchObject({ title: entity.metadata.title, @@ -84,7 +84,7 @@ describe('DefaultCatalogCollatorEntityProcessor', () => { }, }; - const document = entityProcessor.process( + const document = entityTransformer.transform( entityWithoutTitle, locationTemplate, ); @@ -104,7 +104,7 @@ describe('DefaultCatalogCollatorEntityProcessor', () => { }); it('maps a returned entity with custom locationTemplate', async () => { - const document = entityProcessor.process(entity, '/catalog/:name'); + const document = entityTransformer.transform(entity, '/catalog/:name'); expect(document).toMatchObject({ title: entity.metadata.title, @@ -121,7 +121,10 @@ describe('DefaultCatalogCollatorEntityProcessor', () => { }); it('maps a returned user entity', async () => { - const document = entityProcessor.process(userEntity, locationTemplate); + const document = entityTransformer.transform( + userEntity, + locationTemplate, + ); expect(document).toMatchObject({ title: userEntity.metadata.name, @@ -143,7 +146,10 @@ describe('DefaultCatalogCollatorEntityProcessor', () => { spec: undefined, }; - const document = entityProcessor.process(testEntity, locationTemplate); + const document = entityTransformer.transform( + testEntity, + locationTemplate, + ); expect(document).toMatchObject({ title: userEntity.metadata.name, @@ -174,7 +180,10 @@ describe('DefaultCatalogCollatorEntityProcessor', () => { }, }; - const document = entityProcessor.process(groupEntity, locationTemplate); + const document = entityTransformer.transform( + groupEntity, + locationTemplate, + ); expect(document).toMatchObject({ title: groupEntity.metadata.name, diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityProcessor.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityTransformer.ts similarity index 91% rename from plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityProcessor.ts rename to plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityTransformer.ts index 3078ed4310..08410c5eed 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityProcessor.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityTransformer.ts @@ -21,12 +21,12 @@ import { stringifyEntityRef, } from '@backstage/catalog-model'; import { CatalogEntityDocument } from '@backstage/plugin-catalog-common'; -import { CatalogCollatorEntityProcessor } from './CatalogCollatorEntityProcessor'; +import { CatalogCollatorEntityTransformer } from './CatalogCollatorEntityTransformer'; -export class DefaultCatalogCollatorEntityProcessor - implements CatalogCollatorEntityProcessor +export class DefaultCatalogCollatorEntityTransformer + implements CatalogCollatorEntityTransformer { - public process( + public transform( entity: Entity, locationTemplate: string, ): CatalogEntityDocument { diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts index 8b4e5601b4..0399764c34 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts @@ -95,9 +95,19 @@ describe('DefaultCatalogCollatorFactory', () => { ); }); - it('has expected type', () => { - const factory = DefaultCatalogCollatorFactory.fromConfig(config, options); - expect(factory.type).toBe('software-catalog'); + describe('type', () => { + it('has default', () => { + const factory = DefaultCatalogCollatorFactory.fromConfig(config, options); + expect(factory.type).toBe('software-catalog'); + }); + + it('has custom', () => { + const factory = DefaultCatalogCollatorFactory.fromConfig(config, { + ...options, + type: 'custom-type', + }); + expect(factory.type).toBe('custom-type'); + }); }); describe('getCollator', () => { @@ -150,6 +160,36 @@ describe('DefaultCatalogCollatorFactory', () => { }); }); + it('maps a returned entity to an expected CatalogEntityDocument with custom transformer', async () => { + const pipeline = TestPipeline.fromCollator(collator); + const { documents } = await pipeline.execute(); + + expect(documents[0]).toMatchObject({ + title: expectedEntities[0].metadata.name, + location: '/catalog/default/component/test-entity', + text: expectedEntities[0].metadata.description, + namespace: 'default', + componentType: expectedEntities[0]!.spec!.type, + lifecycle: expectedEntities[0]!.spec!.lifecycle, + owner: expectedEntities[0]!.spec!.owner, + authorization: { + resourceRef: 'component:default/test-entity', + }, + }); + expect(documents[1]).toMatchObject({ + title: expectedEntities[1].metadata.title, + location: '/catalog/default/component/test-entity-2', + text: expectedEntities[1].metadata.description, + namespace: 'default', + componentType: expectedEntities[1]!.spec!.type, + lifecycle: expectedEntities[1]!.spec!.lifecycle, + owner: expectedEntities[1]!.spec!.owner, + authorization: { + resourceRef: 'component:default/test-entity-2', + }, + }); + }); + it('maps a returned entity with a custom locationTemplate', async () => { // Provide an alternate location template. factory = DefaultCatalogCollatorFactory.fromConfig(new ConfigReader({}), { diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts index 8fbc2bef60..1b99624fdd 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts @@ -16,25 +16,111 @@ import { Config } from '@backstage/config'; import { - CatalogCollatorFactory, - CatalogCollatorFactoryCreateOptions, -} from './CatalogCollatorFactory'; -import { DefaultCatalogCollatorEntityProcessor } from './DefaultCatalogCollatorEntityProcessor'; + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; +import { + CatalogApi, + CatalogClient, + GetEntitiesRequest, +} from '@backstage/catalog-client'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; +import { + catalogEntityReadPermission, + CatalogEntityDocument, +} from '@backstage/plugin-catalog-common'; +import { Permission } from '@backstage/plugin-permission-common'; +import { Readable } from 'stream'; +import { CatalogCollatorEntityTransformer } from './CatalogCollatorEntityTransformer'; +import { DefaultCatalogCollatorEntityTransformer } from './DefaultCatalogCollatorEntityTransformer'; /** @public */ -export type DefaultCatalogCollatorFactoryOptions = - CatalogCollatorFactoryCreateOptions; +export type DefaultCatalogCollatorFactoryOptions = { + discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; + type?: string; + locationTemplate?: string; + filter?: GetEntitiesRequest['filter']; + batchSize?: number; + catalogClient?: CatalogApi; + entityTransformer?: CatalogCollatorEntityTransformer; +}; /** @public */ -export class DefaultCatalogCollatorFactory extends CatalogCollatorFactory { +export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { + public readonly type: string; + public readonly visibilityPermission: Permission = + catalogEntityReadPermission; + + private locationTemplate: string; + private filter?: GetEntitiesRequest['filter']; + private batchSize: number; + private readonly catalogClient: CatalogApi; + private tokenManager: TokenManager; + private entityTransformer: CatalogCollatorEntityTransformer; + static fromConfig( _config: Config, options: DefaultCatalogCollatorFactoryOptions, ) { - return new DefaultCatalogCollatorFactory({ - ...options, - type: 'software-catalog', - entityProcessor: new DefaultCatalogCollatorEntityProcessor(), - }); + return new DefaultCatalogCollatorFactory(options); + } + + private constructor(options: DefaultCatalogCollatorFactoryOptions) { + const { + batchSize, + discovery, + type, + locationTemplate, + filter, + catalogClient, + tokenManager, + entityTransformer, + } = options; + + this.type = type ?? 'software-catalog'; + this.locationTemplate = + locationTemplate || '/catalog/:namespace/:kind/:name'; + this.filter = filter; + this.batchSize = batchSize || 500; + this.catalogClient = + catalogClient || new CatalogClient({ discoveryApi: discovery }); + this.tokenManager = tokenManager; + this.entityTransformer = + entityTransformer ?? new DefaultCatalogCollatorEntityTransformer(); + } + + async getCollator(): Promise { + return Readable.from(this.execute()); + } + + private async *execute(): AsyncGenerator { + const { token } = await this.tokenManager.getToken(); + let entitiesRetrieved = 0; + let moreEntitiesToGet = true; + + // Offset/limit pagination is used on the Catalog Client in order to + // limit (and allow some control over) memory used by the search backend + // at index-time. + while (moreEntitiesToGet) { + const entities = ( + await this.catalogClient.getEntities( + { + filter: this.filter, + limit: this.batchSize, + offset: entitiesRetrieved, + }, + { token }, + ) + ).items; + + // Control looping through entity batches. + moreEntitiesToGet = entities.length === this.batchSize; + entitiesRetrieved += entities.length; + + for (const entity of entities) { + yield this.entityTransformer.transform(entity, this.locationTemplate); + } + } } } diff --git a/plugins/catalog-backend/src/search/index.ts b/plugins/catalog-backend/src/search/index.ts index fd025ddca4..2675d7be1f 100644 --- a/plugins/catalog-backend/src/search/index.ts +++ b/plugins/catalog-backend/src/search/index.ts @@ -16,11 +16,7 @@ export { DefaultCatalogCollatorFactory } from './DefaultCatalogCollatorFactory'; export type { DefaultCatalogCollatorFactoryOptions } from './DefaultCatalogCollatorFactory'; -export { CatalogCollatorFactory } from './CatalogCollatorFactory'; -export type { - CatalogCollatorFactoryOptions, - CatalogCollatorFactoryCreateOptions, -} from './CatalogCollatorFactory'; +export type { CatalogCollatorEntityTransformer } from './CatalogCollatorEntityTransformer'; /** * todo(backstage/techdocs-core): stop exporting this in a future release. From 8e025f13470a7d11a59a8173ab9b8af68814d30a Mon Sep 17 00:00:00 2001 From: Diego Bardari Date: Wed, 11 Jan 2023 16:40:16 +0100 Subject: [PATCH 003/118] Added support for externalId when assuming role in AwsS3EntityProvider Signed-off-by: Diego Bardari --- .changeset/lemon-tables-train.md | 5 +++++ .../src/credentials/AwsCredentials.ts | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 .changeset/lemon-tables-train.md diff --git a/.changeset/lemon-tables-train.md b/.changeset/lemon-tables-train.md new file mode 100644 index 0000000000..39208a5edf --- /dev/null +++ b/.changeset/lemon-tables-train.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-aws': patch +--- + +Added support for externalId when assuming role in AwsS3EntityProvider diff --git a/plugins/catalog-backend-module-aws/src/credentials/AwsCredentials.ts b/plugins/catalog-backend-module-aws/src/credentials/AwsCredentials.ts index ffb84d1c34..fded78aed4 100644 --- a/plugins/catalog-backend-module-aws/src/credentials/AwsCredentials.ts +++ b/plugins/catalog-backend-module-aws/src/credentials/AwsCredentials.ts @@ -27,6 +27,7 @@ export class AwsCredentials { accessKeyId?: string; secretAccessKey?: string; roleArn?: string; + externalId?: string; }, roleSessionName: string, ): Credentials | CredentialsOptions | undefined { @@ -52,6 +53,7 @@ export class AwsCredentials { params: { RoleArn: roleArn, RoleSessionName: roleSessionName, + ExternalId: config.externalId, }, }); } From 73f4d8764b998a5bca2c6be450005fbe2d94846b Mon Sep 17 00:00:00 2001 From: Evan Fenner <105249079+efenner-cambia@users.noreply.github.com> Date: Thu, 12 Jan 2023 09:25:11 -0800 Subject: [PATCH 004/118] Update Explore link for Backstage Search plugin Fix the explore link for the Backstage Search plugin as identified in https://github.com/backstage/backstage/issues/15716 Signed-off-by: Evan Fenner <105249079+efenner-cambia@users.noreply.github.com> --- microsite/data/plugins/backstage-search-platform.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/data/plugins/backstage-search-platform.yaml b/microsite/data/plugins/backstage-search-platform.yaml index 5a313ae9da..33faaba310 100644 --- a/microsite/data/plugins/backstage-search-platform.yaml +++ b/microsite/data/plugins/backstage-search-platform.yaml @@ -4,7 +4,7 @@ author: Spotify authorUrl: https://github.com/spotify category: Core Feature description: A composable and extensible search platform built to fit your organization’s needs and find information quickly. -documentation: https://backstage.io/docs/features/software-catalog/software-catalog-overview +documentation: https://backstage.io/docs/features/search/search-overview iconUrl: img/backstage-search-platform.svg npmPackageName: '@backstage/plugin-search' order: 5 From 860b1c0902b2f3c359706b3b380e5bb94341b5c7 Mon Sep 17 00:00:00 2001 From: Ilya Savich Date: Tue, 20 Dec 2022 13:16:13 +0100 Subject: [PATCH 005/118] Move authorization and location back, make transformer as a function Signed-off-by: Ilya Savich --- .changeset/clean-queens-judge.md | 2 +- docs/features/search/how-to-guides.md | 36 +++++++++ plugins/catalog-backend/api-report.md | 13 ++-- .../CatalogCollatorEntityTransformer.ts | 6 +- ...DefaultCatalogCollatorEntityTransformer.ts | 78 ------------------- .../DefaultCatalogCollatorFactory.test.ts | 78 +++++++++++-------- .../search/DefaultCatalogCollatorFactory.ts | 35 +++++++-- ...tCatalogCollatorEntityTransformer.test.ts} | 66 ++-------------- ...defaultCatalogCollatorEntityTransformer.ts | 46 +++++++++++ plugins/catalog-backend/src/search/index.ts | 1 + 10 files changed, 175 insertions(+), 186 deletions(-) delete mode 100644 plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityTransformer.ts rename plugins/catalog-backend/src/search/{DefaultCatalogCollatorEntityTransformer.test.ts => defaultCatalogCollatorEntityTransformer.test.ts} (64%) create mode 100644 plugins/catalog-backend/src/search/defaultCatalogCollatorEntityTransformer.ts diff --git a/.changeset/clean-queens-judge.md b/.changeset/clean-queens-judge.md index 22f87e6959..1dd7719c5f 100644 --- a/.changeset/clean-queens-judge.md +++ b/.changeset/clean-queens-judge.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-backend': patch --- -Refactored catalog collator, extracted abstract class and entity processor to make it possible to re-use functionality +The process of adding or modifying fields in the software-catalog search index has been simplified. For more details, see [how to customize fields in the Software Catalog index](../docs/features/search/how-to-guides.md#how-to-customize-fields-in-the-software-catalog-index). diff --git a/docs/features/search/how-to-guides.md b/docs/features/search/how-to-guides.md index 5cc2ab5f06..0eeb9b0454 100644 --- a/docs/features/search/how-to-guides.md +++ b/docs/features/search/how-to-guides.md @@ -114,6 +114,42 @@ of the `SearchType` component. > Check out the documentation around [integrating search into plugins](../../plugins/integrating-search-into-plugins.md#create-a-collator) for how to create your own collator. +## How to customize fields in the Software Catalog index + +Sometimes you will might want to have ability to control +which data passes to search index in catalog collator, or to customize data for specific kind. +You can easily do that by passing `entityTransformer` callback to `DefaultCatalogCollatorFactory`. +You can either just simply amend default behaviour, or even to write completely new document +(which should follow some required basic structure though). + +> `authorization` and `location` cannot be modified via a `entityTransformer`, `location` can be modified only through `locationTemplate`. + +```diff +// packages/backend/src/plugins/search.ts + +const entityTransformer: CatalogCollatorEntityTransformer = (entity: Entity) => { + if (entity.kind === 'SomeKind') { + return { + // customize here output for 'SomeKind' kind + }; + } + + return { + // and customize default output + ...defaultCatalogCollatorEntityTransformer(entity), + text: 'my super cool text', + }; +}; + +indexBuilder.addCollator({ + collator: DefaultCatalogCollatorFactory.fromConfig(env.config, { + discovery: env.discovery, + tokenManager: env.tokenManager, ++ entityTransformer, + }), +}); +``` + ## How to limit what can be searched in the Software Catalog The Software Catalog includes a wealth of information about the components, diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index dfb3f94dc1..1639c8c0cc 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -168,10 +168,9 @@ export class CatalogBuilder { } // @public (undocumented) -export interface CatalogCollatorEntityTransformer { - // (undocumented) - transform(entity: Entity, locationTemplate: string): CatalogEntityDocument; -} +export type CatalogCollatorEntityTransformer = ( + entity: Entity, +) => Omit; // @alpha export const catalogConditions: Conditions<{ @@ -356,6 +355,9 @@ export class DefaultCatalogCollator { readonly visibilityPermission: Permission; } +// @public (undocumented) +export const defaultCatalogCollatorEntityTransformer: CatalogCollatorEntityTransformer; + // @public (undocumented) export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { // (undocumented) @@ -366,7 +368,7 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { // (undocumented) getCollator(): Promise; // (undocumented) - readonly type: string; + readonly type = 'software-catalog'; // (undocumented) readonly visibilityPermission: Permission; } @@ -375,7 +377,6 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { export type DefaultCatalogCollatorFactoryOptions = { discovery: PluginEndpointDiscovery; tokenManager: TokenManager; - type?: string; locationTemplate?: string; filter?: GetEntitiesRequest['filter']; batchSize?: number; diff --git a/plugins/catalog-backend/src/search/CatalogCollatorEntityTransformer.ts b/plugins/catalog-backend/src/search/CatalogCollatorEntityTransformer.ts index 74615f40e9..86d71d4c13 100644 --- a/plugins/catalog-backend/src/search/CatalogCollatorEntityTransformer.ts +++ b/plugins/catalog-backend/src/search/CatalogCollatorEntityTransformer.ts @@ -18,6 +18,6 @@ import { Entity } from '@backstage/catalog-model'; import { CatalogEntityDocument } from '@backstage/plugin-catalog-common'; /** @public */ -export interface CatalogCollatorEntityTransformer { - transform(entity: Entity, locationTemplate: string): CatalogEntityDocument; -} +export type CatalogCollatorEntityTransformer = ( + entity: Entity, +) => Omit; diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityTransformer.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityTransformer.ts deleted file mode 100644 index 08410c5eed..0000000000 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityTransformer.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - Entity, - isGroupEntity, - isUserEntity, - stringifyEntityRef, -} from '@backstage/catalog-model'; -import { CatalogEntityDocument } from '@backstage/plugin-catalog-common'; -import { CatalogCollatorEntityTransformer } from './CatalogCollatorEntityTransformer'; - -export class DefaultCatalogCollatorEntityTransformer - implements CatalogCollatorEntityTransformer -{ - public transform( - entity: Entity, - locationTemplate: string, - ): CatalogEntityDocument { - return { - title: entity.metadata.title ?? entity.metadata.name, - location: this.applyArgsToFormat(locationTemplate, { - namespace: entity.metadata.namespace || 'default', - kind: entity.kind, - name: entity.metadata.name, - }), - text: this.getDocumentText(entity), - componentType: entity.spec?.type?.toString() || 'other', - type: entity.spec?.type?.toString() || 'other', - namespace: entity.metadata.namespace || 'default', - kind: entity.kind, - lifecycle: (entity.spec?.lifecycle as string) || '', - owner: (entity.spec?.owner as string) || '', - authorization: { - resourceRef: stringifyEntityRef(entity), - }, - }; - } - - private applyArgsToFormat( - format: string, - args: Record, - ): string { - let formatted = format; - - for (const [key, value] of Object.entries(args)) { - formatted = formatted.replace(`:${key}`, value); - } - - return formatted.toLowerCase(); - } - - private getDocumentText(entity: Entity): string { - const documentTexts: string[] = []; - documentTexts.push(entity.metadata.description || ''); - - if (isUserEntity(entity) || isGroupEntity(entity)) { - if (entity.spec?.profile?.displayName) { - documentTexts.push(entity.spec.profile.displayName); - } - } - - return documentTexts.join(' : '); - } -} diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts index 0399764c34..462e777dff 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts @@ -95,21 +95,6 @@ describe('DefaultCatalogCollatorFactory', () => { ); }); - describe('type', () => { - it('has default', () => { - const factory = DefaultCatalogCollatorFactory.fromConfig(config, options); - expect(factory.type).toBe('software-catalog'); - }); - - it('has custom', () => { - const factory = DefaultCatalogCollatorFactory.fromConfig(config, { - ...options, - type: 'custom-type', - }); - expect(factory.type).toBe('custom-type'); - }); - }); - describe('getCollator', () => { let factory: DefaultCatalogCollatorFactory; let collator: Readable; @@ -134,24 +119,28 @@ describe('DefaultCatalogCollatorFactory', () => { const pipeline = TestPipeline.fromCollator(collator); const { documents } = await pipeline.execute(); - expect(documents[0]).toMatchObject({ + expect(documents[0]).toEqual({ title: expectedEntities[0].metadata.name, location: '/catalog/default/component/test-entity', text: expectedEntities[0].metadata.description, namespace: 'default', componentType: expectedEntities[0]!.spec!.type, + kind: expectedEntities[0]!.kind, + type: expectedEntities[0]!.spec!.type, lifecycle: expectedEntities[0]!.spec!.lifecycle, owner: expectedEntities[0]!.spec!.owner, authorization: { resourceRef: 'component:default/test-entity', }, }); - expect(documents[1]).toMatchObject({ + expect(documents[1]).toEqual({ title: expectedEntities[1].metadata.title, location: '/catalog/default/component/test-entity-2', text: expectedEntities[1].metadata.description, namespace: 'default', componentType: expectedEntities[1]!.spec!.type, + kind: expectedEntities[1]!.kind, + type: expectedEntities[1]!.spec!.type, lifecycle: expectedEntities[1]!.spec!.lifecycle, owner: expectedEntities[1]!.spec!.owner, authorization: { @@ -161,29 +150,54 @@ describe('DefaultCatalogCollatorFactory', () => { }); it('maps a returned entity to an expected CatalogEntityDocument with custom transformer', async () => { - const pipeline = TestPipeline.fromCollator(collator); + const customFactory = DefaultCatalogCollatorFactory.fromConfig(config, { + ...options, + entityTransformer: entity => ({ + title: `custom-title-${ + entity.metadata.title ?? entity.metadata.name + }`, + namespace: 'custom/namespace', + text: 'custom-text', + type: 'custom-type', + componentType: 'custom-component-type', + kind: 'custom-kind', + lifecycle: 'custom-lifecycle', + owner: 'custom-owner', + authorization: { + resourceRef: 'custom:resource/ref', + }, + location: '/custom/location', + }), + }); + const customCollator = await customFactory.getCollator(); + + const pipeline = TestPipeline.fromCollator(customCollator); const { documents } = await pipeline.execute(); - expect(documents[0]).toMatchObject({ - title: expectedEntities[0].metadata.name, + expect(documents[0]).toEqual({ + title: 'custom-title-test-entity', location: '/catalog/default/component/test-entity', - text: expectedEntities[0].metadata.description, - namespace: 'default', - componentType: expectedEntities[0]!.spec!.type, - lifecycle: expectedEntities[0]!.spec!.lifecycle, - owner: expectedEntities[0]!.spec!.owner, + text: 'custom-text', + namespace: 'custom/namespace', + componentType: 'custom-component-type', + kind: 'custom-kind', + type: 'custom-type', + lifecycle: 'custom-lifecycle', + owner: 'custom-owner', authorization: { resourceRef: 'component:default/test-entity', }, }); - expect(documents[1]).toMatchObject({ - title: expectedEntities[1].metadata.title, + expect(documents[1]).toEqual({ + title: 'custom-title-Test Entity', location: '/catalog/default/component/test-entity-2', - text: expectedEntities[1].metadata.description, - namespace: 'default', - componentType: expectedEntities[1]!.spec!.type, - lifecycle: expectedEntities[1]!.spec!.lifecycle, - owner: expectedEntities[1]!.spec!.owner, + text: 'custom-text', + namespace: 'custom/namespace', + componentType: 'custom-component-type', + kind: 'custom-kind', + type: 'custom-type', + lifecycle: 'custom-lifecycle', + owner: 'custom-owner', authorization: { resourceRef: 'component:default/test-entity-2', }, diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts index 1b99624fdd..1c03836e51 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts @@ -32,13 +32,13 @@ import { import { Permission } from '@backstage/plugin-permission-common'; import { Readable } from 'stream'; import { CatalogCollatorEntityTransformer } from './CatalogCollatorEntityTransformer'; -import { DefaultCatalogCollatorEntityTransformer } from './DefaultCatalogCollatorEntityTransformer'; +import { defaultCatalogCollatorEntityTransformer } from './defaultCatalogCollatorEntityTransformer'; +import { stringifyEntityRef } from '@backstage/catalog-model'; /** @public */ export type DefaultCatalogCollatorFactoryOptions = { discovery: PluginEndpointDiscovery; tokenManager: TokenManager; - type?: string; locationTemplate?: string; filter?: GetEntitiesRequest['filter']; batchSize?: number; @@ -48,7 +48,7 @@ export type DefaultCatalogCollatorFactoryOptions = { /** @public */ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { - public readonly type: string; + public readonly type = 'software-catalog'; public readonly visibilityPermission: Permission = catalogEntityReadPermission; @@ -70,7 +70,6 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { const { batchSize, discovery, - type, locationTemplate, filter, catalogClient, @@ -78,7 +77,6 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { entityTransformer, } = options; - this.type = type ?? 'software-catalog'; this.locationTemplate = locationTemplate || '/catalog/:namespace/:kind/:name'; this.filter = filter; @@ -87,7 +85,7 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { catalogClient || new CatalogClient({ discoveryApi: discovery }); this.tokenManager = tokenManager; this.entityTransformer = - entityTransformer ?? new DefaultCatalogCollatorEntityTransformer(); + entityTransformer ?? defaultCatalogCollatorEntityTransformer; } async getCollator(): Promise { @@ -119,8 +117,31 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { entitiesRetrieved += entities.length; for (const entity of entities) { - yield this.entityTransformer.transform(entity, this.locationTemplate); + yield { + ...this.entityTransformer(entity), + authorization: { + resourceRef: stringifyEntityRef(entity), + }, + location: this.applyArgsToFormat(this.locationTemplate, { + namespace: entity.metadata.namespace || 'default', + kind: entity.kind, + name: entity.metadata.name, + }), + }; } } } + + private applyArgsToFormat( + format: string, + args: Record, + ): string { + let formatted = format; + + for (const [key, value] of Object.entries(args)) { + formatted = formatted.replace(`:${key}`, value); + } + + return formatted.toLowerCase(); + } } diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityTransformer.test.ts b/plugins/catalog-backend/src/search/defaultCatalogCollatorEntityTransformer.test.ts similarity index 64% rename from plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityTransformer.test.ts rename to plugins/catalog-backend/src/search/defaultCatalogCollatorEntityTransformer.test.ts index 648d287cdd..8206a58599 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityTransformer.test.ts +++ b/plugins/catalog-backend/src/search/defaultCatalogCollatorEntityTransformer.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { DefaultCatalogCollatorEntityTransformer } from './DefaultCatalogCollatorEntityTransformer'; +import { defaultCatalogCollatorEntityTransformer } from './defaultCatalogCollatorEntityTransformer'; const entity = { apiVersion: 'backstage.io/v1alpha1', @@ -46,26 +46,18 @@ const userEntity = { }, }; -const locationTemplate = '/catalog/:namespace/:kind/:name'; - describe('DefaultCatalogCollatorEntityTransformer', () => { - const entityTransformer = new DefaultCatalogCollatorEntityTransformer(); - describe('transform', () => { it('maps a returned entity', async () => { - const document = entityTransformer.transform(entity, locationTemplate); + const document = defaultCatalogCollatorEntityTransformer(entity); expect(document).toMatchObject({ title: entity.metadata.title, - location: '/catalog/namespace/component/test-entity', text: entity.metadata.description, namespace: entity.metadata.namespace, componentType: entity.spec.type, lifecycle: entity.spec.lifecycle, owner: entity.spec.owner, - authorization: { - resourceRef: 'component:namespace/test-entity', - }, }); }); @@ -84,59 +76,29 @@ describe('DefaultCatalogCollatorEntityTransformer', () => { }, }; - const document = entityTransformer.transform( - entityWithoutTitle, - locationTemplate, - ); + const document = + defaultCatalogCollatorEntityTransformer(entityWithoutTitle); expect(document).toMatchObject({ title: entity.metadata.name, - location: '/catalog/default/component/test-entity', text: entity.metadata.description, namespace: 'default', componentType: 'other', lifecycle: '', owner: '', - authorization: { - resourceRef: 'component:default/test-entity', - }, - }); - }); - - it('maps a returned entity with custom locationTemplate', async () => { - const document = entityTransformer.transform(entity, '/catalog/:name'); - - expect(document).toMatchObject({ - title: entity.metadata.title, - location: '/catalog/test-entity', - text: entity.metadata.description, - namespace: entity.metadata.namespace, - componentType: entity.spec.type, - lifecycle: entity.spec.lifecycle, - owner: entity.spec.owner, - authorization: { - resourceRef: 'component:namespace/test-entity', - }, }); }); it('maps a returned user entity', async () => { - const document = entityTransformer.transform( - userEntity, - locationTemplate, - ); + const document = defaultCatalogCollatorEntityTransformer(userEntity); expect(document).toMatchObject({ title: userEntity.metadata.name, - location: '/catalog/default/user/test-user-entity', text: `${userEntity.metadata.description} : ${userEntity.spec.profile.displayName}`, namespace: 'default', componentType: 'other', lifecycle: '', owner: '', - authorization: { - resourceRef: 'user:default/test-user-entity', - }, }); }); @@ -146,22 +108,15 @@ describe('DefaultCatalogCollatorEntityTransformer', () => { spec: undefined, }; - const document = entityTransformer.transform( - testEntity, - locationTemplate, - ); + const document = defaultCatalogCollatorEntityTransformer(testEntity); expect(document).toMatchObject({ title: userEntity.metadata.name, - location: '/catalog/default/user/test-user-entity', text: userEntity.metadata.description, namespace: 'default', componentType: 'other', lifecycle: '', owner: '', - authorization: { - resourceRef: 'user:default/test-user-entity', - }, }); }); @@ -180,22 +135,15 @@ describe('DefaultCatalogCollatorEntityTransformer', () => { }, }; - const document = entityTransformer.transform( - groupEntity, - locationTemplate, - ); + const document = defaultCatalogCollatorEntityTransformer(groupEntity); expect(document).toMatchObject({ title: groupEntity.metadata.name, - location: '/catalog/default/group/test-group-entity', text: `${groupEntity.metadata.description} : ${groupEntity.spec.profile.displayName}`, namespace: 'default', componentType: 'other', lifecycle: '', owner: '', - authorization: { - resourceRef: 'group:default/test-group-entity', - }, }); }); }); diff --git a/plugins/catalog-backend/src/search/defaultCatalogCollatorEntityTransformer.ts b/plugins/catalog-backend/src/search/defaultCatalogCollatorEntityTransformer.ts new file mode 100644 index 0000000000..2fc0c5d50d --- /dev/null +++ b/plugins/catalog-backend/src/search/defaultCatalogCollatorEntityTransformer.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity, isGroupEntity, isUserEntity } from '@backstage/catalog-model'; +import { CatalogCollatorEntityTransformer } from './CatalogCollatorEntityTransformer'; + +const getDocumentText = (entity: Entity): string => { + const documentTexts: string[] = []; + documentTexts.push(entity.metadata.description || ''); + + if (isUserEntity(entity) || isGroupEntity(entity)) { + if (entity.spec?.profile?.displayName) { + documentTexts.push(entity.spec.profile.displayName); + } + } + + return documentTexts.join(' : '); +}; + +/** @public */ +export const defaultCatalogCollatorEntityTransformer: CatalogCollatorEntityTransformer = + (entity: Entity) => { + return { + title: entity.metadata.title ?? entity.metadata.name, + text: getDocumentText(entity), + componentType: entity.spec?.type?.toString() || 'other', + type: entity.spec?.type?.toString() || 'other', + namespace: entity.metadata.namespace || 'default', + kind: entity.kind, + lifecycle: (entity.spec?.lifecycle as string) || '', + owner: (entity.spec?.owner as string) || '', + }; + }; diff --git a/plugins/catalog-backend/src/search/index.ts b/plugins/catalog-backend/src/search/index.ts index 2675d7be1f..c2a23e5132 100644 --- a/plugins/catalog-backend/src/search/index.ts +++ b/plugins/catalog-backend/src/search/index.ts @@ -17,6 +17,7 @@ export { DefaultCatalogCollatorFactory } from './DefaultCatalogCollatorFactory'; export type { DefaultCatalogCollatorFactoryOptions } from './DefaultCatalogCollatorFactory'; export type { CatalogCollatorEntityTransformer } from './CatalogCollatorEntityTransformer'; +export type { defaultCatalogCollatorEntityTransformer } from './defaultCatalogCollatorEntityTransformer'; /** * todo(backstage/techdocs-core): stop exporting this in a future release. From c59a5e114aaf40d60c247c6117ddc4e96c4f94f6 Mon Sep 17 00:00:00 2001 From: Mengnan Gong Date: Mon, 16 Jan 2023 16:32:28 +0800 Subject: [PATCH 006/118] Improve the Kubernetes plugin documentation - Add the `objectTypes` config - Add the access needed to fetch pod metrics in the RBAC chapter Signed-off-by: Mengnan Gong --- .github/vale/Vocab/Backstage/accept.txt | 2 ++ docs/features/kubernetes/configuration.md | 42 +++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/.github/vale/Vocab/Backstage/accept.txt b/.github/vale/Vocab/Backstage/accept.txt index 98160a496d..d8c962ecc2 100644 --- a/.github/vale/Vocab/Backstage/accept.txt +++ b/.github/vale/Vocab/Backstage/accept.txt @@ -70,6 +70,7 @@ cron cronjobs crontab css +daemonsets Datadog dataflow dayjs @@ -177,6 +178,7 @@ learnings Leasot lerna Lerna +limitranges LocalStack lockdown lockfile diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 41d72b2be2..8b22ddea1a 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -402,6 +402,41 @@ view the Kubernetes API docs for your Kubernetes version (e.g. [API Groups for v1.22](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#-strong-api-groups-strong-) ) +### `objectTypes` (optional) + +Overrides for the Kubernetes object types fetched from the cluster. The default object types are: + +- pods +- services +- configmaps +- limitranges +- deployments +- replicasets +- horizontalpodautoscalers +- jobs +- cronjobs +- ingresses +- statefulsets +- daemonsets + +You may use this config to override the default object types if you only want a subset of +the default ones. However, it's currently not supported to fetch object types other +than the ones specified in the default types. + +Example: + +```yaml +--- +kubernetes: + objectTypes: + - 'configmaps' + - 'deployments' + - 'limitranges' + - 'pods' + - 'services' + - 'statefulsets' +``` + ### Role Based Access Control The current RBAC permissions required are read-only cluster wide, the below @@ -441,6 +476,13 @@ rules: - get - list - watch + - apiGroups: + - metrics.k8s.io + resources: + - pods + verbs: + - get + - list ``` ## Surfacing your Kubernetes components as part of an entity From 2ebdbdbae0e8872546c83f0e046c8c9ad3fe8ca3 Mon Sep 17 00:00:00 2001 From: Michael Paglione Date: Tue, 17 Jan 2023 20:26:47 -0500 Subject: [PATCH 007/118] Fixes issue #15466: adjust properties on adopoters form, allow for scroll, set feature to fixed height, add media queires for various breakpoints to keep integrity of the feature for smartphones and small devices Signed-off-by: Michael Paglione --- microsite/static/css/custom.css | 41 ++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/microsite/static/css/custom.css b/microsite/static/css/custom.css index 59f9571a70..3330b0ffcf 100644 --- a/microsite/static/css/custom.css +++ b/microsite/static/css/custom.css @@ -1244,12 +1244,24 @@ h3.collapsible span.arrow { transform: translateY(-50%) translateX(500px); } +@media only screen and (max-height: 675px){ + .Sidebar__Container { + top: 65%; + } +} + +@media only screen and (max-width: 650px) { + .Sidebar__Container { + transform: translateY(-50%) translateX(300px); + } +} + .Sidebar__Container--open { transform: translateY(-50%); } .Sidebar__Button { - transform: rotate(-90deg) translateY(48px); + transform: rotate(-90deg) translateY(49px); padding: 12px 16px; border-radius: 8px 8px 0 0; background-color: rgb(92, 214, 200); @@ -1263,18 +1275,34 @@ h3.collapsible span.arrow { cursor: pointer; } +@media only screen and (max-width: 650px) { + .Sidebar__Button { + width: 175px; + font-size: 15px; + } +} + .Sidebar__Button:hover { - transform: rotate(-90deg) translateY(48px) scale(1.01); + transform: rotate(-90deg) translateY(49px) scale(1.01); } #Sidebar__HubSpotContainer { width: 500px; background-color: white; border-radius: 8px 0 0 8px; - padding: 16px; - padding-bottom: 0px; - z-index: 10001; - min-height: 260px; + height: 500px; +} + +@media only screen and (max-width: 650px) { + #Sidebar__HubSpotContainer { + width: 300px; + } +} + +#Sidebar__HubSpotContainer form{ + overflow-y: auto; + height: 100%; + padding: 25px 25px 0px 25px; } #Sidebar__HubSpotContainer .hs-button { @@ -1284,6 +1312,7 @@ h3.collapsible span.arrow { color: rgb(0, 0, 0); border: 0; } + #Sidebar__HubSpotContainer.submitted-message { min-height: 260px; display: flex; From d72866f0cc55b3dac81ed43979fb0de51b633296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 19 Jan 2023 14:57:26 +0100 Subject: [PATCH 008/118] create new package scaffolder-node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/itchy-goats-melt.md | 8 +++ .changeset/lazy-badgers-try.md | 5 ++ .../api-report.md | 2 +- .../package.json | 1 + .../src/actions/fetch/cookiecutter.test.ts | 2 +- .../src/actions/fetch/cookiecutter.ts | 2 +- .../api-report.md | 2 +- .../package.json | 1 + .../src/actions/fetch/rails/index.ts | 6 +- .../api-report.md | 2 +- .../package.json | 2 +- .../src/actions/createProject.ts | 2 +- .../api-report.md | 2 +- .../package.json | 2 +- .../src/actions/run/yeoman.test.ts | 2 +- .../src/actions/run/yeoman.ts | 2 +- plugins/scaffolder-backend/api-report.md | 48 +------------ plugins/scaffolder-backend/package.json | 1 + .../src/ScaffolderPlugin.ts | 31 ++++----- .../actions/TemplateActionRegistry.ts | 2 +- .../actions/builtin/catalog/fetch.ts | 2 +- .../actions/builtin/catalog/register.ts | 2 +- .../actions/builtin/catalog/write.ts | 2 +- .../actions/builtin/createBuiltinActions.ts | 2 +- .../scaffolder/actions/builtin/debug/log.ts | 2 +- .../actions/builtin/fetch/plain.test.ts | 1 + .../scaffolder/actions/builtin/fetch/plain.ts | 2 +- .../actions/builtin/fetch/template.test.ts | 5 +- .../actions/builtin/fetch/template.ts | 2 +- .../actions/builtin/filesystem/delete.ts | 3 +- .../actions/builtin/filesystem/rename.ts | 4 +- .../github/githubActionsDispatch.test.ts | 5 +- .../builtin/github/githubActionsDispatch.ts | 3 +- .../builtin/github/githubIssuesLabel.test.ts | 2 +- .../builtin/github/githubIssuesLabel.ts | 3 +- .../builtin/github/githubRepoCreate.test.ts | 2 +- .../builtin/github/githubRepoCreate.ts | 3 +- .../builtin/github/githubRepoPush.test.ts | 2 +- .../actions/builtin/github/githubRepoPush.ts | 3 +- .../builtin/github/githubWebhook.test.ts | 2 +- .../actions/builtin/github/githubWebhook.ts | 3 +- .../actions/builtin/publish/azure.test.ts | 1 + .../actions/builtin/publish/azure.ts | 2 +- .../actions/builtin/publish/bitbucket.ts | 2 +- .../actions/builtin/publish/bitbucketCloud.ts | 2 +- .../builtin/publish/bitbucketServer.ts | 2 +- .../actions/builtin/publish/gerrit.ts | 3 +- .../actions/builtin/publish/gerritReview.ts | 3 +- .../actions/builtin/publish/github.test.ts | 2 +- .../actions/builtin/publish/github.ts | 3 +- .../builtin/publish/githubPullRequest.test.ts | 5 +- .../builtin/publish/githubPullRequest.ts | 4 +- .../actions/builtin/publish/gitlab.test.ts | 1 + .../actions/builtin/publish/gitlab.ts | 2 +- .../publish/gitlabMergeRequest.test.ts | 2 +- .../builtin/publish/gitlabMergeRequest.ts | 3 +- .../src/scaffolder/actions/index.ts | 2 - .../dryrun/DecoratedActionsRegistry.ts | 3 +- .../src/scaffolder/dryrun/createDryRunner.ts | 7 +- .../tasks/NunjucksWorkflowRunner.test.ts | 3 +- .../tasks/NunjucksWorkflowRunner.ts | 3 +- .../tasks/StorageTaskBroker.test.ts | 3 +- .../src/scaffolder/tasks/StorageTaskBroker.ts | 2 +- .../src/scaffolder/tasks/index.ts | 2 +- .../src/scaffolder/tasks/types.ts | 10 +-- .../scaffolder-backend/src/service/router.ts | 2 +- plugins/scaffolder-node/.eslintrc.js | 1 + plugins/scaffolder-node/README.md | 3 + plugins/scaffolder-node/api-report.md | 68 +++++++++++++++++++ plugins/scaffolder-node/package.json | 41 +++++++++++ .../src}/actions/createTemplateAction.ts | 1 + plugins/scaffolder-node/src/actions/index.ts | 18 +++++ .../src}/actions/types.ts | 4 +- plugins/scaffolder-node/src/extensions.ts | 37 ++++++++++ plugins/scaffolder-node/src/index.ts | 28 ++++++++ plugins/scaffolder-node/src/setupTests.ts | 17 +++++ plugins/scaffolder-node/src/tasks/index.ts | 17 +++++ plugins/scaffolder-node/src/tasks/types.ts | 24 +++++++ yarn.lock | 21 +++++- 79 files changed, 392 insertions(+), 142 deletions(-) create mode 100644 .changeset/itchy-goats-melt.md create mode 100644 .changeset/lazy-badgers-try.md create mode 100644 plugins/scaffolder-node/.eslintrc.js create mode 100644 plugins/scaffolder-node/README.md create mode 100644 plugins/scaffolder-node/api-report.md create mode 100644 plugins/scaffolder-node/package.json rename plugins/{scaffolder-backend/src/scaffolder => scaffolder-node/src}/actions/createTemplateAction.ts (99%) create mode 100644 plugins/scaffolder-node/src/actions/index.ts rename plugins/{scaffolder-backend/src/scaffolder => scaffolder-node/src}/actions/types.ts (95%) create mode 100644 plugins/scaffolder-node/src/extensions.ts create mode 100644 plugins/scaffolder-node/src/index.ts create mode 100644 plugins/scaffolder-node/src/setupTests.ts create mode 100644 plugins/scaffolder-node/src/tasks/index.ts create mode 100644 plugins/scaffolder-node/src/tasks/types.ts diff --git a/.changeset/itchy-goats-melt.md b/.changeset/itchy-goats-melt.md new file mode 100644 index 0000000000..dc61285d11 --- /dev/null +++ b/.changeset/itchy-goats-melt.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch +'@backstage/plugin-scaffolder-backend-module-sentry': patch +'@backstage/plugin-scaffolder-backend-module-yeoman': patch +'@backstage/plugin-scaffolder-backend-module-rails': patch +--- + +Internal refactor to use the new scaffolder-node package for some functionality diff --git a/.changeset/lazy-badgers-try.md b/.changeset/lazy-badgers-try.md new file mode 100644 index 0000000000..99491f4bee --- /dev/null +++ b/.changeset/lazy-badgers-try.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-node': minor +--- + +New package that takes over some of the types and functionality from scaffolder-backend that are shared with other modules diff --git a/plugins/scaffolder-backend-module-cookiecutter/api-report.md b/plugins/scaffolder-backend-module-cookiecutter/api-report.md index e55c6368c4..76f2ced66f 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/api-report.md +++ b/plugins/scaffolder-backend-module-cookiecutter/api-report.md @@ -8,7 +8,7 @@ import { ContainerRunner } from '@backstage/backend-common'; import { JsonObject } from '@backstage/types'; import { ScmIntegrations } from '@backstage/integration'; -import { TemplateAction } from '@backstage/plugin-scaffolder-backend'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { UrlReader } from '@backstage/backend-common'; // @public diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 6a1ee7fd3b..57457dc051 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -28,6 +28,7 @@ "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/plugin-scaffolder-backend": "workspace:^", + "@backstage/plugin-scaffolder-node": "workspace:^", "@backstage/types": "workspace:^", "command-exists": "^1.2.9", "fs-extra": "10.1.0", diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts index 15e5d25183..f8b049f95f 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts @@ -27,7 +27,7 @@ import os from 'os'; import { PassThrough } from 'stream'; import { createFetchCookiecutterAction } from './cookiecutter'; import { join } from 'path'; -import type { ActionContext } from '@backstage/plugin-scaffolder-backend'; +import type { ActionContext } from '@backstage/plugin-scaffolder-node'; const executeShellCommand = jest.fn(); const commandExists = jest.fn(); diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts index 7575627506..5c69b9dce2 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts @@ -27,10 +27,10 @@ import fs from 'fs-extra'; import path, { resolve as resolvePath } from 'path'; import { Writable } from 'stream'; import { - createTemplateAction, fetchContents, executeShellCommand, } from '@backstage/plugin-scaffolder-backend'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; export class CookiecutterRunner { private readonly containerRunner: ContainerRunner; diff --git a/plugins/scaffolder-backend-module-rails/api-report.md b/plugins/scaffolder-backend-module-rails/api-report.md index 551da91314..c91daa1344 100644 --- a/plugins/scaffolder-backend-module-rails/api-report.md +++ b/plugins/scaffolder-backend-module-rails/api-report.md @@ -6,7 +6,7 @@ import { ContainerRunner } from '@backstage/backend-common'; import { JsonObject } from '@backstage/types'; import { ScmIntegrations } from '@backstage/integration'; -import { TemplateAction } from '@backstage/plugin-scaffolder-backend'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { UrlReader } from '@backstage/backend-common'; // @public diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index 408db6d559..98a5db9de8 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -28,6 +28,7 @@ "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/plugin-scaffolder-backend": "workspace:^", + "@backstage/plugin-scaffolder-node": "workspace:^", "@backstage/types": "workspace:^", "command-exists": "^1.2.9", "fs-extra": "^10.0.1" diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts index 4a5bd7dfd1..2a8a2554a2 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts @@ -19,10 +19,8 @@ import { JsonObject } from '@backstage/types'; import { InputError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; import fs from 'fs-extra'; -import { - createTemplateAction, - fetchContents, -} from '@backstage/plugin-scaffolder-backend'; +import { fetchContents } from '@backstage/plugin-scaffolder-backend'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { resolve as resolvePath } from 'path'; import { RailsNewRunner } from './railsNewRunner'; diff --git a/plugins/scaffolder-backend-module-sentry/api-report.md b/plugins/scaffolder-backend-module-sentry/api-report.md index 8b0c229e03..ef09bdebc0 100644 --- a/plugins/scaffolder-backend-module-sentry/api-report.md +++ b/plugins/scaffolder-backend-module-sentry/api-report.md @@ -4,7 +4,7 @@ ```ts import { Config } from '@backstage/config'; -import { TemplateAction } from '@backstage/plugin-scaffolder-backend'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; // @public export function createSentryCreateProjectAction(options: { diff --git a/plugins/scaffolder-backend-module-sentry/package.json b/plugins/scaffolder-backend-module-sentry/package.json index 8eaf5c398d..c99308c5e0 100644 --- a/plugins/scaffolder-backend-module-sentry/package.json +++ b/plugins/scaffolder-backend-module-sentry/package.json @@ -25,7 +25,7 @@ "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", - "@backstage/plugin-scaffolder-backend": "workspace:^" + "@backstage/plugin-scaffolder-node": "workspace:^" }, "peerDependencies": { "react": "^16.13.1 || ^17.0.0" diff --git a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.ts b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.ts index bfcc8f016b..85a74eda07 100644 --- a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.ts +++ b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createTemplateAction } from '@backstage/plugin-scaffolder-backend'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { InputError } from '@backstage/errors'; import { Config } from '@backstage/config'; diff --git a/plugins/scaffolder-backend-module-yeoman/api-report.md b/plugins/scaffolder-backend-module-yeoman/api-report.md index 0f8747e592..562fe52ac2 100644 --- a/plugins/scaffolder-backend-module-yeoman/api-report.md +++ b/plugins/scaffolder-backend-module-yeoman/api-report.md @@ -4,7 +4,7 @@ ```ts import { JsonObject } from '@backstage/types'; -import { TemplateAction } from '@backstage/plugin-scaffolder-backend'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; // @public export function createRunYeomanAction(): TemplateAction<{ diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index 2d4d7e8702..071f7268ed 100644 --- a/plugins/scaffolder-backend-module-yeoman/package.json +++ b/plugins/scaffolder-backend-module-yeoman/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@backstage/config": "workspace:^", - "@backstage/plugin-scaffolder-backend": "workspace:^", + "@backstage/plugin-scaffolder-node": "workspace:^", "@backstage/types": "workspace:^", "winston": "^3.2.1", "yeoman-environment": "^3.9.1" diff --git a/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.test.ts b/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.test.ts index ee5e8449f7..146c29f4ac 100644 --- a/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.test.ts +++ b/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.test.ts @@ -22,7 +22,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import os from 'os'; import { PassThrough } from 'stream'; import { createRunYeomanAction } from './yeoman'; -import type { ActionContext } from '@backstage/plugin-scaffolder-backend'; +import type { ActionContext } from '@backstage/plugin-scaffolder-node'; import { JsonObject } from '@backstage/types'; describe('run:yeoman', () => { diff --git a/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.ts b/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.ts index f7b0c54b41..89ea10b6a6 100644 --- a/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.ts +++ b/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.ts @@ -15,7 +15,7 @@ */ import { JsonObject } from '@backstage/types'; -import { createTemplateAction } from '@backstage/plugin-scaffolder-backend'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { yeomanRun } from './yeomanRun'; /** diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 7f8ab0ad79..8fcc8be47e 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -24,34 +24,16 @@ import { Observable } from '@backstage/types'; import { Octokit } from 'octokit'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; -import { Schema } from 'jsonschema'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { ScmIntegrations } from '@backstage/integration'; import { SpawnOptionsWithoutStdio } from 'child_process'; +import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { TaskSpecV1beta3 } from '@backstage/plugin-scaffolder-common'; -import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { UrlReader } from '@backstage/backend-common'; -import { UserEntity } from '@backstage/catalog-model'; import { Writable } from 'stream'; -// @public -export type ActionContext = { - logger: Logger; - logStream: Writable; - secrets?: TaskSecrets; - workspacePath: string; - input: Input; - output(name: string, value: JsonValue): void; - createTemporaryDirectory(): Promise; - templateInfo?: TemplateInfo; - isDryRun?: boolean; - user?: { - entity?: UserEntity; - ref?: string; - }; -}; - // @public export const createBuiltinActions: ( options: CreateBuiltInActionsOptions, @@ -528,11 +510,6 @@ export const createPublishGitlabMergeRequestAction: (options: { // @public export function createRouter(options: RouterOptions): Promise; -// @public -export const createTemplateAction: ( - templateAction: TemplateAction, -) => TemplateAction; - // @public export type CreateWorkerOptions = { taskBroker: TaskBroker; @@ -798,11 +775,6 @@ export class TaskManager implements TaskContext { get spec(): TaskSpecV1beta3; } -// @public -export type TaskSecrets = Record & { - backstageToken?: string; -}; - // @public export type TaskStatus = | 'open' @@ -890,22 +862,6 @@ export class TaskWorker { start(): void; } -// @public (undocumented) -export type TemplateAction = { - id: string; - description?: string; - examples?: { - description: string; - example: string; - }[]; - supportsDryRun?: boolean; - schema?: { - input?: Schema; - output?: Schema; - }; - handler: (ctx: ActionContext) => Promise; -}; - // @public export class TemplateActionRegistry { // (undocumented) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 8a98ea4a3d..93938e23e1 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -46,6 +46,7 @@ "@backstage/plugin-catalog-backend": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@backstage/plugin-scaffolder-common": "workspace:^", + "@backstage/plugin-scaffolder-node": "workspace:^", "@backstage/types": "workspace:^", "@gitbeaker/core": "^35.6.0", "@gitbeaker/node": "^35.1.0", diff --git a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts index 71472abda6..43b8d30f99 100644 --- a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts +++ b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts @@ -13,20 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { createBackendPlugin, coreServices, - createExtensionPoint, } from '@backstage/backend-plugin-api'; import { loggerToWinstonLogger } from '@backstage/backend-common'; import { ScmIntegrations } from '@backstage/integration'; import { catalogServiceRef } from '@backstage/plugin-catalog-node'; +import { + scaffolderActionsExtensionPoint, + ScaffolderActionsExtensionPoint, + TemplateAction, +} from '@backstage/plugin-scaffolder-node'; import { TemplateFilter, TemplateGlobal } from './lib'; -import { createBuiltinActions, TaskBroker, TemplateAction } from './scaffolder'; +import { createBuiltinActions, TaskBroker } from './scaffolder'; import { createRouter } from './service/router'; /** * Catalog plugin options + * * @alpha */ export type ScaffolderPluginOptions = { @@ -37,37 +43,23 @@ export type ScaffolderPluginOptions = { additionalTemplateGlobals?: Record; }; -/** - * @alpha - * TODO: MOVE to scaffolder-node. - */ -interface ScaffolderActionsExtensionPoint { - addActions(...actions: TemplateAction[]): void; -} - class ScaffolderActionsExtensionPointImpl implements ScaffolderActionsExtensionPoint { #actions = new Array>(); + addActions(...actions: TemplateAction[]): void { this.#actions.push(...actions); } + get actions() { return this.#actions; } } -/** - * @alpha - * TODO: MOVE to scaffolder-node. - */ -export const scaffolderActionsExtensionPoint = - createExtensionPoint({ - id: 'scaffolder.actions', - }); - /** * Catalog plugin + * * @alpha */ export const scaffolderPlugin = createBackendPlugin( @@ -75,6 +67,7 @@ export const scaffolderPlugin = createBackendPlugin( id: 'scaffolder', register(env) { const actionsExtensions = new ScaffolderActionsExtensionPointImpl(); + env.registerExtensionPoint( scaffolderActionsExtensionPoint, actionsExtensions, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts b/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts index 8a2a00d121..d044d095cd 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts @@ -16,7 +16,7 @@ import { JsonObject } from '@backstage/types'; import { ConflictError, NotFoundError } from '@backstage/errors'; -import { TemplateAction } from './types'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; /** * Registry of all registered template actions. diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.ts index c45eb62fce..913bf32cce 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.ts @@ -15,7 +15,7 @@ */ import { CatalogApi } from '@backstage/catalog-client'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import yaml from 'yaml'; const id = 'catalog:fetch'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts index 48fec3db03..e866eb12ba 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts @@ -18,7 +18,7 @@ import { InputError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; import { CatalogApi } from '@backstage/catalog-client'; import { stringifyEntityRef } from '@backstage/catalog-model'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import yaml from 'yaml'; const id = 'catalog:register'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts index 5257839f2d..5c5a62956f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts @@ -15,7 +15,7 @@ */ import fs from 'fs-extra'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import * as yaml from 'yaml'; import { Entity } from '@backstage/catalog-model'; import { resolveSafeChildPath } from '@backstage/backend-common'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index 7e954295a1..d83d9c5b07 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -23,6 +23,7 @@ import { ScmIntegrations, } from '@backstage/integration'; import { JsonObject } from '@backstage/types'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { createCatalogRegisterAction, createCatalogWriteAction, @@ -30,7 +31,6 @@ import { } from './catalog'; import { TemplateFilter, TemplateGlobal } from '../../../lib'; -import { TemplateAction } from '../types'; import { createDebugLogAction } from './debug'; import { createFetchPlainAction, createFetchTemplateAction } from './fetch'; import { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts index 3863623563..481eb9dfdd 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts @@ -16,7 +16,7 @@ import { readdir, stat } from 'fs-extra'; import { relative, join } from 'path'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import yaml from 'yaml'; const id = 'debug:log'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts index 0469ffec06..ed37a39a98 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + jest.mock('./helpers'); import os from 'os'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts index b3b984f544..c10f696bb9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts @@ -17,7 +17,7 @@ import { UrlReader, resolveSafeChildPath } from '@backstage/backend-common'; import { ScmIntegrations } from '@backstage/integration'; import { fetchContents } from './helpers'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; /** * Downloads content and places it in the workspace, or optionally diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts index 6658e1915d..15548ffea7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts @@ -26,8 +26,11 @@ import { import { ScmIntegrations } from '@backstage/integration'; import { PassThrough } from 'stream'; import { fetchContents } from './helpers'; -import { ActionContext, TemplateAction } from '../../types'; import { createFetchTemplateAction } from './template'; +import { + ActionContext, + TemplateAction, +} from '@backstage/plugin-scaffolder-node'; jest.mock('./helpers', () => ({ fetchContents: jest.fn(), diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts index 81b3f4577c..68c205ce59 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts @@ -19,7 +19,7 @@ import { resolveSafeChildPath, UrlReader } from '@backstage/backend-common'; import { InputError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; import { fetchContents } from './helpers'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import globby from 'globby'; import fs from 'fs-extra'; import { isBinaryFile } from 'isbinaryfile'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts index 757e9c682e..2f955c16ff 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createTemplateAction } from '../../createTemplateAction'; + +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { InputError } from '@backstage/errors'; import { resolveSafeChildPath } from '@backstage/backend-common'; import fs from 'fs-extra'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts index 654ee7b150..c29ba221c4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createTemplateAction } from '../../createTemplateAction'; -import { resolveSafeChildPath } from '@backstage/backend-common'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; +import { resolveSafeChildPath } from '@backstage/backend-common'; import { InputError } from '@backstage/errors'; import fs from 'fs-extra'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.test.ts index 0782ea40cd..3d59e43695 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.test.ts @@ -14,9 +14,6 @@ * limitations under the License. */ -import { TemplateAction } from '../../types'; -import { createGithubActionsDispatchAction } from './githubActionsDispatch'; - import { ScmIntegrations, DefaultGithubCredentialsProvider, @@ -24,7 +21,9 @@ import { } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { PassThrough } from 'stream'; +import { createGithubActionsDispatchAction } from './githubActionsDispatch'; const mockOctokit = { rest: { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts index b9d77559e0..cd1593ee9f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { InputError } from '@backstage/errors'; import { GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { Octokit } from 'octokit'; -import { createTemplateAction } from '../../createTemplateAction'; import { parseRepoUrl } from '../publish/util'; import { getOctokitOptions } from './helpers'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.test.ts index bc9b797000..a1a0274304 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.test.ts @@ -22,8 +22,8 @@ import { } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { PassThrough } from 'stream'; -import { TemplateAction } from '../../types'; const mockOctokit = { rest: { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.ts index 9c849edb6f..2dad0aa33a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.ts @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { GithubCredentialsProvider, ScmIntegrationRegistry, } from '@backstage/integration'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { assertError, InputError } from '@backstage/errors'; import { Octokit } from 'octokit'; import { getOctokitOptions } from './helpers'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts index 824db27e84..f15747fd6b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { TemplateAction } from '../../types'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; jest.mock('../helpers'); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts index 835fc79238..b5f779b8a7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { InputError } from '@backstage/errors'; import { GithubCredentialsProvider, ScmIntegrationRegistry, } from '@backstage/integration'; import { Octokit } from 'octokit'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { parseRepoUrl } from '../publish/util'; import { createGithubRepoWithCollaboratorsAndTopics, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.test.ts index 24777163d4..517347c733 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { TemplateAction } from '../../types'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; jest.mock('../helpers'); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts index 5a5ffc015c..6e3892cee5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Config } from '@backstage/config'; import { InputError } from '@backstage/errors'; import { @@ -20,7 +21,7 @@ import { ScmIntegrationRegistry, } from '@backstage/integration'; import { Octokit } from 'octokit'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { parseRepoUrl } from '../publish/util'; import { getOctokitOptions, initRepoPushAndProtect } from './helpers'; import * as inputProps from './inputProperties'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.test.ts index 97fdb3d634..7901210a6e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.test.ts @@ -22,8 +22,8 @@ import { } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { PassThrough } from 'stream'; -import { TemplateAction } from '../../types'; const mockOctokit = { rest: { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts index 4547ce4492..4d487befc7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { GithubCredentialsProvider, ScmIntegrationRegistry, } from '@backstage/integration'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { emitterEventNames } from '@octokit/webhooks'; import { assertError, InputError } from '@backstage/errors'; import { Octokit } from 'octokit'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts index ca5e40bdb0..2d2775572f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + jest.mock('azure-devops-node-api', () => ({ WebApi: jest.fn(), getPersonalAccessTokenHandler: jest.fn().mockReturnValue(() => {}), diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts index 60b13b717d..083c18c94f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts @@ -20,7 +20,7 @@ import { initRepoAndPush } from '../helpers'; import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/GitInterfaces'; import { getPersonalAccessTokenHandler, WebApi } from 'azure-devops-node-api'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { Config } from '@backstage/config'; /** diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts index 8f72157fc2..74e5e8c1ab 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts @@ -19,9 +19,9 @@ import { BitbucketIntegrationConfig, ScmIntegrationRegistry, } from '@backstage/integration'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import fetch, { Response, RequestInit } from 'node-fetch'; import { initRepoAndPush } from '../helpers'; -import { createTemplateAction } from '../../createTemplateAction'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { Config } from '@backstage/config'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.ts index 02e29c33de..2c17bf2dbb 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.ts @@ -16,9 +16,9 @@ import { InputError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import fetch, { Response, RequestInit } from 'node-fetch'; import { initRepoAndPush } from '../helpers'; -import { createTemplateAction } from '../../createTemplateAction'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { Config } from '@backstage/config'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServer.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServer.ts index 8c96c14463..f2f581eadb 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServer.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServer.ts @@ -19,9 +19,9 @@ import { getBitbucketServerRequestOptions, ScmIntegrationRegistry, } from '@backstage/integration'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import fetch, { Response, RequestInit } from 'node-fetch'; import { initRepoAndPush } from '../helpers'; -import { createTemplateAction } from '../../createTemplateAction'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { Config } from '@backstage/config'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerrit.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerrit.ts index 83653b3052..b5bcc31c62 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerrit.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerrit.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import crypto from 'crypto'; import { InputError } from '@backstage/errors'; import { Config } from '@backstage/config'; @@ -21,7 +22,7 @@ import { getGerritRequestOptions, ScmIntegrationRegistry, } from '@backstage/integration'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; import fetch, { Response, RequestInit } from 'node-fetch'; import { initRepoAndPush } from '../helpers'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerritReview.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerritReview.ts index 17068654c0..73e74f5cbc 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerritReview.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerritReview.ts @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import crypto from 'crypto'; import { InputError } from '@backstage/errors'; import { Config } from '@backstage/config'; import { ScmIntegrationRegistry } from '@backstage/integration'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { commitAndPushRepo } from '../helpers'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts index c2681ed527..8ead489ea4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { TemplateAction } from '../../types'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; jest.mock('../helpers'); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index add6fcc79c..185239c73f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Config } from '@backstage/config'; import { InputError } from '@backstage/errors'; import { @@ -20,7 +21,7 @@ import { ScmIntegrationRegistry, } from '@backstage/integration'; import { Octokit } from 'octokit'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { createGithubRepoWithCollaboratorsAndTopics, getOctokitOptions, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts index e6649d1ffe..70fff20446 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts @@ -20,11 +20,14 @@ import { GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; +import { + ActionContext, + TemplateAction, +} from '@backstage/plugin-scaffolder-node'; import mockFs from 'mock-fs'; import os from 'os'; import { resolve as resolvePath } from 'path'; import { Writable } from 'stream'; -import { ActionContext, TemplateAction } from '../../types'; import { createPublishGithubPullRequestAction, OctokitWithPullRequestPluginClient, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts index d0ee979be3..764354e8ec 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts @@ -20,11 +20,11 @@ import { GithubCredentialsProvider, ScmIntegrationRegistry, } from '@backstage/integration'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { Octokit } from 'octokit'; import { InputError, CustomErrorBase } from '@backstage/errors'; -import { createPullRequest } from 'octokit-plugin-create-pull-request'; import { resolveSafeChildPath } from '@backstage/backend-common'; +import { createPullRequest } from 'octokit-plugin-create-pull-request'; import { getOctokitOptions } from '../github/helpers'; import { SerializedFile, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts index dc1d26d416..a9c4522dd3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + jest.mock('../helpers'); import { createPublishGitlabAction } from './gitlab'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts index 53669efd0b..2e7520b77f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts @@ -16,10 +16,10 @@ import { InputError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { Gitlab } from '@gitbeaker/node'; import { initRepoAndPush } from '../helpers'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; -import { createTemplateAction } from '../../createTemplateAction'; import { Config } from '@backstage/config'; /** diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts index d1888fd9d2..c3d0fe58f1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts @@ -16,11 +16,11 @@ import { getRootLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import mockFs from 'mock-fs'; import os from 'os'; import { resolve as resolvePath } from 'path'; import { Writable } from 'stream'; -import { TemplateAction } from '../../types'; import { createPublishGitlabMergeRequestAction } from './gitlabMergeRequest'; const root = os.platform() === 'win32' ? 'C:\\root' : '/root'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts index 2a71c9f748..bb27410402 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createTemplateAction } from '../../createTemplateAction'; + +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { Gitlab } from '@gitbeaker/node'; import { Types } from '@gitbeaker/core'; import path from 'path'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/index.ts index d5f77b7b6f..9ea0607f06 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/index.ts @@ -16,5 +16,3 @@ export * from './builtin'; export { TemplateActionRegistry } from './TemplateActionRegistry'; -export { createTemplateAction } from './createTemplateAction'; -export type { ActionContext, TemplateAction } from './types'; diff --git a/plugins/scaffolder-backend/src/scaffolder/dryrun/DecoratedActionsRegistry.ts b/plugins/scaffolder-backend/src/scaffolder/dryrun/DecoratedActionsRegistry.ts index 6f11d3b6e8..c02375554c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/dryrun/DecoratedActionsRegistry.ts +++ b/plugins/scaffolder-backend/src/scaffolder/dryrun/DecoratedActionsRegistry.ts @@ -14,8 +14,9 @@ * limitations under the License. */ +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { JsonObject } from '@backstage/types'; -import { TemplateAction, TemplateActionRegistry } from '../actions'; +import { TemplateActionRegistry } from '../actions'; /** @internal */ export class DecoratedActionsRegistry extends TemplateActionRegistry { diff --git a/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts b/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts index 17f9092bff..1929e3b8ac 100644 --- a/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts @@ -26,12 +26,15 @@ import { serializeDirectoryContents, } from '../../lib/files'; import { TemplateFilter, TemplateGlobal } from '../../lib/templating'; -import { createTemplateAction, TemplateActionRegistry } from '../actions'; +import { TemplateActionRegistry } from '../actions'; import { NunjucksWorkflowRunner } from '../tasks/NunjucksWorkflowRunner'; -import { TaskSecrets } from '../tasks/types'; import { DecoratedActionsRegistry } from './DecoratedActionsRegistry'; import fs from 'fs-extra'; import { resolveSafeChildPath } from '@backstage/backend-common'; +import { + createTemplateAction, + TaskSecrets, +} from '@backstage/plugin-scaffolder-node'; interface DryRunInput { spec: TaskSpec; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index 24a35078e2..453cfae83b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -22,8 +22,9 @@ import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner'; import { TemplateActionRegistry } from '../actions'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { TaskContext, TaskSecrets } from './types'; +import { TaskContext } from './types'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; +import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; import { UserEntity } from '@backstage/catalog-model'; // The Stream module is lazy loaded, so make sure it's in the module cache before mocking fs diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 5893a88485..516abe79e4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -26,7 +26,7 @@ import { PassThrough } from 'stream'; import { generateExampleOutput, isTruthy } from './helper'; import { validate as validateJsonSchema } from 'jsonschema'; import { parseRepoUrl } from '../actions/builtin/publish/util'; -import { TemplateAction, TemplateActionRegistry } from '../actions'; +import { TemplateActionRegistry } from '../actions'; import { TemplateFilter, SecureTemplater, @@ -38,6 +38,7 @@ import { TaskSpecV1beta3, TaskStep, } from '@backstage/plugin-scaffolder-common'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { UserEntity } from '@backstage/catalog-model'; import { createCounterMetric, createHistogramMetric } from '../../util/metrics'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts index 5eeb73711e..2255798d5c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts @@ -17,9 +17,10 @@ import { getVoidLogger, DatabaseManager } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; +import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; import { DatabaseTaskStore } from './DatabaseTaskStore'; import { StorageTaskBroker, TaskManager } from './StorageTaskBroker'; -import { TaskSecrets, SerializedTaskEvent } from './types'; +import { SerializedTaskEvent } from './types'; async function createStore(): Promise { const manager = DatabaseManager.fromConfig( diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 7a015c111f..d01351d8ae 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -15,6 +15,7 @@ */ import { TaskSpec } from '@backstage/plugin-scaffolder-common'; +import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; import { JsonObject, Observable } from '@backstage/types'; import { Logger } from 'winston'; import ObservableImpl from 'zen-observable'; @@ -25,7 +26,6 @@ import { TaskBrokerDispatchOptions, TaskCompletionState, TaskContext, - TaskSecrets, TaskStore, } from './types'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts index 2af7d6a340..f1c1a7bdac 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { DatabaseTaskStore } from './DatabaseTaskStore'; export type { DatabaseTaskStoreOptions } from './DatabaseTaskStore'; export { TaskManager } from './StorageTaskBroker'; @@ -20,7 +21,6 @@ export type { CurrentClaimedTask } from './StorageTaskBroker'; export { TaskWorker } from './TaskWorker'; export type { CreateWorkerOptions } from './TaskWorker'; export type { - TaskSecrets, TaskCompletionState, TaskStoreEmitOptions, TaskStoreListEventsOptions, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 5d3f8113ee..89f6c3e213 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -16,6 +16,7 @@ import { JsonValue, JsonObject, Observable } from '@backstage/types'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; +import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; /** * The status of each step of the Task @@ -71,15 +72,6 @@ export type SerializedTaskEvent = { createdAt: string; }; -/** - * TaskSecrets - * - * @public - */ -export type TaskSecrets = Record & { - backstageToken?: string; -}; - /** * The result of {@link TaskBroker.dispatch} * diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 9ff8d43625..c339de8d10 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -43,7 +43,6 @@ import { DatabaseTaskStore, TaskBroker, TaskWorker, - TemplateAction, TemplateActionRegistry, } from '../scaffolder'; import { createDryRunner } from '../scaffolder/dryrun'; @@ -53,6 +52,7 @@ import { IdentityApi, IdentityApiGetIdentityRequest, } from '@backstage/plugin-auth-node'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; /** * RouterOptions diff --git a/plugins/scaffolder-node/.eslintrc.js b/plugins/scaffolder-node/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/scaffolder-node/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/scaffolder-node/README.md b/plugins/scaffolder-node/README.md new file mode 100644 index 0000000000..8b52daa785 --- /dev/null +++ b/plugins/scaffolder-node/README.md @@ -0,0 +1,3 @@ +# plugin-scaffolder-node + +Houses types and utilities for building scaffolder-related modules. diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md new file mode 100644 index 0000000000..95a2c81381 --- /dev/null +++ b/plugins/scaffolder-node/api-report.md @@ -0,0 +1,68 @@ +## API Report File for "@backstage/plugin-scaffolder-node" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { ExtensionPoint } from '@backstage/backend-plugin-api'; +import { JsonObject } from '@backstage/types'; +import { JsonValue } from '@backstage/types'; +import { Logger } from 'winston'; +import { Schema } from 'jsonschema'; +import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; +import { UserEntity } from '@backstage/catalog-model'; +import { Writable } from 'stream'; + +// @public +export type ActionContext = { + logger: Logger; + logStream: Writable; + secrets?: TaskSecrets; + workspacePath: string; + input: Input; + output(name: string, value: JsonValue): void; + createTemporaryDirectory(): Promise; + templateInfo?: TemplateInfo; + isDryRun?: boolean; + user?: { + entity?: UserEntity; + ref?: string; + }; +}; + +// @public +export const createTemplateAction: ( + templateAction: TemplateAction, +) => TemplateAction; + +// @alpha +export interface ScaffolderActionsExtensionPoint { + // (undocumented) + addActions(...actions: TemplateAction[]): void; +} + +// @alpha +export const scaffolderActionsExtensionPoint: ExtensionPoint; + +// @public +export type TaskSecrets = Record & { + backstageToken?: string; +}; + +// @public (undocumented) +export type TemplateAction = { + id: string; + description?: string; + examples?: { + description: string; + example: string; + }[]; + supportsDryRun?: boolean; + schema?: { + input?: Schema; + output?: Schema; + }; + handler: (ctx: ActionContext) => Promise; +}; +``` diff --git a/plugins/scaffolder-node/package.json b/plugins/scaffolder-node/package.json new file mode 100644 index 0000000000..d426a33bad --- /dev/null +++ b/plugins/scaffolder-node/package.json @@ -0,0 +1,41 @@ +{ + "name": "@backstage/plugin-scaffolder-node", + "description": "The plugin-scaffolder-node module for @backstage/plugin-scaffolder-backend", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "alphaTypes": "dist/index.alpha.d.ts", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "node-library" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build --experimental-type-build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/plugin-scaffolder-common": "workspace:^", + "@backstage/types": "workspace:^", + "jsonschema": "^1.2.6", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "files": [ + "alpha", + "dist" + ] +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/createTemplateAction.ts b/plugins/scaffolder-node/src/actions/createTemplateAction.ts similarity index 99% rename from plugins/scaffolder-backend/src/scaffolder/actions/createTemplateAction.ts rename to plugins/scaffolder-node/src/actions/createTemplateAction.ts index 8893fa0f20..24d88a1681 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/createTemplateAction.ts +++ b/plugins/scaffolder-node/src/actions/createTemplateAction.ts @@ -19,6 +19,7 @@ import { TemplateAction } from './types'; /** * This function is used to create new template actions to get type safety. + * * @public */ export const createTemplateAction = ( diff --git a/plugins/scaffolder-node/src/actions/index.ts b/plugins/scaffolder-node/src/actions/index.ts new file mode 100644 index 0000000000..7fdee6d692 --- /dev/null +++ b/plugins/scaffolder-node/src/actions/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { createTemplateAction } from './createTemplateAction'; +export { type ActionContext, type TemplateAction } from './types'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts b/plugins/scaffolder-node/src/actions/types.ts similarity index 95% rename from plugins/scaffolder-backend/src/scaffolder/actions/types.ts rename to plugins/scaffolder-node/src/actions/types.ts index 0912cc038b..385096ef4d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts +++ b/plugins/scaffolder-node/src/actions/types.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * Copyright 2023 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import { Logger } from 'winston'; import { Writable } from 'stream'; import { JsonValue, JsonObject } from '@backstage/types'; import { Schema } from 'jsonschema'; -import { TaskSecrets } from '../tasks'; +import { TaskSecrets } from '../tasks/types'; import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; import { UserEntity } from '@backstage/catalog-model'; diff --git a/plugins/scaffolder-node/src/extensions.ts b/plugins/scaffolder-node/src/extensions.ts new file mode 100644 index 0000000000..a706101043 --- /dev/null +++ b/plugins/scaffolder-node/src/extensions.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createExtensionPoint } from '@backstage/backend-plugin-api'; +import { TemplateAction } from './actions'; + +/** + * Extension point for managing scaffolder actions. + * + * @alpha + */ +export interface ScaffolderActionsExtensionPoint { + addActions(...actions: TemplateAction[]): void; +} + +/** + * Extension point for managing scaffolder actions. + * + * @alpha + */ +export const scaffolderActionsExtensionPoint = + createExtensionPoint({ + id: 'scaffolder.actions', + }); diff --git a/plugins/scaffolder-node/src/index.ts b/plugins/scaffolder-node/src/index.ts new file mode 100644 index 0000000000..2a7c06234e --- /dev/null +++ b/plugins/scaffolder-node/src/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The scaffolder-node module for `@backstage/plugin-scaffolder-backend`. + * + * @packageDocumentation + */ + +export * from './actions'; +export * from './tasks'; +export { + scaffolderActionsExtensionPoint, + type ScaffolderActionsExtensionPoint, +} from './extensions'; diff --git a/plugins/scaffolder-node/src/setupTests.ts b/plugins/scaffolder-node/src/setupTests.ts new file mode 100644 index 0000000000..d3232290a7 --- /dev/null +++ b/plugins/scaffolder-node/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export {}; diff --git a/plugins/scaffolder-node/src/tasks/index.ts b/plugins/scaffolder-node/src/tasks/index.ts new file mode 100644 index 0000000000..0c1d0d813e --- /dev/null +++ b/plugins/scaffolder-node/src/tasks/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { type TaskSecrets } from './types'; diff --git a/plugins/scaffolder-node/src/tasks/types.ts b/plugins/scaffolder-node/src/tasks/types.ts new file mode 100644 index 0000000000..c17b3cbde3 --- /dev/null +++ b/plugins/scaffolder-node/src/tasks/types.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * TaskSecrets + * + * @public + */ +export type TaskSecrets = Record & { + backstageToken?: string; +}; diff --git a/yarn.lock b/yarn.lock index c73cbf3a20..1b4e8cfbfb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7075,6 +7075,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-backend": "workspace:^" + "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/types": "workspace:^" "@types/command-exists": ^1.2.0 "@types/fs-extra": ^9.0.1 @@ -7098,6 +7099,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-backend": "workspace:^" + "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/types": "workspace:^" "@types/command-exists": ^1.2.0 "@types/fs-extra": ^9.0.1 @@ -7120,7 +7122,7 @@ __metadata: "@backstage/dev-utils": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" - "@backstage/plugin-scaffolder-backend": "workspace:^" + "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/test-utils": "workspace:^" "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 @@ -7140,7 +7142,7 @@ __metadata: "@backstage/backend-common": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" - "@backstage/plugin-scaffolder-backend": "workspace:^" + "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/types": "workspace:^" winston: ^3.2.1 yeoman-environment: ^3.9.1 @@ -7165,6 +7167,7 @@ __metadata: "@backstage/plugin-catalog-backend": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" "@backstage/plugin-scaffolder-common": "workspace:^" + "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/types": "workspace:^" "@gitbeaker/core": ^35.6.0 "@gitbeaker/node": ^35.1.0 @@ -7224,6 +7227,20 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-scaffolder-node@workspace:^, @backstage/plugin-scaffolder-node@workspace:plugins/scaffolder-node": + version: 0.0.0-use.local + resolution: "@backstage/plugin-scaffolder-node@workspace:plugins/scaffolder-node" + dependencies: + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/plugin-scaffolder-common": "workspace:^" + "@backstage/types": "workspace:^" + jsonschema: ^1.2.6 + winston: ^3.2.1 + languageName: unknown + linkType: soft + "@backstage/plugin-scaffolder-react@workspace:^, @backstage/plugin-scaffolder-react@workspace:plugins/scaffolder-react": version: 0.0.0-use.local resolution: "@backstage/plugin-scaffolder-react@workspace:plugins/scaffolder-react" From ad3edc402d125fb2d965d2291cae15434420eaf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 19 Jan 2023 15:33:22 +0100 Subject: [PATCH 009/118] propagate the deprecations properly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/lovely-ladybugs-taste.md | 10 ++ .../building-backends/08-migrating.md | 3 +- plugins/scaffolder-backend/api-report.md | 90 ++++++++------- plugins/scaffolder-backend/src/deprecated.ts | 104 ++++++++++++++++++ plugins/scaffolder-backend/src/index.ts | 2 + plugins/scaffolder-node/api-report.md | 8 +- plugins/scaffolder-node/src/actions/types.ts | 8 +- 7 files changed, 178 insertions(+), 47 deletions(-) create mode 100644 .changeset/lovely-ladybugs-taste.md create mode 100644 plugins/scaffolder-backend/src/deprecated.ts diff --git a/.changeset/lovely-ladybugs-taste.md b/.changeset/lovely-ladybugs-taste.md new file mode 100644 index 0000000000..24017e4dee --- /dev/null +++ b/.changeset/lovely-ladybugs-taste.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +**Deprecations**: The following are deprecated and should instead be imported from the new package `@backstage/plugin-scaffolder-node`: + +- `ActionContext` +- `createTemplateAction` +- `TaskSecrets` +- `TemplateAction` diff --git a/docs/backend-system/building-backends/08-migrating.md b/docs/backend-system/building-backends/08-migrating.md index fcac599227..71e5b52c24 100644 --- a/docs/backend-system/building-backends/08-migrating.md +++ b/docs/backend-system/building-backends/08-migrating.md @@ -390,8 +390,7 @@ depends on the appropriate extension point and interacts with it. ```diff // packages/backend/src/index.ts - // TODO: This might be moved to @backstage/plugin-scaffolder-node -+import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-backend/alpha'; ++import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node'; +import { createBackendModule } from '@backstage/backend-plugin-api'; +const scaffolderExtensionsModule = createBackendModule({ diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 8fcc8be47e..22cc0cae20 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -5,6 +5,7 @@ ```ts /// +import { ActionContext as ActionContext_2 } from '@backstage/plugin-scaffolder-node'; import { BackendFeature } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; @@ -27,17 +28,20 @@ import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { ScmIntegrations } from '@backstage/integration'; import { SpawnOptionsWithoutStdio } from 'child_process'; -import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; +import { TaskSecrets as TaskSecrets_2 } from '@backstage/plugin-scaffolder-node'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { TaskSpecV1beta3 } from '@backstage/plugin-scaffolder-common'; -import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { TemplateAction as TemplateAction_2 } from '@backstage/plugin-scaffolder-node'; import { UrlReader } from '@backstage/backend-common'; import { Writable } from 'stream'; +// @public @deprecated (undocumented) +export type ActionContext = ActionContext_2; + // @public export const createBuiltinActions: ( options: CreateBuiltInActionsOptions, -) => TemplateAction[]; +) => TemplateAction_2[]; // @public export interface CreateBuiltInActionsOptions { @@ -54,7 +58,7 @@ export interface CreateBuiltInActionsOptions { export function createCatalogRegisterAction(options: { catalogClient: CatalogApi; integrations: ScmIntegrations; -}): TemplateAction< +}): TemplateAction_2< | { catalogInfoUrl: string; optional?: boolean | undefined; @@ -67,13 +71,13 @@ export function createCatalogRegisterAction(options: { >; // @public -export function createCatalogWriteAction(): TemplateAction<{ +export function createCatalogWriteAction(): TemplateAction_2<{ filePath?: string | undefined; entity: Entity; }>; // @public -export function createDebugLogAction(): TemplateAction<{ +export function createDebugLogAction(): TemplateAction_2<{ message?: string | undefined; listWorkspace?: boolean | undefined; }>; @@ -81,7 +85,7 @@ export function createDebugLogAction(): TemplateAction<{ // @public export function createFetchCatalogEntityAction(options: { catalogClient: CatalogApi; -}): TemplateAction<{ +}): TemplateAction_2<{ entityRef: string; optional?: boolean | undefined; }>; @@ -90,7 +94,7 @@ export function createFetchCatalogEntityAction(options: { export function createFetchPlainAction(options: { reader: UrlReader; integrations: ScmIntegrations; -}): TemplateAction<{ +}): TemplateAction_2<{ url: string; targetPath?: string | undefined; }>; @@ -101,7 +105,7 @@ export function createFetchTemplateAction(options: { integrations: ScmIntegrations; additionalTemplateFilters?: Record; additionalTemplateGlobals?: Record; -}): TemplateAction<{ +}): TemplateAction_2<{ url: string; targetPath?: string | undefined; values: any; @@ -113,12 +117,12 @@ export function createFetchTemplateAction(options: { }>; // @public -export const createFilesystemDeleteAction: () => TemplateAction<{ +export const createFilesystemDeleteAction: () => TemplateAction_2<{ files: string[]; }>; // @public -export const createFilesystemRenameAction: () => TemplateAction<{ +export const createFilesystemRenameAction: () => TemplateAction_2<{ files: Array<{ from: string; to: string; @@ -130,7 +134,7 @@ export const createFilesystemRenameAction: () => TemplateAction<{ export function createGithubActionsDispatchAction(options: { integrations: ScmIntegrations; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; workflowId: string; branchOrTagName: string; @@ -146,7 +150,7 @@ export function createGithubActionsDispatchAction(options: { export function createGithubIssuesLabelAction(options: { integrations: ScmIntegrationRegistry; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; number: number; labels: string[]; @@ -176,7 +180,7 @@ export type CreateGithubPullRequestClientFactoryInput = { export function createGithubRepoCreateAction(options: { integrations: ScmIntegrationRegistry; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; description?: string | undefined; homepage?: string | undefined; @@ -243,7 +247,7 @@ export function createGithubRepoPushAction(options: { integrations: ScmIntegrationRegistry; config: Config; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; description?: string | undefined; defaultBranch?: string | undefined; @@ -282,7 +286,7 @@ export function createGithubWebhookAction(options: { integrations: ScmIntegrationRegistry; defaultWebhookSecret?: string; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; webhookUrl: string; webhookSecret?: string | undefined; @@ -297,7 +301,7 @@ export function createGithubWebhookAction(options: { export function createPublishAzureAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; description?: string | undefined; defaultBranch?: string | undefined; @@ -312,7 +316,7 @@ export function createPublishAzureAction(options: { export function createPublishBitbucketAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; description?: string | undefined; defaultBranch?: string | undefined; @@ -329,7 +333,7 @@ export function createPublishBitbucketAction(options: { export function createPublishBitbucketCloudAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; description?: string | undefined; defaultBranch?: string | undefined; @@ -342,7 +346,7 @@ export function createPublishBitbucketCloudAction(options: { export function createPublishBitbucketServerAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; description?: string | undefined; defaultBranch?: string | undefined; @@ -359,7 +363,7 @@ export function createPublishBitbucketServerAction(options: { export function createPublishGerritAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; description: string; defaultBranch?: string | undefined; @@ -373,7 +377,7 @@ export function createPublishGerritAction(options: { export function createPublishGerritReviewAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; branch?: string | undefined; sourcePath?: string | undefined; @@ -387,7 +391,7 @@ export function createPublishGithubAction(options: { integrations: ScmIntegrationRegistry; config: Config; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; description?: string | undefined; homepage?: string | undefined; @@ -460,7 +464,7 @@ export const createPublishGithubPullRequestAction: ({ integrations, githubCredentialsProvider, clientFactory, -}: CreateGithubPullRequestActionOptions) => TemplateAction<{ +}: CreateGithubPullRequestActionOptions) => TemplateAction_2<{ title: string; branchName: string; description: string; @@ -477,7 +481,7 @@ export const createPublishGithubPullRequestAction: ({ export function createPublishGitlabAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; defaultBranch?: string | undefined; repoVisibility?: 'internal' | 'private' | 'public' | undefined; @@ -493,7 +497,7 @@ export function createPublishGitlabAction(options: { // @public export const createPublishGitlabMergeRequestAction: (options: { integrations: ScmIntegrationRegistry; -}) => TemplateAction<{ +}) => TemplateAction_2<{ repoUrl: string; title: string; description: string; @@ -510,6 +514,11 @@ export const createPublishGitlabMergeRequestAction: (options: { // @public export function createRouter(options: RouterOptions): Promise; +// @public @deprecated (undocumented) +export const createTemplateAction: ( + templateAction: TemplateAction_2, +) => TemplateAction_2; + // @public export type CreateWorkerOptions = { taskBroker: TaskBroker; @@ -525,7 +534,7 @@ export type CreateWorkerOptions = { // @public export interface CurrentClaimedTask { createdBy?: string; - secrets?: TaskSecrets; + secrets?: TaskSecrets_2; spec: TaskSpec; taskId: string; } @@ -606,7 +615,7 @@ export type OctokitWithPullRequestPluginClient = Octokit & { // @public export interface RouterOptions { // (undocumented) - actions?: TemplateAction[]; + actions?: TemplateAction_2[]; // (undocumented) additionalTemplateFilters?: Record; // (undocumented) @@ -664,7 +673,7 @@ export const scaffolderPlugin: ( // @alpha export type ScaffolderPluginOptions = { - actions?: TemplateAction[]; + actions?: TemplateAction_2[]; taskWorkers?: number; taskBroker?: TaskBroker; additionalTemplateFilters?: Record; @@ -679,7 +688,7 @@ export type SerializedTask = { createdAt: string; lastHeartbeatAt?: string; createdBy?: string; - secrets?: TaskSecrets; + secrets?: TaskSecrets_2; }; // @public @@ -716,7 +725,7 @@ export interface TaskBroker { // @public export type TaskBrokerDispatchOptions = { spec: TaskSpec; - secrets?: TaskSecrets; + secrets?: TaskSecrets_2; createdBy?: string; }; @@ -743,7 +752,7 @@ export interface TaskContext { // (undocumented) isDryRun?: boolean; // (undocumented) - secrets?: TaskSecrets; + secrets?: TaskSecrets_2; // (undocumented) spec: TaskSpec; } @@ -770,11 +779,14 @@ export class TaskManager implements TaskContext { // (undocumented) getWorkspaceName(): Promise; // (undocumented) - get secrets(): TaskSecrets | undefined; + get secrets(): TaskSecrets_2 | undefined; // (undocumented) get spec(): TaskSpecV1beta3; } +// @public @deprecated (undocumented) +export type TaskSecrets = TaskSecrets_2; + // @public export type TaskStatus = | 'open' @@ -825,7 +837,7 @@ export interface TaskStore { export type TaskStoreCreateTaskOptions = { spec: TaskSpec; createdBy?: string; - secrets?: TaskSecrets; + secrets?: TaskSecrets_2; }; // @public @@ -862,14 +874,18 @@ export class TaskWorker { start(): void; } +// @public @deprecated (undocumented) +export type TemplateAction = + TemplateAction_2; + // @public export class TemplateActionRegistry { // (undocumented) - get(actionId: string): TemplateAction; + get(actionId: string): TemplateAction_2; // (undocumented) - list(): TemplateAction[]; + list(): TemplateAction_2[]; // (undocumented) - register(action: TemplateAction): void; + register(action: TemplateAction_2): void; } // @public (undocumented) diff --git a/plugins/scaffolder-backend/src/deprecated.ts b/plugins/scaffolder-backend/src/deprecated.ts new file mode 100644 index 0000000000..363e72a867 --- /dev/null +++ b/plugins/scaffolder-backend/src/deprecated.ts @@ -0,0 +1,104 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ActionContext as ActionContextNode, + createTemplateAction as createTemplateActionNode, + TaskSecrets as TaskSecretsNode, + TemplateAction as TemplateActionNode, +} from '@backstage/plugin-scaffolder-node'; +import { JsonObject } from '@backstage/types'; + +/** + * @public + * @deprecated Use `ActionContext` from `@backstage/plugin-scaffolder-node` instead + */ +export type ActionContext = + ActionContextNode; + +/** + * @public + * @deprecated Use `createTemplateAction` from `@backstage/plugin-scaffolder-node` instead + */ +export const createTemplateAction = createTemplateActionNode; + +/** + * @public + * @deprecated Use `TaskSecrets` from `@backstage/plugin-scaffolder-node` instead + */ +export type TaskSecrets = TaskSecretsNode; + +/** + * @public + * @deprecated Use `TemplateAction` from `@backstage/plugin-scaffolder-node` instead + */ +export type TemplateAction = + TemplateActionNode; + +/* +// @public +export type ActionContext = { + logger: Logger; + logStream: Writable; + secrets?: TaskSecrets; + workspacePath: string; + input: Input; + output(name: string, value: JsonValue): void; + createTemporaryDirectory(): Promise; + templateInfo?: TemplateInfo; + isDryRun?: boolean; + user?: { + entity?: UserEntity; + ref?: string; + }; +}; + +// @public +export const createTemplateAction: ( + templateAction: TemplateAction, +) => TemplateAction; + +// @alpha +export interface ScaffolderActionsExtensionPoint { + // (undocumented) + addActions(...actions: TemplateAction[]): void; +} + +// @alpha +export const scaffolderActionsExtensionPoint: ExtensionPoint; + +// @public +export type TaskSecrets = Record & { + backstageToken?: string; +}; + +// @public (undocumented) +export type TemplateAction = { + id: string; + description?: string; + examples?: { + description: string; + example: string; + }[]; + supportsDryRun?: boolean; + schema?: { + input?: Schema; + output?: Schema; + }; + handler: (ctx: ActionContext) => Promise; +}; +``` +*/ diff --git a/plugins/scaffolder-backend/src/index.ts b/plugins/scaffolder-backend/src/index.ts index 985ada405e..eb7a831bd0 100644 --- a/plugins/scaffolder-backend/src/index.ts +++ b/plugins/scaffolder-backend/src/index.ts @@ -27,3 +27,5 @@ export * from './processor'; export * from './extension'; export { scaffolderPlugin } from './ScaffolderPlugin'; export type { ScaffolderPluginOptions } from './ScaffolderPlugin'; + +export * from './deprecated'; diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md index 95a2c81381..506ea0517d 100644 --- a/plugins/scaffolder-node/api-report.md +++ b/plugins/scaffolder-node/api-report.md @@ -15,12 +15,12 @@ import { UserEntity } from '@backstage/catalog-model'; import { Writable } from 'stream'; // @public -export type ActionContext = { +export type ActionContext = { logger: Logger; logStream: Writable; secrets?: TaskSecrets; workspacePath: string; - input: Input; + input: TInput; output(name: string, value: JsonValue): void; createTemporaryDirectory(): Promise; templateInfo?: TemplateInfo; @@ -51,7 +51,7 @@ export type TaskSecrets = Record & { }; // @public (undocumented) -export type TemplateAction = { +export type TemplateAction = { id: string; description?: string; examples?: { @@ -63,6 +63,6 @@ export type TemplateAction = { input?: Schema; output?: Schema; }; - handler: (ctx: ActionContext) => Promise; + handler: (ctx: ActionContext) => Promise; }; ``` diff --git a/plugins/scaffolder-node/src/actions/types.ts b/plugins/scaffolder-node/src/actions/types.ts index 385096ef4d..5a4f5796a1 100644 --- a/plugins/scaffolder-node/src/actions/types.ts +++ b/plugins/scaffolder-node/src/actions/types.ts @@ -26,12 +26,12 @@ import { UserEntity } from '@backstage/catalog-model'; * ActionContext is passed into scaffolder actions. * @public */ -export type ActionContext = { +export type ActionContext = { logger: Logger; logStream: Writable; secrets?: TaskSecrets; workspacePath: string; - input: Input; + input: TInput; output(name: string, value: JsonValue): void; /** @@ -63,7 +63,7 @@ export type ActionContext = { }; /** @public */ -export type TemplateAction = { +export type TemplateAction = { id: string; description?: string; examples?: { description: string; example: string }[]; @@ -72,5 +72,5 @@ export type TemplateAction = { input?: Schema; output?: Schema; }; - handler: (ctx: ActionContext) => Promise; + handler: (ctx: ActionContext) => Promise; }; From b358c24fc386295d443eac3d2f7d6a1c42e1d80d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 19 Jan 2023 16:54:48 +0100 Subject: [PATCH 010/118] remove commented-out code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- plugins/scaffolder-backend/src/deprecated.ts | 55 -------------------- 1 file changed, 55 deletions(-) diff --git a/plugins/scaffolder-backend/src/deprecated.ts b/plugins/scaffolder-backend/src/deprecated.ts index 363e72a867..46fc358cfa 100644 --- a/plugins/scaffolder-backend/src/deprecated.ts +++ b/plugins/scaffolder-backend/src/deprecated.ts @@ -47,58 +47,3 @@ export type TaskSecrets = TaskSecretsNode; */ export type TemplateAction = TemplateActionNode; - -/* -// @public -export type ActionContext = { - logger: Logger; - logStream: Writable; - secrets?: TaskSecrets; - workspacePath: string; - input: Input; - output(name: string, value: JsonValue): void; - createTemporaryDirectory(): Promise; - templateInfo?: TemplateInfo; - isDryRun?: boolean; - user?: { - entity?: UserEntity; - ref?: string; - }; -}; - -// @public -export const createTemplateAction: ( - templateAction: TemplateAction, -) => TemplateAction; - -// @alpha -export interface ScaffolderActionsExtensionPoint { - // (undocumented) - addActions(...actions: TemplateAction[]): void; -} - -// @alpha -export const scaffolderActionsExtensionPoint: ExtensionPoint; - -// @public -export type TaskSecrets = Record & { - backstageToken?: string; -}; - -// @public (undocumented) -export type TemplateAction = { - id: string; - description?: string; - examples?: { - description: string; - example: string; - }[]; - supportsDryRun?: boolean; - schema?: { - input?: Schema; - output?: Schema; - }; - handler: (ctx: ActionContext) => Promise; -}; -``` -*/ From 3f88ae9d0aae8eaf39b68b309b467f37ff2c6655 Mon Sep 17 00:00:00 2001 From: Divyanshi Gupta Date: Thu, 19 Jan 2023 22:15:19 +0530 Subject: [PATCH 011/118] Fix usage examples Signed-off-by: Divyanshi Gupta --- .changeset/shiny-years-tap.md | 5 +++++ plugins/github-issues/README.md | 16 ++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 .changeset/shiny-years-tap.md diff --git a/.changeset/shiny-years-tap.md b/.changeset/shiny-years-tap.md new file mode 100644 index 0000000000..d0d0c14c0a --- /dev/null +++ b/.changeset/shiny-years-tap.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-github-issues': patch +--- + +Updated README.md examples to use correct components and fixed some syntax errors. diff --git a/plugins/github-issues/README.md b/plugins/github-issues/README.md index d35f2c5d3f..f6b383bfcc 100644 --- a/plugins/github-issues/README.md +++ b/plugins/github-issues/README.md @@ -25,8 +25,8 @@ After installation, the plugin can be used as a Card or as a Page. ```typescript import { - GitHubIssuesCard, - GitHubIssuesPage, + GithubIssuesCard, + GithubIssuesPage, } from '@backstage/plugin-github-issues'; // To use as a page Plugin needs to be wrapped in EntityLayout.Route @@ -34,9 +34,9 @@ const RenderGitHubIssuesPage = () => ( - + - + ); @@ -46,17 +46,17 @@ const RenderGitHubIssuesCard = () => ( - + - + ); ``` ## Configuration -Both `GitHubIssuesPage` and `GitHubIssuesCard` provide default configuration. It is ready to use out of the box. +Both `GithubIssuesPage` and `GithubIssuesCard` provide default configuration. It is ready to use out of the box. However, you can configure the plugin with props: - `itemsPerPage: number = 10` - Issues in the list are paginated, number of issues on a single page is controlled with this prop @@ -67,7 +67,7 @@ However, you can configure the plugin with props: ### `filterBy` and `orderBy` example ```ts - Date: Fri, 20 Jan 2023 04:07:07 +0000 Subject: [PATCH 012/118] fix(deps): update dependency @types/jest to v29.2.6 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 380571d49c..552c7c39ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14443,12 +14443,12 @@ __metadata: linkType: hard "@types/jest@npm:*, @types/jest@npm:^29.0.0": - version: 29.2.5 - resolution: "@types/jest@npm:29.2.5" + version: 29.2.6 + resolution: "@types/jest@npm:29.2.6" dependencies: expect: ^29.0.0 pretty-format: ^29.0.0 - checksum: d668470f00ec4cb8b8457f1fd90f7358fad8f22d74b85006dad6be522d6b9bf10f49f597e88d1d1a518d211c1b65be32a1f27f0e49ce0658d110a9206b8ea310 + checksum: 90190ac830334af1470d255853f9621fe657e5030b4d96773fc1f884833cd303c76580b00c1b86dc38a8db94f1c7141d462190437a10af31852b8845a57c48ba languageName: node linkType: hard From 898f1c87eb113b2c58efb5781f2692dfa25efdd5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Jan 2023 04:08:16 +0000 Subject: [PATCH 013/118] fix(deps): update dependency @uiw/react-codemirror to v4.19.7 Signed-off-by: Renovate Bot --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 380571d49c..d46283a3f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15715,9 +15715,9 @@ __metadata: languageName: node linkType: hard -"@uiw/codemirror-extensions-basic-setup@npm:4.19.6": - version: 4.19.6 - resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.19.6" +"@uiw/codemirror-extensions-basic-setup@npm:4.19.7": + version: 4.19.7 + resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.19.7" dependencies: "@codemirror/autocomplete": ^6.0.0 "@codemirror/commands": ^6.0.0 @@ -15734,19 +15734,19 @@ __metadata: "@codemirror/search": ">=6.0.0" "@codemirror/state": ">=6.0.0" "@codemirror/view": ">=6.0.0" - checksum: 775c6c190b27ed0ffeb9fec9b03c60a4d621f33480e7c878c3841cb0fa983baab46d13612d7ac9a14a3e810b83d9ab12aa387a291e5e5a0726bf33f28d89d1c0 + checksum: 4c32d3b41b78776fd41b229edc8777dff4137ff6125836df5a6573e01a0a74f2ae66293a09a3b095a1fbe660b2fc8b2bde93ba4aa92636702a74baea261fcf94 languageName: node linkType: hard "@uiw/react-codemirror@npm:^4.9.3": - version: 4.19.6 - resolution: "@uiw/react-codemirror@npm:4.19.6" + version: 4.19.7 + resolution: "@uiw/react-codemirror@npm:4.19.7" dependencies: "@babel/runtime": ^7.18.6 "@codemirror/commands": ^6.1.0 "@codemirror/state": ^6.1.1 "@codemirror/theme-one-dark": ^6.0.0 - "@uiw/codemirror-extensions-basic-setup": 4.19.6 + "@uiw/codemirror-extensions-basic-setup": 4.19.7 codemirror: ^6.0.0 peerDependencies: "@babel/runtime": ">=7.11.0" @@ -15756,7 +15756,7 @@ __metadata: codemirror: ">=6.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: 64c0374dd221e7ddab2f8c2531ad5271f595fad116e37b45acaa9f1d8b8d5659bccdd4aea47a558cce4c62d46e90b8d074760a5b328e9e99c253c79ec99e8b6a + checksum: 333c7b0c7181219bb0506cd956eb10fa07aad5f26d3c5a9e7b9875e6c60f1e9bf1c8f74bf160735c75b7dbe9e9b05879c17f5ea641e212ba8f454887f04b0e7e languageName: node linkType: hard From 3598136ac7dc17dd2a6812224d75ddc99373fad3 Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Fri, 20 Jan 2023 11:32:01 +0100 Subject: [PATCH 014/118] Refactor plugin Card component to not rerender contents unnecessarily. Signed-off-by: Jussi Hallila --- .changeset/young-singers-learn.md | 5 ++ .../src/components/EntitySplunkOnCallCard.tsx | 80 ++++++++++++------- 2 files changed, 54 insertions(+), 31 deletions(-) create mode 100644 .changeset/young-singers-learn.md diff --git a/.changeset/young-singers-learn.md b/.changeset/young-singers-learn.md new file mode 100644 index 0000000000..9df5d91fe3 --- /dev/null +++ b/.changeset/young-singers-learn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-splunk-on-call': patch +--- + +Refactor plugin Card component to not rerender contents unnecessarily. diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx index 26521ea9ee..0f32c925f0 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx @@ -99,6 +99,48 @@ export const MissingEventsRestEndpoint = () => ( ); +const Content = ({ + team, + routingKey, + usersHashMap, + readOnly, + showDialog, + refreshIncidents, + actions, +}: { + team: Team | undefined; + routingKey: RoutingKey | undefined; + usersHashMap: any; + readOnly: boolean; + showDialog: boolean; + refreshIncidents: boolean; + actions: { + handleDialog: () => void; + handleRefresh: () => void; + }; +}) => { + const teamName = team?.name ?? ''; + + return ( + <> + + {usersHashMap && team && ( + + )} + + + ); +}; + /** @public */ export const isSplunkOnCallAvailable = (entity: Entity) => Boolean(entity.metadata.annotations?.[SPLUNK_ON_CALL_TEAM]) || @@ -219,37 +261,6 @@ export const EntitySplunkOnCallCard = (props: EntitySplunkOnCallCardProps) => { ); } - const Content = ({ - team, - routingKey, - usersHashMap, - }: { - team: Team | undefined; - routingKey: RoutingKey | undefined; - usersHashMap: any; - }) => { - const teamName = team?.name ?? ''; - - return ( - <> - - {usersHashMap && team && ( - - )} - - - ); - }; - const triggerLink: IconLinkVerticalProps = { label: 'Create Incident', onClick: handleDialog, @@ -287,6 +298,13 @@ export const EntitySplunkOnCallCard = (props: EntitySplunkOnCallCardProps) => { team={team} routingKey={entityData?.foundRoutingKey} usersHashMap={entityData?.usersHashMap} + readOnly={readOnly ?? false} + refreshIncidents={refreshIncidents} + showDialog={showDialog} + actions={{ + handleRefresh, + handleDialog, + }} /> From effd71ead2453ece89fb645d93bc960693e99ac3 Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Fri, 20 Jan 2023 11:36:09 +0100 Subject: [PATCH 015/118] Inline the content, no need for a separate component. Signed-off-by: Jussi Hallila --- .../src/components/EntitySplunkOnCallCard.tsx | 112 ++++++------------ 1 file changed, 39 insertions(+), 73 deletions(-) diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx index 0f32c925f0..54784b1f4e 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx @@ -33,7 +33,7 @@ import { MissingApiKeyOrApiIdError } from './Errors'; import { EscalationPolicy } from './Escalation'; import { Incidents } from './Incident'; import { TriggerDialog } from './TriggerDialog'; -import { RoutingKey, Team, User } from './types'; +import { RoutingKey, User } from './types'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; import { @@ -99,48 +99,6 @@ export const MissingEventsRestEndpoint = () => ( ); -const Content = ({ - team, - routingKey, - usersHashMap, - readOnly, - showDialog, - refreshIncidents, - actions, -}: { - team: Team | undefined; - routingKey: RoutingKey | undefined; - usersHashMap: any; - readOnly: boolean; - showDialog: boolean; - refreshIncidents: boolean; - actions: { - handleDialog: () => void; - handleRefresh: () => void; - }; -}) => { - const teamName = team?.name ?? ''; - - return ( - <> - - {usersHashMap && team && ( - - )} - - - ); -}; - /** @public */ export const isSplunkOnCallAvailable = (entity: Entity) => Boolean(entity.metadata.annotations?.[SPLUNK_ON_CALL_TEAM]) || @@ -278,37 +236,45 @@ export const EntitySplunkOnCallCard = (props: EntitySplunkOnCallCardProps) => { return ( <> - {teams.map((team, i) => ( - - - Team: {team && team.name ? team.name : ''} - , - , - ]} - /> - - - { + const teamName = team?.name ?? ''; + return ( + + + Team: {team && team.name ? team.name : ''} + , + , + ]} /> - - - ))} + + + + {entityData?.usersHashMap && team && ( + + )} + + + + ); + })} ); }; From f39b97b4e9bc27cb52140e167d53808a7deefdd2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Jan 2023 12:29:06 +0000 Subject: [PATCH 016/118] fix(deps): update dependency core-js to v3.27.2 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index a3917b9e36..3a5f5322a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19227,9 +19227,9 @@ __metadata: linkType: hard "core-js@npm:^3.4.1, core-js@npm:^3.6.5": - version: 3.27.1 - resolution: "core-js@npm:3.27.1" - checksum: d50b5f88aea4302512ad9446c18e90f4d35dea1e6d8d3f87337690677061565ff11a670f1e0c87de57aa6074375fbb25ed5784076c040d3c4de8b4bce7d2ebeb + version: 3.27.2 + resolution: "core-js@npm:3.27.2" + checksum: 718debd426f55a6b97cf9b757c936be258afd6d4f7052f89d0f96c982d7013e9000b0b006df42831a0cf32adad298e34d6a19052dce9ae1c7ab87162c0c665e0 languageName: node linkType: hard From 7b53f18e44657e6fd58efb0023bd3b8bb884c46b Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 19 Jan 2023 13:57:03 +0100 Subject: [PATCH 017/118] chore: started to write some more documentation about the built in services Signed-off-by: blam --- docs/backend-system/core-services/01-index.md | 163 +++++++++++++++++- .../httpRouter/httpRouterFactory.ts | 2 +- 2 files changed, 158 insertions(+), 7 deletions(-) diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index cb1ec157ae..8b314e9b6b 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -18,6 +18,8 @@ import { coreServices } from '@backstage/backend-plugin-api'; One of the most common services is the HTTP router service which is used to expose HTTP endpoints for other plugins to consume. +### Using the service + The following example shows how to register a HTTP router for the `example` plugin. This single route will be available at the `/api/example/hello` path. @@ -46,9 +48,31 @@ createBackendPlugin({ }); ``` -## Logging and Configuration Service +### Configuration of the service -It is common for plugins to need access to configuration values and log messages. +There's additional configuration that you can optionally pass to setup the `httpRouter` core service. + +- `getPath` - Can be used to generate a path for each plugin. Currently defaults to `/api/${pluginId}` + +You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: + +```ts +import { httpRouterFactory } from '@backstage/backend-app-api`; + +const backend = createBackend({ + services: [ + httpRouterFactory({ getPath: (pluginId: string) => `/plugins/${pluginId}` }), + ], +}); +``` + +## Config + +You will probably want to be able to reference config that is deployed alongside your plugin that can be referenced in `app-config.yaml`. + +### Using the service + +The following example shows how you can use the default config service to be able to get a config value, and then log it to the console. ```ts import { @@ -65,10 +89,137 @@ createBackendPlugin({ log: coreServices.logger, config: coreServices.config, }, - async init({ config, log }) { - log.warn('Brace yourself for more log output'); - const url = config.getString('backend.baseUrl'); - log.info(`Backend URL is running on ${url}`); + async init({ log, config }) { + log.warn( + `The backend is running at ${config.getString('backend.baseUrl')}`, + ); + }, + }); + }, +}); +``` + +### Configuration of the service + +There's additional configuration that you can optionally pass to setup the `config` core service. + +- `argv` - Override the arguments that are passed to the config loader, instead of using `process.argv` +- `remote` - Configure the `remote` config loading + +You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: + +```ts +import { configFactory } from '@backstage/backend-app-api`; + +const backend = createBackend({ + services: [ + configFactory({ + argv: ['--config', '/backstage/app-config.development.yaml', '--config', '/backstage/app-config.yaml'], + remote: { reloadIntervalSeconds: 60 } + }), + ], +}); +``` + +## Logging + +It is common for your plugins to be able to use the logger. This logger is bound to your plugin, so that you will get nice messages with the plugin ID referenced in the log lines. + +### Using the service + +The following example shows how to get config in your `example` backend plugin and create a `warn` that will be printed nicely to the console. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + id: 'example', + register(env) { + env.registerInit({ + deps: { + log: coreServices.logger, + }, + async init({ log }) { + log.warn('Heres a nice log line thats a warning!'); + }, + }); + }, +}); +``` + +## Cache + +There's a core service provided with the backend system that can be used to interact with a cache in your plugins. This cache is bound to your plugin too, so that you will only set and get values in your plugins namespace. + +### Using the service + +The following example shows how to get a cache client in your `example` backend plugin and `set` and `get` values from the cache. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + id: 'example', + register(env) { + env.registerInit({ + deps: { + cache: coreServices.cache, + }, + async init({ cache }) { + const { key, value } = { key: 'test:key', value: 'bob' }; + await cache.set(key, value, { ttl: 1000 }); + + // .. some other stuff. + + await cache.get(key); // 'bob' + }, + }); + }, +}); +``` + +## Database + +Interacting with databases inside your plugin is something that is quite common, and we provide a `PluginDatabaseManager` part of the core services that you can get `knex` client hooked up to your database which is configured in `app-config.yaml`. + +If there's no config provided in `backend.database` then you will automatically get a simple in memory `sqlite3` client for your plugin. + +These `PluginDatabaseManager`s are scoped per plugin too, so that table names do not conflict across plugins either. + +### Using the service + +The following example shows how to get a `PluginDatabaseManager` in your `example` backend plugin and get a `client` for interacting with the database and running some migrations from a `migrationsDir` for your plugin. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + id: 'example', + register(env) { + env.registerInit({ + deps: { + database: coreServices.database, + }, + async init({ database }) { + const client = database.getClient(); + + if (!database.migrations?.skip) { + await client.migrate.latest({ + directory: migrationsDir, + }); + } }, }); }, diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.ts b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.ts index 1d52af3b76..218910345d 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.ts @@ -27,7 +27,7 @@ export interface HttpRouterFactoryOptions { /** * A callback used to generate the path for each plugin, defaults to `/api/{pluginId}`. */ - getPath(pluginId: string): string; + getPath?(pluginId: string): string; } /** @public */ From 2154b539473beb440f906c4ca98f6fd15b52dba7 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 19 Jan 2023 14:57:08 +0100 Subject: [PATCH 018/118] chore: added some more thigns Signed-off-by: blam --- docs/backend-system/core-services/01-index.md | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index 8b314e9b6b..2a9e01244d 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -225,3 +225,151 @@ createBackendPlugin({ }, }); ``` + +## Discovery + +When building plugins, you might find that you will need to lookup where in fact another plugins `baseUrl`. This could be for example, a `http` route or some `ws` protocol URL. For this we have the `discovery` service that you can query both the internal and external `baseUrl`s given a plugin ID. + +### Using the service + +The following example shows how to get the `DiscoveryService` in your `example` backend plugin and making a request to both the internal and external `baseUrl`s for the `derp` plugin. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { fetch } from 'node-fetch'; +import { Router } from 'express'; + +createBackendPlugin({ + id: 'example', + register(env) { + env.registerInit({ + deps: { + discovery: coreServices.discovery, + }, + async init({ discovery }) { + const urls = await Promise.all[ + discovery.getBaseUrl('derp'), + discovery.getExternalBaseUrl('derp'), + ]; + + await Promise.all( + urls.map( + (url) => fetch(url).then((r) => r.json()), + ), + ); + }, + }); + }, +}); +``` + +## Identity + +When working with backend plugins, you might find that you will need to interact with the `auth-backend` plugin to both authenticate backstage tokens, and get things like the `entityRef` of the authenticated user, and anything that they might claim to own through `ownershipEntityRefs`. + +### Using the service + +The following example shows how to get the `IdentityService` in your `example` backend plugin and retrieve the users `entityRef` and ownership claims for the incoming `http` request. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + id: 'example', + register(env) { + env.registerInit({ + deps: { + identity: coreServices.identity, + http: coreServices.httpRouter, + }, + async init({ http, identity }) { + const router = Router(); + router.get('/test-me', (request, response) => { + // use the identityService pull out the header from the request and get the user + const { identity: userEntityRef, ownershipEntityRefs } = + await identity.getIdentity({ + request, + }); + + // sent the decoded and validated things back to the user + response.json({ + userEntityRef, + ownershipEntityRefs, + }); + }); + + http.use(router); + }, + }); + }, +}); +``` + +### Configuration of the service + +There's additional configuration that you can optionally pass to setup the `identity` core service. + +- `issuer` - Set an optional issuer for validation of the `jwt` +- `algorithms` - `jws` `alg` header for validation of the `jwt`, defaults to `ES256`. More info on supported algorithms under [jose](https://github.com/panva/jose) + +You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: + +```ts +import { identityFactory } from '@backstage/backend-app-api`; + +const backend = createBackend({ + services: [ + identityFactory({ + issuer: 'backstage', + algorithms: ['ES256', 'RS256'] + }), + ], +}); +``` + +## Lifecycle + +When writing plugins, it's often that you will have long running things that you might want to ensure clean shutdowns of when the plugins are torn down, or when the backend is quit (think local development). You shouldn't have to worry too much about providing shutdowns for any of the core services that you use, and should really only need to take care of anything that you create that you should stop when your plugin stops. + +### Using the service + +The following example shows how to get the `LifecycleService` in your `example` backend plugin to clean a long running interval on teardown. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + id: 'example', + register(env) { + env.registerInit({ + deps: { + lifecycle: coreServices.lifecycle, + logger: coreServices.logger, + }, + async init({ lifecycle, logger }) { + // setup by creating an interval that does something that we want to stop after the plugin is stopped. + const interval = setInterval(async () => { + await fetch('http://google.com/keepalive').then(r => r.json()); + // do some other stuff. + }); + + lifecycle.addShutdownHook({ + fn: () => clearInterval(interval), + logger, + }); + }, + }); + }, +}); +``` From cc1b2ac08899b5383ba35eb44990ef76a52408d7 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 19 Jan 2023 14:58:14 +0100 Subject: [PATCH 019/118] chore: fixing api-reports Signed-off-by: blam --- packages/backend-app-api/api-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index dcdc105b17..7ad4b590ab 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -125,7 +125,7 @@ export const httpRouterFactory: ( // @public (undocumented) export interface HttpRouterFactoryOptions { - getPath(pluginId: string): string; + getPath?(pluginId: string): string; } // @public From ed8b5967d7488fc5ff623071d3a79e340b570ee5 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 19 Jan 2023 14:58:52 +0100 Subject: [PATCH 020/118] chore: added changeset Signed-off-by: blam --- .changeset/swift-fishes-smash.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/swift-fishes-smash.md diff --git a/.changeset/swift-fishes-smash.md b/.changeset/swift-fishes-smash.md new file mode 100644 index 0000000000..a4d32c59ea --- /dev/null +++ b/.changeset/swift-fishes-smash.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +`getPath` should be optional as we provide a default value for it From 9cd16d0b36edbf5eca7ca59dc17c1296a58fd829 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 19 Jan 2023 16:51:43 +0100 Subject: [PATCH 021/118] chore: done a first pass for now Signed-off-by: blam --- docs/backend-system/core-services/01-index.md | 100 ++++++++++++++++-- 1 file changed, 90 insertions(+), 10 deletions(-) diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index 2a9e01244d..909b1aaee7 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -79,7 +79,6 @@ import { coreServices, createBackendPlugin, } from '@backstage/backend-plugin-api'; -import { Router } from 'express'; createBackendPlugin({ id: 'example', @@ -134,7 +133,6 @@ import { coreServices, createBackendPlugin, } from '@backstage/backend-plugin-api'; -import { Router } from 'express'; createBackendPlugin({ id: 'example', @@ -164,7 +162,6 @@ import { coreServices, createBackendPlugin, } from '@backstage/backend-plugin-api'; -import { Router } from 'express'; createBackendPlugin({ id: 'example', @@ -203,7 +200,6 @@ import { coreServices, createBackendPlugin, } from '@backstage/backend-plugin-api'; -import { Router } from 'express'; createBackendPlugin({ id: 'example', @@ -240,7 +236,6 @@ import { createBackendPlugin, } from '@backstage/backend-plugin-api'; import { fetch } from 'node-fetch'; -import { Router } from 'express'; createBackendPlugin({ id: 'example', @@ -293,10 +288,11 @@ createBackendPlugin({ const router = Router(); router.get('/test-me', (request, response) => { // use the identityService pull out the header from the request and get the user - const { identity: userEntityRef, ownershipEntityRefs } = - await identity.getIdentity({ - request, - }); + const { + identity: { userEntityRef, ownershipEntityRefs }, + } = await identity.getIdentity({ + request, + }); // sent the decoded and validated things back to the user response.json({ @@ -347,7 +343,6 @@ import { coreServices, createBackendPlugin, } from '@backstage/backend-plugin-api'; -import { Router } from 'express'; createBackendPlugin({ id: 'example', @@ -373,3 +368,88 @@ createBackendPlugin({ }, }); ``` + +## Permissions + +Sometimes you want to include permissions and making sure that a user that is authorized to do some actions in your plugin. We've provide a core service out of the box for you to interact with the permissions framework. You can find out more about the permissions framework in [the documentation](https://backstage.io/docs/permissions/overview) + +### Using the service + +The following example shows how to get the `PermissionsSerice` in your `example` backend to check to see if the user has the correct permissions for `myCustomPermission`. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + id: 'example', + register(env) { + env.registerInit({ + deps: { + permissions: coreServices.permissions, + http: coreServices.httpRouter, + }, + async init({ permissions, http }) { + const router = Router(); + router.get('/test-me', (request, response) => { + // use the identityService pull out the header from the request and get the token + const { token } = await identity.getIdentity({ + request, + }); + + // ask the permissions framework what the decision is for the permission + const permissionResponse = await permissions.authorize( + [ + { + permission: myCustomPermission, + }, + ], + { token }, + ); + }); + + http.use(router); + }, + }); + }, +}); +``` + +## Scheduler + +When writing plugins, it's often that you want to have things running on a schedule, or something similar to cron jobs that are distributed through instances that your backend plugin might be running on. We supply a `TaskScheduler` that is scoped per plugin so that you can create these tasks and orchestrate the running of them. + +### Using the service + +The following example shows how to get the `SchedulerService` in your `example` backend to schedule a scheduled task that runs once across your instances at a given interval. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { fetch } from 'node-fetch'; + +createBackendPlugin({ + id: 'example', + register(env) { + env.registerInit({ + deps: { + scheduler: coreServices.scheduler, + }, + async init({ scheduler }) { + await scheduler.scheduleTask({ + frequency: Duration.fromObject({ minutes: 10 }), + id: 'ping-google', + fn: async () => { + await fetch('http://google.com/ping'); + }, + }); + }, + }); + }, +}); +``` From dae0550b77320a4a41a4c84b2d0b22a554946dc4 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Fri, 20 Jan 2023 13:20:44 +0100 Subject: [PATCH 022/118] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Ben Lambert --- docs/backend-system/core-services/01-index.md | 67 ++++++++++--------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index 909b1aaee7..6cf74aeeea 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -48,7 +48,7 @@ createBackendPlugin({ }); ``` -### Configuration of the service +### Configuring the service There's additional configuration that you can optionally pass to setup the `httpRouter` core service. @@ -57,7 +57,7 @@ There's additional configuration that you can optionally pass to setup the `http You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: ```ts -import { httpRouterFactory } from '@backstage/backend-app-api`; +import { httpRouterFactory } from '@backstage/backend-app-api'; const backend = createBackend({ services: [ @@ -68,7 +68,7 @@ const backend = createBackend({ ## Config -You will probably want to be able to reference config that is deployed alongside your plugin that can be referenced in `app-config.yaml`. +This service allows you to read configuration values out of your `app-config` YAML files. ### Using the service @@ -103,12 +103,12 @@ createBackendPlugin({ There's additional configuration that you can optionally pass to setup the `config` core service. - `argv` - Override the arguments that are passed to the config loader, instead of using `process.argv` -- `remote` - Configure the `remote` config loading +- `remote` - Configure remote configuration loading You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: ```ts -import { configFactory } from '@backstage/backend-app-api`; +import { configFactory } from '@backstage/backend-app-api'; const backend = createBackend({ services: [ @@ -122,11 +122,11 @@ const backend = createBackend({ ## Logging -It is common for your plugins to be able to use the logger. This logger is bound to your plugin, so that you will get nice messages with the plugin ID referenced in the log lines. +This service allows plugins to output logging information. There are actually two logger services: a root logger, and a plugin logger which is bound to individual plugins, so that you will get nice messages with the plugin ID referenced in the log lines. ### Using the service -The following example shows how to get config in your `example` backend plugin and create a `warn` that will be printed nicely to the console. +The following example shows how to get the logger in your `example` backend plugin and create a warning message that will be printed nicely to the console. ```ts import { @@ -142,7 +142,7 @@ createBackendPlugin({ log: coreServices.logger, }, async init({ log }) { - log.warn('Heres a nice log line thats a warning!'); + log.warn("Here's a nice log line that's a warning!"); }, }); }, @@ -151,11 +151,11 @@ createBackendPlugin({ ## Cache -There's a core service provided with the backend system that can be used to interact with a cache in your plugins. This cache is bound to your plugin too, so that you will only set and get values in your plugins namespace. +This service lets your plugin interact with a cache. It is bound to your plugin too, so that you will only set and get values in your plugin's private namespace. ### Using the service -The following example shows how to get a cache client in your `example` backend plugin and `set` and `get` values from the cache. +The following example shows how to get a cache client in your `example` backend plugin and setting and getting values from the cache. ```ts import { @@ -185,15 +185,15 @@ createBackendPlugin({ ## Database -Interacting with databases inside your plugin is something that is quite common, and we provide a `PluginDatabaseManager` part of the core services that you can get `knex` client hooked up to your database which is configured in `app-config.yaml`. +This service lets your plugins get a `knex` client hooked up to a database which is configured in your `app-config` YAML files, for your persistence needs. -If there's no config provided in `backend.database` then you will automatically get a simple in memory `sqlite3` client for your plugin. +If there's no config provided in `backend.database` then you will automatically get a simple in-memory SQLite 3 database for your plugin whose contents will be lost when the service restarts. -These `PluginDatabaseManager`s are scoped per plugin too, so that table names do not conflict across plugins either. +This service is scoped per plugin too, so that table names do not conflict across plugins. ### Using the service -The following example shows how to get a `PluginDatabaseManager` in your `example` backend plugin and get a `client` for interacting with the database and running some migrations from a `migrationsDir` for your plugin. +The following example shows how to get access to the database service in your `example` backend plugin and getting a client for interacting with the database. It also runs some migrations from a certain directory for your plugin. ```ts import { @@ -209,7 +209,7 @@ createBackendPlugin({ database: coreServices.database, }, async init({ database }) { - const client = database.getClient(); + const client = await database.getClient(); if (!database.migrations?.skip) { await client.migrate.latest({ @@ -224,11 +224,11 @@ createBackendPlugin({ ## Discovery -When building plugins, you might find that you will need to lookup where in fact another plugins `baseUrl`. This could be for example, a `http` route or some `ws` protocol URL. For this we have the `discovery` service that you can query both the internal and external `baseUrl`s given a plugin ID. +When building plugins, you might find that you will need to look up another plugin's base URL to be able to communicate with it. This could be for example an HTTP route or some `ws` protocol URL. For this we have a discovery service which can provide both internal and external base URLs for a given a plugin ID. ### Using the service -The following example shows how to get the `DiscoveryService` in your `example` backend plugin and making a request to both the internal and external `baseUrl`s for the `derp` plugin. +The following example shows how to get the discovery service in your `example` backend plugin and making a request to both the internal and external base URLs for the `derp` plugin. ```ts import { @@ -263,11 +263,11 @@ createBackendPlugin({ ## Identity -When working with backend plugins, you might find that you will need to interact with the `auth-backend` plugin to both authenticate backstage tokens, and get things like the `entityRef` of the authenticated user, and anything that they might claim to own through `ownershipEntityRefs`. +When working with backend plugins, you might find that you will need to interact with the `auth-backend` plugin to both authenticate backstage tokens, and to deconstruct them to get the user's entity ref and/or ownership claims out of them. ### Using the service -The following example shows how to get the `IdentityService` in your `example` backend plugin and retrieve the users `entityRef` and ownership claims for the incoming `http` request. +The following example shows how to get the identity service in your `example` backend plugin and retrieve the user's entity ref and ownership claims for the incoming request. ```ts import { @@ -287,14 +287,14 @@ createBackendPlugin({ async init({ http, identity }) { const router = Router(); router.get('/test-me', (request, response) => { - // use the identityService pull out the header from the request and get the user + // use the identity service to pull out the header from the request and get the user const { identity: { userEntityRef, ownershipEntityRefs }, } = await identity.getIdentity({ request, }); - // sent the decoded and validated things back to the user + // send the decoded and validated things back to the user response.json({ userEntityRef, ownershipEntityRefs, @@ -312,13 +312,13 @@ createBackendPlugin({ There's additional configuration that you can optionally pass to setup the `identity` core service. -- `issuer` - Set an optional issuer for validation of the `jwt` -- `algorithms` - `jws` `alg` header for validation of the `jwt`, defaults to `ES256`. More info on supported algorithms under [jose](https://github.com/panva/jose) +- `issuer` - Set an optional issuer for validation of the JWT token +- `algorithms` - `alg` header for validation of the JWT token, defaults to `ES256`. More info on supported algorithms can be found in the [`jose` library documentation](https://github.com/panva/jose) You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: ```ts -import { identityFactory } from '@backstage/backend-app-api`; +import { identityFactory } from '@backstage/backend-app-api'; const backend = createBackend({ services: [ @@ -332,11 +332,11 @@ const backend = createBackend({ ## Lifecycle -When writing plugins, it's often that you will have long running things that you might want to ensure clean shutdowns of when the plugins are torn down, or when the backend is quit (think local development). You shouldn't have to worry too much about providing shutdowns for any of the core services that you use, and should really only need to take care of anything that you create that you should stop when your plugin stops. +This service allows your plugins to register hooks for cleaning up resources as the service is shutting down (e.g. when a pod is being torn down, or when pressing `Ctrl+C` during local development). Other core services also leverage this same mechanism internally to stop themselves cleanly. ### Using the service -The following example shows how to get the `LifecycleService` in your `example` backend plugin to clean a long running interval on teardown. +The following example shows how to get the lifecycle service in your `example` backend plugin to clean up a long running interval when the service is shutting down. ```ts import { @@ -353,7 +353,7 @@ createBackendPlugin({ logger: coreServices.logger, }, async init({ lifecycle, logger }) { - // setup by creating an interval that does something that we want to stop after the plugin is stopped. + // some example work that we want to stop when shutting down const interval = setInterval(async () => { await fetch('http://google.com/keepalive').then(r => r.json()); // do some other stuff. @@ -371,11 +371,11 @@ createBackendPlugin({ ## Permissions -Sometimes you want to include permissions and making sure that a user that is authorized to do some actions in your plugin. We've provide a core service out of the box for you to interact with the permissions framework. You can find out more about the permissions framework in [the documentation](https://backstage.io/docs/permissions/overview) +This service allows your plugins to ask [the permissions framework](https://backstage.io/docs/permissions/overview) for authorization of user actions. ### Using the service -The following example shows how to get the `PermissionsSerice` in your `example` backend to check to see if the user has the correct permissions for `myCustomPermission`. +The following example shows how to get the permissions service in your `example` backend to check to see if the user is allowed to perform a certain action with a custom permission rule. ```ts import { @@ -395,7 +395,7 @@ createBackendPlugin({ async init({ permissions, http }) { const router = Router(); router.get('/test-me', (request, response) => { - // use the identityService pull out the header from the request and get the token + // use the identity service to pull out the token from request headers const { token } = await identity.getIdentity({ request, }); @@ -420,11 +420,11 @@ createBackendPlugin({ ## Scheduler -When writing plugins, it's often that you want to have things running on a schedule, or something similar to cron jobs that are distributed through instances that your backend plugin might be running on. We supply a `TaskScheduler` that is scoped per plugin so that you can create these tasks and orchestrate the running of them. +When writing plugins, you sometimes want to have things running on a schedule, or something similar to cron jobs that are distributed through instances that your backend plugin is running on. We supply a task scheduler for this purpose that is scoped per plugin so that you can create these tasks and orchestrate their execution. ### Using the service -The following example shows how to get the `SchedulerService` in your `example` backend to schedule a scheduled task that runs once across your instances at a given interval. +The following example shows how to get the scheduler service in your `example` backend to issue a scheduled task that runs across your instances at a given interval. ```ts import { @@ -442,7 +442,8 @@ createBackendPlugin({ }, async init({ scheduler }) { await scheduler.scheduleTask({ - frequency: Duration.fromObject({ minutes: 10 }), + frequency: { minutes: 10 }, + timeout: { seconds: 30 }, id: 'ping-google', fn: async () => { await fetch('http://google.com/ping'); From 4fbd05dd83c1595a345ef2ced18348417da07269 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 20 Jan 2023 13:27:22 +0100 Subject: [PATCH 023/118] chore: fixing and pretty: Signed-off-by: blam Signed-off-by: blam --- docs/backend-system/core-services/01-index.md | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index 6cf74aeeea..8477137d11 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -61,7 +61,9 @@ import { httpRouterFactory } from '@backstage/backend-app-api'; const backend = createBackend({ services: [ - httpRouterFactory({ getPath: (pluginId: string) => `/plugins/${pluginId}` }), + httpRouterFactory({ + getPath: (pluginId: string) => `/plugins/${pluginId}`, + }), ], }); ``` @@ -89,9 +91,8 @@ createBackendPlugin({ config: coreServices.config, }, async init({ log, config }) { - log.warn( - `The backend is running at ${config.getString('backend.baseUrl')}`, - ); + const baseUrl = config.getString('backend.baseUrl'); + log.warn(`The backend is running at ${baseUrl}`); }, }); }, @@ -113,8 +114,13 @@ import { configFactory } from '@backstage/backend-app-api'; const backend = createBackend({ services: [ configFactory({ - argv: ['--config', '/backstage/app-config.development.yaml', '--config', '/backstage/app-config.yaml'], - remote: { reloadIntervalSeconds: 60 } + argv: [ + '--config', + '/backstage/app-config.development.yaml', + '--config', + '/backstage/app-config.yaml', + ], + remote: { reloadIntervalSeconds: 60 }, }), ], }); @@ -324,7 +330,7 @@ const backend = createBackend({ services: [ identityFactory({ issuer: 'backstage', - algorithms: ['ES256', 'RS256'] + algorithms: ['ES256', 'RS256'], }), ], }); @@ -357,7 +363,7 @@ createBackendPlugin({ const interval = setInterval(async () => { await fetch('http://google.com/keepalive').then(r => r.json()); // do some other stuff. - }); + }, 1000); lifecycle.addShutdownHook({ fn: () => clearInterval(interval), From 4db2af02c63a686c188583bd88c2a40217c8af44 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 20 Jan 2023 13:28:02 +0100 Subject: [PATCH 024/118] chore: rewrd Signed-off-by: blam Signed-off-by: blam --- .changeset/swift-fishes-smash.md | 2 +- docs/backend-system/core-services/01-index.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/swift-fishes-smash.md b/.changeset/swift-fishes-smash.md index a4d32c59ea..cf3f00129c 100644 --- a/.changeset/swift-fishes-smash.md +++ b/.changeset/swift-fishes-smash.md @@ -2,4 +2,4 @@ '@backstage/backend-app-api': patch --- -`getPath` should be optional as we provide a default value for it +`HttpRouterFactoryOptions.getPath` is now optional as a default value is always provided in the factory. diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index 8477137d11..1e37b08c63 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -99,7 +99,7 @@ createBackendPlugin({ }); ``` -### Configuration of the service +### Configuring the service There's additional configuration that you can optionally pass to setup the `config` core service. @@ -314,7 +314,7 @@ createBackendPlugin({ }); ``` -### Configuration of the service +### Configuring the service There's additional configuration that you can optionally pass to setup the `identity` core service. From 25a15af2d643780e3fb4887408968bc6db598437 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 20 Jan 2023 13:31:36 +0100 Subject: [PATCH 025/118] chore: some more small additions Signed-off-by: blam --- docs/backend-system/core-services/01-index.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index 1e37b08c63..f10c4c1e02 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -206,6 +206,7 @@ import { coreServices, createBackendPlugin, } from '@backstage/backend-plugin-api'; +import { resolvePackagePath } from '@backstage/backend-common'; createBackendPlugin({ id: 'example', @@ -216,7 +217,10 @@ createBackendPlugin({ }, async init({ database }) { const client = await database.getClient(); - + const migrationsDir = resolvePackagePath( + '@internal/my-plugin', + 'migrations', + ); if (!database.migrations?.skip) { await client.migrate.latest({ directory: migrationsDir, @@ -251,16 +255,8 @@ createBackendPlugin({ discovery: coreServices.discovery, }, async init({ discovery }) { - const urls = await Promise.all[ - discovery.getBaseUrl('derp'), - discovery.getExternalBaseUrl('derp'), - ]; - - await Promise.all( - urls.map( - (url) => fetch(url).then((r) => r.json()), - ), - ); + const url = await discoverty.getBaseUrl('derp'); // can also use discovery.getBaseUrl to retrieve external URL + const response = await fetch(`${url}/hello`); }, }); }, From bb8fbac4b882e0312c3676709d253e8b7c4cbc33 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 20 Jan 2023 14:33:07 +0100 Subject: [PATCH 026/118] chore: added some docs for the rootHttpRouter Signed-off-by: blam --- docs/backend-system/core-services/01-index.md | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index f10c4c1e02..9f01337eae 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -456,3 +456,116 @@ createBackendPlugin({ }, }); ``` + +## URL Readers + +Plugins will require communication with certain integrations that users have configured. Popular integrations are things like Version Control Systems (VSC), such as GitHub, BitBucket GitLab etc. These integrations are configured in the `integrations` section of the `app-config.yaml` file. + +These URL readers are basically wrappers with authentication for files and folders that could be stored in these VCS repositories. + +### Using the service + +The following example shows how to get the URL Reader service in your `example` backend plugin to read a file and a directory from a GitHub repository. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import os from 'os'; + +createBackendPlugin({ + id: 'example', + register(env) { + env.registerInit({ + deps: { + urlReader: coreServices.urlReader, + }, + async init({ urlReader }) { + const reader = await urlReader + .read('https://github.com/backstage/backstage/blob/master/README.md') + .then(r => r.buffer()); + + const tmpDir = os.tmpdir(); + const directory = await urlReader + .readTree( + 'https://github.com/backstage/backstage/tree/master/packages/backend', + ) + .then(tree => tree.dir({ targetDir: tmpDir })); + }, + }); + }, +}); +``` + +## Root HTTP Router + +The root HTTP router is a service that allows you to register routes on the root of the backend service. This is useful for things like health checks, or other routes that you want to expose on the root of the backend service. It is used as the base router that backs the `httpRouter` service. Most likely you won't need to use this service directly, but rather use the `httpRouter` service. + +### Using the service + +The following example shows how to get the root HTTP router service in your `example` backend plugin to register a health check route. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + id: 'example', + register(env) { + env.registerInit({ + deps: { + rootRouter: coreServices.rootRouter, + }, + async init({ rootRouter }) { + const router = Router(); + router.get('/health', (request, response) => { + response.send('OK'); + }); + + rootRouter.use(router); + }, + }); + }, +}); +``` + +### Configuring the service + +There's additional options that you can pass to configure the root HTTP Router serivce. These options are passed when you call `createBackend`. + +- `indexPath` - optional path to forward all unmatched requests to. Defaults to `/api/app` which is the `app-backend` plugin responsible for serving the frontend application through the backend. + +- `configure` - this is an optional function that you can use to configure the `express` instance. This is useful if you want to add your own middleware to the root router, such as logging, or other things that you want to do before the request is handled by the backend. It's also useful to override the order in which middleware is applied. + +You can configure the root HTTP Router service by passing the options to the `createBackend` function. + +```ts +import { rootHttpRouterFactory } from '@backstage/backend-app-api'; + +const backend = createBackend({ + services: [ + rootHttpRouterFactory({ + configure: ({ app, middleware, routes, config, logger, lifecycle }) => { + // the built in middleware is provided through an option in the configure function + app.use(middleware.helmet()); + app.use(middleware.cors()); + app.use(middleware.compression()); + + // you can add you your own middleware in here + app.use(custom.logging()); + + // here the routes that are registered by other plugins + app.use(routes); + + // some other middleware that comes after the other routes + app.use(middleware.notFound()); + app.use(middleware.error()); + }, + }), + ], +}); +``` From 947bbb0dca86ff125743e57bcbf5120ec055002e Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 20 Jan 2023 14:36:14 +0100 Subject: [PATCH 027/118] chore more docs Signed-off-by: blam --- docs/backend-system/core-services/01-index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index 9f01337eae..a68e492112 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -518,15 +518,15 @@ createBackendPlugin({ register(env) { env.registerInit({ deps: { - rootRouter: coreServices.rootRouter, + rootHttpRouter: coreServices.rootHttpRouter, }, - async init({ rootRouter }) { + async init({ rootHttpRouter }) { const router = Router(); router.get('/health', (request, response) => { response.send('OK'); }); - rootRouter.use(router); + rootHttpRouter.use(router); }, }); }, From 6a89a4e934a456532eeb51edce56174bb8c75407 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 20 Jan 2023 14:51:21 +0100 Subject: [PATCH 028/118] chore: last of the root deps Signed-off-by: blam --- docs/backend-system/core-services/01-index.md | 246 +++++++++++++----- 1 file changed, 174 insertions(+), 72 deletions(-) diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index a68e492112..d1e89ba69f 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -68,6 +68,78 @@ const backend = createBackend({ }); ``` +## Root HTTP Router + +The root HTTP router is a service that allows you to register routes on the root of the backend service. This is useful for things like health checks, or other routes that you want to expose on the root of the backend service. It is used as the base router that backs the `httpRouter` service. Most likely you won't need to use this service directly, but rather use the `httpRouter` service. + +### Using the service + +The following example shows how to get the root HTTP router service in your `example` backend plugin to register a health check route. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + id: 'example', + register(env) { + env.registerInit({ + deps: { + rootHttpRouter: coreServices.rootHttpRouter, + }, + async init({ rootHttpRouter }) { + const router = Router(); + router.get('/health', (request, response) => { + response.send('OK'); + }); + + rootHttpRouter.use(router); + }, + }); + }, +}); +``` + +### Configuring the service + +There's additional options that you can pass to configure the root HTTP Router serivce. These options are passed when you call `createBackend`. + +- `indexPath` - optional path to forward all unmatched requests to. Defaults to `/api/app` which is the `app-backend` plugin responsible for serving the frontend application through the backend. + +- `configure` - this is an optional function that you can use to configure the `express` instance. This is useful if you want to add your own middleware to the root router, such as logging, or other things that you want to do before the request is handled by the backend. It's also useful to override the order in which middleware is applied. + +You can configure the root HTTP Router service by passing the options to the `createBackend` function. + +```ts +import { rootHttpRouterFactory } from '@backstage/backend-app-api'; + +const backend = createBackend({ + services: [ + rootHttpRouterFactory({ + configure: ({ app, middleware, routes, config, logger, lifecycle }) => { + // the built in middleware is provided through an option in the configure function + app.use(middleware.helmet()); + app.use(middleware.cors()); + app.use(middleware.compression()); + + // you can add you your own middleware in here + app.use(custom.logging()); + + // here the routes that are registered by other plugins + app.use(routes); + + // some other middleware that comes after the other routes + app.use(middleware.notFound()); + app.use(middleware.error()); + }, + }), + ], +}); +``` + ## Config This service allows you to read configuration values out of your `app-config` YAML files. @@ -155,6 +227,49 @@ createBackendPlugin({ }); ``` +### Root Logger + +The root logger is the logger that is used by other root services. It's where the implemenation lies for creating child loggers around the backstage ecosystem including child loggers for plugins with the correct metadata and annotations. + +If you want to override the implementation for logging across all of the backend, this is the service that you should override. + +### Configuring the service + +The following example is how you can override the root logger service to add additional metadata to all log lines. + +```ts +import { coreServices } from '@backstage/backend-plugin-api'; + +const backend = createBackend({ + services: [ + createServiceFactory({ + service: coreServices.rootLogger, + deps: { + config: coreServices.config, + }, + async factory({ config }) { + const logger = WinstonLogger.create({ + meta: { + service: 'backstage', + // here's some additional information that is not part of the + // original implementation + podName: 'myk8spod', + }, + level: process.env.LOG_LEVEL || 'info', + format: + process.env.NODE_ENV === 'production' + ? format.json() + : WinstonLogger.colorFormat(), + transports: [new transports.Console()], + }); + + return logger; + }, + }), + ], +}); +``` + ## Cache This service lets your plugin interact with a cache. It is bound to your plugin too, so that you will only set and get values in your plugin's private namespace. @@ -371,6 +486,65 @@ createBackendPlugin({ }); ``` +## Root Lifecycle + +This service is the same as the lifecycle service, but should only be used by the root services. This is also where the implementation for the actual lifecycle hooks torn, so if you want to override the implementation of how the lifecycle hooks are executed, you should override this service. + +### Configure the service + +The following example shows how to override the default implementation of the lifecycle service with something that listens on different process events to the original. + +```ts +class MyCustomLifecycleService implements RootLifecycleService { + constructor(private readonly logger: LoggerService) { + ['SIGKILL', 'SIGTERM'].map(signal => + process.on(signal, () => this.shutdown()), + ); + } + + #isCalled = false; + #shutdownTasks: Array = []; + + addShutdownHook(options: LifecycleServiceShutdownHook): void { + this.#shutdownTasks.push(options); + } + + async shutdown(): Promise { + if (this.#isCalled) { + return; + } + this.#isCalled = true; + + this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`); + await Promise.all( + this.#shutdownTasks.map(async hook => { + const { logger = this.logger } = hook; + try { + await hook.fn(); + logger.info(`Shutdown hook succeeded`); + } catch (error) { + logger.error(`Shutdown hook failed, ${error}`); + } + }), + ); + } +} + +const backend = createBackend({ + services: [ + createServiceFactory({ + service: coreServices.rootLifecycle, + deps: { + logger: coreServices.rootLogger, + }, + async factory({ logger }) { + return new MyCustomLifecycleService(logger); + }, + }), + ], +}); +``` + ## Permissions This service allows your plugins to ask [the permissions framework](https://backstage.io/docs/permissions/overview) for authorization of user actions. @@ -497,75 +671,3 @@ createBackendPlugin({ }, }); ``` - -## Root HTTP Router - -The root HTTP router is a service that allows you to register routes on the root of the backend service. This is useful for things like health checks, or other routes that you want to expose on the root of the backend service. It is used as the base router that backs the `httpRouter` service. Most likely you won't need to use this service directly, but rather use the `httpRouter` service. - -### Using the service - -The following example shows how to get the root HTTP router service in your `example` backend plugin to register a health check route. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; -import { Router } from 'express'; - -createBackendPlugin({ - id: 'example', - register(env) { - env.registerInit({ - deps: { - rootHttpRouter: coreServices.rootHttpRouter, - }, - async init({ rootHttpRouter }) { - const router = Router(); - router.get('/health', (request, response) => { - response.send('OK'); - }); - - rootHttpRouter.use(router); - }, - }); - }, -}); -``` - -### Configuring the service - -There's additional options that you can pass to configure the root HTTP Router serivce. These options are passed when you call `createBackend`. - -- `indexPath` - optional path to forward all unmatched requests to. Defaults to `/api/app` which is the `app-backend` plugin responsible for serving the frontend application through the backend. - -- `configure` - this is an optional function that you can use to configure the `express` instance. This is useful if you want to add your own middleware to the root router, such as logging, or other things that you want to do before the request is handled by the backend. It's also useful to override the order in which middleware is applied. - -You can configure the root HTTP Router service by passing the options to the `createBackend` function. - -```ts -import { rootHttpRouterFactory } from '@backstage/backend-app-api'; - -const backend = createBackend({ - services: [ - rootHttpRouterFactory({ - configure: ({ app, middleware, routes, config, logger, lifecycle }) => { - // the built in middleware is provided through an option in the configure function - app.use(middleware.helmet()); - app.use(middleware.cors()); - app.use(middleware.compression()); - - // you can add you your own middleware in here - app.use(custom.logging()); - - // here the routes that are registered by other plugins - app.use(routes); - - // some other middleware that comes after the other routes - app.use(middleware.notFound()); - app.use(middleware.error()); - }, - }), - ], -}); -``` From f8bf7fa2df34251983871153d3b880d12d7922ee Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 20 Jan 2023 15:20:06 +0100 Subject: [PATCH 029/118] chore: make the lord and almighty saviour vale ahppy. Signed-off-by: blam --- docs/backend-system/core-services/01-index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index d1e89ba69f..fbf228a937 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -105,7 +105,7 @@ createBackendPlugin({ ### Configuring the service -There's additional options that you can pass to configure the root HTTP Router serivce. These options are passed when you call `createBackend`. +There's additional options that you can pass to configure the root HTTP Router service. These options are passed when you call `createBackend`. - `indexPath` - optional path to forward all unmatched requests to. Defaults to `/api/app` which is the `app-backend` plugin responsible for serving the frontend application through the backend. @@ -229,7 +229,7 @@ createBackendPlugin({ ### Root Logger -The root logger is the logger that is used by other root services. It's where the implemenation lies for creating child loggers around the backstage ecosystem including child loggers for plugins with the correct metadata and annotations. +The root logger is the logger that is used by other root services. It's where the implementation lies for creating child loggers around the backstage ecosystem including child loggers for plugins with the correct metadata and annotations. If you want to override the implementation for logging across all of the backend, this is the service that you should override. From 536693b2383d1cee002eb47fea1f711891299e80 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 20 Jan 2023 16:51:30 +0100 Subject: [PATCH 030/118] chore: code review comments Signed-off-by: blam --- docs/backend-system/core-services/01-index.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index fbf228a937..4ef9d75bde 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -239,6 +239,7 @@ The following example is how you can override the root logger service to add add ```ts import { coreServices } from '@backstage/backend-plugin-api'; +import { WinstonLogger } from '@backstage/backend-app-api'; const backend = createBackend({ services: [ @@ -488,7 +489,7 @@ createBackendPlugin({ ## Root Lifecycle -This service is the same as the lifecycle service, but should only be used by the root services. This is also where the implementation for the actual lifecycle hooks torn, so if you want to override the implementation of how the lifecycle hooks are executed, you should override this service. +This service is the same as the lifecycle service, but should only be used by the root services. This is also where the implementation for the actual lifecycle hooks are collected and executed, so if you want to override the implementation of how those are processed, you should override this service. ### Configure the service @@ -656,7 +657,7 @@ createBackendPlugin({ urlReader: coreServices.urlReader, }, async init({ urlReader }) { - const reader = await urlReader + const buffer = await urlReader .read('https://github.com/backstage/backstage/blob/master/README.md') .then(r => r.buffer()); From 65ca9b2a1c5a756ca8587b364636ff732db19c22 Mon Sep 17 00:00:00 2001 From: Mitchell Hentges Date: Mon, 16 Jan 2023 15:51:06 -0800 Subject: [PATCH 031/118] Remove `react-dev-utils` error compatibility workaround To work around a `react-dev-utils` compatibility problem, errors and warnings had context stripped before being formatted. Since this workaround was was only needed until `react-dev-utils` v5 was release (now over a year ago, nice! [1]), I think that it can be removed. [1] https://github.com/facebook/create-react-app/releases/tag/v5.0.0 Signed-off-by: Mitchell Hentges --- packages/cli/src/lib/bundler/bundle.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/lib/bundler/bundle.ts b/packages/cli/src/lib/bundler/bundle.ts index a8967bb250..32e4c1a181 100644 --- a/packages/cli/src/lib/bundler/bundle.ts +++ b/packages/cli/src/lib/bundler/bundle.ts @@ -122,15 +122,9 @@ async function build(config: webpack.Configuration, isCi: boolean) { warnings: true, errors: true, }); - // NOTE(freben): The code below that extracts the message part of the errors, - // is due to react-dev-utils not yet being compatible with webpack 5. This - // may be possible to remove (just passing the serialized stats object - // directly into the format function) after a new release of react-dev-utils - // has been made available. - // See https://github.com/facebook/create-react-app/issues/9880 const { errors, warnings } = formatWebpackMessages({ - errors: serializedStats.errors?.map(e => (e.message ? e.message : e)), - warnings: serializedStats.warnings?.map(e => (e.message ? e.message : e)), + errors: serializedStats.errors, + warnings: serializedStats.warnings, }); if (errors.length) { From edd9c270b63b1c7c5dd19f36adf432f3216bafb2 Mon Sep 17 00:00:00 2001 From: Mitchell Hentges Date: Wed, 18 Jan 2023 14:09:37 -0800 Subject: [PATCH 032/118] Simplify redundant `catch()` during CLI bundling Remove extra "Failed to compile" message that was redundant, and inline "Failed to compile" message into upstream errors. Note that now _other_ errors (such as `webpack(...)` failures) will not be prefixed with "Failed to compile". However, it should be sufficiently obvious that they're serious failures, so we should be OK without the extra information. Signed-off-by: Mitchell Hentges --- packages/cli/src/lib/bundler/bundle.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/lib/bundler/bundle.ts b/packages/cli/src/lib/bundler/bundle.ts index 32e4c1a181..9b146aa115 100644 --- a/packages/cli/src/lib/bundler/bundle.ts +++ b/packages/cli/src/lib/bundler/bundle.ts @@ -63,10 +63,7 @@ export async function buildBundle(options: BuildOptions) { ); } - const { stats } = await build(config, isCi).catch(error => { - console.log(chalk.red('Failed to compile.\n')); - throw new Error(`Failed to compile.\n${error.message || error}`); - }); + const { stats } = await build(config, isCi); if (!stats) { throw new Error('No stats returned'); @@ -114,7 +111,7 @@ async function build(config: webpack.Configuration, isCi: boolean) { ); if (!stats) { - throw new Error('No stats provided'); + throw new Error('Failed to compile: No stats provided'); } const serializedStats = stats.toJson({ @@ -130,7 +127,7 @@ async function build(config: webpack.Configuration, isCi: boolean) { if (errors.length) { // Only keep the first error. Others are often indicative // of the same problem, but confuse the reader with noise. - throw new Error(errors[0]); + throw new Error(`Failed to compile.\n${errors[0]}`); } if (isCi && warnings.length) { console.log( @@ -138,7 +135,7 @@ async function build(config: webpack.Configuration, isCi: boolean) { '\nTreating warnings as errors because process.env.CI = true.\n', ), ); - throw new Error(warnings.join('\n\n')); + throw new Error(`Failed to compile.\n${warnings.join('\n\n')}`); } return { stats }; From 281598105708d1db80ab6d77cfd4f99806d329fe Mon Sep 17 00:00:00 2001 From: Mitchell Hentges Date: Tue, 17 Jan 2023 16:18:34 -0800 Subject: [PATCH 033/118] Make build error cause more clear by prepending with module name Simplify the diagnosis of build failures by showing the module name encountering the issue right before the error itself is printed. Also apply the fix to warnings. Fixes #15815 Signed-off-by: Mitchell Hentges --- .changeset/eight-hotels-sparkle.md | 5 +++++ packages/cli/src/lib/bundler/bundle.ts | 18 ++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 .changeset/eight-hotels-sparkle.md diff --git a/.changeset/eight-hotels-sparkle.md b/.changeset/eight-hotels-sparkle.md new file mode 100644 index 0000000000..6846630e02 --- /dev/null +++ b/.changeset/eight-hotels-sparkle.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Show module name causing error during build diff --git a/packages/cli/src/lib/bundler/bundle.ts b/packages/cli/src/lib/bundler/bundle.ts index 9b146aa115..ba553b3b76 100644 --- a/packages/cli/src/lib/bundler/bundle.ts +++ b/packages/cli/src/lib/bundler/bundle.ts @@ -32,6 +32,10 @@ import chalk from 'chalk'; const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; +function applyContextToError(error: string, moduleName: string): string { + return `Failed to compile '${moduleName}':\n ${error}`; +} + export async function buildBundle(options: BuildOptions) { const { statsJsonEnabled, schema: configSchema } = options; @@ -127,15 +131,25 @@ async function build(config: webpack.Configuration, isCi: boolean) { if (errors.length) { // Only keep the first error. Others are often indicative // of the same problem, but confuse the reader with noise. - throw new Error(`Failed to compile.\n${errors[0]}`); + const errorWithContext = applyContextToError( + errors[0], + serializedStats.errors?.[0].moduleName || '', + ); + throw new Error(errorWithContext); } if (isCi && warnings.length) { + const warningsWithContext = warnings.map((warning, i) => { + return applyContextToError( + warning, + serializedStats.warnings?.[i].moduleName || '', + ); + }); console.log( chalk.yellow( '\nTreating warnings as errors because process.env.CI = true.\n', ), ); - throw new Error(`Failed to compile.\n${warnings.join('\n\n')}`); + throw new Error(warningsWithContext.join('\n\n')); } return { stats }; From 0daa328c3a5b065313760a7ef1e2eabfd0cc2e5a Mon Sep 17 00:00:00 2001 From: Alex Crome Date: Sat, 21 Jan 2023 11:41:30 +0000 Subject: [PATCH 034/118] Extract default transformers to their own file Signed-off-by: Alex Crome --- .changeset/nervous-mangos-rhyme.md | 5 + .../src/microsoftGraph/defaultTransformers.ts | 153 ++++++++++++++++++ .../src/microsoftGraph/index.ts | 4 +- .../src/microsoftGraph/read.ts | 142 +--------------- 4 files changed, 165 insertions(+), 139 deletions(-) create mode 100644 .changeset/nervous-mangos-rhyme.md create mode 100644 plugins/catalog-backend-module-msgraph/src/microsoftGraph/defaultTransformers.ts diff --git a/.changeset/nervous-mangos-rhyme.md b/.changeset/nervous-mangos-rhyme.md new file mode 100644 index 0000000000..23e92c65a7 --- /dev/null +++ b/.changeset/nervous-mangos-rhyme.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +--- + +Extract default transformers to their own file diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/defaultTransformers.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/defaultTransformers.ts new file mode 100644 index 0000000000..9d123505e8 --- /dev/null +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/defaultTransformers.ts @@ -0,0 +1,153 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + MICROSOFT_EMAIL_ANNOTATION, + MICROSOFT_GRAPH_GROUP_ID_ANNOTATION, + MICROSOFT_GRAPH_TENANT_ID_ANNOTATION, + MICROSOFT_GRAPH_USER_ID_ANNOTATION, +} from './constants'; +import { normalizeEntityName } from './helper'; +import { GroupEntity, UserEntity } from '@backstage/catalog-model'; +import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; + +export async function defaultOrganizationTransformer( + organization: MicrosoftGraph.Organization, +): Promise { + if (!organization.id || !organization.displayName) { + return undefined; + } + + const name = normalizeEntityName(organization.displayName!); + return { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: name, + description: organization.displayName!, + annotations: { + [MICROSOFT_GRAPH_TENANT_ID_ANNOTATION]: organization.id!, + }, + }, + spec: { + type: 'root', + profile: { + displayName: organization.displayName!, + }, + children: [], + }, + }; +} + +function extractGroupName(group: MicrosoftGraph.Group): string { + if (group.securityEnabled) { + return group.displayName as string; + } + return (group.mailNickname || group.displayName) as string; +} + +/** + * The default implementation of the transformation from a graph group entry to + * a Group entity. + * + * @public + */ +export async function defaultGroupTransformer( + group: MicrosoftGraph.Group, + groupPhoto?: string, +): Promise { + if (!group.id || !group.displayName) { + return undefined; + } + + const name = normalizeEntityName(extractGroupName(group)); + const entity: GroupEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: name, + annotations: { + [MICROSOFT_GRAPH_GROUP_ID_ANNOTATION]: group.id, + }, + }, + spec: { + type: 'team', + profile: {}, + children: [], + }, + }; + + if (group.description) { + entity.metadata.description = group.description; + } + if (group.displayName) { + entity.spec.profile!.displayName = group.displayName; + } + if (group.mail) { + entity.spec.profile!.email = group.mail; + } + if (groupPhoto) { + entity.spec.profile!.picture = groupPhoto; + } + + return entity; +} + +/** + * The default implementation of the transformation from a graph user entry to + * a User entity. + * + * @public + */ +export async function defaultUserTransformer( + user: MicrosoftGraph.User, + userPhoto?: string, +): Promise { + if (!user.id || !user.displayName || !user.mail) { + return undefined; + } + + const name = normalizeEntityName(user.mail); + const entity: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name, + annotations: { + [MICROSOFT_EMAIL_ANNOTATION]: user.mail!, + [MICROSOFT_GRAPH_USER_ID_ANNOTATION]: user.id!, + }, + }, + spec: { + profile: { + displayName: user.displayName!, + email: user.mail!, + + // TODO: Additional fields? + // jobTitle: user.jobTitle || undefined, + // officeLocation: user.officeLocation || undefined, + // mobilePhone: user.mobilePhone || undefined, + }, + memberOf: [], + }, + }; + + if (userPhoto) { + entity.spec.profile!.picture = userPhoto; + } + + return entity; +} diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts index e9ab49f7d1..37dcffec22 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts @@ -29,8 +29,8 @@ export { defaultGroupTransformer, defaultOrganizationTransformer, defaultUserTransformer, - readMicrosoftGraphOrg, -} from './read'; +} from './defaultTransformers'; +export { readMicrosoftGraphOrg } from './read'; export type { GroupTransformer, OrganizationTransformer, diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index 0ed411bfea..6699af7764 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -19,69 +19,25 @@ import { stringifyEntityRef, UserEntity, } from '@backstage/catalog-model'; -import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; import limiterFactory from 'p-limit'; import { Logger } from 'winston'; import { MicrosoftGraphClient } from './client'; import { - MICROSOFT_EMAIL_ANNOTATION, MICROSOFT_GRAPH_GROUP_ID_ANNOTATION, MICROSOFT_GRAPH_TENANT_ID_ANNOTATION, MICROSOFT_GRAPH_USER_ID_ANNOTATION, } from './constants'; -import { normalizeEntityName } from './helper'; import { buildMemberOf, buildOrgHierarchy } from './org'; import { GroupTransformer, OrganizationTransformer, UserTransformer, } from './types'; - -/** - * The default implementation of the transformation from a graph user entry to - * a User entity. - * - * @public - */ -export async function defaultUserTransformer( - user: MicrosoftGraph.User, - userPhoto?: string, -): Promise { - if (!user.id || !user.displayName || !user.mail) { - return undefined; - } - - const name = normalizeEntityName(user.mail); - const entity: UserEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'User', - metadata: { - name, - annotations: { - [MICROSOFT_EMAIL_ANNOTATION]: user.mail!, - [MICROSOFT_GRAPH_USER_ID_ANNOTATION]: user.id!, - }, - }, - spec: { - profile: { - displayName: user.displayName!, - email: user.mail!, - - // TODO: Additional fields? - // jobTitle: user.jobTitle || undefined, - // officeLocation: user.officeLocation || undefined, - // mobilePhone: user.mobilePhone || undefined, - }, - memberOf: [], - }, - }; - - if (userPhoto) { - entity.spec.profile!.picture = userPhoto; - } - - return entity; -} +import { + defaultGroupTransformer, + defaultOrganizationTransformer, + defaultUserTransformer, +} from './defaultTransformers'; export async function readMicrosoftGraphUsers( client: MicrosoftGraphClient, @@ -237,40 +193,6 @@ export async function readMicrosoftGraphUsersInGroups( return { users }; } -/** - * The default implementation of the transformation from a graph organization - * entry to a Group entity. - * - * @public - */ -export async function defaultOrganizationTransformer( - organization: MicrosoftGraph.Organization, -): Promise { - if (!organization.id || !organization.displayName) { - return undefined; - } - - const name = normalizeEntityName(organization.displayName!); - return { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Group', - metadata: { - name: name, - description: organization.displayName!, - annotations: { - [MICROSOFT_GRAPH_TENANT_ID_ANNOTATION]: organization.id!, - }, - }, - spec: { - type: 'root', - profile: { - displayName: organization.displayName!, - }, - children: [], - }, - }; -} - export async function readMicrosoftGraphOrganization( client: MicrosoftGraphClient, tenantId: string, @@ -286,60 +208,6 @@ export async function readMicrosoftGraphOrganization( return { rootGroup }; } -function extractGroupName(group: MicrosoftGraph.Group): string { - if (group.securityEnabled) { - return group.displayName as string; - } - return (group.mailNickname || group.displayName) as string; -} - -/** - * The default implementation of the transformation from a graph group entry to - * a Group entity. - * - * @public - */ -export async function defaultGroupTransformer( - group: MicrosoftGraph.Group, - groupPhoto?: string, -): Promise { - if (!group.id || !group.displayName) { - return undefined; - } - - const name = normalizeEntityName(extractGroupName(group)); - const entity: GroupEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Group', - metadata: { - name: name, - annotations: { - [MICROSOFT_GRAPH_GROUP_ID_ANNOTATION]: group.id, - }, - }, - spec: { - type: 'team', - profile: {}, - children: [], - }, - }; - - if (group.description) { - entity.metadata.description = group.description; - } - if (group.displayName) { - entity.spec.profile!.displayName = group.displayName; - } - if (group.mail) { - entity.spec.profile!.email = group.mail; - } - if (groupPhoto) { - entity.spec.profile!.picture = groupPhoto; - } - - return entity; -} - export async function readMicrosoftGraphGroups( client: MicrosoftGraphClient, tenantId: string, From a77974963ce8babb03b0ee6016769b3b426d789d Mon Sep 17 00:00:00 2001 From: Viet Nguyen <19592926+v-ngu@users.noreply.github.com> Date: Sat, 21 Jan 2023 10:35:00 -0500 Subject: [PATCH 035/118] Fix typo Signed-off-by: Viet Nguyen <19592926+v-ngu@users.noreply.github.com> --- docs/plugins/feature-flags.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/feature-flags.md b/docs/plugins/feature-flags.md index d9841598bb..a572143d6e 100644 --- a/docs/plugins/feature-flags.md +++ b/docs/plugins/feature-flags.md @@ -56,7 +56,7 @@ The users selection is saved in the users browsers local storage. Once toggled i The easiest way to control content based on the state of a feature flag is to use the [FeatureFlagged](https://backstage.io/docs/reference/core-app-api.featureflagged) component. ```ts -import { FeatureFlagged } from '@backstage/core-app-api' +import { FeatureFlagged } from '@backstage/core-app-api'; ... From ff53f764e1f4f171364eeb6b48b00d5f31b60f64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Sun, 22 Jan 2023 11:33:03 +0100 Subject: [PATCH 036/118] add plugin / module testing docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../02-testing.md | 153 +++++++++++++++++- 1 file changed, 147 insertions(+), 6 deletions(-) diff --git a/docs/backend-system/building-plugins-and-modules/02-testing.md b/docs/backend-system/building-plugins-and-modules/02-testing.md index e8a8619f72..9a2042c4d3 100644 --- a/docs/backend-system/building-plugins-and-modules/02-testing.md +++ b/docs/backend-system/building-plugins-and-modules/02-testing.md @@ -6,21 +6,162 @@ sidebar_label: Testing description: Learn how to test your backend plugins and modules --- -Utilities for testing backend plugins and modules are available in `@backstage/backend-test-utils`. -`startTestBackend` returns a server which can be used together with `supertest` to test the plugins. +Utilities for testing backend plugins and modules are available in +`@backstage/backend-test-utils`. This section describes those facilities. + +## Testing Backend Plugins + +To facilitate testing of backend plugins, the `@backstage/backend-test-utils` +package provides a `startTestBackend` function which starts up an entire backend +harness, complete with a number of mock services. You can then provide overrides +for services whose behavior you need to adjust for the test run. + +The function returns an HTTP server instance which can be used together with +e.g. `supertest` to easily test the actual REST service surfaces of plugins who +register routes with [the HTTP router service +API](../core-services/01-index.md). ```ts -import { startTestBackend } from '@backstage/backend-test-utils'; +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import request from 'supertest'; +import { myPlugin } from './plugin.ts'; + +describe('myPlugin', () => { + it('can serve values from config', async () => { + const fakeConfig = { myPlugin: { value: 7 } }; -describe('My plugin tests', () => { - it('should return 200', async () => { const { server } = await startTestBackend({ features: [myPlugin()], + services: [mockServices.config.factory({ data: fakeConfig })], }); - const response = await request(server).get('/api/example/hello'); + const response = await request(server).get('/api/example/get-value'); expect(response.status).toBe(200); + expect(response.body).toEqual({ value: 7 }); }); }); ``` + +This example shows how to easily access the factories for mock services and +passing options to them, which will override the default mocks. + +The returned server also has a `port()` method which returns the dynamically +bound listening port. You can use this to perform lower level network +interactions with the running test service. + +## Testing Remote Service Interactions + +If your backend plugin or service interacts with external services using HTTP +calls, we recommend leveraging the `msw` package to intercept actual outgoing +requests and return mock responses. This lets you stub out remote services +rather than the local clients, leading to more thorough and robust tests. You +can read more about how it works [in their documentation](https://mswjs.io/). + +The `@backstage/backend-test-utils` package exports a `setupRequestMockHandlers` +function which ensures that the correct `jest` lifecycle hooks are invoked to +set up and tear down your `msw` instance, and enables the option that completely +rejects requests that don't match one of your mock rules. This ensures that your +tests cannot accidentally leak traffic into production from tests. + +Example: + +```ts +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; + +describe('read from remote', () => { + const worker = setupServer(); + setupRequestMockHandlers(worker); + + it('should auth and read successfully', async () => { + expect.assertions(1); + + worker.use( + rest.get('https://remote-server.com/api/v3/foo', (req, res, ctx) => { + expect(req.headers.get('authorization')).toBe('Bearer fake'); + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.body(JSON.stringify({ value: 7 })), + ); + }), + ); + + // exercise your plugin or service as usual, with real clients + }); +}); +``` + +## Testing Database Interactions + +The `@backstage/backend-test-utils` package includes facilities for testing your +plugins' interactions with databases, including spinning up `testcontainers` +powered Docker images with real database engines to connect to. + +The base setup for such a test could look as follows: + +```ts +// MyDatabaseClass.test.ts +import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { + MyDatabaseClass, + applyDatabaseMigrations, + type FooTableRow, +} from './MyDatabaseClass'; + +describe('MyDatabaseClass', () => { + // Change this to the set of constants that you actually actively intend to + // support. Make sure to create only one TestDatabases instance per file, + // since spinning up "physical" databases to test against is much costlier + // than creating the "logical" databases within them that the individual + // tests use. + const databases = TestDatabases.create({ + ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + }); + + // Just an example of how to conveniently bundle up the setup code + async function createSut(databaseId: TestDatabaseId) { + const knex = await databases.init(databaseId); + const sut = new MyDatabaseClass({ database: knex }); + await sut.runMigrations(); + return { knex, sut }; + } + + describe('foo', () => { + // Easily run the exact same test onto all supported databases + it.each(databases.eachSupportedId())( + 'should run foo on %p', + async databaseId => { + const { knex, sut } = await createSut(databaseId); + // raw knex is available for underlying manipulation + await knex('foo').insert({ value: 2 }); + // drive your system under test as usual + await expect(sut.foos()).resolves.toEqual([{ value: 2 }]); + }); + }); +``` + +If you want to pass the test database instance into backend plugins or services, +you can supply it in the form of a mock instance of `coreServices.database` to +your test database. + +```ts +const { knex, sut } = await createSut(databaseId); +const { server } = await startTestBackend({ + features: [myPlugin()], + services: [[coreServices.database, { getClient: async () => knex }]], +}); +``` + +When running locally, the tests only run against SQLite for the sake of speed. +When the `CI` environment variable is set, all given database engines are used. + +If you do not want or are unable to use docker based database engines, e.g. if +your CI environment is able to supply databases natively, the `TestDatabases` +support custom connection strings through the use of environment variables that +it'll take into account when present. + +- `BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING` +- `BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING` +- `BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING` From 753a48982a53547b68fa51e35a3395bbd773147f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 23 Jan 2023 10:23:14 +0100 Subject: [PATCH 037/118] Update .changeset/itchy-goats-melt.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ben Lambert Signed-off-by: Fredrik Adelöw --- .changeset/itchy-goats-melt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/itchy-goats-melt.md b/.changeset/itchy-goats-melt.md index dc61285d11..d073dd4103 100644 --- a/.changeset/itchy-goats-melt.md +++ b/.changeset/itchy-goats-melt.md @@ -5,4 +5,4 @@ '@backstage/plugin-scaffolder-backend-module-rails': patch --- -Internal refactor to use the new scaffolder-node package for some functionality +Internal refactor to use the new `@backstage/plugin-scaffolder-node` package for some functionality From c691d62bfb92adf390074cdf13b74335c150dfeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 23 Jan 2023 10:23:21 +0100 Subject: [PATCH 038/118] Update .changeset/lazy-badgers-try.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ben Lambert Signed-off-by: Fredrik Adelöw --- .changeset/lazy-badgers-try.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/lazy-badgers-try.md b/.changeset/lazy-badgers-try.md index 99491f4bee..4ffb32304a 100644 --- a/.changeset/lazy-badgers-try.md +++ b/.changeset/lazy-badgers-try.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder-node': minor --- -New package that takes over some of the types and functionality from scaffolder-backend that are shared with other modules +New package that takes over some of the types and functionality from `@backstage/plugin-scaffolder-backend` that are shared with other modules From a4b70713db3b6d1bbf3d1a724cde608eb848ad59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 23 Jan 2023 10:23:32 +0100 Subject: [PATCH 039/118] Update plugins/scaffolder-backend/src/deprecated.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ben Lambert Signed-off-by: Fredrik Adelöw --- plugins/scaffolder-backend/src/deprecated.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/deprecated.ts b/plugins/scaffolder-backend/src/deprecated.ts index 46fc358cfa..f8358b9071 100644 --- a/plugins/scaffolder-backend/src/deprecated.ts +++ b/plugins/scaffolder-backend/src/deprecated.ts @@ -24,7 +24,7 @@ import { JsonObject } from '@backstage/types'; /** * @public - * @deprecated Use `ActionContext` from `@backstage/plugin-scaffolder-node` instead + * @deprecated Import from {@link @backstage/plugin-scaffolder-node#ActionContext} instead */ export type ActionContext = ActionContextNode; From 3f956a154b09e554c2512397440061df2fc2cddf Mon Sep 17 00:00:00 2001 From: Peter Macdonald Date: Mon, 23 Jan 2023 13:48:28 +0100 Subject: [PATCH 040/118] Added a message under feature flag(s) to advise a page reload is needed when toggling them Signed-off-by: Peter Macdonald --- .../src/components/FeatureFlags/UserSettingsFeatureFlags.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/user-settings/src/components/FeatureFlags/UserSettingsFeatureFlags.tsx b/plugins/user-settings/src/components/FeatureFlags/UserSettingsFeatureFlags.tsx index a3b5f593ae..c086f70dd8 100644 --- a/plugins/user-settings/src/components/FeatureFlags/UserSettingsFeatureFlags.tsx +++ b/plugins/user-settings/src/components/FeatureFlags/UserSettingsFeatureFlags.tsx @@ -82,6 +82,10 @@ export const UserSettingsFeatureFlags = () => { Feature Flags + + {' '} + Please refresh the page when toggling feature flags{' '} + {featureFlags.length >= 10 && ( From 1412f0ea62632e2eb791d224a05c6af724e6ac69 Mon Sep 17 00:00:00 2001 From: Peter Macdonald Date: Mon, 23 Jan 2023 14:01:12 +0100 Subject: [PATCH 041/118] Added a message under feature flag(s) to advise a page reload is needed when toggling them Signed-off-by: Peter Macdonald --- .../src/components/FeatureFlags/UserSettingsFeatureFlags.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/user-settings/src/components/FeatureFlags/UserSettingsFeatureFlags.tsx b/plugins/user-settings/src/components/FeatureFlags/UserSettingsFeatureFlags.tsx index c086f70dd8..c58e637bd2 100644 --- a/plugins/user-settings/src/components/FeatureFlags/UserSettingsFeatureFlags.tsx +++ b/plugins/user-settings/src/components/FeatureFlags/UserSettingsFeatureFlags.tsx @@ -83,8 +83,7 @@ export const UserSettingsFeatureFlags = () => { Feature Flags - {' '} - Please refresh the page when toggling feature flags{' '} + Please refresh the page when toggling feature flags {featureFlags.length >= 10 && ( From cad5607411a24315aef399ad9e476b0b18e280f5 Mon Sep 17 00:00:00 2001 From: Damien Vitrac Date: Wed, 11 Jan 2023 16:33:58 +0100 Subject: [PATCH 042/118] fix(techdocs): remove footer overlay on large screen Uses static positions on the child links instead of the container Resolves #15653 Signed-off-by: Damien Vitrac --- .changeset/nervous-apricots-whisper.md | 5 +++++ .../techdocs/src/reader/transformers/styles/rules/layout.ts | 6 ++++++ 2 files changed, 11 insertions(+) create mode 100644 .changeset/nervous-apricots-whisper.md diff --git a/.changeset/nervous-apricots-whisper.md b/.changeset/nervous-apricots-whisper.md new file mode 100644 index 0000000000..ce71486364 --- /dev/null +++ b/.changeset/nervous-apricots-whisper.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Improve view: remove footer overlay on large screen diff --git a/plugins/techdocs/src/reader/transformers/styles/rules/layout.ts b/plugins/techdocs/src/reader/transformers/styles/rules/layout.ts index c4e6938fdc..e6bad41045 100644 --- a/plugins/techdocs/src/reader/transformers/styles/rules/layout.ts +++ b/plugins/techdocs/src/reader/transformers/styles/rules/layout.ts @@ -105,7 +105,13 @@ export default ({ theme, sidebar }: RuleOptions) => ` .md-footer { position: fixed; bottom: 0px; + pointer-events: none; } + +.md-footer-nav__link { + pointer-events: all; +} + .md-footer__title { background-color: unset; } From c4940b6322668292e46b9455db194a6ba259195b Mon Sep 17 00:00:00 2001 From: Peter Macdonald Date: Mon, 23 Jan 2023 14:05:00 +0100 Subject: [PATCH 043/118] Forgot Changeset Signed-off-by: Peter Macdonald --- .changeset/rotten-mayflies-love.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/rotten-mayflies-love.md diff --git a/.changeset/rotten-mayflies-love.md b/.changeset/rotten-mayflies-love.md new file mode 100644 index 0000000000..24d4e4d1be --- /dev/null +++ b/.changeset/rotten-mayflies-love.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-user-settings': patch +--- + +Added a message to advise a page reload when toggling feature flags From 69df5e168ce35ac702e9ab0659304d68b6a353fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 23 Jan 2023 14:23:41 +0100 Subject: [PATCH 044/118] Update docs/backend-system/building-plugins-and-modules/02-testing.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Patrik Oldsberg Signed-off-by: Fredrik Adelöw --- .../backend-system/building-plugins-and-modules/02-testing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/backend-system/building-plugins-and-modules/02-testing.md b/docs/backend-system/building-plugins-and-modules/02-testing.md index 9a2042c4d3..70685e0664 100644 --- a/docs/backend-system/building-plugins-and-modules/02-testing.md +++ b/docs/backend-system/building-plugins-and-modules/02-testing.md @@ -42,8 +42,8 @@ describe('myPlugin', () => { }); ``` -This example shows how to easily access the factories for mock services and -passing options to them, which will override the default mocks. +This example shows how to access the mock service factories and +pass options to them, which will override the default mock services. The returned server also has a `port()` method which returns the dynamically bound listening port. You can use this to perform lower level network From 965b91b97b9bff3cd4dd88b854e6e4d033339e1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 23 Jan 2023 14:34:19 +0100 Subject: [PATCH 045/118] address comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../02-testing.md | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/docs/backend-system/building-plugins-and-modules/02-testing.md b/docs/backend-system/building-plugins-and-modules/02-testing.md index 70685e0664..3b19a7cfc8 100644 --- a/docs/backend-system/building-plugins-and-modules/02-testing.md +++ b/docs/backend-system/building-plugins-and-modules/02-testing.md @@ -9,12 +9,15 @@ description: Learn how to test your backend plugins and modules Utilities for testing backend plugins and modules are available in `@backstage/backend-test-utils`. This section describes those facilities. -## Testing Backend Plugins +## Testing Backend Plugins and Modules -To facilitate testing of backend plugins, the `@backstage/backend-test-utils` -package provides a `startTestBackend` function which starts up an entire backend -harness, complete with a number of mock services. You can then provide overrides -for services whose behavior you need to adjust for the test run. +To facilitate testing of backend plugins and modules, the +`@backstage/backend-test-utils` package provides a `startTestBackend` function +which starts up an entire backend harness, complete with a number of mock +services. You can then provide overrides for services whose behavior you need to +adjust for the test run. The function also accepts a number of _features_ (a +collective term for backend [plugins](../architecture/04-plugins.md) and +[modules](../architecture/06-modules.md)), that are the subjects of the test. The function returns an HTTP server instance which can be used together with e.g. `supertest` to easily test the actual REST service surfaces of plugins who @@ -112,20 +115,20 @@ import { describe('MyDatabaseClass', () => { // Change this to the set of constants that you actually actively intend to - // support. Make sure to create only one TestDatabases instance per file, - // since spinning up "physical" databases to test against is much costlier - // than creating the "logical" databases within them that the individual - // tests use. + // support. This create call must be made inside a describe block. Make sure + // to create only one TestDatabases instance per file, since spinning up + // "physical" databases to test against is much costlier than creating the + // "logical" databases within them that the individual tests use. const databases = TestDatabases.create({ ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], }); // Just an example of how to conveniently bundle up the setup code - async function createSut(databaseId: TestDatabaseId) { + async function createSubject(databaseId: TestDatabaseId) { const knex = await databases.init(databaseId); - const sut = new MyDatabaseClass({ database: knex }); - await sut.runMigrations(); - return { knex, sut }; + const subject = new MyDatabaseClass({ database: knex }); + await subject.runMigrations(); + return { knex, subject }; } describe('foo', () => { @@ -133,11 +136,11 @@ describe('MyDatabaseClass', () => { it.each(databases.eachSupportedId())( 'should run foo on %p', async databaseId => { - const { knex, sut } = await createSut(databaseId); + const { knex, subject } = await createSubject(databaseId); // raw knex is available for underlying manipulation await knex('foo').insert({ value: 2 }); // drive your system under test as usual - await expect(sut.foos()).resolves.toEqual([{ value: 2 }]); + await expect(subject.foos()).resolves.toEqual([{ value: 2 }]); }); }); ``` @@ -147,7 +150,7 @@ you can supply it in the form of a mock instance of `coreServices.database` to your test database. ```ts -const { knex, sut } = await createSut(databaseId); +const { knex, subject } = await createSubject(databaseId); const { server } = await startTestBackend({ features: [myPlugin()], services: [[coreServices.database, { getClient: async () => knex }]], From 135242e78a8bbc0206e4c979ce168638d93c0bc5 Mon Sep 17 00:00:00 2001 From: Alex Crome Date: Mon, 23 Jan 2023 14:07:13 +0000 Subject: [PATCH 046/118] Added missing doc comment Signed-off-by: Alex Crome --- .../src/microsoftGraph/defaultTransformers.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/defaultTransformers.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/defaultTransformers.ts index 9d123505e8..7a8c4ec463 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/defaultTransformers.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/defaultTransformers.ts @@ -24,6 +24,12 @@ import { normalizeEntityName } from './helper'; import { GroupEntity, UserEntity } from '@backstage/catalog-model'; import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; +/** + * The default implementation of the transformation from a graph organization + * entry to a Group entity. + * + * @public + */ export async function defaultOrganizationTransformer( organization: MicrosoftGraph.Organization, ): Promise { From cd63a3d39fdee990db895dc1595e7c2ce93a4f1b Mon Sep 17 00:00:00 2001 From: Paul Cowan Date: Mon, 19 Dec 2022 10:01:55 +0000 Subject: [PATCH 047/118] add layouts to NextScaffolderPage Signed-off-by: Paul Cowan --- packages/app/src/App.tsx | 3 +++ plugins/scaffolder/src/next/Router/Router.tsx | 18 ++++++++++++++++-- .../TemplateWizardPage/TemplateWizardPage.tsx | 2 +- plugins/scaffolder/src/next/types.ts | 5 ++++- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index c5410c4f80..8984a69259 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -244,6 +244,9 @@ const routes = ( + + + } /> ) => { ), ] as NextFieldExtensionOptions[]; + const customLayouts = useElementFilter(outlet, elements => + elements + .selectByComponentData({ + key: LAYOUTS_WRAPPER_KEY, + }) + .findComponentData({ + key: LAYOUTS_KEY, + }), + ); + return ( ) => { } diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx index 24c529b094..99f2577459 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx @@ -34,7 +34,7 @@ import { scaffolderTaskRouteRef, selectedTemplateRouteRef } from '../../routes'; import { Header, Page } from '@backstage/core-components'; import { Workflow } from '@backstage/plugin-scaffolder-react'; -type TemplateWizardPageProps = { +export type TemplateWizardPageProps = { customFieldExtensions: NextFieldExtensionOptions[]; FormProps?: FormProps; }; diff --git a/plugins/scaffolder/src/next/types.ts b/plugins/scaffolder/src/next/types.ts index e9c77aa00e..2700137724 100644 --- a/plugins/scaffolder/src/next/types.ts +++ b/plugins/scaffolder/src/next/types.ts @@ -21,6 +21,7 @@ */ import type { FormProps as SchemaFormProps } from '@rjsf/core-v5'; +import { LayoutOptions } from '../layouts/types'; /** * Any `@rjsf/core` form properties that are publicly exposed to the `NextScaffolderpage` @@ -31,4 +32,6 @@ import type { FormProps as SchemaFormProps } from '@rjsf/core-v5'; export type FormProps = Pick< SchemaFormProps, 'transformErrors' | 'noHtml5Validate' ->; +> & { + layouts?: LayoutOptions[]; +}; From d2ddde21081da2b0396ad0ab9d093c860392e7c8 Mon Sep 17 00:00:00 2001 From: Paul Cowan Date: Fri, 23 Dec 2022 16:36:52 +0000 Subject: [PATCH 048/118] add ScaffolderLayouts to NextScaffolderPage Signed-off-by: Paul Cowan --- .changeset/silly-turkeys-hang.md | 5 +++ .../next/components/Stepper/Stepper.test.tsx | 43 +++++++++++++++++++ .../src/next/components/Stepper/Stepper.tsx | 14 +++--- .../src/next/hooks/useTemplateSchema.test.tsx | 38 ++++++++++++++++ plugins/scaffolder-react/src/next/types.ts | 19 ++++++++ .../src/next/Router/Router.test.tsx | 1 + plugins/scaffolder/src/next/Router/Router.tsx | 8 ++-- plugins/scaffolder/src/next/types.ts | 22 +++++++++- 8 files changed, 138 insertions(+), 12 deletions(-) create mode 100644 .changeset/silly-turkeys-hang.md diff --git a/.changeset/silly-turkeys-hang.md b/.changeset/silly-turkeys-hang.md new file mode 100644 index 0000000000..e2345fb35d --- /dev/null +++ b/.changeset/silly-turkeys-hang.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Add `ScaffolderLayouts` to `NextScaffolderPage` diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx index 5dbce3243f..57ff949c62 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx @@ -392,4 +392,47 @@ describe('Stepper', () => { await fireEvent.click(getByRole('button', { name: 'Make' })); }); }); + + describe('Scaffolder Layouts', () => { + it('should render the step in the scaffolder layout', async () => { + const ScaffolderLayout: LayoutTemplate = ({ properties }) => ( + <> +

A Scaffolder Layout

+ {properties.map((prop, i) => ( +
{prop.content}
+ ))} + + ); + + const manifest: TemplateParameterSchema = { + steps: [ + { + title: 'Step 1', + schema: { + type: 'object', + 'ui:ObjectFieldTemplate': 'Layout', + properties: { + field1: { + type: 'string', + }, + }, + }, + }, + ], + title: 'scaffolder layouts', + }; + + const { getByText, getByRole } = await renderInTestApp( + , + ); + + expect(getByText('A Scaffolder Layout')).toBeInTheDocument(); + expect(getByRole('textbox', { name: 'field1' })).toBeInTheDocument(); + }); + }); }); diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx index 950c9720de..c2c4f06ffd 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx @@ -28,11 +28,14 @@ import React, { useCallback, useMemo, useState, type ReactNode } from 'react'; import { NextFieldExtensionOptions } from '../../extensions'; import { TemplateParameterSchema } from '../../../types'; import { createAsyncValidators } from './createAsyncValidators'; -import type { FormProps } from '../../types'; +import type { FormProps, LayoutOptions } from '../../types'; import { ReviewState, type ReviewStateProps } from '../ReviewState'; import { useTemplateSchema } from '../../hooks/useTemplateSchema'; import { useFormDataFromQuery } from '../../hooks/useFormDataFromQuery'; import validator from '@rjsf/validator-ajv6'; +import { useFormDataFromQuery } from '../../hooks'; +import type { FormProps } from '../../types'; +import { selectedTemplateRouteRef } from '../../../routes'; const useStyles = makeStyles(theme => ({ backButton: { @@ -65,6 +68,7 @@ export type StepperProps = { createButtonText?: ReactNode; reviewButtonText?: ReactNode; }; + layouts?: LayoutOptions[]; }; // TODO(blam): We require here, as the types in this package depend on @rjsf/core explicitly @@ -76,17 +80,15 @@ const Form = withTheme(require('@rjsf/material-ui-v5').Theme); * The `Stepper` component is the Wizard that is rendered when a user selects a template * @alpha */ - export const Stepper = (stepperProps: StepperProps) => { - const { components = {}, ...props } = stepperProps; + const { layouts = [], components = {}, ...props } = stepperProps; const { ReviewStateComponent = ReviewState, createButtonText = 'Create', reviewButtonText = 'Review', } = components; - const analytics = useAnalytics(); - const { steps } = useTemplateSchema(props.manifest); + const { steps } = useTemplateSchema(props.manifest, layouts); const apiHolder = useApiHolder(); const [activeStep, setActiveStep] = useState(0); const [formState, setFormState] = useFormDataFromQuery(props.initialState); @@ -177,7 +179,7 @@ export const Stepper = (stepperProps: StepperProps) => { fields={extensions} showErrorList={false} onChange={handleChange} - {...(props.FormProps ?? {})} + {...(formProps ?? {})} >