From 3574c51b248cceab6c75758a9e20ed926f6ac30f Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Thu, 26 Sep 2024 15:36:34 +0200 Subject: [PATCH 001/237] feat: add level config to catalog-backend-module-bitbucket-cloud plugin Signed-off-by: Benjamin Janssens --- .../config.d.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/plugins/catalog-backend-module-bitbucket-cloud/config.d.ts b/plugins/catalog-backend-module-bitbucket-cloud/config.d.ts index 1051976b24..6ba4cb135d 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/config.d.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/config.d.ts @@ -59,6 +59,14 @@ export interface Config { * (Optional) TaskScheduleDefinition for the discovery. */ schedule?: SchedulerServiceTaskScheduleDefinitionConfig; + /** + * (Optional) On what level discovery should take place, affecting Bitbucket Cloud API limits. + * + * Possible values: + * - `workspace` (default): 1 API call per workspace, limited to 900 repositories per workspace. + * - `project`: 1 API call per project, limited to 900 repositories per project. + */ + level?: 'workspace' | 'project'; } | { [name: string]: { @@ -92,6 +100,14 @@ export interface Config { * (Optional) TaskScheduleDefinition for the discovery. */ schedule?: SchedulerServiceTaskScheduleDefinitionConfig; + /** + * (Optional) On what level discovery should take place, affecting Bitbucket Cloud API limits. + * + * Possible values: + * - `workspace` (default): 1 API call per workspace, limited to 900 repositories per workspace. + * - `project`: 1 API call per project, limited to 900 repositories per project. + */ + level?: 'workspace' | 'project'; }; }; }; From 8dbc4ab48246580d2d0ceefa587b5deede7e8507 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Thu, 26 Sep 2024 16:03:22 +0200 Subject: [PATCH 002/237] chore: add level config to BitbucketCloudEntityProviderConfig Signed-off-by: Benjamin Janssens --- .../src/providers/BitbucketCloudEntityProviderConfig.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.ts index 5a806d2e0b..1326c53e29 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.ts @@ -32,6 +32,7 @@ export type BitbucketCloudEntityProviderConfig = { repoSlug?: RegExp; }; schedule?: SchedulerServiceTaskScheduleDefinition; + level?: 'workspace' | 'project'; }; export function readProviderConfigs( From a1219e94cb2eb103e8cbed663a17fe0f4f37e487 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 27 Sep 2024 11:11:12 +0200 Subject: [PATCH 003/237] feat: implement project-level Bitbucket Cloud discovery Signed-off-by: Benjamin Janssens --- .../providers/BitbucketCloudEntityProvider.ts | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts index 6b19bea4f2..6826804053 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts @@ -207,7 +207,7 @@ export class BitbucketCloudEntityProvider implements EntityProvider { logger.info('Discovering catalog files in Bitbucket Cloud repositories'); - const targets = await this.findCatalogFiles(); + const targets = await this.findCatalogFiles(this.config.level); const entities = this.toDeferredEntities(targets); await this.connection.applyMutation({ @@ -271,7 +271,7 @@ export class BitbucketCloudEntityProvider implements EntityProvider { // Hence, we will just trigger a refresh for catalog file(s) within the repository // if we get notified about changes there. - const targets = await this.findCatalogFiles(repoSlug); + const targets = await this.findCatalogFiles('workspace', repoSlug); const { token } = await this.tokenManager!.getToken(); const existing = await this.findExistingLocations(repoUrl, token); @@ -334,6 +334,7 @@ export class BitbucketCloudEntityProvider implements EntityProvider { } private async findCatalogFiles( + level: 'workspace' | 'project' = 'workspace', repoSlug?: string, ): Promise { const workspace = this.config.workspace; @@ -343,6 +344,32 @@ export class BitbucketCloudEntityProvider implements EntityProvider { catalogPath.lastIndexOf('/') + 1, ); + const optRepoFilter = repoSlug ? ` repo:${repoSlug}` : ''; + const query = `"${catalogFilename}" path:${catalogPath}${optRepoFilter}`; + + if (level === 'project') { + const projects = this.client + .listProjectsByWorkspace(workspace) + .iterateResults(); + + const results: IngestionTarget[] = []; + + for await (const project of projects) { + const projectQuery = `${query} project:${project.key}`; + const result = await this.processQuery(workspace, projectQuery); + results.push(...result); + } + + return results; + } + + return this.processQuery(workspace, query); + } + + private async processQuery( + workspace: string, + query: string, + ): Promise { // load all fields relevant for creating refs later, but not more const fields = [ // exclude code/content match details @@ -358,8 +385,7 @@ export class BitbucketCloudEntityProvider implements EntityProvider { // ...except the one we need '+values.file.commit.repository.links.html.href', ].join(','); - const optRepoFilter = repoSlug ? ` repo:${repoSlug}` : ''; - const query = `"${catalogFilename}" path:${catalogPath}${optRepoFilter}`; + const searchResults = this.client .searchCode(workspace, query, { fields }) .iterateResults(); From bcb31c766cf689e0d8806f5154dd7729b461f5a2 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 27 Sep 2024 12:09:00 +0200 Subject: [PATCH 004/237] test: add tests; fix config Signed-off-by: Benjamin Janssens --- .../BitbucketCloudEntityProvider.test.ts | 192 ++++++++++++++++++ .../providers/BitbucketCloudEntityProvider.ts | 2 +- .../BitbucketCloudEntityProviderConfig.ts | 9 +- 3 files changed, 201 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts index 262582b99a..f95cb34025 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts @@ -87,6 +87,23 @@ describe('BitbucketCloudEntityProvider', () => { }, }, }); + const projectLevelConfig = new ConfigReader({ + catalog: { + providers: { + bitbucketCloud: { + myProvider: { + workspace: 'test-ws', + catalogPath: 'catalog-custom.yaml', + filters: { + projectKey: 'test-.*', + repoSlug: 'test-.*', + }, + level: 'project', + }, + }, + }, + }, + }); const schedule = new PersistingTaskRunner(); const entityProviderConnection: EntityProviderConnection = { applyMutation: jest.fn(), @@ -419,6 +436,181 @@ describe('BitbucketCloudEntityProvider', () => { }); }); + it('apply full update on scheduled execution on project-level', async () => { + const provider = BitbucketCloudEntityProvider.fromConfig( + projectLevelConfig, + { + logger, + schedule, + }, + )[0]; + expect(provider.getProviderName()).toEqual( + 'bitbucketCloud-provider:myProvider', + ); + + server.use( + rest.get( + `https://api.bitbucket.org/2.0/workspaces/test-ws/projects`, + (_req, res, ctx) => { + const response = { + values: [ + { + key: 'TEST', + }, + ], + }; + return res(ctx.json(response)); + }, + ), + rest.get( + `https://api.bitbucket.org/2.0/workspaces/test-ws/search/code`, + (req, res, ctx) => { + const query = req.url.searchParams.get('search_query'); + if (!query || !query.includes('project:TEST')) { + return res(ctx.json({ values: [] })); + } + + const response = { + values: [ + { + // skipped as empty + path_matches: [], + file: { + type: 'commit_file', + path: 'path/to/ignored/file', + }, + }, + { + path_matches: [ + { + match: true, + text: 'catalog-custom.yaml', + }, + ], + file: { + type: 'commit_file', + path: 'custom/path/catalog-custom.yaml', + commit: { + repository: { + // skipped as no match with filter + slug: 'repo', + project: { + key: 'test-project', + }, + mainbranch: { + name: 'main', + }, + links: { + html: { + href: 'https://bitbucket.org/test-ws/repo', + }, + }, + }, + }, + }, + }, + { + path_matches: [ + { + match: true, + text: 'catalog-custom.yaml', + }, + ], + file: { + type: 'commit_file', + path: 'custom/path/catalog-custom.yaml', + commit: { + repository: { + slug: 'test-repo1', + project: { + // skipped as no match with filter + key: 'project', + }, + mainbranch: { + name: 'main', + }, + links: { + html: { + href: 'https://bitbucket.org/test-ws/test-repo1', + }, + }, + }, + }, + }, + }, + { + path_matches: [ + { + match: true, + text: 'catalog-custom.yaml', + }, + ], + file: { + type: 'commit_file', + path: 'custom/path/catalog-custom.yaml', + commit: { + repository: { + slug: 'test-repo2', + project: { + key: 'test-project', + }, + mainbranch: { + name: 'main', + }, + links: { + html: { + href: 'https://bitbucket.org/test-ws/test-repo2', + }, + }, + }, + }, + }, + }, + ], + }; + return res(ctx.json(response)); + }, + ), + ); + + await provider.connect(entityProviderConnection); + + const taskDef = schedule.getTasks()[0]; + expect(taskDef.id).toEqual('bitbucketCloud-provider:myProvider:refresh'); + await (taskDef.fn as () => Promise)(); + + const url = `https://bitbucket.org/test-ws/test-repo2/src/main/custom/path/catalog-custom.yaml`; + const expectedEntities = [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Location', + metadata: { + annotations: { + 'backstage.io/managed-by-location': `url:${url}`, + 'backstage.io/managed-by-origin-location': `url:${url}`, + 'bitbucket.org/repo-url': + 'https://bitbucket.org/test-ws/test-repo2', + }, + name: 'generated-7c2e6263b6cc2d14e69fd4d029afba601ad6dc3b', + }, + spec: { + presence: 'required', + target: `${url}`, + type: 'url', + }, + }, + locationKey: 'bitbucketCloud-provider:myProvider', + }, + ]; + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: expectedEntities, + }); + }); + it('update onRepoPush', async () => { const keptModule = createLocationEntity( 'https://bitbucket.org/test-ws/test-repo', diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts index 6826804053..aac5056687 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts @@ -334,7 +334,7 @@ export class BitbucketCloudEntityProvider implements EntityProvider { } private async findCatalogFiles( - level: 'workspace' | 'project' = 'workspace', + level: 'workspace' | 'project', repoSlug?: string, ): Promise { const workspace = this.config.workspace; diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.ts index 1326c53e29..12f8db1c9c 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.ts @@ -32,7 +32,7 @@ export type BitbucketCloudEntityProviderConfig = { repoSlug?: RegExp; }; schedule?: SchedulerServiceTaskScheduleDefinition; - level?: 'workspace' | 'project'; + level: 'workspace' | 'project'; }; export function readProviderConfigs( @@ -73,6 +73,12 @@ function readProviderConfig( ) : undefined; + const level = + (config.getOptionalString('level') as + | 'workspace' + | 'project' + | undefined) ?? 'workspace'; + return { id, catalogPath, @@ -84,6 +90,7 @@ function readProviderConfig( repoSlug: repoSlugPattern ? compileRegExp(repoSlugPattern) : undefined, }, schedule, + level, }; } From 51fdc5ebf502de39a4ec367b9bd3b8a55cc98c34 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 27 Sep 2024 12:12:17 +0200 Subject: [PATCH 005/237] test: add config tests Signed-off-by: Benjamin Janssens --- ...BitbucketCloudEntityProviderConfig.test.ts | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.test.ts index 7f13616586..0491971ea9 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.test.ts @@ -77,13 +77,17 @@ describe('readProviderConfigs', () => { }, }, }, + providerWithProjectLevel: { + workspace: 'test-ws6', + level: 'project', + }, }, }, }, }); const providerConfigs = readProviderConfigs(config); - expect(providerConfigs).toHaveLength(5); + expect(providerConfigs).toHaveLength(6); expect(providerConfigs[0]).toEqual({ id: 'providerWorkspaceOnly', workspace: 'test-ws1', @@ -92,6 +96,7 @@ describe('readProviderConfigs', () => { projectKey: undefined, repoSlug: undefined, }, + level: 'workspace', }); expect(providerConfigs[1]).toEqual({ id: 'providerCustomCatalogPath', @@ -101,6 +106,7 @@ describe('readProviderConfigs', () => { projectKey: undefined, repoSlug: undefined, }, + level: 'workspace', }); expect(providerConfigs[2]).toEqual({ id: 'providerWithProjectKeyFilter', @@ -110,6 +116,7 @@ describe('readProviderConfigs', () => { projectKey: /^projectKey.*filter$/, repoSlug: undefined, }, + level: 'workspace', }); expect(providerConfigs[3]).toEqual({ id: 'providerWithRepoSlugFilter', @@ -119,6 +126,7 @@ describe('readProviderConfigs', () => { projectKey: undefined, repoSlug: /^repoSlug.*filter$/, }, + level: 'workspace', }); expect(providerConfigs[4]).toEqual({ id: 'providerWithSchedule', @@ -134,6 +142,17 @@ describe('readProviderConfigs', () => { minutes: 3, }, }, + level: 'workspace', + }); + expect(providerConfigs[5]).toEqual({ + id: 'providerWithProjectLevel', + workspace: 'test-ws6', + catalogPath: '/catalog-info.yaml', + filters: { + projectKey: undefined, + repoSlug: undefined, + }, + level: 'project', }); }); }); From e07d64016acef6f12427e6e4aebec6117d8b2719 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 27 Sep 2024 13:37:18 +0200 Subject: [PATCH 006/237] docs: add docs Signed-off-by: Benjamin Janssens --- docs/integrations/bitbucketCloud/discovery.md | 5 +++++ plugins/catalog-backend-module-bitbucket-cloud/config.d.ts | 4 ++-- .../src/providers/BitbucketCloudEntityProvider.test.ts | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/integrations/bitbucketCloud/discovery.md b/docs/integrations/bitbucketCloud/discovery.md index 6ba9c0aa1f..c533f61a2a 100644 --- a/docs/integrations/bitbucketCloud/discovery.md +++ b/docs/integrations/bitbucketCloud/discovery.md @@ -152,6 +152,7 @@ catalog: # supports ISO duration, "human duration" as used in code timeout: { minutes: 3 } workspace: workspace-name + level: workspace # default value ``` > **Note:** It is possible but certainly not recommended to skip the provider ID level. @@ -180,3 +181,7 @@ catalog: - **`workspace`**: Name of your organization account/workspace. If you want to add multiple workspaces, you need to add one provider config each. +- **`level`** _(optional)_: + `'workspace'` (default) or `'project'`. At what level discovery should take place, affecting Bitbucket Cloud API limits. + +> **Note:** By default, discovery will take place at the `workspace` level. While being the most efficient in terms of API calls to Bitbucket Cloud, discovery at the workspace level is limited to 900 repositories per workspace. If your workspace consists of more than 900 repositories, you should switch to discovery at the `project` level, shifting the limit to 900 repositories per project. diff --git a/plugins/catalog-backend-module-bitbucket-cloud/config.d.ts b/plugins/catalog-backend-module-bitbucket-cloud/config.d.ts index 6ba4cb135d..a56d3cd87a 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/config.d.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/config.d.ts @@ -60,7 +60,7 @@ export interface Config { */ schedule?: SchedulerServiceTaskScheduleDefinitionConfig; /** - * (Optional) On what level discovery should take place, affecting Bitbucket Cloud API limits. + * (Optional) At what level discovery should take place, affecting Bitbucket Cloud API limits. * * Possible values: * - `workspace` (default): 1 API call per workspace, limited to 900 repositories per workspace. @@ -101,7 +101,7 @@ export interface Config { */ schedule?: SchedulerServiceTaskScheduleDefinitionConfig; /** - * (Optional) On what level discovery should take place, affecting Bitbucket Cloud API limits. + * (Optional) At what level discovery should take place, affecting Bitbucket Cloud API limits. * * Possible values: * - `workspace` (default): 1 API call per workspace, limited to 900 repositories per workspace. diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts index f95cb34025..32e8d41551 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts @@ -436,7 +436,7 @@ describe('BitbucketCloudEntityProvider', () => { }); }); - it('apply full update on scheduled execution on project-level', async () => { + it('apply full update on scheduled execution on project level', async () => { const provider = BitbucketCloudEntityProvider.fromConfig( projectLevelConfig, { From f6b4b8a55bc803fbb8f0cecc6ed3d9785268e127 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 27 Sep 2024 13:39:53 +0200 Subject: [PATCH 007/237] chore: add changeset Signed-off-by: Benjamin Janssens --- .changeset/wise-snakes-sleep.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/wise-snakes-sleep.md diff --git a/.changeset/wise-snakes-sleep.md b/.changeset/wise-snakes-sleep.md new file mode 100644 index 0000000000..f0fa018c9a --- /dev/null +++ b/.changeset/wise-snakes-sleep.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch +--- + +Added discovery level configuration to shift Bitbucket Cloud API limits From f61d4ccc2fe2af4cc8e1bc75cae3c59a6b15ca2f Mon Sep 17 00:00:00 2001 From: Stephen Glass Date: Wed, 2 Oct 2024 23:13:55 -0400 Subject: [PATCH 008/237] add scaffolder permission for template management Signed-off-by: Stephen Glass --- .changeset/lemon-gifts-crash.md | 7 +++ plugins/scaffolder-common/report-alpha.api.md | 6 +++ plugins/scaffolder-common/src/permissions.ts | 17 +++++++ .../ScaffolderPageContextMenu.tsx | 8 ++- .../ActionsPage/ActionsPage.test.tsx | 7 ++- .../ListTasksPage/ListTaskPage.test.tsx | 6 +++ .../src/components/Router/Router.tsx | 50 +++++++++++-------- 7 files changed, 79 insertions(+), 22 deletions(-) create mode 100644 .changeset/lemon-gifts-crash.md diff --git a/.changeset/lemon-gifts-crash.md b/.changeset/lemon-gifts-crash.md new file mode 100644 index 0000000000..162b0b3817 --- /dev/null +++ b/.changeset/lemon-gifts-crash.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-scaffolder-common': patch +'@backstage/plugin-scaffolder-react': patch +'@backstage/plugin-scaffolder': patch +--- + +Add scaffolder permission for accessing the template management features diff --git a/plugins/scaffolder-common/report-alpha.api.md b/plugins/scaffolder-common/report-alpha.api.md index 36b6a7cdb0..47ca033731 100644 --- a/plugins/scaffolder-common/report-alpha.api.md +++ b/plugins/scaffolder-common/report-alpha.api.md @@ -18,6 +18,9 @@ export const RESOURCE_TYPE_SCAFFOLDER_TEMPLATE = 'scaffolder-template'; // @alpha export const scaffolderActionPermissions: ResourcePermission<'scaffolder-action'>[]; +// @alpha +export const scaffolderManagementPermissions: BasicPermission[]; + // @alpha export const scaffolderPermissions: ( | BasicPermission @@ -40,6 +43,9 @@ export const taskCreatePermission: BasicPermission; // @alpha export const taskReadPermission: BasicPermission; +// @alpha +export const templateManagementPermission: BasicPermission; + // @alpha export const templateParameterReadPermission: ResourcePermission<'scaffolder-template'>; diff --git a/plugins/scaffolder-common/src/permissions.ts b/plugins/scaffolder-common/src/permissions.ts index c441b48d5d..a81952a568 100644 --- a/plugins/scaffolder-common/src/permissions.ts +++ b/plugins/scaffolder-common/src/permissions.ts @@ -113,6 +113,16 @@ export const taskCancelPermission = createPermission({ attributes: {}, }); +/** + * This permission is used to authorize template management features. + * + * @alpha + */ +export const templateManagementPermission = createPermission({ + name: 'scaffolder.template.management', + attributes: {}, +}); + /** * List of the scaffolder permissions that are associated with template steps and parameters. * @alpha @@ -138,6 +148,12 @@ export const scaffolderTaskPermissions = [ taskReadPermission, ]; +/** + * List of the scaffolder permissions that are associated with scaffolder management. + * @alpha + */ +export const scaffolderManagementPermissions = [templateManagementPermission]; + /** * List of all the scaffolder permissions * @alpha @@ -146,4 +162,5 @@ export const scaffolderPermissions = [ ...scaffolderTemplatePermissions, ...scaffolderActionPermissions, ...scaffolderTaskPermissions, + ...scaffolderManagementPermissions, ]; diff --git a/plugins/scaffolder-react/src/next/components/ScaffolderPageContextMenu/ScaffolderPageContextMenu.tsx b/plugins/scaffolder-react/src/next/components/ScaffolderPageContextMenu/ScaffolderPageContextMenu.tsx index 12ca4135f0..bbd3ae018e 100644 --- a/plugins/scaffolder-react/src/next/components/ScaffolderPageContextMenu/ScaffolderPageContextMenu.tsx +++ b/plugins/scaffolder-react/src/next/components/ScaffolderPageContextMenu/ScaffolderPageContextMenu.tsx @@ -27,6 +27,8 @@ import Edit from '@material-ui/icons/Edit'; import List from '@material-ui/icons/List'; import MoreVert from '@material-ui/icons/MoreVert'; import React, { useState } from 'react'; +import { usePermission } from '@backstage/plugin-permission-react'; +import { templateManagementPermission } from '@backstage/plugin-scaffolder-common/alpha'; const useStyles = makeStyles(theme => ({ button: { @@ -55,6 +57,10 @@ export function ScaffolderPageContextMenu( const classes = useStyles(); const [anchorEl, setAnchorEl] = useState(); + const { allowed: canManageTemplates } = usePermission({ + permission: templateManagementPermission, + }); + if (!onEditorClicked && !onActionsClicked) { return null; } @@ -100,7 +106,7 @@ export function ScaffolderPageContextMenu( )} - {onEditorClicked && ( + {onEditorClicked && canManageTemplates && ( diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx index bf056dd3b3..acfdabac5b 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx @@ -23,6 +23,7 @@ import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import { ApiProvider } from '@backstage/core-app-api'; import { rootRouteRef } from '../../routes'; import { userEvent } from '@testing-library/user-event'; +import { permissionApiRef } from '@backstage/plugin-permission-react'; const scaffolderApiMock: jest.Mocked = { scaffold: jest.fn(), @@ -36,7 +37,11 @@ const scaffolderApiMock: jest.Mocked = { autocomplete: jest.fn(), }; -const apis = TestApiRegistry.from([scaffolderApiRef, scaffolderApiMock]); +const mockPermissionApi = { authorize: jest.fn() }; +const apis = TestApiRegistry.from( + [scaffolderApiRef, scaffolderApiMock], + [permissionApiRef, mockPermissionApi], +); describe('TemplatePage', () => { beforeEach(() => jest.resetAllMocks()); diff --git a/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx b/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx index f7f75bc3e8..ad3ca7fbde 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx @@ -30,6 +30,7 @@ import { } from '@backstage/plugin-scaffolder-react'; import { act, fireEvent } from '@testing-library/react'; import { rootRouteRef } from '../../routes'; +import { permissionApiRef } from '@backstage/plugin-permission-react'; describe('', () => { const catalogApi: jest.Mocked = { @@ -49,6 +50,8 @@ describe('', () => { listTasks: jest.fn(), } as any; + const mockPermissionApi = { authorize: jest.fn() }; + it('should render the page', async () => { const entity: Entity = { apiVersion: 'v1', @@ -72,6 +75,7 @@ describe('', () => { [catalogApiRef, catalogApi], [identityApiRef, identityApi], [scaffolderApiRef, scaffolderApiMock], + [permissionApiRef, mockPermissionApi], ]} > @@ -132,6 +136,7 @@ describe('', () => { [catalogApiRef, catalogApi], [identityApiRef, identityApi], [scaffolderApiRef, scaffolderApiMock], + [permissionApiRef, mockPermissionApi], ]} > @@ -230,6 +235,7 @@ describe('', () => { [catalogApiRef, catalogApi], [identityApiRef, identityApi], [scaffolderApiRef, scaffolderApiMock], + [permissionApiRef, mockPermissionApi], ]} > diff --git a/plugins/scaffolder/src/components/Router/Router.tsx b/plugins/scaffolder/src/components/Router/Router.tsx index 5766e3d043..4f578d9ff2 100644 --- a/plugins/scaffolder/src/components/Router/Router.tsx +++ b/plugins/scaffolder/src/components/Router/Router.tsx @@ -59,6 +59,8 @@ import { TemplateEditorPage, CustomFieldsPage, } from '../../alpha/components/TemplateEditorPage'; +import { RequirePermission } from '@backstage/plugin-permission-react'; +import { templateManagementPermission } from '@backstage/plugin-scaffolder-common/alpha'; /** * The Props for the Scaffolder Router @@ -170,29 +172,35 @@ export const Router = (props: PropsWithChildren) => { - - + + + + + } /> - - + + + + + } /> - - + + + + + } /> @@ -204,13 +212,15 @@ export const Router = (props: PropsWithChildren) => { - - + + + + + } /> Date: Mon, 7 Oct 2024 16:57:21 -0400 Subject: [PATCH 009/237] Add region param to the getDefaultCredentialsChain Signed-off-by: KaemonIsland --- .../src/DefaultAwsCredentialsManager.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.ts b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.ts index 12e1e2dffd..2b7ae3e1e5 100644 --- a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.ts +++ b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.ts @@ -77,8 +77,15 @@ function getProfileCredentials( }); } -function getDefaultCredentialsChain(): AwsCredentialIdentityProvider { - return fromNodeProviderChain(); +/** + * Include the region if present, otherwise use the default region + * + * @see https://www.npmjs.com/package/@aws-sdk/credential-provider-node + */ +function getDefaultCredentialsChain( + region = 'us-east-1', +): AwsCredentialIdentityProvider { + return fromNodeProviderChain({ clientConfig: { region } }); } /** @@ -123,7 +130,7 @@ function getSdkCredentialProvider( return getProfileCredentials(config.profile!, config.region); } - return getDefaultCredentialsChain(); + return getDefaultCredentialsChain(config.region); } /** @@ -145,7 +152,7 @@ function getMainAccountSdkCredentialProvider( return getProfileCredentials(config.profile!, config.region); } - return getDefaultCredentialsChain(); + return getDefaultCredentialsChain(config.region); } /** From 9e3e04d231e3d6c5f97704889da600fcba160d2a Mon Sep 17 00:00:00 2001 From: KaemonIsland Date: Tue, 8 Oct 2024 12:50:55 -0400 Subject: [PATCH 010/237] Add unit tests Signed-off-by: KaemonIsland --- .../src/DefaultAwsCredentialsManager.test.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts index 237428c3e9..ed61f45199 100644 --- a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts +++ b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts @@ -24,12 +24,20 @@ import { } from '@aws-sdk/client-sts'; import { Config, ConfigReader } from '@backstage/config'; import { promises } from 'fs'; +import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; const env = process.env; let stsMock: AwsClientStub; let config: Config; jest.mock('fs', () => ({ promises: { readFile: jest.fn() } })); +jest.mock('@aws-sdk/credential-providers', () => { + const originalModule = jest.requireActual('@aws-sdk/credential-providers'); + return { + ...originalModule, + fromNodeProviderChain: jest.fn(), + }; +}); describe('DefaultAwsCredentialsManager', () => { beforeEach(() => { @@ -134,6 +142,16 @@ describe('DefaultAwsCredentialsManager', () => { '2022-01-10', ).toISOString(); + // Return creds from env + (fromNodeProviderChain as jest.Mock).mockReturnValue(() => + Promise.resolve({ + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + sessionToken: process.env.AWS_SESSION_TOKEN, + expiration: new Date(process.env.AWS_CREDENTIAL_EXPIRATION), + }), + ); + const mockProfile = `[my-profile] aws_access_key_id=ACCESS_KEY_ID_9 aws_secret_access_key=SECRET_ACCESS_KEY_9 @@ -431,5 +449,49 @@ describe('DefaultAwsCredentialsManager', () => { provider.getCredentialProvider({ accountId: '123456789012' }), ).rejects.toThrow(/No credentials found/); }); + + it('passes the region to getDefaultCredentialsChain', async () => { + const region = 'us-west-2'; + const configWithRegion = new ConfigReader({ + aws: { + mainAccount: { + region, + }, + }, + }); + + const provider = + DefaultAwsCredentialsManager.fromConfig(configWithRegion); + const awsCredentialProvider = await provider.getCredentialProvider(); + + // Trigger the call to fromNodeProviderChain + await awsCredentialProvider.sdkCredentialProvider(); + + expect(fromNodeProviderChain).toHaveBeenCalledWith({ + clientConfig: { + region, + }, + }); + }); + + it('uses default region when none is specified', async () => { + const configWithoutRegion = new ConfigReader({ + aws: { + mainAccount: {}, + }, + }); + + const provider = + DefaultAwsCredentialsManager.fromConfig(configWithoutRegion); + const awsCredentialProvider = await provider.getCredentialProvider(); + + await awsCredentialProvider.sdkCredentialProvider(); + + expect(fromNodeProviderChain).toHaveBeenCalledWith({ + clientConfig: { + region: 'us-east-1', + }, + }); + }); }); }); From 52ae92d52532da504e043572a7c21d6faf754a18 Mon Sep 17 00:00:00 2001 From: KaemonIsland Date: Tue, 8 Oct 2024 13:18:26 -0400 Subject: [PATCH 011/237] Add changeset Signed-off-by: KaemonIsland --- .changeset/friendly-hats-push.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/friendly-hats-push.md diff --git a/.changeset/friendly-hats-push.md b/.changeset/friendly-hats-push.md new file mode 100644 index 0000000000..e730fcbf8c --- /dev/null +++ b/.changeset/friendly-hats-push.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration-aws-node': patch +--- + +The `getDefaultCredentialsChain` function now accepts and applies a `region` parameter, preventing it from defaulting to `us-east-1` when no region is specified. From aa21f6ef6c809544872220b906ed97380642c997 Mon Sep 17 00:00:00 2001 From: KaemonIsland Date: Tue, 8 Oct 2024 13:44:15 -0400 Subject: [PATCH 012/237] Update unit tests Signed-off-by: KaemonIsland --- .../src/DefaultAwsCredentialsManager.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts index ed61f45199..4f57cb2db1 100644 --- a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts +++ b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts @@ -135,12 +135,12 @@ describe('DefaultAwsCredentialsManager', () => { }, }); + const testDate = new Date('2022-01-10'); + process.env.AWS_ACCESS_KEY_ID = 'ACCESS_KEY_ID_10'; process.env.AWS_SECRET_ACCESS_KEY = 'SECRET_ACCESS_KEY_10'; process.env.AWS_SESSION_TOKEN = 'SESSION_TOKEN_10'; - process.env.AWS_CREDENTIAL_EXPIRATION = new Date( - '2022-01-10', - ).toISOString(); + process.env.AWS_CREDENTIAL_EXPIRATION = testDate.toISOString(); // Return creds from env (fromNodeProviderChain as jest.Mock).mockReturnValue(() => @@ -148,7 +148,7 @@ describe('DefaultAwsCredentialsManager', () => { accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, sessionToken: process.env.AWS_SESSION_TOKEN, - expiration: new Date(process.env.AWS_CREDENTIAL_EXPIRATION), + expiration: testDate, }), ); From 15a6a960de819093655dcc73610c57afb1c9bb6f Mon Sep 17 00:00:00 2001 From: Stephen Glass Date: Mon, 14 Oct 2024 14:30:35 -0400 Subject: [PATCH 013/237] add template management permission check to dry run endpoint Signed-off-by: Stephen Glass --- .changeset/lemon-gifts-crash.md | 3 ++- plugins/scaffolder-backend/src/service/router.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.changeset/lemon-gifts-crash.md b/.changeset/lemon-gifts-crash.md index 162b0b3817..6c791ee86f 100644 --- a/.changeset/lemon-gifts-crash.md +++ b/.changeset/lemon-gifts-crash.md @@ -1,7 +1,8 @@ --- '@backstage/plugin-scaffolder-common': patch '@backstage/plugin-scaffolder-react': patch +'@backstage/plugin-scaffolder-backend' '@backstage/plugin-scaffolder': patch --- -Add scaffolder permission for accessing the template management features +Add scaffolder permission `scaffolder.template.management` for accessing the template management features diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 80406496d2..c404549a3f 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -46,6 +46,7 @@ import { taskCancelPermission, taskCreatePermission, taskReadPermission, + templateManagementPermission, templateParameterReadPermission, templateStepReadPermission, } from '@backstage/plugin-scaffolder-common/alpha'; @@ -761,7 +762,7 @@ export async function createRouter( const credentials = await httpAuth.credentials(req); await checkPermission({ credentials, - permissions: [taskCreatePermission], + permissions: [taskCreatePermission, templateManagementPermission], permissionService: permissions, }); From a3b10fa12e99b9b3e808f7f66ce779f400571107 Mon Sep 17 00:00:00 2001 From: Stephen Glass Date: Mon, 14 Oct 2024 14:35:42 -0400 Subject: [PATCH 014/237] fix changeset Signed-off-by: Stephen Glass --- .changeset/lemon-gifts-crash.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/lemon-gifts-crash.md b/.changeset/lemon-gifts-crash.md index 6c791ee86f..cd473f6c58 100644 --- a/.changeset/lemon-gifts-crash.md +++ b/.changeset/lemon-gifts-crash.md @@ -1,7 +1,7 @@ --- '@backstage/plugin-scaffolder-common': patch '@backstage/plugin-scaffolder-react': patch -'@backstage/plugin-scaffolder-backend' +'@backstage/plugin-scaffolder-backend': patch '@backstage/plugin-scaffolder': patch --- From 45221d9ea3e566f19da69832da0d0de25a31c515 Mon Sep 17 00:00:00 2001 From: KaemonIsland Date: Mon, 14 Oct 2024 15:21:08 -0400 Subject: [PATCH 015/237] Update unit tests for correct mocking Signed-off-by: KaemonIsland --- .../src/DefaultAwsCredentialsManager.test.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts index 4f57cb2db1..d81f717a58 100644 --- a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts +++ b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts @@ -143,13 +143,8 @@ describe('DefaultAwsCredentialsManager', () => { process.env.AWS_CREDENTIAL_EXPIRATION = testDate.toISOString(); // Return creds from env - (fromNodeProviderChain as jest.Mock).mockReturnValue(() => - Promise.resolve({ - accessKeyId: process.env.AWS_ACCESS_KEY_ID, - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, - sessionToken: process.env.AWS_SESSION_TOKEN, - expiration: testDate, - }), + (fromNodeProviderChain as jest.Mock).mockImplementation( + jest.requireActual('@aws-sdk/credential-providers').fromNodeProviderChain, ); const mockProfile = `[my-profile] From d583c32a21c525fc9f0af4afbfebf0a9475bde62 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 15 Oct 2024 14:58:01 +0200 Subject: [PATCH 016/237] fix: use concat instead of spread operator Signed-off-by: Benjamin Janssens --- .../src/providers/BitbucketCloudEntityProvider.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts index aac5056687..b1a8ac9234 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts @@ -352,12 +352,12 @@ export class BitbucketCloudEntityProvider implements EntityProvider { .listProjectsByWorkspace(workspace) .iterateResults(); - const results: IngestionTarget[] = []; + let results: IngestionTarget[] = []; for await (const project of projects) { const projectQuery = `${query} project:${project.key}`; const result = await this.processQuery(workspace, projectQuery); - results.push(...result); + results = results.concat(result); } return results; From a8ba32421770740252cafaf2cb8464c5a04672c0 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 15 Oct 2024 15:14:41 +0200 Subject: [PATCH 017/237] style: reorder config property Signed-off-by: Benjamin Janssens --- docs/integrations/bitbucketCloud/discovery.md | 6 +++--- .../config.d.ts | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/integrations/bitbucketCloud/discovery.md b/docs/integrations/bitbucketCloud/discovery.md index c533f61a2a..d623a5d47c 100644 --- a/docs/integrations/bitbucketCloud/discovery.md +++ b/docs/integrations/bitbucketCloud/discovery.md @@ -146,13 +146,13 @@ catalog: filters: # optional projectKey: '^apis-.*$' # optional; RegExp repoSlug: '^service-.*$' # optional; RegExp + level: workspace # default value schedule: # same options as in SchedulerServiceTaskScheduleDefinition # supports cron, ISO duration, "human duration" as used in code frequency: { minutes: 30 } # supports ISO duration, "human duration" as used in code timeout: { minutes: 3 } workspace: workspace-name - level: workspace # default value ``` > **Note:** It is possible but certainly not recommended to skip the provider ID level. @@ -169,6 +169,8 @@ catalog: Regular expression used to filter results based on the project key. - **`repoSlug`** _(optional)_: Regular expression used to filter results based on the repo slug. +- **`level`** _(optional)_: + `'workspace'` (default) or `'project'`. At what level discovery should take place, affecting Bitbucket Cloud API limits. - **`schedule`**: - **`frequency`**: How often you want the task to run. The system does its best to avoid overlapping invocations. @@ -181,7 +183,5 @@ catalog: - **`workspace`**: Name of your organization account/workspace. If you want to add multiple workspaces, you need to add one provider config each. -- **`level`** _(optional)_: - `'workspace'` (default) or `'project'`. At what level discovery should take place, affecting Bitbucket Cloud API limits. > **Note:** By default, discovery will take place at the `workspace` level. While being the most efficient in terms of API calls to Bitbucket Cloud, discovery at the workspace level is limited to 900 repositories per workspace. If your workspace consists of more than 900 repositories, you should switch to discovery at the `project` level, shifting the limit to 900 repositories per project. diff --git a/plugins/catalog-backend-module-bitbucket-cloud/config.d.ts b/plugins/catalog-backend-module-bitbucket-cloud/config.d.ts index a56d3cd87a..d216efbd8c 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/config.d.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/config.d.ts @@ -55,10 +55,6 @@ export interface Config { */ projectKey?: string; }; - /** - * (Optional) TaskScheduleDefinition for the discovery. - */ - schedule?: SchedulerServiceTaskScheduleDefinitionConfig; /** * (Optional) At what level discovery should take place, affecting Bitbucket Cloud API limits. * @@ -67,6 +63,10 @@ export interface Config { * - `project`: 1 API call per project, limited to 900 repositories per project. */ level?: 'workspace' | 'project'; + /** + * (Optional) TaskScheduleDefinition for the discovery. + */ + schedule?: SchedulerServiceTaskScheduleDefinitionConfig; } | { [name: string]: { @@ -96,10 +96,6 @@ export interface Config { */ projectKey?: string; }; - /** - * (Optional) TaskScheduleDefinition for the discovery. - */ - schedule?: SchedulerServiceTaskScheduleDefinitionConfig; /** * (Optional) At what level discovery should take place, affecting Bitbucket Cloud API limits. * @@ -108,6 +104,10 @@ export interface Config { * - `project`: 1 API call per project, limited to 900 repositories per project. */ level?: 'workspace' | 'project'; + /** + * (Optional) TaskScheduleDefinition for the discovery. + */ + schedule?: SchedulerServiceTaskScheduleDefinitionConfig; }; }; }; From 7cc909a2c20e943dda9918109b33ff7b3e24bd2f Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 15 Oct 2024 17:08:16 +0200 Subject: [PATCH 018/237] refactor: remove level config Signed-off-by: Benjamin Janssens --- .changeset/wise-snakes-sleep.md | 2 +- docs/integrations/bitbucketCloud/discovery.md | 5 - .../config.d.ts | 16 -- .../BitbucketCloudEntityProvider.test.ts | 194 ++---------------- .../providers/BitbucketCloudEntityProvider.ts | 27 +-- ...BitbucketCloudEntityProviderConfig.test.ts | 21 +- .../BitbucketCloudEntityProviderConfig.ts | 8 - 7 files changed, 27 insertions(+), 246 deletions(-) diff --git a/.changeset/wise-snakes-sleep.md b/.changeset/wise-snakes-sleep.md index f0fa018c9a..ca51972f57 100644 --- a/.changeset/wise-snakes-sleep.md +++ b/.changeset/wise-snakes-sleep.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch --- -Added discovery level configuration to shift Bitbucket Cloud API limits +Implemented discovery on project-level to shift Bitbucket Cloud API limits diff --git a/docs/integrations/bitbucketCloud/discovery.md b/docs/integrations/bitbucketCloud/discovery.md index d623a5d47c..6ba9c0aa1f 100644 --- a/docs/integrations/bitbucketCloud/discovery.md +++ b/docs/integrations/bitbucketCloud/discovery.md @@ -146,7 +146,6 @@ catalog: filters: # optional projectKey: '^apis-.*$' # optional; RegExp repoSlug: '^service-.*$' # optional; RegExp - level: workspace # default value schedule: # same options as in SchedulerServiceTaskScheduleDefinition # supports cron, ISO duration, "human duration" as used in code frequency: { minutes: 30 } @@ -169,8 +168,6 @@ catalog: Regular expression used to filter results based on the project key. - **`repoSlug`** _(optional)_: Regular expression used to filter results based on the repo slug. -- **`level`** _(optional)_: - `'workspace'` (default) or `'project'`. At what level discovery should take place, affecting Bitbucket Cloud API limits. - **`schedule`**: - **`frequency`**: How often you want the task to run. The system does its best to avoid overlapping invocations. @@ -183,5 +180,3 @@ catalog: - **`workspace`**: Name of your organization account/workspace. If you want to add multiple workspaces, you need to add one provider config each. - -> **Note:** By default, discovery will take place at the `workspace` level. While being the most efficient in terms of API calls to Bitbucket Cloud, discovery at the workspace level is limited to 900 repositories per workspace. If your workspace consists of more than 900 repositories, you should switch to discovery at the `project` level, shifting the limit to 900 repositories per project. diff --git a/plugins/catalog-backend-module-bitbucket-cloud/config.d.ts b/plugins/catalog-backend-module-bitbucket-cloud/config.d.ts index d216efbd8c..1051976b24 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/config.d.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/config.d.ts @@ -55,14 +55,6 @@ export interface Config { */ projectKey?: string; }; - /** - * (Optional) At what level discovery should take place, affecting Bitbucket Cloud API limits. - * - * Possible values: - * - `workspace` (default): 1 API call per workspace, limited to 900 repositories per workspace. - * - `project`: 1 API call per project, limited to 900 repositories per project. - */ - level?: 'workspace' | 'project'; /** * (Optional) TaskScheduleDefinition for the discovery. */ @@ -96,14 +88,6 @@ export interface Config { */ projectKey?: string; }; - /** - * (Optional) At what level discovery should take place, affecting Bitbucket Cloud API limits. - * - * Possible values: - * - `workspace` (default): 1 API call per workspace, limited to 900 repositories per workspace. - * - `project`: 1 API call per project, limited to 900 repositories per project. - */ - level?: 'workspace' | 'project'; /** * (Optional) TaskScheduleDefinition for the discovery. */ diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts index 32e8d41551..6036fa73f4 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts @@ -87,23 +87,6 @@ describe('BitbucketCloudEntityProvider', () => { }, }, }); - const projectLevelConfig = new ConfigReader({ - catalog: { - providers: { - bitbucketCloud: { - myProvider: { - workspace: 'test-ws', - catalogPath: 'catalog-custom.yaml', - filters: { - projectKey: 'test-.*', - repoSlug: 'test-.*', - }, - level: 'project', - }, - }, - }, - }, - }); const schedule = new PersistingTaskRunner(); const entityProviderConnection: EntityProviderConnection = { applyMutation: jest.fn(), @@ -291,163 +274,6 @@ describe('BitbucketCloudEntityProvider', () => { 'bitbucketCloud-provider:myProvider', ); - server.use( - rest.get( - `https://api.bitbucket.org/2.0/workspaces/test-ws/search/code`, - (_req, res, ctx) => { - const response = { - values: [ - { - // skipped as empty - path_matches: [], - file: { - type: 'commit_file', - path: 'path/to/ignored/file', - }, - }, - { - path_matches: [ - { - match: true, - text: 'catalog-custom.yaml', - }, - ], - file: { - type: 'commit_file', - path: 'custom/path/catalog-custom.yaml', - commit: { - repository: { - // skipped as no match with filter - slug: 'repo', - project: { - key: 'test-project', - }, - mainbranch: { - name: 'main', - }, - links: { - html: { - href: 'https://bitbucket.org/test-ws/repo', - }, - }, - }, - }, - }, - }, - { - path_matches: [ - { - match: true, - text: 'catalog-custom.yaml', - }, - ], - file: { - type: 'commit_file', - path: 'custom/path/catalog-custom.yaml', - commit: { - repository: { - slug: 'test-repo1', - project: { - // skipped as no match with filter - key: 'project', - }, - mainbranch: { - name: 'main', - }, - links: { - html: { - href: 'https://bitbucket.org/test-ws/test-repo1', - }, - }, - }, - }, - }, - }, - { - path_matches: [ - { - match: true, - text: 'catalog-custom.yaml', - }, - ], - file: { - type: 'commit_file', - path: 'custom/path/catalog-custom.yaml', - commit: { - repository: { - slug: 'test-repo2', - project: { - key: 'test-project', - }, - mainbranch: { - name: 'main', - }, - links: { - html: { - href: 'https://bitbucket.org/test-ws/test-repo2', - }, - }, - }, - }, - }, - }, - ], - }; - return res(ctx.json(response)); - }, - ), - ); - - await provider.connect(entityProviderConnection); - - const taskDef = schedule.getTasks()[0]; - expect(taskDef.id).toEqual('bitbucketCloud-provider:myProvider:refresh'); - await (taskDef.fn as () => Promise)(); - - const url = `https://bitbucket.org/test-ws/test-repo2/src/main/custom/path/catalog-custom.yaml`; - const expectedEntities = [ - { - entity: { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Location', - metadata: { - annotations: { - 'backstage.io/managed-by-location': `url:${url}`, - 'backstage.io/managed-by-origin-location': `url:${url}`, - 'bitbucket.org/repo-url': - 'https://bitbucket.org/test-ws/test-repo2', - }, - name: 'generated-7c2e6263b6cc2d14e69fd4d029afba601ad6dc3b', - }, - spec: { - presence: 'required', - target: `${url}`, - type: 'url', - }, - }, - locationKey: 'bitbucketCloud-provider:myProvider', - }, - ]; - - expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); - expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ - type: 'full', - entities: expectedEntities, - }); - }); - - it('apply full update on scheduled execution on project level', async () => { - const provider = BitbucketCloudEntityProvider.fromConfig( - projectLevelConfig, - { - logger, - schedule, - }, - )[0]; - expect(provider.getProviderName()).toEqual( - 'bitbucketCloud-provider:myProvider', - ); - server.use( rest.get( `https://api.bitbucket.org/2.0/workspaces/test-ws/projects`, @@ -464,12 +290,7 @@ describe('BitbucketCloudEntityProvider', () => { ), rest.get( `https://api.bitbucket.org/2.0/workspaces/test-ws/search/code`, - (req, res, ctx) => { - const query = req.url.searchParams.get('search_query'); - if (!query || !query.includes('project:TEST')) { - return res(ctx.json({ values: [] })); - } - + (_req, res, ctx) => { const response = { values: [ { @@ -657,6 +478,19 @@ describe('BitbucketCloudEntityProvider', () => { })[0]; server.use( + rest.get( + `https://api.bitbucket.org/2.0/workspaces/test-ws/projects`, + (_req, res, ctx) => { + const response = { + values: [ + { + key: 'TEST', + }, + ], + }; + return res(ctx.json(response)); + }, + ), rest.get( `https://api.bitbucket.org/2.0/workspaces/test-ws/search/code`, (req, res, ctx) => { diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts index b1a8ac9234..891ad14da5 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts @@ -207,7 +207,7 @@ export class BitbucketCloudEntityProvider implements EntityProvider { logger.info('Discovering catalog files in Bitbucket Cloud repositories'); - const targets = await this.findCatalogFiles(this.config.level); + const targets = await this.findCatalogFiles(); const entities = this.toDeferredEntities(targets); await this.connection.applyMutation({ @@ -271,7 +271,7 @@ export class BitbucketCloudEntityProvider implements EntityProvider { // Hence, we will just trigger a refresh for catalog file(s) within the repository // if we get notified about changes there. - const targets = await this.findCatalogFiles('workspace', repoSlug); + const targets = await this.findCatalogFiles(repoSlug); const { token } = await this.tokenManager!.getToken(); const existing = await this.findExistingLocations(repoUrl, token); @@ -334,7 +334,6 @@ export class BitbucketCloudEntityProvider implements EntityProvider { } private async findCatalogFiles( - level: 'workspace' | 'project', repoSlug?: string, ): Promise { const workspace = this.config.workspace; @@ -347,23 +346,19 @@ export class BitbucketCloudEntityProvider implements EntityProvider { const optRepoFilter = repoSlug ? ` repo:${repoSlug}` : ''; const query = `"${catalogFilename}" path:${catalogPath}${optRepoFilter}`; - if (level === 'project') { - const projects = this.client - .listProjectsByWorkspace(workspace) - .iterateResults(); + const projects = this.client + .listProjectsByWorkspace(workspace) + .iterateResults(); - let results: IngestionTarget[] = []; + let results: IngestionTarget[] = []; - for await (const project of projects) { - const projectQuery = `${query} project:${project.key}`; - const result = await this.processQuery(workspace, projectQuery); - results = results.concat(result); - } - - return results; + for await (const project of projects) { + const projectQuery = `${query} project:${project.key}`; + const result = await this.processQuery(workspace, projectQuery); + results = results.concat(result); } - return this.processQuery(workspace, query); + return results; } private async processQuery( diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.test.ts index 0491971ea9..7f13616586 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.test.ts @@ -77,17 +77,13 @@ describe('readProviderConfigs', () => { }, }, }, - providerWithProjectLevel: { - workspace: 'test-ws6', - level: 'project', - }, }, }, }, }); const providerConfigs = readProviderConfigs(config); - expect(providerConfigs).toHaveLength(6); + expect(providerConfigs).toHaveLength(5); expect(providerConfigs[0]).toEqual({ id: 'providerWorkspaceOnly', workspace: 'test-ws1', @@ -96,7 +92,6 @@ describe('readProviderConfigs', () => { projectKey: undefined, repoSlug: undefined, }, - level: 'workspace', }); expect(providerConfigs[1]).toEqual({ id: 'providerCustomCatalogPath', @@ -106,7 +101,6 @@ describe('readProviderConfigs', () => { projectKey: undefined, repoSlug: undefined, }, - level: 'workspace', }); expect(providerConfigs[2]).toEqual({ id: 'providerWithProjectKeyFilter', @@ -116,7 +110,6 @@ describe('readProviderConfigs', () => { projectKey: /^projectKey.*filter$/, repoSlug: undefined, }, - level: 'workspace', }); expect(providerConfigs[3]).toEqual({ id: 'providerWithRepoSlugFilter', @@ -126,7 +119,6 @@ describe('readProviderConfigs', () => { projectKey: undefined, repoSlug: /^repoSlug.*filter$/, }, - level: 'workspace', }); expect(providerConfigs[4]).toEqual({ id: 'providerWithSchedule', @@ -142,17 +134,6 @@ describe('readProviderConfigs', () => { minutes: 3, }, }, - level: 'workspace', - }); - expect(providerConfigs[5]).toEqual({ - id: 'providerWithProjectLevel', - workspace: 'test-ws6', - catalogPath: '/catalog-info.yaml', - filters: { - projectKey: undefined, - repoSlug: undefined, - }, - level: 'project', }); }); }); diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.ts index 12f8db1c9c..5a806d2e0b 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.ts @@ -32,7 +32,6 @@ export type BitbucketCloudEntityProviderConfig = { repoSlug?: RegExp; }; schedule?: SchedulerServiceTaskScheduleDefinition; - level: 'workspace' | 'project'; }; export function readProviderConfigs( @@ -73,12 +72,6 @@ function readProviderConfig( ) : undefined; - const level = - (config.getOptionalString('level') as - | 'workspace' - | 'project' - | undefined) ?? 'workspace'; - return { id, catalogPath, @@ -90,7 +83,6 @@ function readProviderConfig( repoSlug: repoSlugPattern ? compileRegExp(repoSlugPattern) : undefined, }, schedule, - level, }; } From 9790c02d16e986fc70f0d66daf20420a7b4f1691 Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Wed, 16 Oct 2024 11:48:56 -0300 Subject: [PATCH 019/237] fix(catalog-backend-module-github): update parent to not send a object with empty value fix #26109 Signed-off-by: Rogerio Angeliski --- .changeset/rotten-mangos-hug.md | 5 ++ .../providers/GithubOrgEntityProvider.test.ts | 89 +++++++++++++++++++ .../src/providers/GithubOrgEntityProvider.ts | 4 +- 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 .changeset/rotten-mangos-hug.md diff --git a/.changeset/rotten-mangos-hug.md b/.changeset/rotten-mangos-hug.md new file mode 100644 index 0000000000..013a815653 --- /dev/null +++ b/.changeset/rotten-mangos-hug.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-github': patch +--- + +Fix bug when receive a `team.creted` github event without parent diff --git a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts index 82c93b0d57..52dc2b6d14 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts @@ -530,6 +530,95 @@ describe('GithubOrgEntityProvider', () => { }); }); + it('should apply delta added on receive a created team without parent', async () => { + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + + const logger = mockServices.logger.mock(); + const events = DefaultEventsService.create({ logger }); + const gitHubConfig: GithubIntegrationConfig = { + host: 'github.com', + }; + + const mockGetCredentials = jest.fn().mockReturnValue({ + headers: { token: 'blah' }, + type: 'app', + }); + + const githubCredentialsProvider: GithubCredentialsProvider = { + getCredentials: mockGetCredentials, + }; + + const entityProvider = new GithubOrgEntityProvider({ + events, + id: 'my-id', + githubCredentialsProvider, + orgUrl: 'https://github.com/backstage', + gitHubConfig, + logger, + }); + + entityProvider.connect(entityProviderConnection); + + const expectedEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'new-team', + description: 'description from the new team', + annotations: { + 'backstage.io/edit-url': + 'https://github.com/orgs/test-org/teams/new-team/edit', + 'backstage.io/managed-by-location': + 'url:https://github.com/orgs/test-org/teams/new-team', + 'backstage.io/managed-by-origin-location': + 'url:https://github.com/orgs/test-org/teams/new-team', + 'github.com/team-slug': 'test-org/new-team', + }, + }, + spec: { + type: 'team', + children: [], + members: [], + profile: { + displayName: 'New Team', + }, + }, + }; + + const event: EventParams = { + topic: 'github.team', + eventPayload: { + action: 'created', + team: { + name: 'New Team', + slug: 'new-team', + description: 'description from the new team', + html_url: 'https://github.com/orgs/test-org/teams/new-team', + }, + organization: { + login: 'test-org', + }, + }, + }; + + await events.publish(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: [ + { + locationKey: 'github-org-provider:my-id', + entity: expectedEntity, + }, + ], + removed: [], + }); + }); + it('should apply delta removed on receive a deleted team', async () => { const entityProviderConnection: EntityProviderConnection = { applyMutation: jest.fn(), diff --git a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts index f7183b4df9..6e83b8d65f 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts @@ -511,7 +511,9 @@ export class GithubOrgEntityProvider implements EntityProvider { editTeamUrl: `${url}/edit`, combinedSlug: `${org}/${slug}`, description: description || undefined, - parentTeam: { slug: event.team?.parent?.slug || '' } as GithubTeam, + parentTeam: event.team?.parent?.slug + ? ({ slug: event.team.parent.slug } as GithubTeam) + : undefined, // entity will be removed members: [], }, From c19f109d9e79801df258aa2c49c418fe0ffaf771 Mon Sep 17 00:00:00 2001 From: KaemonIsland Date: Thu, 17 Oct 2024 10:52:49 -0400 Subject: [PATCH 020/237] Update comment Signed-off-by: KaemonIsland --- .../integration-aws-node/src/DefaultAwsCredentialsManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.ts b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.ts index 2b7ae3e1e5..3048fdfe40 100644 --- a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.ts +++ b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.ts @@ -78,7 +78,7 @@ function getProfileCredentials( } /** - * Include the region if present, otherwise use the default region + * Include the region if present, otherwise use the default region. * * @see https://www.npmjs.com/package/@aws-sdk/credential-provider-node */ From 8bb8c302ae007a7fb4a92d50a9320313edb929c6 Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Fri, 18 Oct 2024 11:53:18 -0300 Subject: [PATCH 021/237] Update .changeset/rotten-mangos-hug.md Co-authored-by: Vincenzo Scamporlino Signed-off-by: Rogerio Angeliski --- .changeset/rotten-mangos-hug.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/rotten-mangos-hug.md b/.changeset/rotten-mangos-hug.md index 013a815653..022d3f3d45 100644 --- a/.changeset/rotten-mangos-hug.md +++ b/.changeset/rotten-mangos-hug.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-backend-module-github': patch --- -Fix bug when receive a `team.creted` github event without parent +Fixed an issue in `GithubOrgEntityProvider` that caused an error when processing teams without a parent. From b533056c81cd355561119dbd483668b85c09dfd6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 06:05:26 +0000 Subject: [PATCH 022/237] fix(deps): update dependency css-loader to v7 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .changeset/renovate-6193787.md | 5 +++++ packages/cli/package.json | 2 +- yarn.lock | 12 ++++++------ 3 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 .changeset/renovate-6193787.md diff --git a/.changeset/renovate-6193787.md b/.changeset/renovate-6193787.md new file mode 100644 index 0000000000..ce392b7c81 --- /dev/null +++ b/.changeset/renovate-6193787.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Updated dependency `css-loader` to `^7.0.0`. diff --git a/packages/cli/package.json b/packages/cli/package.json index de183b4d15..278c0aec3e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -88,7 +88,7 @@ "commander": "^12.0.0", "cross-fetch": "^4.0.0", "cross-spawn": "^7.0.3", - "css-loader": "^6.5.1", + "css-loader": "^7.0.0", "ctrlc-windows": "^2.1.0", "esbuild": "^0.24.0", "esbuild-loader": "^4.0.0", diff --git a/yarn.lock b/yarn.lock index 22aaacbd73..b22ada9924 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3992,7 +3992,7 @@ __metadata: commander: ^12.0.0 cross-fetch: ^4.0.0 cross-spawn: ^7.0.3 - css-loader: ^6.5.1 + css-loader: ^7.0.0 ctrlc-windows: ^2.1.0 del: ^8.0.0 esbuild: ^0.24.0 @@ -24483,9 +24483,9 @@ __metadata: languageName: node linkType: hard -"css-loader@npm:^6.5.1": - version: 6.11.0 - resolution: "css-loader@npm:6.11.0" +"css-loader@npm:^7.0.0": + version: 7.1.2 + resolution: "css-loader@npm:7.1.2" dependencies: icss-utils: ^5.1.0 postcss: ^8.4.33 @@ -24497,13 +24497,13 @@ __metadata: semver: ^7.5.4 peerDependencies: "@rspack/core": 0.x || 1.x - webpack: ^5.0.0 + webpack: ^5.27.0 peerDependenciesMeta: "@rspack/core": optional: true webpack: optional: true - checksum: 5c8d35975a7121334905394e88e28f05df72f037dbed2fb8fec4be5f0b313ae73a13894ba791867d4a4190c35896da84a7fd0c54fb426db55d85ba5e714edbe3 + checksum: 15bfd90d778ddab90ee1d04c8c8bcc13ea6c0791d01b52b09d1b1c753b3410f7a7788a510d93726a9878e70b7c1a140f21efdf5c96e1857872107551d3897822 languageName: node linkType: hard From 9816f510dc9184b77e467ac648eea07ddb524bde Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Mon, 14 Oct 2024 19:52:54 +0200 Subject: [PATCH 023/237] fix(events,github): fixes signature validation by using raw req body Adds raw body information (body as buffer, encoding) to `RequestDetails` to support more request validation use cases. Additionally, uses the raw body to retrieve the transmitted JSON string unparsed/raw to correctly validate the signature. Previously, we re-stringified the parsed JSON payload which could lead to different JSON strings. Those differences can lead to the rejection of requests due to a mismatch in expected signature. Fixes: #26709 Relates-to: PR #26884 Co-authored-by: Christopher Diaz Signed-off-by: Patrick Jungermann --- .changeset/seven-hotels-move.md | 6 + .changeset/shiny-falcons-fly.md | 8 + .../createGithubSignatureValidator.test.ts | 9 +- .../http/createGithubSignatureValidator.ts | 6 +- .../service/eventsModuleGithubWebhook.test.ts | 9 +- .../http/createGitlabTokenValidator.test.ts | 3 +- .../service/eventsModuleGitlabWebhook.test.ts | 3 +- plugins/events-backend/package.json | 2 + .../src/service/EventsPlugin.test.ts | 6 +- .../src/service/EventsPlugin.ts | 24 +-- .../HttpPostIngressEventPublisher.test.ts | 145 ++++++++++++++++-- .../http/HttpPostIngressEventPublisher.ts | 85 ++++++++-- plugins/events-node/report.api.md | 8 +- .../src/api/http/validation/RequestDetails.ts | 16 ++ yarn.lock | 9 ++ 15 files changed, 293 insertions(+), 46 deletions(-) create mode 100644 .changeset/seven-hotels-move.md create mode 100644 .changeset/shiny-falcons-fly.md diff --git a/.changeset/seven-hotels-move.md b/.changeset/seven-hotels-move.md new file mode 100644 index 0000000000..03bb39116a --- /dev/null +++ b/.changeset/seven-hotels-move.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-events-backend-module-github': patch +--- + +Fix the event request validation for incoming requests for GitHub webhook events +by using the raw body when verifying the signature. diff --git a/.changeset/shiny-falcons-fly.md b/.changeset/shiny-falcons-fly.md new file mode 100644 index 0000000000..899bbfbda4 --- /dev/null +++ b/.changeset/shiny-falcons-fly.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-events-node': patch +'@backstage/plugin-events-backend-module-github': patch +'@backstage/plugin-events-backend': patch +--- + +Add raw body information to `RequestDetails` +and use the raw body when validating incoming event requests. diff --git a/plugins/events-backend-module-github/src/http/createGithubSignatureValidator.test.ts b/plugins/events-backend-module-github/src/http/createGithubSignatureValidator.test.ts index 32d67437a8..3c96c98996 100644 --- a/plugins/events-backend-module-github/src/http/createGithubSignatureValidator.test.ts +++ b/plugins/events-backend-module-github/src/http/createGithubSignatureValidator.test.ts @@ -47,8 +47,9 @@ describe('createGithubSignatureValidator', () => { }, }, }); - const payload = { test: 'payload' }; - const payloadString = JSON.stringify(payload); + const payloadString = '{"test": "payload", "score": 5.0}'; + const payload = JSON.parse(payloadString); + const payloadBuffer = Buffer.from(payloadString); const validSignature = sign({ secret, algorithm: 'sha256' }, payloadString); const requestWithSignature = async (signature: string | undefined) => { @@ -57,6 +58,10 @@ describe('createGithubSignatureValidator', () => { headers: { 'x-hub-signature-256': signature, }, + raw: { + body: payloadBuffer, + encoding: 'utf-8', + }, } as RequestDetails; }; diff --git a/plugins/events-backend-module-github/src/http/createGithubSignatureValidator.ts b/plugins/events-backend-module-github/src/http/createGithubSignatureValidator.ts index e977640ce8..87cb710697 100644 --- a/plugins/events-backend-module-github/src/http/createGithubSignatureValidator.ts +++ b/plugins/events-backend-module-github/src/http/createGithubSignatureValidator.ts @@ -48,7 +48,11 @@ export function createGithubSignatureValidator( if ( !signature || - !(await verify(secret, JSON.stringify(request.body), signature)) + !(await verify( + secret, + request.raw.body.toString(request.raw.encoding), + signature, + )) ) { context.reject({ status: 403, diff --git a/plugins/events-backend-module-github/src/service/eventsModuleGithubWebhook.test.ts b/plugins/events-backend-module-github/src/service/eventsModuleGithubWebhook.test.ts index 460022b04d..ec5ec66946 100644 --- a/plugins/events-backend-module-github/src/service/eventsModuleGithubWebhook.test.ts +++ b/plugins/events-backend-module-github/src/service/eventsModuleGithubWebhook.test.ts @@ -25,8 +25,9 @@ import { eventsModuleGithubWebhook } from './eventsModuleGithubWebhook'; describe('eventsModuleGithubWebhook', () => { const secret = 'valid-secret'; - const payload = { test: 'payload' }; - const payloadString = JSON.stringify(payload); + const payloadString = '{"test": "payload", "score": 5.0}'; + const payload = JSON.parse(payloadString); + const payloadBuffer = Buffer.from(payloadString); const validSignature = sign({ secret, algorithm: 'sha256' }, payloadString); const requestWithSignature = async (signature?: string) => { return { @@ -34,6 +35,10 @@ describe('eventsModuleGithubWebhook', () => { headers: { 'x-hub-signature-256': signature, }, + raw: { + body: payloadBuffer, + encoding: 'utf-8', + }, } as RequestDetails; }; diff --git a/plugins/events-backend-module-gitlab/src/http/createGitlabTokenValidator.test.ts b/plugins/events-backend-module-gitlab/src/http/createGitlabTokenValidator.test.ts index 33e677450e..72fee279ea 100644 --- a/plugins/events-backend-module-gitlab/src/http/createGitlabTokenValidator.test.ts +++ b/plugins/events-backend-module-gitlab/src/http/createGitlabTokenValidator.test.ts @@ -49,11 +49,10 @@ describe('createGitlabTokenValidator', () => { const requestWithToken = (token: string | undefined) => { return { - body: undefined, headers: { 'x-gitlab-token': token, }, - } as RequestDetails; + } as Partial as unknown as RequestDetails; }; it('no secret configured, throw error', async () => { diff --git a/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabWebhook.test.ts b/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabWebhook.test.ts index 9a4f4326da..d3452dd813 100644 --- a/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabWebhook.test.ts +++ b/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabWebhook.test.ts @@ -25,11 +25,10 @@ import { eventsModuleGitlabWebhook } from './eventsModuleGitlabWebhook'; describe('gitlabWebhookEventsModule', () => { const requestWithToken = (token?: string) => { return { - body: undefined, headers: { 'x-gitlab-token': token, }, - } as RequestDetails; + } as Partial as unknown as RequestDetails; }; it('should be correctly wired and set up', async () => { diff --git a/plugins/events-backend/package.json b/plugins/events-backend/package.json index e2360e5d36..f44e269900 100644 --- a/plugins/events-backend/package.json +++ b/plugins/events-backend/package.json @@ -61,6 +61,7 @@ "@backstage/plugin-events-node": "workspace:^", "@backstage/types": "workspace:^", "@types/express": "^4.17.6", + "content-type": "^1.0.5", "express": "^4.17.1", "express-promise-router": "^4.1.0", "knex": "^3.0.0", @@ -73,6 +74,7 @@ "@backstage/cli": "workspace:^", "@backstage/plugin-events-backend-test-utils": "workspace:^", "@backstage/repo-tools": "workspace:^", + "@types/content-type": "^1.1.8", "supertest": "^7.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/events-backend/src/service/EventsPlugin.test.ts b/plugins/events-backend/src/service/EventsPlugin.test.ts index 2c0befe1fd..39962afdcc 100644 --- a/plugins/events-backend/src/service/EventsPlugin.test.ts +++ b/plugins/events-backend/src/service/EventsPlugin.test.ts @@ -83,14 +83,16 @@ describe('eventsPlugin', () => { const response1 = await request(server) .post('/api/events/http/fake') + .type('application/json') .timeout(1000) - .send({ test: 'fake' }); + .send(JSON.stringify({ test: 'fake' })); expect(response1.status).toBe(202); const response2 = await request(server) .post('/api/events/http/fake-ext') + .type('application/json') .timeout(1000) - .send({ test: 'fake-ext' }); + .send(JSON.stringify({ test: 'fake-ext' })); expect(response2.status).toBe(202); expect(eventsService.published).toHaveLength(2); diff --git a/plugins/events-backend/src/service/EventsPlugin.ts b/plugins/events-backend/src/service/EventsPlugin.ts index 10af3798ec..89f50d835b 100644 --- a/plugins/events-backend/src/service/EventsPlugin.ts +++ b/plugins/events-backend/src/service/EventsPlugin.ts @@ -76,21 +76,21 @@ export const eventsPlugin = createBackendPlugin({ config: coreServices.rootConfig, events: eventsServiceRef, database: coreServices.database, + httpAuth: coreServices.httpAuth, + httpRouter: coreServices.httpRouter, + lifecycle: coreServices.lifecycle, logger: coreServices.logger, scheduler: coreServices.scheduler, - lifecycle: coreServices.lifecycle, - httpAuth: coreServices.httpAuth, - router: coreServices.httpRouter, }, async init({ config, events, database, + httpAuth, + httpRouter, + lifecycle, logger, scheduler, - lifecycle, - httpAuth, - router, }) { const ingresses = Object.fromEntries( extensionPoint.httpPostIngresses.map(ingress => [ @@ -108,18 +108,22 @@ export const eventsPlugin = createBackendPlugin({ const eventsRouter = Router(); http.bind(eventsRouter); - router.use( + // MUST be registered *before* the event bus router. + // Otherwise, it would already make use of `express.json()` + // that is used there as part of the middleware stack. + httpRouter.use(eventsRouter); + + httpRouter.use( await createEventBusRouter({ database, + lifecycle, logger, httpAuth, scheduler, - lifecycle, }), ); - router.use(eventsRouter); - router.addAuthPolicy({ + httpRouter.addAuthPolicy({ allow: 'unauthenticated', path: '/http', }); diff --git a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts index 2bffa4e511..32a24de169 100644 --- a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts +++ b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts @@ -50,22 +50,25 @@ describe('HttpPostIngressEventPublisher', () => { const notFoundResponse = await request(app) .post('/http/unknown') + .type('application/json') .timeout(1000) - .send({ test: 'data' }); + .send(JSON.stringify({ test: 'data' })); expect(notFoundResponse.status).toBe(404); const response1 = await request(app) .post('/http/testA') + .type('application/json') .set('X-Custom-Header', 'test-value') .timeout(1000) - .send({ testA: 'data' }); + .send(JSON.stringify({ testA: 'data' })); expect(response1.status).toBe(202); const response2 = await request(app) .post('/http/testB') + .type('application/json') .set('X-Custom-Header', 'test-value') .timeout(1000) - .send({ testB: 'data' }); + .send(JSON.stringify({ testB: 'data' })); expect(response2.status).toBe(202); expect(events.published).toHaveLength(2); @@ -87,6 +90,124 @@ describe('HttpPostIngressEventPublisher', () => { ); }); + it('no raw body', async () => { + const config = new ConfigReader({ + events: { + http: { + topics: ['testA'], + }, + }, + }); + + const router = Router(); + router.use(express.json()); // will prevent the raw body from being available + const app = express().use(router); + const events = new TestEventsService(); + + const publisher = HttpPostIngressEventPublisher.fromConfig({ + config, + events, + logger, + }); + publisher.bind(router); + + const response = await request(app) + .post('/http/testA') + .type('application/json; charset=utf-8') + .timeout(1000) + .send(JSON.stringify({ testA: 'data' })); + expect(response.status).toBe(500); + expect(response.body).toEqual( + expect.objectContaining({ + error: { + message: + 'Failed to retrieve raw body from incoming event for topic testA; not a buffer: object', + name: 'Error', + }, + request: { method: 'POST', url: '/testA' }, + response: { statusCode: 500 }, + }), + ); + }); + + it('invalid charset', async () => { + const config = new ConfigReader({ + events: { + http: { + topics: ['testA'], + }, + }, + }); + + const router = Router(); + const app = express().use(router); + const events = new TestEventsService(); + + const publisher = HttpPostIngressEventPublisher.fromConfig({ + config, + events, + logger, + }); + publisher.bind(router); + + const response = await request(app) + .post('/http/testA') + .type('application/json; charset=invalid') + .timeout(1000) + .send(JSON.stringify({ testA: 'data' })); + expect(response.status).toBe(415); + expect(response.body).toEqual( + expect.objectContaining({ + error: { + message: 'Unsupported charset: invalid', + name: 'UnsupportedCharsetError', + statusCode: 415, + }, + request: { method: 'POST', url: '/testA' }, + response: { statusCode: 415 }, + }), + ); + }); + + it('non-JSON media type', async () => { + const config = new ConfigReader({ + events: { + http: { + topics: ['testA'], + }, + }, + }); + + const router = Router(); + const app = express().use(router); + const events = new TestEventsService(); + + const publisher = HttpPostIngressEventPublisher.fromConfig({ + config, + events, + logger, + }); + publisher.bind(router); + + const response = await request(app) + .post('/http/testA') + .type('text/plain') + .timeout(1000) + .send('Textual information'); + expect(response.status).toBe(415); + expect(response.body).toEqual( + expect.objectContaining({ + error: { + message: 'Unsupported media type: text/plain', + name: 'UnsupportedMediaTypeError', + statusCode: 415, + }, + request: { method: 'POST', url: '/testA' }, + response: { statusCode: 415 }, + }), + ); + }); + it('with validator', async () => { const config = new ConfigReader({ events: { @@ -149,43 +270,49 @@ describe('HttpPostIngressEventPublisher', () => { const response1 = await request(app) .post('/http/testA') + .type('application/json') .timeout(1000) - .send({ test: 'data' }); + .send(JSON.stringify({ test: 'data' })); expect(response1.status).toBe(202); const response2 = await request(app) .post('/http/testB') + .type('application/json') .timeout(1000) - .send({ test: 'data' }); + .send(JSON.stringify({ test: 'data' })); expect(response2.status).toBe(400); expect(response2.body).toEqual({ message: 'wrong signature' }); const response3 = await request(app) .post('/http/testB') + .type('application/json') .set('X-Test-Signature', 'wrong') .timeout(1000) - .send({ test: 'data' }); + .send(JSON.stringify({ test: 'data' })); expect(response3.status).toBe(400); expect(response3.body).toEqual({ message: 'wrong signature' }); const response4 = await request(app) .post('/http/testB') + .type('application/json') .set('X-Test-Signature', 'testB-signature') .timeout(1000) - .send({ test: 'data' }); + .send(JSON.stringify({ test: 'data' })); expect(response4.status).toBe(202); const response5 = await request(app) .post('/http/testC') + .type('application/json') .timeout(1000) - .send({ test: 'data' }); + .send(JSON.stringify({ test: 'data' })); expect(response5.status).toBe(404); expect(response5.body).toEqual({}); const response6 = await request(app) .post('/http/testD') + .type('application/json') .timeout(1000) - .send({ test: 'data' }); + .send(JSON.stringify({ test: 'data' })); expect(response6.status).toBe(403); expect(response6.body).toEqual({}); diff --git a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts index 06dc4e463a..fe17186856 100644 --- a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts +++ b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts @@ -17,15 +17,35 @@ import { errorHandler } from '@backstage/backend-common'; import { LoggerService } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; +import { CustomErrorBase } from '@backstage/errors'; import { EventsService, HttpPostIngressOptions, RequestValidator, } from '@backstage/plugin-events-node'; +import contentType from 'content-type'; import express from 'express'; import Router from 'express-promise-router'; import { RequestValidationContextImpl } from './validation'; +class UnsupportedCharsetError extends CustomErrorBase { + name = 'UnsupportedCharsetError' as const; + statusCode = 415 as const; + + constructor(charset: string) { + super(`Unsupported charset: ${charset}`); + } +} + +class UnsupportedMediaTypeError extends CustomErrorBase { + name = 'UnsupportedMediaTypeError' as const; + statusCode = 415 as const; + + constructor(mediaType?: string) { + super(`Unsupported media type: ${mediaType ?? 'unknown'}`); + } +} + /** * Publishes events received from their origin (e.g., webhook events from an SCM system) * via HTTP POST endpoint and passes the request body as event payload to the registered subscribers. @@ -71,7 +91,7 @@ export class HttpPostIngressEventPublisher { [topic: string]: Omit; }): express.Router { const router = Router(); - router.use(express.json()); + router.use(express.raw({ type: '*/*' })); Object.keys(ingresses).forEach(topic => this.addRouteForTopic(router, topic, ingresses[topic].validator), @@ -87,25 +107,60 @@ export class HttpPostIngressEventPublisher { validator?: RequestValidator, ): void { const path = `/${topic}`; + const logger = this.logger; router.post(path, async (request, response) => { - const requestDetails = { - body: request.body, - headers: request.headers, - }; - const context = new RequestValidationContextImpl(); - await validator?.(requestDetails, context); - if (context.wasRejected()) { - response - .status(context.rejectionDetails!.status) - .json(context.rejectionDetails!.payload); - return; + const requestBody = request.body; + if (!Buffer.isBuffer(requestBody)) { + throw new Error( + `Failed to retrieve raw body from incoming event for topic ${topic}; not a buffer: ${typeof requestBody}`, + ); + } + + const bodyBuffer: Buffer = requestBody; + const parsedContentType = contentType.parse(request); + if ( + !parsedContentType.type || + parsedContentType.type !== 'application/json' + ) { + throw new UnsupportedMediaTypeError(parsedContentType.type); + } + + const encoding = parsedContentType.parameters.charset ?? 'utf-8'; + if (!Buffer.isEncoding(encoding)) { + throw new UnsupportedCharsetError(encoding); + } + + const bodyString = bodyBuffer.toString(encoding); + const bodyParsed = + parsedContentType.type === 'application/json' + ? JSON.parse(bodyString) + : bodyString; + + if (validator) { + const requestDetails = { + body: bodyParsed, + headers: request.headers, + raw: { + body: bodyBuffer, + encoding: encoding as BufferEncoding, + }, + }; + + const context = new RequestValidationContextImpl(); + await validator(requestDetails, context); + + if (context.wasRejected()) { + response + .status(context.rejectionDetails!.status) + .json(context.rejectionDetails!.payload); + return; + } } - const eventPayload = request.body; await this.events.publish({ topic, - eventPayload, + eventPayload: bodyParsed, metadata: request.headers, }); @@ -114,6 +169,6 @@ export class HttpPostIngressEventPublisher { // TODO(pjungermann): We don't really know the externally defined path prefix here, // however it is more useful for users to have it. Is there a better way? - this.logger.info(`Registered /api/events/http${path} to receive events`); + logger.info(`Registered /api/events/http${path} to receive events`); } } diff --git a/plugins/events-node/report.api.md b/plugins/events-node/report.api.md index a9e028e049..0b9c31090a 100644 --- a/plugins/events-node/report.api.md +++ b/plugins/events-node/report.api.md @@ -3,6 +3,8 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// + import { AuthService } from '@backstage/backend-plugin-api'; import { DiscoveryService } from '@backstage/backend-plugin-api'; import { LifecycleService } from '@backstage/backend-plugin-api'; @@ -114,10 +116,14 @@ export interface HttpPostIngressOptions { validator?: RequestValidator; } -// @public (undocumented) +// @public export interface RequestDetails { body: unknown; headers: Record; + raw: { + body: Buffer; + encoding: BufferEncoding; + }; } // @public diff --git a/plugins/events-node/src/api/http/validation/RequestDetails.ts b/plugins/events-node/src/api/http/validation/RequestDetails.ts index 83c2669503..fc99b6a228 100644 --- a/plugins/events-node/src/api/http/validation/RequestDetails.ts +++ b/plugins/events-node/src/api/http/validation/RequestDetails.ts @@ -15,6 +15,8 @@ */ /** + * View on an incoming request that has to be validated. + * * @public */ export interface RequestDetails { @@ -26,4 +28,18 @@ export interface RequestDetails { * Key-value pairs of header names and values. Header names are lower-cased. */ headers: Record; + /** + * Raw request details. + */ + raw: { + /** + * Raw request body (buffer). + */ + body: Buffer; + /** + * Encoding of the raw request body. + * Can be used to decode the raw request body like `raw.body.toString(raw.encoding)`. + */ + encoding: BufferEncoding; + }; } diff --git a/yarn.lock b/yarn.lock index 22aaacbd73..f57d896cba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6599,7 +6599,9 @@ __metadata: "@backstage/plugin-events-node": "workspace:^" "@backstage/repo-tools": "workspace:^" "@backstage/types": "workspace:^" + "@types/content-type": ^1.1.8 "@types/express": ^4.17.6 + content-type: ^1.0.5 express: ^4.17.1 express-promise-router: ^4.1.0 knex: ^3.0.0 @@ -17843,6 +17845,13 @@ __metadata: languageName: node linkType: hard +"@types/content-type@npm:^1.1.8": + version: 1.1.8 + resolution: "@types/content-type@npm:1.1.8" + checksum: 2dd15e51925db7208b0d989c3a93d805a0e5e0942aa9edd70a1c3520896b772526d8280e344a674ae68a96a24aa8fce290843a07512460176f36a3020d99c792 + languageName: node + linkType: hard + "@types/cookie-parser@npm:^1.4.2": version: 1.4.7 resolution: "@types/cookie-parser@npm:1.4.7" From 5838d47e54a293c978a90cad8c3e8e0041f50d1c Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 25 Oct 2024 11:18:41 +0200 Subject: [PATCH 024/237] test: use extra project in tests Signed-off-by: Benjamin Janssens --- .../BitbucketCloudEntityProvider.test.ts | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts index 4d5103a718..fe460b8d8b 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts @@ -320,6 +320,9 @@ describe('BitbucketCloudEntityProvider', () => { { key: 'TEST', }, + { + key: 'TEST2', + }, ], }; return res(ctx.json(response)); @@ -460,6 +463,27 @@ describe('BitbucketCloudEntityProvider', () => { }, locationKey: 'bitbucketCloud-provider:myProvider', }, + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Location', + metadata: { + annotations: { + 'backstage.io/managed-by-location': `url:${url}`, + 'backstage.io/managed-by-origin-location': `url:${url}`, + 'bitbucket.org/repo-url': + 'https://bitbucket.org/test-ws/test-repo2', + }, + name: 'generated-7c2e6263b6cc2d14e69fd4d029afba601ad6dc3b', + }, + spec: { + presence: 'required', + target: `${url}`, + type: 'url', + }, + }, + locationKey: 'bitbucketCloud-provider:myProvider', + }, ]; expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); @@ -526,6 +550,9 @@ describe('BitbucketCloudEntityProvider', () => { { key: 'TEST', }, + { + key: 'TEST2', + }, ], }; return res(ctx.json(response)); @@ -535,7 +562,11 @@ describe('BitbucketCloudEntityProvider', () => { `https://api.bitbucket.org/2.0/workspaces/test-ws/search/code`, (req, res, ctx) => { const query = req.url.searchParams.get('search_query'); - if (!query || !query.includes('repo:test-repo')) { + if ( + !query || + !query.includes('repo:test-repo') || + !query.includes('project:TEST') + ) { return res(ctx.json({ values: [] })); } @@ -612,6 +643,10 @@ describe('BitbucketCloudEntityProvider', () => { entity: addedModule, locationKey: 'bitbucketCloud-provider:myProvider', }, + { + entity: addedModule, + locationKey: 'bitbucketCloud-provider:myProvider', + }, ]; const removedEntities = [ { From adde47f07648fe238dfa3939dd6e8c5df986c645 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 25 Oct 2024 11:27:34 +0200 Subject: [PATCH 025/237] fix(plugin-catalog): add searchable column for displayName Signed-off-by: Julien --- .../catalog/src/components/CatalogTable/columns.tsx | 10 ++++++++++ .../CatalogTable/defaultCatalogTableColumnsFunc.tsx | 10 +++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/plugins/catalog/src/components/CatalogTable/columns.tsx b/plugins/catalog/src/components/CatalogTable/columns.tsx index d5362badda..fc804fc22f 100644 --- a/plugins/catalog/src/components/CatalogTable/columns.tsx +++ b/plugins/catalog/src/components/CatalogTable/columns.tsx @@ -180,6 +180,16 @@ export const columnFactories = Object.freeze({ searchable: true, }; }, + createDisplayNameColumn(options?: { + hidden?: boolean; + }): TableColumn { + return { + title: 'Display Name', + field: 'entity.spec.profile.displayName', + hidden: options?.hidden, + searchable: true, + }; + }, createLabelColumn( key: string, options?: { title?: string; defaultValue?: string }, diff --git a/plugins/catalog/src/components/CatalogTable/defaultCatalogTableColumnsFunc.tsx b/plugins/catalog/src/components/CatalogTable/defaultCatalogTableColumnsFunc.tsx index 54dd701770..f5f41c1e32 100644 --- a/plugins/catalog/src/components/CatalogTable/defaultCatalogTableColumnsFunc.tsx +++ b/plugins/catalog/src/components/CatalogTable/defaultCatalogTableColumnsFunc.tsx @@ -46,11 +46,19 @@ export const defaultCatalogTableColumnsFunc: CatalogTableColumnsFunc = ({ ]; switch (filters.kind?.value) { case 'user': - return [...descriptionTagColumns]; + return [ + columnFactories.createDisplayNameColumn({ hidden: true }), + ...descriptionTagColumns, + ]; case 'domain': case 'system': return [columnFactories.createOwnerColumn(), ...descriptionTagColumns]; case 'group': + return [ + columnFactories.createDisplayNameColumn({ hidden: true }), + columnFactories.createSpecTypeColumn({ hidden: !showTypeColumn }), + ...descriptionTagColumns, + ]; case 'template': return [ columnFactories.createSpecTypeColumn({ hidden: !showTypeColumn }), From b5c8fb4aafa35a9d9ea1efaeab88c645db946b4a Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 25 Oct 2024 11:28:46 +0200 Subject: [PATCH 026/237] fix(plugin-catalog-react): add partial match text filter for displayName Signed-off-by: Julien --- plugins/catalog-react/src/filters.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/catalog-react/src/filters.ts b/plugins/catalog-react/src/filters.ts index 08be27a561..6c68dad9aa 100644 --- a/plugins/catalog-react/src/filters.ts +++ b/plugins/catalog-react/src/filters.ts @@ -94,6 +94,7 @@ export class EntityTextFilter implements EntityFilter { const partialMatch = this.toUpperArray([ entity.metadata.name, entity.metadata.title, + (entity.spec?.profile as { displayName?: string })?.displayName, ]); for (const word of words) { From e3f5e20e9493818d033773a67f58c32b7f738d8f Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 25 Oct 2024 11:29:47 +0200 Subject: [PATCH 027/237] fix(plugin-catalog-backend): lowercase text filter fields to match database values Signed-off-by: Julien --- .../catalog-backend/src/service/DefaultEntitiesCatalog.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts index c71f242969..bb46691e76 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts @@ -422,7 +422,11 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { } else { const matchQuery = db('search') .select('search.entity_id') - .whereIn('key', textFilterFields) + // textFilterFields must be lowercased to match searchable keys in database, i.e. spec.profile.displayName -> spec.profile.displayname + .whereIn( + 'key', + textFilterFields.map(field => field.toLocaleLowerCase('en-US')), + ) .andWhere(function keyFilter() { this.andWhereRaw( 'value like ?', From db7ea24112afdb9d606a70f847f0b929b406dd89 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 25 Oct 2024 11:59:50 +0200 Subject: [PATCH 028/237] test(catalog): add test for search by displayName Signed-off-by: Julien --- .../service/DefaultEntitiesCatalog.test.ts | 55 ++++++++++++++++++- plugins/catalog-react/src/filters.test.ts | 19 +++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts index 92acd9e3ce..da84103e86 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts @@ -1124,7 +1124,6 @@ describe('DefaultEntitiesCatalog', () => { const request: QueryEntitiesInitialRequest = { filter, limit: 100, - orderFields: [{ field: 'metadata.name', order: 'asc' }], fullTextFilter: { term: 'cAt ' }, credentials: mockCredentials.none(), @@ -1141,6 +1140,60 @@ describe('DefaultEntitiesCatalog', () => { }, ); + it.each(databases.eachSupportedId())( + 'should filter the results when query is provided with fullTextFilter for camelCase fields, %p', + async databaseId => { + await createDatabase(databaseId); + + const entities: Entity[] = [ + { + apiVersion: 'a', + kind: 'k', + metadata: { + name: 'camelCase', + }, + spec: { + shouldSearchCamelCase: 'searched', + }, + }, + ]; + + const notFoundEntities: Entity[] = [ + { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'something' }, + spec: {}, + }, + ]; + + await Promise.all( + entities.concat(notFoundEntities).map(e => addEntityToSearch(e)), + ); + + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: mockServices.logger.mock(), + stitcher, + }); + + const request: QueryEntitiesInitialRequest = { + limit: 100, + orderFields: [{ field: 'metadata.name', order: 'asc' }], + fullTextFilter: { + term: 'sear', + fields: ['spec.shouldSearchCamelCase'], + }, + credentials: mockCredentials.none(), + }; + const response = await catalog.queryEntities(request); + expect(response.items).toEqual(entities); + expect(response.pageInfo.nextCursor).toBeUndefined(); + expect(response.pageInfo.prevCursor).toBeUndefined(); + expect(response.totalItems).toBe(1); + }, + ); + it.each(databases.eachSupportedId())( 'should filter the text results when sortOrder is not provided, %p', async databaseId => { diff --git a/plugins/catalog-react/src/filters.test.ts b/plugins/catalog-react/src/filters.test.ts index 8ddac7e53b..332f4c22c1 100644 --- a/plugins/catalog-react/src/filters.test.ts +++ b/plugins/catalog-react/src/filters.test.ts @@ -41,6 +41,18 @@ const entities: Entity[] = [ tags: ['gRPC', 'java'], }, }, + { + apiVersion: '1', + kind: 'User', + metadata: { + name: 'user', + }, + spec: { + profile: { + displayName: 'John Doe', + }, + }, + }, ]; const templates: TemplateEntityV1beta3[] = [ @@ -79,6 +91,13 @@ describe('EntityTextFilter', () => { expect(filter.filterEntity(entities[1])).toBeFalsy(); }); + it('should search displayName', () => { + const filter = new EntityTextFilter('John D'); + expect(filter.filterEntity(entities[0])).toBeFalsy(); + expect(filter.filterEntity(entities[1])).toBeFalsy(); + expect(filter.filterEntity(entities[2])).toBeTruthy(); + }); + it('should search template title', () => { const filter = new EntityTextFilter('spring'); expect(filter.filterEntity(templates[0])).toBeFalsy(); From 1bf02cc6ababf619482dc7f42d26da1120f60c12 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 25 Oct 2024 12:11:46 +0200 Subject: [PATCH 029/237] chore: add changeset Signed-off-by: Julien --- .changeset/sixty-sheep-drive.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/sixty-sheep-drive.md diff --git a/.changeset/sixty-sheep-drive.md b/.changeset/sixty-sheep-drive.md new file mode 100644 index 0000000000..0b96248c93 --- /dev/null +++ b/.changeset/sixty-sheep-drive.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-catalog': patch +--- + +Fixed bug when searching an entity by `spec.profile.displayName` in the catalog on the frontend. Text filter fields were not applied correctly to the database query resulting in empty results. From 9f642dbdb8cd4052283c1e7adb421ce70dc176c0 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 25 Oct 2024 13:17:13 +0200 Subject: [PATCH 030/237] chore: generate api-reports Signed-off-by: Julien --- plugins/catalog/report.api.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/catalog/report.api.md b/plugins/catalog/report.api.md index d817118e3a..9eeae32fb2 100644 --- a/plugins/catalog/report.api.md +++ b/plugins/catalog/report.api.md @@ -181,6 +181,13 @@ export const CatalogTable: { } | undefined, ): TableColumn; + createDisplayNameColumn( + options?: + | { + hidden?: boolean | undefined; + } + | undefined, + ): TableColumn; createLabelColumn( key: string, options?: From db507dd9e6a86b5ff0b6f5da3e7ee4c393c35168 Mon Sep 17 00:00:00 2001 From: Stephen Glass Date: Mon, 28 Oct 2024 16:23:13 -0400 Subject: [PATCH 031/237] address comments Signed-off-by: Stephen Glass --- .changeset/lemon-gifts-crash.md | 3 +-- plugins/scaffolder-backend/src/service/router.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.changeset/lemon-gifts-crash.md b/.changeset/lemon-gifts-crash.md index cd473f6c58..c4ab159dc8 100644 --- a/.changeset/lemon-gifts-crash.md +++ b/.changeset/lemon-gifts-crash.md @@ -1,8 +1,7 @@ --- '@backstage/plugin-scaffolder-common': patch '@backstage/plugin-scaffolder-react': patch -'@backstage/plugin-scaffolder-backend': patch -'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder': minor --- Add scaffolder permission `scaffolder.template.management` for accessing the template management features diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 59f52af0c4..35dacf6559 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -46,7 +46,6 @@ import { taskCancelPermission, taskCreatePermission, taskReadPermission, - templateManagementPermission, templateParameterReadPermission, templateStepReadPermission, } from '@backstage/plugin-scaffolder-common/alpha'; @@ -767,7 +766,7 @@ export async function createRouter( const credentials = await httpAuth.credentials(req); await checkPermission({ credentials, - permissions: [taskCreatePermission, templateManagementPermission], + permissions: [taskCreatePermission], permissionService: permissions, }); From 50df3c87eb1598efc1376eba22a160405c303497 Mon Sep 17 00:00:00 2001 From: Yash Oswal Date: Tue, 29 Oct 2024 11:13:10 +0530 Subject: [PATCH 032/237] feat(catalog): Implement breadcrumbs for entity navigation (#26898) updadted tests Signed-off-by: Yash Oswal --- .changeset/afraid-carrots-greet.md | 7 ++ .../components/catalog/EntityPage.test.tsx | 4 +- .../app/src/components/catalog/EntityPage.tsx | 17 ++-- plugins/catalog/report.api.md | 1 + .../EntityLayout/EntityLayout.test.tsx | 32 +++++++ .../components/EntityLayout/EntityLayout.tsx | 92 ++++++++++++++++++- 6 files changed, 139 insertions(+), 14 deletions(-) create mode 100644 .changeset/afraid-carrots-greet.md diff --git a/.changeset/afraid-carrots-greet.md b/.changeset/afraid-carrots-greet.md new file mode 100644 index 0000000000..9f77cba9b8 --- /dev/null +++ b/.changeset/afraid-carrots-greet.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog': minor +--- + +- Updated EntityLayout component to implement breadcrumb navigation based on the entity relations. + +- Added parentEntityRelations prop to EntityLayoutProps to specify relation types for parent entities. diff --git a/packages/app/src/components/catalog/EntityPage.test.tsx b/packages/app/src/components/catalog/EntityPage.test.tsx index 0943a82efb..06a568279b 100644 --- a/packages/app/src/components/catalog/EntityPage.test.tsx +++ b/packages/app/src/components/catalog/EntityPage.test.tsx @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { EntityLayout, catalogPlugin } from '@backstage/plugin-catalog'; import { EntityProvider, starredEntitiesApiRef, MockStarredEntitiesApi, + catalogApiRef, } from '@backstage/plugin-catalog-react'; import { permissionApiRef } from '@backstage/plugin-permission-react'; import { @@ -28,6 +28,7 @@ import { } from '@backstage/test-utils'; import React from 'react'; import { cicdContent } from './EntityPage'; +import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; describe('EntityPage Test', () => { const entity = { @@ -55,6 +56,7 @@ describe('EntityPage Test', () => { apis={[ [starredEntitiesApiRef, new MockStarredEntitiesApi()], [permissionApiRef, mockApis.permission()], + [catalogApiRef, catalogApiMock()], ]} > diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index fe5a7e0ace..11fc60bd0e 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -85,15 +85,14 @@ const customEntityFilterKind = ['Component', 'API', 'System']; const EntityLayoutWrapper = (props: { children?: ReactNode }) => { return ( - <> - - {props.children} - - + + {props.children} + ); }; diff --git a/plugins/catalog/report.api.md b/plugins/catalog/report.api.md index d817118e3a..ffb46afc6e 100644 --- a/plugins/catalog/report.api.md +++ b/plugins/catalog/report.api.md @@ -414,6 +414,7 @@ export interface EntityLayoutProps { children?: React_2.ReactNode; // (undocumented) NotFoundComponent?: React_2.ReactNode; + parentEntityRelations?: string[]; // Warning: (ae-forgotten-export) The symbol "EntityContextMenuOptions" needs to be exported by the entry point index.d.ts // // (undocumented) diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx index 239b081051..ff38e53d96 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx @@ -169,6 +169,38 @@ describe('EntityLayout', () => { expect(screen.queryByText('tabbed-test-content')).not.toBeInTheDocument(); }); + it('renders the breadcrumbs if defined', async () => { + const mockEntityWithRelation = { + kind: 'MyKind', + metadata: { + name: 'my-entity', + namespace: 'default', + title: 'My Entity', + }, + relations: [{ type: 'partOf', targetRef: 'system:default/my-system' }], + } as Entity; + + await renderInTestApp( + + + + +
tabbed-test-content
+
+
+
+
, + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + '/catalog': rootRouteRef, + }, + }, + ); + + expect(screen.getByText('my-system')).toBeInTheDocument(); + }); + it('navigates when user clicks different tab', async () => { await renderInTestApp( diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx index e8b957497d..c06344c2a6 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx @@ -15,11 +15,13 @@ */ import { - Entity, DEFAULT_NAMESPACE, + Entity, + EntityRelation, RELATION_OWNED_BY, } from '@backstage/catalog-model'; import { + Breadcrumbs, Content, Header, HeaderLabel, @@ -32,12 +34,16 @@ import { import { attachComponentData, IconComponent, + useApi, useElementFilter, useRouteRef, useRouteRefParams, } from '@backstage/core-plugin-api'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import { + catalogApiRef, EntityDisplayName, + EntityRefLink, EntityRefLinks, entityRouteRef, FavoriteEntity, @@ -47,14 +53,15 @@ import { useAsyncEntity, } from '@backstage/plugin-catalog-react'; import Box from '@material-ui/core/Box'; +import { makeStyles } from '@material-ui/core/styles'; import { TabProps } from '@material-ui/core/Tab'; import Alert from '@material-ui/lab/Alert'; import React, { useEffect, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; -import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; -import { rootRouteRef, unregisterRedirectRouteRef } from '../../routes'; +import useAsync from 'react-use/esm/useAsync'; import { catalogTranslationRef } from '../../alpha/translation'; -import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { rootRouteRef, unregisterRedirectRouteRef } from '../../routes'; +import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; /** @public */ export type EntityLayoutRouteProps = { @@ -101,6 +108,7 @@ function headerProps( const namespace = paramNamespace ?? entity?.metadata.namespace ?? ''; const name = entity?.metadata.title ?? paramName ?? entity?.metadata.name ?? ''; + return { headerTitle: `${name}${ namespace && namespace !== DEFAULT_NAMESPACE ? ` in ${namespace}` : '' @@ -167,8 +175,49 @@ export interface EntityLayoutProps { UNSTABLE_contextMenuOptions?: EntityContextMenuOptions; children?: React.ReactNode; NotFoundComponent?: React.ReactNode; + /** + * An array of relation types used to determine the parent entities in the hierarchy. + * These relations are prioritized in the order provided, allowing for flexible + * navigation through entity relationships. + * + * For example, use relation types like `["partOf", "memberOf", "ownedBy"]` to define how the entity is related to + * its parents in the Entity Catalog. + * + * It adds breadcrumbs in the Entity page to enhance user navigation and context awareness. + */ + parentEntityRelations?: string[]; } +function findParentRelation( + entityRelations: EntityRelation[] = [], + relationTypes: string[] = [], +) { + for (const type of relationTypes) { + const foundRelation = entityRelations.find( + relation => relation.type === type, + ); + if (foundRelation) { + return foundRelation; // Return the first found relation and stop + } + } + return null; +} + +const useStyles = makeStyles(theme => ({ + breadcrumbs: { + color: theme.page.fontColor, + fontSize: theme.typography.caption.fontSize, + textTransform: 'uppercase', + marginTop: theme.spacing(1), + opacity: 0.8, + '& span ': { + color: theme.page.fontColor, + textDecoration: 'underline', + textUnderlineOffset: '3px', + }, + }, +})); + /** * EntityLayout is a compound component, which allows you to define a layout for * entities using a sub-navigation mechanism. @@ -192,7 +241,9 @@ export const EntityLayout = (props: EntityLayoutProps) => { UNSTABLE_contextMenuOptions, children, NotFoundComponent, + parentEntityRelations, } = props; + const classes = useStyles(); const { kind, namespace, name } = useRouteRefParams(entityRouteRef); const { entity, loading, error } = useAsyncEntity(); const location = useLocation(); @@ -247,6 +298,22 @@ export const EntityLayout = (props: EntityLayoutProps) => { ); }; + const parentEntity = findParentRelation( + entity?.relations ?? [], + parentEntityRelations ?? [], + ); + + const catalogApi = useApi(catalogApiRef); + const { value: ancestorEntity } = useAsync(async () => { + if (parentEntity) { + return findParentRelation( + (await catalogApi.getEntityByRef(parentEntity?.targetRef))?.relations, + parentEntityRelations, + ); + } + return null; + }, [parentEntity]); + // Make sure to close the dialog if the user clicks links in it that navigate // to another entity. useEffect(() => { @@ -261,6 +328,23 @@ export const EntityLayout = (props: EntityLayoutProps) => { title={} pageTitleOverride={headerTitle} type={headerType} + subtitle={ + parentEntity && ( + + {ancestorEntity && ( + + )} + + {name} + + ) + } > {entity && ( <> From 1a1e2f431618513ad7cd766a1fc3dde6889361fb Mon Sep 17 00:00:00 2001 From: Stephen Glass Date: Wed, 30 Oct 2024 18:41:21 -0400 Subject: [PATCH 033/237] fix collator text formatting for entities without description Signed-off-by: Stephen Glass --- .changeset/three-parrots-dress.md | 5 ++++ ...ltCatalogCollatorEntityTransformer.test.ts | 29 +++++++++++++++++++ ...defaultCatalogCollatorEntityTransformer.ts | 4 ++- 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 .changeset/three-parrots-dress.md diff --git a/.changeset/three-parrots-dress.md b/.changeset/three-parrots-dress.md new file mode 100644 index 0000000000..836abbf21c --- /dev/null +++ b/.changeset/three-parrots-dress.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-module-catalog': patch +--- + +Fix search collator text formatting for catalog entities without description diff --git a/plugins/search-backend-module-catalog/src/collators/defaultCatalogCollatorEntityTransformer.test.ts b/plugins/search-backend-module-catalog/src/collators/defaultCatalogCollatorEntityTransformer.test.ts index 3fe6a9c3e5..b0b7465f5b 100644 --- a/plugins/search-backend-module-catalog/src/collators/defaultCatalogCollatorEntityTransformer.test.ts +++ b/plugins/search-backend-module-catalog/src/collators/defaultCatalogCollatorEntityTransformer.test.ts @@ -147,5 +147,34 @@ describe('DefaultCatalogCollatorEntityTransformer', () => { owner: '', }); }); + + it('sets text correctly when description is not provided', async () => { + const userEntityWithoutDescription = { + ...userEntity, + metadata: { + ...userEntity.metadata, + description: undefined, + }, + spec: { + profile: { + ...userEntity.spec.profile, + email: undefined, + }, + }, + }; + + const document = defaultCatalogCollatorEntityTransformer( + userEntityWithoutDescription, + ); + + expect(document).toMatchObject({ + title: userEntity.metadata.name, + text: userEntity.spec.profile.displayName, + namespace: 'default', + componentType: 'other', + lifecycle: '', + owner: '', + }); + }); }); }); diff --git a/plugins/search-backend-module-catalog/src/collators/defaultCatalogCollatorEntityTransformer.ts b/plugins/search-backend-module-catalog/src/collators/defaultCatalogCollatorEntityTransformer.ts index db239b8669..8acc3e24b8 100644 --- a/plugins/search-backend-module-catalog/src/collators/defaultCatalogCollatorEntityTransformer.ts +++ b/plugins/search-backend-module-catalog/src/collators/defaultCatalogCollatorEntityTransformer.ts @@ -19,7 +19,9 @@ import { CatalogCollatorEntityTransformer } from './CatalogCollatorEntityTransfo const getDocumentText = (entity: Entity): string => { const documentTexts: string[] = []; - documentTexts.push(entity.metadata.description || ''); + if (entity.metadata.description) { + documentTexts.push(entity.metadata.description); + } if (isUserEntity(entity) || isGroupEntity(entity)) { if (entity.spec?.profile?.displayName) { From c66fe900d553e54f420cc9ba3fa9b3620012a349 Mon Sep 17 00:00:00 2001 From: Ethan <182492+ethanwillis@users.noreply.github.com> Date: Wed, 30 Oct 2024 19:29:37 -0500 Subject: [PATCH 034/237] Update incorrect comments about optional app options for createApp(...) Signed-off-by: Ethan <182492+ethanwillis@users.noreply.github.com> --- packages/app-defaults/src/createApp.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-defaults/src/createApp.tsx b/packages/app-defaults/src/createApp.tsx index 6cac337d22..1d400aff7a 100644 --- a/packages/app-defaults/src/createApp.tsx +++ b/packages/app-defaults/src/createApp.tsx @@ -86,7 +86,7 @@ export type OptionalAppOptions = { /** * A set of components to override the default components with. * - * The override is applied for each icon individually. + * The override is applied for each app component individually. * * @public */ From f32a2d3198debb7a0e5b19dde3c494711e4abf7f Mon Sep 17 00:00:00 2001 From: Paulo Eduardo Peixoto Date: Fri, 1 Nov 2024 11:19:20 -0300 Subject: [PATCH 035/237] docs(docs/features/software-templates/writing-custom-field-extensions.md): add pending imports. Signed-off-by: Paulo Eduardo Peixoto --- .../software-templates/writing-custom-field-extensions.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/features/software-templates/writing-custom-field-extensions.md b/docs/features/software-templates/writing-custom-field-extensions.md index feb0f68048..7f6e4b8796 100644 --- a/docs/features/software-templates/writing-custom-field-extensions.md +++ b/docs/features/software-templates/writing-custom-field-extensions.md @@ -33,6 +33,12 @@ import React from 'react'; import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react'; import type { FieldValidation } from '@rjsf/utils'; import FormControl from '@material-ui/core/FormControl'; +import { + FormControl, + FormHelperText, + Input, + InputLabel, +} from '@material-ui/core'; /* This is the actual component that will get rendered in the form */ From b89834bfa65c48bca616e8894cb1ad55faf0e26f Mon Sep 17 00:00:00 2001 From: Jordan Slott Date: Thu, 31 Oct 2024 17:03:55 -0400 Subject: [PATCH 036/237] Fixes #27325 Stitch entity for which target of relationship has changed Signed-off-by: Jordan Slott --- .changeset/short-pots-remember.md | 5 + .../DefaultCatalogProcessingEngine.test.ts | 105 ++++++++++++++++++ .../DefaultCatalogProcessingEngine.ts | 8 +- 3 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 .changeset/short-pots-remember.md diff --git a/.changeset/short-pots-remember.md b/.changeset/short-pots-remember.md new file mode 100644 index 0000000000..9a30bc222c --- /dev/null +++ b/.changeset/short-pots-remember.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fixed an issue where entities would not be marked for restitching if only the target of a relationship changed. diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.test.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.test.ts index 3f283a5b38..de7c3600c2 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.test.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.test.ts @@ -464,6 +464,111 @@ describe('DefaultCatalogProcessingEngine', () => { await engine.stop(); }); + it('should stitch both the previous and new sources when relation target changes', async () => { + const engine = new DefaultCatalogProcessingEngine({ + config: new ConfigReader({}), + logger: mockServices.logger.mock(), + processingDatabase: db, + knex: {} as any, + orchestrator: orchestrator, + stitcher: stitcher, + createHash: () => hash, + pollingIntervalMs: 100, + }); + + db.transaction.mockImplementation(cb => cb((() => {}) as any)); + + const entity = { + apiVersion: '1', + kind: 'k', + metadata: { name: 'me', namespace: 'ns' }, + }; + const processableEntity = { + entityRef: 'foo', + id: '1', + unprocessedEntity: entity, + resultHash: '', + state: [] as any, + nextUpdateAt: DateTime.now(), + lastDiscoveryAt: DateTime.now(), + }; + + db.listParents.mockResolvedValue({ entityRefs: [] }); + db.getProcessableEntities + .mockResolvedValueOnce({ + items: [processableEntity], + }) + .mockResolvedValueOnce({ + items: [processableEntity], + }); + db.updateProcessedEntity + .mockImplementationOnce(async () => ({ + previous: { relations: [] }, + })) + .mockImplementationOnce(async () => ({ + previous: { + relations: [ + { + originating_entity_id: '', + type: 't', + source_entity_ref: 'k:ns/other1', + target_entity_ref: 'k:ns/me', + }, + ], + }, + })); + + orchestrator.process + .mockResolvedValueOnce({ + ok: true, + completedEntity: entity, + relations: [ + { + type: 't', + source: { kind: 'k', namespace: 'ns', name: 'other1' }, + target: { kind: 'k', namespace: 'ns', name: 'me' }, + }, + ], + errors: [], + deferredEntities: [], + state: {}, + refreshKeys: [], + }) + .mockResolvedValueOnce({ + ok: true, + completedEntity: entity, + // change just the target of the relationship to a new entity, + // leaving the source and relation type the same. + // see: https://github.com/backstage/backstage/issues/27325 + relations: [ + { + type: 't', + source: { kind: 'k', namespace: 'ns', name: 'other1' }, + target: { kind: 'k', namespace: 'ns', name: 'newtarget' }, + }, + ], + errors: [], + deferredEntities: [], + state: {}, + refreshKeys: [], + }); + + await engine.start(); + await waitForExpect(() => { + expect(stitcher.stitch).toHaveBeenCalledTimes(2); + }); + expect([...stitcher.stitch.mock.calls[0][0].entityRefs!]).toEqual( + expect.arrayContaining(['k:ns/me', 'k:ns/other1']), + ); + // As a result of switching the relationship for source other1 to + // a new target entity, the other1 relationship source must be + // restitched. + expect([...stitcher.stitch.mock.calls[1][0].entityRefs!]).toEqual( + expect.arrayContaining(['k:ns/me', 'k:ns/other1']), + ); + await engine.stop(); + }); + it('should not stitch sources entities when relations are the same', async () => { const engine = new DefaultCatalogProcessingEngine({ config: new ConfigReader({}), diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts index e42847bafe..0f7acba652 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts @@ -295,7 +295,7 @@ export class DefaultCatalogProcessingEngine { }); oldRelationSources = new Map( previous.relations.map(r => [ - `${r.source_entity_ref}:${r.type}`, + `${r.source_entity_ref}:${r.type}->${r.target_entity_ref}`, r.source_entity_ref, ]), ); @@ -304,7 +304,11 @@ export class DefaultCatalogProcessingEngine { const newRelationSources = new Map( result.relations.map(relation => { const sourceEntityRef = stringifyEntityRef(relation.source); - return [`${sourceEntityRef}:${relation.type}`, sourceEntityRef]; + const targetEntityRef = stringifyEntityRef(relation.target); + return [ + `${sourceEntityRef}:${relation.type}->${targetEntityRef}`, + sourceEntityRef, + ]; }), ); From 69208ae3ed649dec61ce87ccb1b791ddca1cd7b8 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Mon, 4 Nov 2024 14:32:51 +0530 Subject: [PATCH 037/237] fix(catalog-react) : fixing owner text overflow Signed-off-by: its-mitesh-kumar --- .../EntityDisplayName/EntityDisplayName.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-react/src/components/EntityDisplayName/EntityDisplayName.tsx b/plugins/catalog-react/src/components/EntityDisplayName/EntityDisplayName.tsx index 68d5ecefdb..7a00ff3809 100644 --- a/plugins/catalog-react/src/components/EntityDisplayName/EntityDisplayName.tsx +++ b/plugins/catalog-react/src/components/EntityDisplayName/EntityDisplayName.tsx @@ -77,7 +77,16 @@ export const EntityDisplayName = ( ); // The innermost "body" content - let content = <>{primaryTitle}; + let content = ( +
+ {primaryTitle} +
+ ); // Optionally an icon, and wrapper around them both content = ( From 9cc82c02bff06cae796f572001825e3516c27136 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Mon, 4 Nov 2024 14:50:51 +0530 Subject: [PATCH 038/237] fix(catalog-react) : adding changeset file Signed-off-by: its-mitesh-kumar --- .changeset/healthy-planets-confess.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/healthy-planets-confess.md diff --git a/.changeset/healthy-planets-confess.md b/.changeset/healthy-planets-confess.md new file mode 100644 index 0000000000..a55b5c1044 --- /dev/null +++ b/.changeset/healthy-planets-confess.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Fixed bug in `EntityDisplayName` where text was overflowing. From 1b23511acdc041d3a38516e6cf150c9d245e887e Mon Sep 17 00:00:00 2001 From: Paulo Eduardo Peixoto Date: Mon, 4 Nov 2024 08:39:21 -0300 Subject: [PATCH 039/237] docs(docs/features/software-templates/writing-custom-field-extensions.md): remove import from "@material-ui/core/FormControl". Signed-off-by: Paulo Eduardo Peixoto --- .../software-templates/writing-custom-field-extensions.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/features/software-templates/writing-custom-field-extensions.md b/docs/features/software-templates/writing-custom-field-extensions.md index 7f6e4b8796..7431b91e64 100644 --- a/docs/features/software-templates/writing-custom-field-extensions.md +++ b/docs/features/software-templates/writing-custom-field-extensions.md @@ -32,7 +32,6 @@ As an example, we will create a component that validates whether a string is in import React from 'react'; import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react'; import type { FieldValidation } from '@rjsf/utils'; -import FormControl from '@material-ui/core/FormControl'; import { FormControl, FormHelperText, From 9368cdec1251d11b1f45379c5db682755be91bca Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:20:35 +0000 Subject: [PATCH 040/237] fix(deps): update dependency pg to v8.13.1 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7cc664edc7..4ea884e19f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -37370,8 +37370,8 @@ __metadata: linkType: hard "pg@npm:^8.11.3, pg@npm:^8.9.0": - version: 8.13.0 - resolution: "pg@npm:8.13.0" + version: 8.13.1 + resolution: "pg@npm:8.13.1" dependencies: pg-cloudflare: ^1.1.1 pg-connection-string: ^2.7.0 @@ -37387,7 +37387,7 @@ __metadata: peerDependenciesMeta: pg-native: optional: true - checksum: 81560755ff4ee62b71bf1204dd696f66451574d1db56cbd5aa514ce91c6474030ee8078461b3cb85cce8d2f185be5846e0a7a707a818f5e2e3fb198a7ea795ea + checksum: 22cb97fcbee3348d5ee0b195071cc572f9c88eb40cbb61fe6726af68d55d5962121b2d630509bb907703e1c8bdc33de775462029c5399e2a841fa9e6c9da0242 languageName: node linkType: hard From ac71154cc9ed8ebabecab0b913b16fb14cf07216 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 20:20:50 +0000 Subject: [PATCH 041/237] chore(deps): update actions/setup-python action to v5.3.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/verify_e2e-techdocs.yml | 2 +- .github/workflows/verify_e2e-windows.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/verify_e2e-techdocs.yml b/.github/workflows/verify_e2e-techdocs.yml index 9aad8e9699..b42cae8896 100644 --- a/.github/workflows/verify_e2e-techdocs.yml +++ b/.github/workflows/verify_e2e-techdocs.yml @@ -35,7 +35,7 @@ jobs: egress-policy: audit - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: '3.9' diff --git a/.github/workflows/verify_e2e-windows.yml b/.github/workflows/verify_e2e-windows.yml index 2934874460..34a0672b56 100644 --- a/.github/workflows/verify_e2e-windows.yml +++ b/.github/workflows/verify_e2e-windows.yml @@ -56,7 +56,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: setup python - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: '3.10' From f09a17e509cbe22a12a03e90ba4bc49fb568e874 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 5 Nov 2024 00:18:47 +0000 Subject: [PATCH 042/237] chore(deps): update dependency knip to v5.36.2 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3e6754985d..21c87c1fa3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31975,12 +31975,12 @@ __metadata: languageName: node linkType: hard -"jiti@npm:^2.0.0, jiti@npm:^2.3.3": - version: 2.3.3 - resolution: "jiti@npm:2.3.3" +"jiti@npm:^2.0.0, jiti@npm:^2.4.0": + version: 2.4.0 + resolution: "jiti@npm:2.4.0" bin: jiti: lib/jiti-cli.mjs - checksum: f1a2b87d937569c966f00a8c9153c8f3e02445b31e034fdaed1b9639a4ecfae3c4df24c7644d5b4764c566f8dee09132a5b55cd049b48e618024accd31d8e6b3 + checksum: b7d8c441214e48f6c1be2952a83f40e2b1eb6e94fe81b1fd89370d11a7e322c61eb3fbd9a8d47029e14338414091ebbb575e1a92c645ab30fea6240c5c4957c7 languageName: node linkType: hard @@ -32829,18 +32829,18 @@ __metadata: linkType: hard "knip@npm:^5.0.0": - version: 5.33.3 - resolution: "knip@npm:5.33.3" + version: 5.36.2 + resolution: "knip@npm:5.36.2" dependencies: "@nodelib/fs.walk": 1.2.8 "@snyk/github-codeowners": 1.1.0 easy-table: 1.2.0 enhanced-resolve: ^5.17.1 fast-glob: ^3.3.2 - jiti: ^2.3.3 + jiti: ^2.4.0 js-yaml: ^4.1.0 minimist: ^1.2.8 - picocolors: ^1.0.0 + picocolors: ^1.1.0 picomatch: ^4.0.1 pretty-ms: ^9.0.0 smol-toml: ^1.3.0 @@ -32854,7 +32854,7 @@ __metadata: bin: knip: bin/knip.js knip-bun: bin/knip-bun.js - checksum: 636e26fde892c590a65326d370fc19f1d73575ac014fb84d55d67a71dc05ea0ba2114ae587ba0192232b4ac1094710ee7d9582cf3bbe0773f6c667c0bba9b3e4 + checksum: add434c880a6346f6a660df4961a5e7e6be62aabbc148d8f401ff64423dabd96004ef9b21921682971ade0c7798faee967a9e4832bec647875fccf73674c9f8e languageName: node linkType: hard From 360a2c2c9b48781192f9babfe11a21cf5eb7fddf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 5 Nov 2024 01:30:53 +0000 Subject: [PATCH 043/237] chore(deps): update dependency typescript-eslint to v8.13.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- storybook/yarn.lock | 112 ++++++++++++++++++++++---------------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/storybook/yarn.lock b/storybook/yarn.lock index 2a2371ef8e..1ce8d11ac9 100644 --- a/storybook/yarn.lock +++ b/storybook/yarn.lock @@ -1772,15 +1772,15 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:8.10.0": - version: 8.10.0 - resolution: "@typescript-eslint/eslint-plugin@npm:8.10.0" +"@typescript-eslint/eslint-plugin@npm:8.13.0": + version: 8.13.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.13.0" dependencies: "@eslint-community/regexpp": ^4.10.0 - "@typescript-eslint/scope-manager": 8.10.0 - "@typescript-eslint/type-utils": 8.10.0 - "@typescript-eslint/utils": 8.10.0 - "@typescript-eslint/visitor-keys": 8.10.0 + "@typescript-eslint/scope-manager": 8.13.0 + "@typescript-eslint/type-utils": 8.13.0 + "@typescript-eslint/utils": 8.13.0 + "@typescript-eslint/visitor-keys": 8.13.0 graphemer: ^1.4.0 ignore: ^5.3.1 natural-compare: ^1.4.0 @@ -1791,7 +1791,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 2bb311eb9a882d530fc94f790f3e1f4745cd4e3523fd8d62ee0ed14d65c4230dc0c797c490c3421c1456fd71349e9bfa146c0b78f63860b75aae6e2a32a6c27c + checksum: 42d5c14abdf97167147f3d753398cf62f44c05ae69615c2630720007a87f70aabe0440de744eb1f95eb72a6f5d3943069d4c2e030789590d7ccf7210b39d9db1 languageName: node linkType: hard @@ -1819,21 +1819,21 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/parser@npm:8.10.0": - version: 8.10.0 - resolution: "@typescript-eslint/parser@npm:8.10.0" +"@typescript-eslint/parser@npm:8.13.0": + version: 8.13.0 + resolution: "@typescript-eslint/parser@npm:8.13.0" dependencies: - "@typescript-eslint/scope-manager": 8.10.0 - "@typescript-eslint/types": 8.10.0 - "@typescript-eslint/typescript-estree": 8.10.0 - "@typescript-eslint/visitor-keys": 8.10.0 + "@typescript-eslint/scope-manager": 8.13.0 + "@typescript-eslint/types": 8.13.0 + "@typescript-eslint/typescript-estree": 8.13.0 + "@typescript-eslint/visitor-keys": 8.13.0 debug: ^4.3.4 peerDependencies: eslint: ^8.57.0 || ^9.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 2e38f34d9d044e251450116cc081a8f84ba13183e9c3e1dda919ddc00eebe634a37d4dfd785998f259b64cdd770e863ecc6c5cf7c8f422baf3d2bc2a0f9241cf + checksum: 5e2d5b2eb5a30c4eeb75ab05975fd793c6d809399c5f000a918747283c760201311b1df85a699fd260a3d7cff1be5f39938d59a1d2f8e92141402bf32b4ad748 languageName: node linkType: hard @@ -1864,13 +1864,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.10.0": - version: 8.10.0 - resolution: "@typescript-eslint/scope-manager@npm:8.10.0" +"@typescript-eslint/scope-manager@npm:8.13.0": + version: 8.13.0 + resolution: "@typescript-eslint/scope-manager@npm:8.13.0" dependencies: - "@typescript-eslint/types": 8.10.0 - "@typescript-eslint/visitor-keys": 8.10.0 - checksum: 3df8df342e227b80514dcc9151774dea9a71bc649204f702d5b4a1b76a54b4814c5d5a970a6a9213462dd4df0d42342796fab35549e8663d4c0e5d84bd902bba + "@typescript-eslint/types": 8.13.0 + "@typescript-eslint/visitor-keys": 8.13.0 + checksum: 7c80fddb07b3b4e77f05c3ad8aec9a4dda553638188618bc993352ed2b39a8db464c8f28dad8dfc4d82e06ac793fa83a9983198231a7a4711a0dc6f0955b8ad5 languageName: node linkType: hard @@ -1891,18 +1891,18 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.10.0": - version: 8.10.0 - resolution: "@typescript-eslint/type-utils@npm:8.10.0" +"@typescript-eslint/type-utils@npm:8.13.0": + version: 8.13.0 + resolution: "@typescript-eslint/type-utils@npm:8.13.0" dependencies: - "@typescript-eslint/typescript-estree": 8.10.0 - "@typescript-eslint/utils": 8.10.0 + "@typescript-eslint/typescript-estree": 8.13.0 + "@typescript-eslint/utils": 8.13.0 debug: ^4.3.4 ts-api-utils: ^1.3.0 peerDependenciesMeta: typescript: optional: true - checksum: 8b0cec8cff1926a08c2bd675b24b2ccff36e59a8d9169eed38343f70c4e3bba18796fc39f30a9307ded3f345881aded80dbd6dc1d78b9ae76cff04fbe8708788 + checksum: 98e369a49c4334d8871283f995f010ef38b023f80f922cfef60c21c635cf3a2992ce634613b931de129bb5f4d4939b36025f4cc5aa958bb21fee8eb4d8b78c60 languageName: node linkType: hard @@ -1913,10 +1913,10 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:8.10.0": - version: 8.10.0 - resolution: "@typescript-eslint/types@npm:8.10.0" - checksum: 3839fd43b0f21b432a9f6090a39d5b2254ee48c1eecf14f8f66bea0cbaba9f2f33a7fc78aea37dfe8841442332d0a8f99cc65cd2d01ca43db99550d30d6f7fe8 +"@typescript-eslint/types@npm:8.13.0": + version: 8.13.0 + resolution: "@typescript-eslint/types@npm:8.13.0" + checksum: 361489858f07cba8a331d360d73b51a174a902612fd7bb212560a4d7dc2bd704daf252debc410b09e92217aedca9076c3b2892ec76bcf83a7e1575a175942c2e languageName: node linkType: hard @@ -1938,12 +1938,12 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.10.0": - version: 8.10.0 - resolution: "@typescript-eslint/typescript-estree@npm:8.10.0" +"@typescript-eslint/typescript-estree@npm:8.13.0": + version: 8.13.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.13.0" dependencies: - "@typescript-eslint/types": 8.10.0 - "@typescript-eslint/visitor-keys": 8.10.0 + "@typescript-eslint/types": 8.13.0 + "@typescript-eslint/visitor-keys": 8.13.0 debug: ^4.3.4 fast-glob: ^3.3.2 is-glob: ^4.0.3 @@ -1953,7 +1953,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 3fc774f51d0a891a5e09bc77f5544b6aa268abec9c01cd9ec831f92dde9c9d61a5c818ca2800c124fb5d61d40ce7ac34740b347c21ba3493e756c052084afd65 + checksum: 43d33fa341b44e11f3dcd627ea38ebe4433320e569d4a502e44acb370f3a6f64609cf4f98f874eefc161aa42487e35b6e499e74ec422f3c629c7bba155c3d88a languageName: node linkType: hard @@ -1975,17 +1975,17 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.10.0": - version: 8.10.0 - resolution: "@typescript-eslint/utils@npm:8.10.0" +"@typescript-eslint/utils@npm:8.13.0": + version: 8.13.0 + resolution: "@typescript-eslint/utils@npm:8.13.0" dependencies: "@eslint-community/eslint-utils": ^4.4.0 - "@typescript-eslint/scope-manager": 8.10.0 - "@typescript-eslint/types": 8.10.0 - "@typescript-eslint/typescript-estree": 8.10.0 + "@typescript-eslint/scope-manager": 8.13.0 + "@typescript-eslint/types": 8.13.0 + "@typescript-eslint/typescript-estree": 8.13.0 peerDependencies: eslint: ^8.57.0 || ^9.0.0 - checksum: db67603baacba9cccbbc625801a44e5320bc558be846646ff9962818c64a9ab07edcfdcad98b15a3f8954d3e398e3a41f085c1ec458f7169a1ce7b3674032d59 + checksum: 6d6ec83c4806aeeba94777bf82230a2cde9bd5aa90969ac73cd2e3ba22eb6b1e4f7d3710dbe13a1a1734857c3cd3e8522bb043a04e85cea583c91618a28cc200 languageName: node linkType: hard @@ -1999,13 +1999,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.10.0": - version: 8.10.0 - resolution: "@typescript-eslint/visitor-keys@npm:8.10.0" +"@typescript-eslint/visitor-keys@npm:8.13.0": + version: 8.13.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.13.0" dependencies: - "@typescript-eslint/types": 8.10.0 + "@typescript-eslint/types": 8.13.0 eslint-visitor-keys: ^3.4.3 - checksum: 0b3060a036dd3b6acacc32b1d81b3ada1ac5523cc2d16a369ecffd3ab5b389cd98802b248bf65ee8a266a166125a9e38acd7e917d4dd26044bdf2c805537b7e3 + checksum: eeefa461dbf60c967bcc2905bfd80fd6f5d015e8139c7d7a44a46d8ffa9339089a3a0eb937423e3c59aff306c238ed8821bda935db1da28ae063f2ce1deafe08 languageName: node linkType: hard @@ -5673,16 +5673,16 @@ __metadata: linkType: hard "typescript-eslint@npm:^8.7.0": - version: 8.10.0 - resolution: "typescript-eslint@npm:8.10.0" + version: 8.13.0 + resolution: "typescript-eslint@npm:8.13.0" dependencies: - "@typescript-eslint/eslint-plugin": 8.10.0 - "@typescript-eslint/parser": 8.10.0 - "@typescript-eslint/utils": 8.10.0 + "@typescript-eslint/eslint-plugin": 8.13.0 + "@typescript-eslint/parser": 8.13.0 + "@typescript-eslint/utils": 8.13.0 peerDependenciesMeta: typescript: optional: true - checksum: cf0ead50af444a887097f2ed6cf854c318082e5cebde92c218b7616086b844af0a8e80e8b9e946de7f582ed8ed96934c91d27a816130611217c1e9d34d59f20d + checksum: 9996944f33446b642e017f09387a4c73fe42c8c683dffdfbe5c3342142d3dc30406789aa9def2649003cc9760e0f4cb620fcbdb2df33deeec2f5b5f5987fc407 languageName: node linkType: hard From 6836522a8e936594c33073cacf54594d72f28759 Mon Sep 17 00:00:00 2001 From: luccas Date: Mon, 4 Nov 2024 23:25:29 -0300 Subject: [PATCH 044/237] added pagination to defaultApiExplorerPage Signed-off-by: luccas --- .changeset/angry-bags-compete.md | 5 +++++ .../components/ApiExplorerPage/DefaultApiExplorerPage.tsx | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 .changeset/angry-bags-compete.md diff --git a/.changeset/angry-bags-compete.md b/.changeset/angry-bags-compete.md new file mode 100644 index 0000000000..87e9bf36b8 --- /dev/null +++ b/.changeset/angry-bags-compete.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': minor +--- + +Added support for pagination in api-docs plugin - DefaultApiExplorerPage diff --git a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.tsx b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.tsx index 6b5917c52d..b5a02b2f9e 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.tsx @@ -29,6 +29,7 @@ import { EntityKindPicker, EntityLifecyclePicker, EntityListProvider, + EntityListPagination, EntityOwnerPicker, EntityTagPicker, EntityTypePicker, @@ -62,6 +63,7 @@ export type DefaultApiExplorerPageProps = { columns?: TableColumn[]; actions?: TableProps['actions']; ownerPickerMode?: EntityOwnerPickerProps['mode']; + pagination?: EntityListPagination; }; /** @@ -74,6 +76,7 @@ export const DefaultApiExplorerPage = (props: DefaultApiExplorerPageProps) => { columns, actions, ownerPickerMode, + pagination, } = props; const configApi = useApi(configApiRef); @@ -102,7 +105,7 @@ export const DefaultApiExplorerPage = (props: DefaultApiExplorerPageProps) => { )} All your APIs - +