From 06b924a3d6f093087c5bf3ec07f90b4477eb7d1b Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Tue, 10 Dec 2024 18:29:32 -0700 Subject: [PATCH 01/69] chore: more sidebar consolidation Signed-off-by: aramissennyeydd --- docs/faq/product.md | 2 +- microsite/sidebars.js | 65 +++++++++++++++++++++++-------------------- 2 files changed, 36 insertions(+), 31 deletions(-) diff --git a/docs/faq/product.md b/docs/faq/product.md index edbb1ffb4d..f09b5696b8 100644 --- a/docs/faq/product.md +++ b/docs/faq/product.md @@ -1,6 +1,6 @@ --- id: product -title: Product FAQ +title: Non-technical FAQ description: Questions related to product and design. --- diff --git a/microsite/sidebars.js b/microsite/sidebars.js index e48e3b7b52..0e3837e5bc 100644 --- a/microsite/sidebars.js +++ b/microsite/sidebars.js @@ -4,15 +4,9 @@ module.exports = { docs: { Overview: [ 'overview/what-is-backstage', - 'overview/architecture-overview', - 'overview/roadmap', - 'overview/vision', 'overview/background', - 'overview/adopting', - 'overview/versioning-policy', - 'overview/threat-model', - 'overview/support', - 'overview/logos', + 'overview/vision', + 'overview/roadmap', ], 'Getting Started': [ 'getting-started/index', @@ -46,6 +40,7 @@ module.exports = { 'getting-started/create-a-component', ], }, + 'overview/support', 'getting-started/keeping-backstage-updated', ], 'Core Features': [ @@ -301,6 +296,7 @@ module.exports = { ], Plugins: [ 'plugins/index', + 'overview/architecture-overview', 'plugins/existing-plugins', 'plugins/create-a-plugin', 'plugins/plugin-development', @@ -489,34 +485,40 @@ module.exports = { ], }, ], - 'Designing for Backstage': [ - 'dls/design', - 'dls/component-design-guidelines', - 'dls/contributing-to-storybook', - 'dls/figma', - ], Tutorials: [ - 'tutorials/quickstart-app-plugin', - 'tutorials/react-router-stable-migration', - 'tutorials/react18-migration', - 'tutorials/package-role-migration', - 'tutorials/migrating-away-from-core', - 'tutorials/configuring-plugin-databases', - 'tutorials/switching-sqlite-postgres', - 'tutorials/using-backstage-proxy-within-plugin', - 'tutorials/yarn-migration', - 'tutorials/migrate-to-mui5', - 'tutorials/auth-service-migration', - 'tutorials/enable-public-entry', - 'tutorials/setup-opentelemetry', + { 'Non-technical': ['overview/adopting'] }, + { + Techical: [ + 'tutorials/quickstart-app-plugin', + 'tutorials/react-router-stable-migration', + 'tutorials/react18-migration', + 'tutorials/package-role-migration', + 'tutorials/migrating-away-from-core', + 'tutorials/configuring-plugin-databases', + 'tutorials/switching-sqlite-postgres', + 'tutorials/using-backstage-proxy-within-plugin', + 'tutorials/yarn-migration', + 'tutorials/migrate-to-mui5', + 'tutorials/auth-service-migration', + 'tutorials/enable-public-entry', + 'tutorials/setup-opentelemetry', + 'accessibility/index' + ], + }, ], - - FAQ: ['faq/index', 'faq/product', 'faq/technical'], - Accessibility: ['accessibility/index'], + FAQ: ['faq/index', 'faq/technical', 'faq/product'], Contribute: [ 'contribute/index', 'contribute/getting-involved', 'contribute/project-structure', + { + 'Designing for Backstage': [ + 'dls/design', + 'dls/component-design-guidelines', + 'dls/contributing-to-storybook', + 'dls/figma', + ], + }, ], References: [ { @@ -541,10 +543,13 @@ module.exports = { }, 'api/deprecations', 'references/glossary', + 'overview/threat-model', 'api/utility-apis', + 'overview/versioning-policy', 'reference/index', ], }, + Assets: ['overview/logos'], releases: { 'Release Notes': releases.map(release => `releases/${release}`), }, From 9019646784c930e8f7a003d865aa98a9cf622712 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Wed, 11 Dec 2024 18:03:37 -0700 Subject: [PATCH 02/69] fix prettier Signed-off-by: aramissennyeydd --- microsite/sidebars.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/sidebars.js b/microsite/sidebars.js index 0e3837e5bc..749b0a6a5f 100644 --- a/microsite/sidebars.js +++ b/microsite/sidebars.js @@ -502,7 +502,7 @@ module.exports = { 'tutorials/auth-service-migration', 'tutorials/enable-public-entry', 'tutorials/setup-opentelemetry', - 'accessibility/index' + 'accessibility/index', ], }, ], From e8207106b43b027af80ce39db16e4441e20dfcd3 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Sun, 29 Dec 2024 17:32:41 -0700 Subject: [PATCH 03/69] add security section Signed-off-by: aramissennyeydd --- microsite/sidebars.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/microsite/sidebars.js b/microsite/sidebars.js index 749b0a6a5f..de2333e765 100644 --- a/microsite/sidebars.js +++ b/microsite/sidebars.js @@ -543,7 +543,11 @@ module.exports = { }, 'api/deprecations', 'references/glossary', - 'overview/threat-model', + { + 'Security': [ + 'overview/threat-model', + ] + }, 'api/utility-apis', 'overview/versioning-policy', 'reference/index', From af8975b8abc3371266e4c0fe32d4766069e13026 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Sat, 4 Jan 2025 14:25:40 -0700 Subject: [PATCH 04/69] fix prettier Signed-off-by: aramissennyeydd --- microsite/sidebars.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/microsite/sidebars.js b/microsite/sidebars.js index de2333e765..d1230b9abd 100644 --- a/microsite/sidebars.js +++ b/microsite/sidebars.js @@ -544,9 +544,7 @@ module.exports = { 'api/deprecations', 'references/glossary', { - 'Security': [ - 'overview/threat-model', - ] + Security: ['overview/threat-model'], }, 'api/utility-apis', 'overview/versioning-policy', From 2003fc2360c4987a30a059906791e621dd7f7cca Mon Sep 17 00:00:00 2001 From: Vladimir Kobzev Date: Thu, 16 Jan 2025 10:24:18 +0100 Subject: [PATCH 05/69] Add option to hide text output buttons Signed-off-by: Vladimir Kobzev --- .changeset/brown-cycles-beg.md | 5 +++ plugins/scaffolder-react/src/api/types.ts | 1 + .../DefaultTemplateOutputs.test.tsx | 18 +++++++++++ .../DefaultTemplateOutputs.tsx | 4 ++- .../TemplateOutputs/TextOutputs.tsx | 32 ++++++++++--------- 5 files changed, 44 insertions(+), 16 deletions(-) create mode 100644 .changeset/brown-cycles-beg.md diff --git a/.changeset/brown-cycles-beg.md b/.changeset/brown-cycles-beg.md new file mode 100644 index 0000000000..78e6e3e73f --- /dev/null +++ b/.changeset/brown-cycles-beg.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Adds an option to hide text output buttons diff --git a/plugins/scaffolder-react/src/api/types.ts b/plugins/scaffolder-react/src/api/types.ts index 8e4d989a55..6284e8dcaa 100644 --- a/plugins/scaffolder-react/src/api/types.ts +++ b/plugins/scaffolder-react/src/api/types.ts @@ -89,6 +89,7 @@ export type ScaffolderOutputText = { title?: string; icon?: string; content?: string; + showButton?: boolean; default?: boolean; }; diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx index 7ec3b0867c..d0849f6027 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx @@ -78,4 +78,22 @@ describe('', () => { expect(queryByTestId('output-box')).toBeNull(); expect(queryByTestId('text-output-box')).toBeNull(); }); + it('should not render the link output box when output only contains text with showButton set to false', async () => { + const output = { + text: [ + { title: 'Text 1', content: 'Hello, **world**!', showButton: false }, + { title: 'Text 2', content: 'Hello, **mars**!', showButton: false }, + ], + }; + const { queryByTestId } = await renderInTestApp( + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + expect(queryByTestId('output-box')).toBeNull(); + }); }); diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx index 70f3b1fa59..9788d194ae 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx @@ -56,7 +56,9 @@ export const DefaultTemplateOutputs = (props: { return null; } - const emptyOutput = Object.keys(output).length === 0; + const emptyOutput = + (output.links || []).length === 0 && + Object.values(output.text || {}).filter(o => o.showButton).length === 0; return ( <> diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx index 5da1dab476..373db4d632 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx @@ -39,23 +39,25 @@ export const TextOutputs = (props: { <> {text .filter(({ content }) => content !== undefined) - .map(({ title, icon }, i) => { + .map(({ title, icon, showButton = true }, i) => { const Icon = iconResolver(icon); return ( - + showButton && ( + + ) ); })} From 4fa69273db3f7639b85fab6f2f8fb5bb64779523 Mon Sep 17 00:00:00 2001 From: Vladimir Kobzev Date: Thu, 16 Jan 2025 10:56:46 +0100 Subject: [PATCH 06/69] Build api-reports Signed-off-by: Vladimir Kobzev --- plugins/scaffolder-react/report.api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/scaffolder-react/report.api.md b/plugins/scaffolder-react/report.api.md index 8a6b4739a4..c1f415a334 100644 --- a/plugins/scaffolder-react/report.api.md +++ b/plugins/scaffolder-react/report.api.md @@ -322,6 +322,7 @@ export type ScaffolderOutputText = { title?: string; icon?: string; content?: string; + showButton?: boolean; default?: boolean; }; From 662ba8c55db95a9a61ed20ea1165629c11441185 Mon Sep 17 00:00:00 2001 From: Vladimir Kobzev Date: Thu, 16 Jan 2025 14:54:04 +0100 Subject: [PATCH 07/69] Hide the text output button if only one text output is defined Signed-off-by: Vladimir Kobzev --- .changeset/brown-cycles-beg.md | 2 +- .../notifications-demo/template.yaml | 9 ++++++ plugins/scaffolder-react/src/api/types.ts | 1 - .../DefaultTemplateOutputs.test.tsx | 23 +++++++++++-- .../DefaultTemplateOutputs.tsx | 18 ++++++----- .../TemplateOutputs/TextOutputs.tsx | 32 +++++++++---------- 6 files changed, 56 insertions(+), 29 deletions(-) diff --git a/.changeset/brown-cycles-beg.md b/.changeset/brown-cycles-beg.md index 78e6e3e73f..98df27a00f 100644 --- a/.changeset/brown-cycles-beg.md +++ b/.changeset/brown-cycles-beg.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder-react': patch --- -Adds an option to hide text output buttons +Hide text output button if only one is present diff --git a/plugins/scaffolder-backend/sample-templates/notifications-demo/template.yaml b/plugins/scaffolder-backend/sample-templates/notifications-demo/template.yaml index a85f53b12f..9d5224ccb4 100644 --- a/plugins/scaffolder-backend/sample-templates/notifications-demo/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/notifications-demo/template.yaml @@ -67,3 +67,12 @@ spec: link: ${{ parameters.link }} severity: ${{ parameters.severity }} scope: ${{ parameters.scope }} + + output: + text: + - title: Your next steps + showButton: false + content: > + # Success! + + Text diff --git a/plugins/scaffolder-react/src/api/types.ts b/plugins/scaffolder-react/src/api/types.ts index 6284e8dcaa..8e4d989a55 100644 --- a/plugins/scaffolder-react/src/api/types.ts +++ b/plugins/scaffolder-react/src/api/types.ts @@ -89,7 +89,6 @@ export type ScaffolderOutputText = { title?: string; icon?: string; content?: string; - showButton?: boolean; default?: boolean; }; diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx index d0849f6027..cea00b15b2 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx @@ -78,11 +78,11 @@ describe('', () => { expect(queryByTestId('output-box')).toBeNull(); expect(queryByTestId('text-output-box')).toBeNull(); }); - it('should not render the link output box when output only contains text with showButton set to false', async () => { + it('should not render anything when only a single text output is defined', async () => { + // This is the default case when no output field is present in the template const output = { text: [ { title: 'Text 1', content: 'Hello, **world**!', showButton: false }, - { title: 'Text 2', content: 'Hello, **mars**!', showButton: false }, ], }; const { queryByTestId } = await renderInTestApp( @@ -94,6 +94,25 @@ describe('', () => { }, ); + // Ensure that nothing renders from this component expect(queryByTestId('output-box')).toBeNull(); }); + it('should not render text output buttons if there is only one output', async () => { + const output = { + links: [{ title: 'Link 1', url: 'https://backstage.io/' }], + text: [ + { title: 'Text 1', content: 'Hello, **world**!', showButton: false }, + ], + }; + const { queryByTestId } = await renderInTestApp( + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + expect(queryByTestId('text-outputs')).toBeNull(); + }); }); diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx index 9788d194ae..40abad376b 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx @@ -56,9 +56,8 @@ export const DefaultTemplateOutputs = (props: { return null; } - const emptyOutput = - (output.links || []).length === 0 && - Object.values(output.text || {}).filter(o => o.showButton).length === 0; + const displayTextButtons = (output.text || []).length > 1; + const emptyOutput = (output.links || []).length === 0 && !displayTextButtons; return ( <> @@ -72,11 +71,14 @@ export const DefaultTemplateOutputs = (props: { gridGap={16} flexWrap="wrap" > - + {displayTextButtons && ( + + )} diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx index 373db4d632..5da1dab476 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx @@ -39,25 +39,23 @@ export const TextOutputs = (props: { <> {text .filter(({ content }) => content !== undefined) - .map(({ title, icon, showButton = true }, i) => { + .map(({ title, icon }, i) => { const Icon = iconResolver(icon); return ( - showButton && ( - - ) + ); })} From cef66616141e2b8e6463e06136d411b53143a54c Mon Sep 17 00:00:00 2001 From: Vladimir Kobzev Date: Thu, 16 Jan 2025 14:56:56 +0100 Subject: [PATCH 08/69] build api reports Signed-off-by: Vladimir Kobzev --- plugins/scaffolder-react/report.api.md | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/scaffolder-react/report.api.md b/plugins/scaffolder-react/report.api.md index c1f415a334..8a6b4739a4 100644 --- a/plugins/scaffolder-react/report.api.md +++ b/plugins/scaffolder-react/report.api.md @@ -322,7 +322,6 @@ export type ScaffolderOutputText = { title?: string; icon?: string; content?: string; - showButton?: boolean; default?: boolean; }; From a55d665044b141ba3ead886227231e7b9c32d2ad Mon Sep 17 00:00:00 2001 From: Vladimir Kobzev Date: Fri, 17 Jan 2025 09:08:47 +0100 Subject: [PATCH 09/69] revert accidental change Signed-off-by: Vladimir Kobzev --- .../sample-templates/notifications-demo/template.yaml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/plugins/scaffolder-backend/sample-templates/notifications-demo/template.yaml b/plugins/scaffolder-backend/sample-templates/notifications-demo/template.yaml index 9d5224ccb4..a85f53b12f 100644 --- a/plugins/scaffolder-backend/sample-templates/notifications-demo/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/notifications-demo/template.yaml @@ -67,12 +67,3 @@ spec: link: ${{ parameters.link }} severity: ${{ parameters.severity }} scope: ${{ parameters.scope }} - - output: - text: - - title: Your next steps - showButton: false - content: > - # Success! - - Text From ca3da299e2615ca9ef167ec55872f48e77394365 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Tue, 5 Nov 2024 16:24:21 +0100 Subject: [PATCH 10/69] Introduce kubernetes resource permission Signed-off-by: Dominika Zemanovicova --- .changeset/real-foxes-film.md | 6 ++ .../src/auth/requirePermission.ts | 49 +++++++++++++++++ .../src/routes/resourceRoutes.test.ts | 55 ++++++++++++++++++- .../src/routes/resourcesRoutes.ts | 5 ++ .../src/service/KubernetesBuilder.test.ts | 41 ++++++++++---- .../src/service/KubernetesBuilder.ts | 4 ++ plugins/kubernetes-common/report.api.md | 3 + plugins/kubernetes-common/src/index.ts | 1 + plugins/kubernetes-common/src/permissions.ts | 13 ++++- 9 files changed, 162 insertions(+), 15 deletions(-) create mode 100644 .changeset/real-foxes-film.md create mode 100644 plugins/kubernetes-backend/src/auth/requirePermission.ts diff --git a/.changeset/real-foxes-film.md b/.changeset/real-foxes-film.md new file mode 100644 index 0000000000..757146d624 --- /dev/null +++ b/.changeset/real-foxes-film.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes-backend': minor +'@backstage/plugin-kubernetes-common': minor +--- + +Introduced resource permission type to be used with the kubernetes endpoint's permission framework integration for all endpoints except the proxy endpoints. diff --git a/plugins/kubernetes-backend/src/auth/requirePermission.ts b/plugins/kubernetes-backend/src/auth/requirePermission.ts new file mode 100644 index 0000000000..c95cd12c72 --- /dev/null +++ b/plugins/kubernetes-backend/src/auth/requirePermission.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { + HttpAuthService, + PermissionsService, +} from '@backstage/backend-plugin-api'; +import { NotAllowedError } from '@backstage/errors'; +import { kubernetesResourcePermission } from '@backstage/plugin-kubernetes-common'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; + +import express from 'express'; + +export async function requirePermission( + permissionApi: PermissionsService, + httpAuth: HttpAuthService, + req: express.Request, +) { + const decision = ( + await permissionApi.authorize( + [ + { + permission: kubernetesResourcePermission, + }, + ], + { + credentials: await httpAuth.credentials(req), + }, + ) + )[0]; + + if (decision.result === AuthorizeResult.DENY) { + const err = new NotAllowedError('Unauthorized'); + throw err; + } +} diff --git a/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts b/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts index 7ace17e805..462312a404 100644 --- a/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts +++ b/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts @@ -18,17 +18,48 @@ import request from 'supertest'; import { mockCredentials, mockServices, + type ServiceMock, startTestBackend, } from '@backstage/backend-test-utils'; import { kubernetesObjectsProviderExtensionPoint } from '@backstage/plugin-kubernetes-node'; -import { createBackendModule } from '@backstage/backend-plugin-api'; +import { + createBackendModule, + type PermissionsService, +} from '@backstage/backend-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { ExtendedHttpServer } from '@backstage/backend-defaults/rootHttpRouter'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; describe('resourcesRoutes', () => { let app: ExtendedHttpServer; + const permissionsMock: ServiceMock = + mockServices.permissions.mock({ + authorize: jest.fn(), + authorizeConditional: jest.fn(), + }); - beforeAll(async () => { + const startPermissionDeniedTestServer = async () => { + permissionsMock.authorize.mockResolvedValue([ + { result: AuthorizeResult.DENY }, + ]); + const { server } = await startTestBackend({ + features: [ + mockServices.rootConfig.factory({ + data: { + kubernetes: { + serviceLocatorMethod: { type: 'multiTenant' }, + clusterLocatorMethods: [], + }, + }, + }), + permissionsMock.factory, + import('@backstage/plugin-kubernetes-backend'), + ], + }); + return server; + }; + + beforeEach(async () => { const objectsProviderMock = { getKubernetesObjectsByEntity: jest.fn().mockImplementation(args => { if (args.entity.metadata.name === 'inject500') { @@ -109,6 +140,8 @@ describe('resourcesRoutes', () => { }, }), import('@backstage/plugin-kubernetes-backend'), + import('@backstage/plugin-permission-backend'), + import('@backstage/plugin-permission-backend-module-allow-all-policy'), createBackendModule({ pluginId: 'kubernetes', moduleId: 'test-objects-provider', @@ -127,6 +160,10 @@ describe('resourcesRoutes', () => { app = server; }); + afterEach(() => { + app.stop(); + }); + describe('POST /resources/workloads/query', () => { // eslint-disable-next-line jest/expect-expect it('200 happy path', async () => { @@ -269,6 +306,13 @@ describe('resourcesRoutes', () => { response: { statusCode: 401 }, }); }); + it('403 when permission blocks endpoint', async () => { + app = await startPermissionDeniedTestServer(); + const response = await request(app).post( + '/api/kubernetes/resources/workloads/query', + ); + expect(response.status).toEqual(403); + }); // eslint-disable-next-line jest/expect-expect it('500 handle gracefully', async () => { await request(app) @@ -548,6 +592,13 @@ describe('resourcesRoutes', () => { response: { statusCode: 401 }, }); }); + it('403 when permission blocks endpoint', async () => { + app = await startPermissionDeniedTestServer(); + const response = await request(app).post( + '/api/kubernetes/resources/custom/query', + ); + expect(response.status).toEqual(403); + }); // eslint-disable-next-line jest/expect-expect it('500 handle gracefully', async () => { await request(app) diff --git a/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts b/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts index 838e039d27..091080bdcc 100644 --- a/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts +++ b/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts @@ -23,6 +23,8 @@ import { InputError } from '@backstage/errors'; import express, { Request } from 'express'; import { KubernetesObjectsProvider } from '@backstage/plugin-kubernetes-node'; import { AuthService, HttpAuthService } from '@backstage/backend-plugin-api'; +import { PermissionEvaluator } from '@backstage/plugin-permission-common'; +import { requirePermission } from '../auth/requirePermission'; export const addResourceRoutesToRouter = ( router: express.Router, @@ -30,6 +32,7 @@ export const addResourceRoutesToRouter = ( objectsProvider: KubernetesObjectsProvider, auth: AuthService, httpAuth: HttpAuthService, + permissionApi: PermissionEvaluator, ) => { const getEntityByReq = async (req: Request) => { const rawEntityRef = req.body.entityRef; @@ -62,6 +65,7 @@ export const addResourceRoutesToRouter = ( }; router.post('/resources/workloads/query', async (req, res) => { + await requirePermission(permissionApi, httpAuth, req); const entity = await getEntityByReq(req); const response = await objectsProvider.getKubernetesObjectsByEntity( { @@ -74,6 +78,7 @@ export const addResourceRoutesToRouter = ( }); router.post('/resources/custom/query', async (req, res) => { + await requirePermission(permissionApi, httpAuth, req); const entity = await getEntityByReq(req); if (!req.body.customResources) { diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index b38bf9121a..8669299d2e 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -92,6 +92,19 @@ describe('API integration tests', () => { }); }, }); + const startPermissionDeniedTestServer = async () => { + permissionsMock.authorize.mockResolvedValue([ + { result: AuthorizeResult.DENY }, + ]); + const { server } = await startTestBackend({ + features: [ + minimalValidConfigService, + permissionsMock.factory, + import('@backstage/plugin-kubernetes-backend'), + ], + }); + return server; + }; beforeEach(async () => { jest.resetAllMocks(); @@ -305,6 +318,12 @@ describe('API integration tests', () => { items: [expect.objectContaining({ title: 'cluster-title' })], }); }); + + it('returns 403 response when permission blocks endpoint', async () => { + app = await startPermissionDeniedTestServer(); + const response = await request(app).get('/api/kubernetes/clusters'); + expect(response.status).toEqual(403); + }); }); describe('post /services/:serviceId', () => { @@ -504,6 +523,14 @@ describe('API integration tests', () => { }), ); }); + + it('returns 403 response when permission blocks endpoint', async () => { + app = await startPermissionDeniedTestServer(); + const response = await request(app).post( + '/api/kubernetes/services/test-service', + ); + expect(response.status).toEqual(403); + }); }); describe('/proxy', () => { @@ -571,18 +598,7 @@ metadata: }); it('returns 403 response when permission blocks endpoint', async () => { - permissionsMock.authorize.mockResolvedValue([ - { result: AuthorizeResult.DENY }, - ]); - - const { server } = await startTestBackend({ - features: [ - minimalValidConfigService, - permissionsMock.factory, - import('@backstage/plugin-kubernetes-backend'), - ], - }); - app = server; + app = await startPermissionDeniedTestServer(); const proxyEndpointRequest = request(app) .post('/api/kubernetes/proxy/api/v1/namespaces') @@ -779,6 +795,7 @@ metadata: expect(response.body).toMatchObject({ permissions: [ { type: 'basic', name: 'kubernetes.proxy', attributes: {} }, + { type: 'basic', name: 'kubernetes.resource', attributes: {} }, ], rules: [], }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 6a4e9cc296..ea5c725d8f 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -73,6 +73,7 @@ import { } from './KubernetesFanOutHandler'; import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; import { KubernetesProxy } from './KubernetesProxy'; +import { requirePermission } from '../auth/requirePermission'; /** * @deprecated Please migrate to the new backend system as this will be removed in the future. @@ -393,6 +394,7 @@ export class KubernetesBuilder { ); // @deprecated router.post('/services/:serviceId', async (req, res) => { + await requirePermission(permissionApi, httpAuth, req); const serviceId = req.params.serviceId; const requestBody: ObjectsByEntityRequest = req.body; try { @@ -413,6 +415,7 @@ export class KubernetesBuilder { }); router.get('/clusters', async (req, res) => { + await requirePermission(permissionApi, httpAuth, req); const credentials = await httpAuth.credentials(req); const clusterDetails = await this.fetchClusterDetails(clusterSupplier, { credentials, @@ -447,6 +450,7 @@ export class KubernetesBuilder { objectsProvider, authService, httpAuth, + permissionApi, ); return router; diff --git a/plugins/kubernetes-common/report.api.md b/plugins/kubernetes-common/report.api.md index 8cdac7ed45..b40ae89c82 100644 --- a/plugins/kubernetes-common/report.api.md +++ b/plugins/kubernetes-common/report.api.md @@ -347,6 +347,9 @@ export interface KubernetesRequestBody { entity: Entity; } +// @public +export const kubernetesResourcePermission: BasicPermission; + // @public (undocumented) export interface LimitRangeFetchResponse { // (undocumented) diff --git a/plugins/kubernetes-common/src/index.ts b/plugins/kubernetes-common/src/index.ts index faaaaab92c..76a4c0b7e7 100644 --- a/plugins/kubernetes-common/src/index.ts +++ b/plugins/kubernetes-common/src/index.ts @@ -25,6 +25,7 @@ export * from './catalog-entity-constants'; export * from './certificate-authority-constants'; export { kubernetesProxyPermission, + kubernetesResourcePermission, kubernetesPermissions, } from './permissions'; export * from './error-detection'; diff --git a/plugins/kubernetes-common/src/permissions.ts b/plugins/kubernetes-common/src/permissions.ts index d3ccbe8171..928b5c7d06 100644 --- a/plugins/kubernetes-common/src/permissions.ts +++ b/plugins/kubernetes-common/src/permissions.ts @@ -24,8 +24,19 @@ export const kubernetesProxyPermission = createPermission({ attributes: {}, }); +/** This permission is used to check access to the resources endpoints + * @public + */ +export const kubernetesResourcePermission = createPermission({ + name: 'kubernetes.resource', + attributes: {}, +}); + /** * List of all Kubernetes permissions. * @public */ -export const kubernetesPermissions = [kubernetesProxyPermission]; +export const kubernetesPermissions = [ + kubernetesProxyPermission, + kubernetesResourcePermission, +]; From 4cb841c42075aecf19ce0804e45ab0dffcefe8ac Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Tue, 19 Nov 2024 10:40:38 +0100 Subject: [PATCH 11/69] Support future compatibility for permissions Co-authored by: Vincenzo Scamporlino Signed-off-by: Dominika Zemanovicova --- plugins/kubernetes-backend/src/auth/requirePermission.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/kubernetes-backend/src/auth/requirePermission.ts b/plugins/kubernetes-backend/src/auth/requirePermission.ts index c95cd12c72..8f0463bd5b 100644 --- a/plugins/kubernetes-backend/src/auth/requirePermission.ts +++ b/plugins/kubernetes-backend/src/auth/requirePermission.ts @@ -42,8 +42,8 @@ export async function requirePermission( ) )[0]; - if (decision.result === AuthorizeResult.DENY) { - const err = new NotAllowedError('Unauthorized'); - throw err; + if (decision.result === AuthorizeResult.ALLOW) { + return; } + throw new NotAllowedError('Unauthorized'); } From 73115bcc898254fa4d97198331cdd450c71672be Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Fri, 22 Nov 2024 10:36:10 +0100 Subject: [PATCH 12/69] Split resource permission Signed-off-by: Dominika Zemanovicova --- .changeset/real-foxes-film.md | 4 +++- .../src/auth/requirePermission.ts | 9 ++++++--- .../src/routes/resourcesRoutes.ts | 15 ++++++++++++-- .../src/service/KubernetesBuilder.test.ts | 2 ++ .../src/service/KubernetesBuilder.ts | 16 +++++++++++++-- plugins/kubernetes-common/report.api.md | 6 ++++++ plugins/kubernetes-common/src/index.ts | 2 ++ plugins/kubernetes-common/src/permissions.ts | 20 ++++++++++++++++++- 8 files changed, 65 insertions(+), 9 deletions(-) diff --git a/.changeset/real-foxes-film.md b/.changeset/real-foxes-film.md index 757146d624..750db7e175 100644 --- a/.changeset/real-foxes-film.md +++ b/.changeset/real-foxes-film.md @@ -3,4 +3,6 @@ '@backstage/plugin-kubernetes-common': minor --- -Introduced resource permission type to be used with the kubernetes endpoint's permission framework integration for all endpoints except the proxy endpoints. +The `/clusters` endpoint is now protected by the `kubernetes.cluster` permission. +The `/services` endpoint is now protected by the `kubernetes.service` permission. +The `/resources` endpoints are now protected by the `kubernetes.resource` permission. diff --git a/plugins/kubernetes-backend/src/auth/requirePermission.ts b/plugins/kubernetes-backend/src/auth/requirePermission.ts index 8f0463bd5b..9934ed653d 100644 --- a/plugins/kubernetes-backend/src/auth/requirePermission.ts +++ b/plugins/kubernetes-backend/src/auth/requirePermission.ts @@ -19,13 +19,16 @@ import type { PermissionsService, } from '@backstage/backend-plugin-api'; import { NotAllowedError } from '@backstage/errors'; -import { kubernetesResourcePermission } from '@backstage/plugin-kubernetes-common'; -import { AuthorizeResult } from '@backstage/plugin-permission-common'; +import { + AuthorizeResult, + type BasicPermission, +} from '@backstage/plugin-permission-common'; import express from 'express'; export async function requirePermission( permissionApi: PermissionsService, + permissionRequired: BasicPermission, httpAuth: HttpAuthService, req: express.Request, ) { @@ -33,7 +36,7 @@ export async function requirePermission( await permissionApi.authorize( [ { - permission: kubernetesResourcePermission, + permission: permissionRequired, }, ], { diff --git a/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts b/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts index 091080bdcc..ab3c7d8e06 100644 --- a/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts +++ b/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts @@ -25,6 +25,7 @@ import { KubernetesObjectsProvider } from '@backstage/plugin-kubernetes-node'; import { AuthService, HttpAuthService } from '@backstage/backend-plugin-api'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { requirePermission } from '../auth/requirePermission'; +import { kubernetesResourcePermission } from '@backstage/plugin-kubernetes-common'; export const addResourceRoutesToRouter = ( router: express.Router, @@ -65,7 +66,12 @@ export const addResourceRoutesToRouter = ( }; router.post('/resources/workloads/query', async (req, res) => { - await requirePermission(permissionApi, httpAuth, req); + await requirePermission( + permissionApi, + kubernetesResourcePermission, + httpAuth, + req, + ); const entity = await getEntityByReq(req); const response = await objectsProvider.getKubernetesObjectsByEntity( { @@ -78,7 +84,12 @@ export const addResourceRoutesToRouter = ( }); router.post('/resources/custom/query', async (req, res) => { - await requirePermission(permissionApi, httpAuth, req); + await requirePermission( + permissionApi, + kubernetesResourcePermission, + httpAuth, + req, + ); const entity = await getEntityByReq(req); if (!req.body.customResources) { diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index 8669299d2e..84de5d5575 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -796,6 +796,8 @@ metadata: permissions: [ { type: 'basic', name: 'kubernetes.proxy', attributes: {} }, { type: 'basic', name: 'kubernetes.resource', attributes: {} }, + { type: 'basic', name: 'kubernetes.service', attributes: {} }, + { type: 'basic', name: 'kubernetes.cluster', attributes: {} }, ], rules: [], }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index ea5c725d8f..ed864dcf60 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -18,7 +18,9 @@ import { Config } from '@backstage/config'; import { ANNOTATION_KUBERNETES_AUTH_PROVIDER, ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, + kubernetesClusterPermission, kubernetesPermissions, + kubernetesServicePermission, } from '@backstage/plugin-kubernetes-common'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; @@ -394,7 +396,12 @@ export class KubernetesBuilder { ); // @deprecated router.post('/services/:serviceId', async (req, res) => { - await requirePermission(permissionApi, httpAuth, req); + await requirePermission( + permissionApi, + kubernetesServicePermission, + httpAuth, + req, + ); const serviceId = req.params.serviceId; const requestBody: ObjectsByEntityRequest = req.body; try { @@ -415,7 +422,12 @@ export class KubernetesBuilder { }); router.get('/clusters', async (req, res) => { - await requirePermission(permissionApi, httpAuth, req); + await requirePermission( + permissionApi, + kubernetesClusterPermission, + httpAuth, + req, + ); const credentials = await httpAuth.credentials(req); const clusterDetails = await this.fetchClusterDetails(clusterSupplier, { credentials, diff --git a/plugins/kubernetes-common/report.api.md b/plugins/kubernetes-common/report.api.md index b40ae89c82..bc953c9781 100644 --- a/plugins/kubernetes-common/report.api.md +++ b/plugins/kubernetes-common/report.api.md @@ -317,6 +317,9 @@ export interface JobsFetchResponse { type: 'jobs'; } +// @public +export const kubernetesClusterPermission: BasicPermission; + // @public (undocumented) export type KubernetesErrorTypes = | 'BAD_REQUEST' @@ -350,6 +353,9 @@ export interface KubernetesRequestBody { // @public export const kubernetesResourcePermission: BasicPermission; +// @public +export const kubernetesServicePermission: BasicPermission; + // @public (undocumented) export interface LimitRangeFetchResponse { // (undocumented) diff --git a/plugins/kubernetes-common/src/index.ts b/plugins/kubernetes-common/src/index.ts index 76a4c0b7e7..43a0500a9c 100644 --- a/plugins/kubernetes-common/src/index.ts +++ b/plugins/kubernetes-common/src/index.ts @@ -25,7 +25,9 @@ export * from './catalog-entity-constants'; export * from './certificate-authority-constants'; export { kubernetesProxyPermission, + kubernetesClusterPermission, kubernetesResourcePermission, + kubernetesServicePermission, kubernetesPermissions, } from './permissions'; export * from './error-detection'; diff --git a/plugins/kubernetes-common/src/permissions.ts b/plugins/kubernetes-common/src/permissions.ts index 928b5c7d06..c96c183af6 100644 --- a/plugins/kubernetes-common/src/permissions.ts +++ b/plugins/kubernetes-common/src/permissions.ts @@ -24,7 +24,7 @@ export const kubernetesProxyPermission = createPermission({ attributes: {}, }); -/** This permission is used to check access to the resources endpoints +/** This permission is used to check access to the /resources endpoints * @public */ export const kubernetesResourcePermission = createPermission({ @@ -32,6 +32,22 @@ export const kubernetesResourcePermission = createPermission({ attributes: {}, }); +/** This permission is used to check access to the /services endpoint + * @public + */ +export const kubernetesServicePermission = createPermission({ + name: 'kubernetes.service', + attributes: {}, +}); + +/** This permission is used to check access to the /clusters endpoint + * @public + */ +export const kubernetesClusterPermission = createPermission({ + name: 'kubernetes.cluster', + attributes: {}, +}); + /** * List of all Kubernetes permissions. * @public @@ -39,4 +55,6 @@ export const kubernetesResourcePermission = createPermission({ export const kubernetesPermissions = [ kubernetesProxyPermission, kubernetesResourcePermission, + kubernetesServicePermission, + kubernetesClusterPermission, ]; From 66803d5ab7a0b61e6e9e76245a61e30fa49d876a Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Mon, 9 Dec 2024 10:22:41 +0100 Subject: [PATCH 13/69] Update names to plural Signed-off-by: Dominika Zemanovicova --- .changeset/real-foxes-film.md | 6 +++--- .../src/routes/resourcesRoutes.ts | 6 +++--- .../src/service/KubernetesBuilder.test.ts | 6 +++--- .../src/service/KubernetesBuilder.ts | 8 ++++---- plugins/kubernetes-common/report.api.md | 6 +++--- plugins/kubernetes-common/src/index.ts | 6 +++--- plugins/kubernetes-common/src/permissions.ts | 18 +++++++++--------- 7 files changed, 28 insertions(+), 28 deletions(-) diff --git a/.changeset/real-foxes-film.md b/.changeset/real-foxes-film.md index 750db7e175..5fabbacd9e 100644 --- a/.changeset/real-foxes-film.md +++ b/.changeset/real-foxes-film.md @@ -3,6 +3,6 @@ '@backstage/plugin-kubernetes-common': minor --- -The `/clusters` endpoint is now protected by the `kubernetes.cluster` permission. -The `/services` endpoint is now protected by the `kubernetes.service` permission. -The `/resources` endpoints are now protected by the `kubernetes.resource` permission. +The `/clusters` endpoint is now protected by the `kubernetes.clusters` permission. +The `/services` endpoint is now protected by the `kubernetes.services` permission. +The `/resources` endpoints are now protected by the `kubernetes.resources` permission. diff --git a/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts b/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts index ab3c7d8e06..6e737a1e4f 100644 --- a/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts +++ b/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts @@ -25,7 +25,7 @@ import { KubernetesObjectsProvider } from '@backstage/plugin-kubernetes-node'; import { AuthService, HttpAuthService } from '@backstage/backend-plugin-api'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { requirePermission } from '../auth/requirePermission'; -import { kubernetesResourcePermission } from '@backstage/plugin-kubernetes-common'; +import { kubernetesResourcesPermission } from '@backstage/plugin-kubernetes-common'; export const addResourceRoutesToRouter = ( router: express.Router, @@ -68,7 +68,7 @@ export const addResourceRoutesToRouter = ( router.post('/resources/workloads/query', async (req, res) => { await requirePermission( permissionApi, - kubernetesResourcePermission, + kubernetesResourcesPermission, httpAuth, req, ); @@ -86,7 +86,7 @@ export const addResourceRoutesToRouter = ( router.post('/resources/custom/query', async (req, res) => { await requirePermission( permissionApi, - kubernetesResourcePermission, + kubernetesResourcesPermission, httpAuth, req, ); diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index 84de5d5575..145c9e6a06 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -795,9 +795,9 @@ metadata: expect(response.body).toMatchObject({ permissions: [ { type: 'basic', name: 'kubernetes.proxy', attributes: {} }, - { type: 'basic', name: 'kubernetes.resource', attributes: {} }, - { type: 'basic', name: 'kubernetes.service', attributes: {} }, - { type: 'basic', name: 'kubernetes.cluster', attributes: {} }, + { type: 'basic', name: 'kubernetes.resources', attributes: {} }, + { type: 'basic', name: 'kubernetes.services', attributes: {} }, + { type: 'basic', name: 'kubernetes.clusters', attributes: {} }, ], rules: [], }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index ed864dcf60..a8b1deaa9c 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -18,9 +18,9 @@ import { Config } from '@backstage/config'; import { ANNOTATION_KUBERNETES_AUTH_PROVIDER, ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, - kubernetesClusterPermission, + kubernetesClustersPermission, kubernetesPermissions, - kubernetesServicePermission, + kubernetesServicesPermission, } from '@backstage/plugin-kubernetes-common'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; @@ -398,7 +398,7 @@ export class KubernetesBuilder { router.post('/services/:serviceId', async (req, res) => { await requirePermission( permissionApi, - kubernetesServicePermission, + kubernetesServicesPermission, httpAuth, req, ); @@ -424,7 +424,7 @@ export class KubernetesBuilder { router.get('/clusters', async (req, res) => { await requirePermission( permissionApi, - kubernetesClusterPermission, + kubernetesClustersPermission, httpAuth, req, ); diff --git a/plugins/kubernetes-common/report.api.md b/plugins/kubernetes-common/report.api.md index bc953c9781..079169d8ab 100644 --- a/plugins/kubernetes-common/report.api.md +++ b/plugins/kubernetes-common/report.api.md @@ -318,7 +318,7 @@ export interface JobsFetchResponse { } // @public -export const kubernetesClusterPermission: BasicPermission; +export const kubernetesClustersPermission: BasicPermission; // @public (undocumented) export type KubernetesErrorTypes = @@ -351,10 +351,10 @@ export interface KubernetesRequestBody { } // @public -export const kubernetesResourcePermission: BasicPermission; +export const kubernetesResourcesPermission: BasicPermission; // @public -export const kubernetesServicePermission: BasicPermission; +export const kubernetesServicesPermission: BasicPermission; // @public (undocumented) export interface LimitRangeFetchResponse { diff --git a/plugins/kubernetes-common/src/index.ts b/plugins/kubernetes-common/src/index.ts index 43a0500a9c..d6e97dc8e6 100644 --- a/plugins/kubernetes-common/src/index.ts +++ b/plugins/kubernetes-common/src/index.ts @@ -25,9 +25,9 @@ export * from './catalog-entity-constants'; export * from './certificate-authority-constants'; export { kubernetesProxyPermission, - kubernetesClusterPermission, - kubernetesResourcePermission, - kubernetesServicePermission, + kubernetesClustersPermission, + kubernetesResourcesPermission, + kubernetesServicesPermission, kubernetesPermissions, } from './permissions'; export * from './error-detection'; diff --git a/plugins/kubernetes-common/src/permissions.ts b/plugins/kubernetes-common/src/permissions.ts index c96c183af6..7c7a087dc5 100644 --- a/plugins/kubernetes-common/src/permissions.ts +++ b/plugins/kubernetes-common/src/permissions.ts @@ -27,24 +27,24 @@ export const kubernetesProxyPermission = createPermission({ /** This permission is used to check access to the /resources endpoints * @public */ -export const kubernetesResourcePermission = createPermission({ - name: 'kubernetes.resource', +export const kubernetesResourcesPermission = createPermission({ + name: 'kubernetes.resources', attributes: {}, }); /** This permission is used to check access to the /services endpoint * @public */ -export const kubernetesServicePermission = createPermission({ - name: 'kubernetes.service', +export const kubernetesServicesPermission = createPermission({ + name: 'kubernetes.services', attributes: {}, }); /** This permission is used to check access to the /clusters endpoint * @public */ -export const kubernetesClusterPermission = createPermission({ - name: 'kubernetes.cluster', +export const kubernetesClustersPermission = createPermission({ + name: 'kubernetes.clusters', attributes: {}, }); @@ -54,7 +54,7 @@ export const kubernetesClusterPermission = createPermission({ */ export const kubernetesPermissions = [ kubernetesProxyPermission, - kubernetesResourcePermission, - kubernetesServicePermission, - kubernetesClusterPermission, + kubernetesResourcesPermission, + kubernetesServicesPermission, + kubernetesClustersPermission, ]; From 07efa3a8d87d9cd80f960acdb62b237dc3fd0cc1 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Tue, 17 Dec 2024 13:39:23 +0100 Subject: [PATCH 14/69] Put /services/:serviceId under resources permission Signed-off-by: Dominika Zemanovicova --- .changeset/real-foxes-film.md | 2 +- .../src/service/KubernetesBuilder.test.ts | 1 - .../src/service/KubernetesBuilder.ts | 4 ++-- plugins/kubernetes-common/report.api.md | 3 --- plugins/kubernetes-common/src/index.ts | 1 - plugins/kubernetes-common/src/permissions.ts | 11 +---------- 6 files changed, 4 insertions(+), 18 deletions(-) diff --git a/.changeset/real-foxes-film.md b/.changeset/real-foxes-film.md index 5fabbacd9e..d32585ea7f 100644 --- a/.changeset/real-foxes-film.md +++ b/.changeset/real-foxes-film.md @@ -4,5 +4,5 @@ --- The `/clusters` endpoint is now protected by the `kubernetes.clusters` permission. -The `/services` endpoint is now protected by the `kubernetes.services` permission. +The `/services/:serviceId` endpoint is now protected by the `kubernetes.resources` permission. The `/resources` endpoints are now protected by the `kubernetes.resources` permission. diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index 145c9e6a06..8814e729a6 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -796,7 +796,6 @@ metadata: permissions: [ { type: 'basic', name: 'kubernetes.proxy', attributes: {} }, { type: 'basic', name: 'kubernetes.resources', attributes: {} }, - { type: 'basic', name: 'kubernetes.services', attributes: {} }, { type: 'basic', name: 'kubernetes.clusters', attributes: {} }, ], rules: [], diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index a8b1deaa9c..de301b7969 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -20,7 +20,7 @@ import { ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, kubernetesClustersPermission, kubernetesPermissions, - kubernetesServicesPermission, + kubernetesResourcesPermission, } from '@backstage/plugin-kubernetes-common'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; @@ -398,7 +398,7 @@ export class KubernetesBuilder { router.post('/services/:serviceId', async (req, res) => { await requirePermission( permissionApi, - kubernetesServicesPermission, + kubernetesResourcesPermission, httpAuth, req, ); diff --git a/plugins/kubernetes-common/report.api.md b/plugins/kubernetes-common/report.api.md index 079169d8ab..b7a5e68ea2 100644 --- a/plugins/kubernetes-common/report.api.md +++ b/plugins/kubernetes-common/report.api.md @@ -353,9 +353,6 @@ export interface KubernetesRequestBody { // @public export const kubernetesResourcesPermission: BasicPermission; -// @public -export const kubernetesServicesPermission: BasicPermission; - // @public (undocumented) export interface LimitRangeFetchResponse { // (undocumented) diff --git a/plugins/kubernetes-common/src/index.ts b/plugins/kubernetes-common/src/index.ts index d6e97dc8e6..d8eaaaf083 100644 --- a/plugins/kubernetes-common/src/index.ts +++ b/plugins/kubernetes-common/src/index.ts @@ -27,7 +27,6 @@ export { kubernetesProxyPermission, kubernetesClustersPermission, kubernetesResourcesPermission, - kubernetesServicesPermission, kubernetesPermissions, } from './permissions'; export * from './error-detection'; diff --git a/plugins/kubernetes-common/src/permissions.ts b/plugins/kubernetes-common/src/permissions.ts index 7c7a087dc5..7e57d93c93 100644 --- a/plugins/kubernetes-common/src/permissions.ts +++ b/plugins/kubernetes-common/src/permissions.ts @@ -24,7 +24,7 @@ export const kubernetesProxyPermission = createPermission({ attributes: {}, }); -/** This permission is used to check access to the /resources endpoints +/** This permission is used to check access to the /resources and /services/:serviceId endpoints * @public */ export const kubernetesResourcesPermission = createPermission({ @@ -32,14 +32,6 @@ export const kubernetesResourcesPermission = createPermission({ attributes: {}, }); -/** This permission is used to check access to the /services endpoint - * @public - */ -export const kubernetesServicesPermission = createPermission({ - name: 'kubernetes.services', - attributes: {}, -}); - /** This permission is used to check access to the /clusters endpoint * @public */ @@ -55,6 +47,5 @@ export const kubernetesClustersPermission = createPermission({ export const kubernetesPermissions = [ kubernetesProxyPermission, kubernetesResourcesPermission, - kubernetesServicesPermission, kubernetesClustersPermission, ]; From 51c1a5f6069822064bcc43a77c84125bda6ea065 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Wed, 8 Jan 2025 17:45:01 +0100 Subject: [PATCH 15/69] Introduce permission check in kubernetes frontend Signed-off-by: Dominika Zemanovicova --- plugins/kubernetes-cluster/package.json | 1 + .../KubernetesClusterContent.tsx | 12 ++++++++- .../hooks/useKubernetesClustersPermission.ts | 26 +++++++++++++++++++ plugins/kubernetes/package.json | 1 + plugins/kubernetes/src/KubernetesContent.tsx | 21 +++++++++++++++ .../hooks/useKubernetesClustersPermission.ts | 26 +++++++++++++++++++ .../hooks/useKubernetesResourcesPermission.ts | 26 +++++++++++++++++++ yarn.lock | 2 ++ 8 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 plugins/kubernetes-cluster/src/hooks/useKubernetesClustersPermission.ts create mode 100644 plugins/kubernetes/src/hooks/useKubernetesClustersPermission.ts create mode 100644 plugins/kubernetes/src/hooks/useKubernetesResourcesPermission.ts diff --git a/plugins/kubernetes-cluster/package.json b/plugins/kubernetes-cluster/package.json index 12b7dcad21..78ff79e43b 100644 --- a/plugins/kubernetes-cluster/package.json +++ b/plugins/kubernetes-cluster/package.json @@ -47,6 +47,7 @@ "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-kubernetes-common": "workspace:^", "@backstage/plugin-kubernetes-react": "workspace:^", + "@backstage/plugin-permission-react": "workspace:^", "@kubernetes-models/apimachinery": "^2.0.0", "@kubernetes-models/base": "^5.0.0", "@material-ui/core": "^4.12.2", diff --git a/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx b/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx index c46ac4c8e2..d005a6a2aa 100644 --- a/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx +++ b/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx @@ -24,6 +24,8 @@ import { useKubernetesClusterError, } from '../KubernetesClusterErrorContext/KubernetesClusterErrorContext'; import { WarningPanel } from '@backstage/core-components'; +import { useKubernetesClustersPermission } from '../../hooks/useKubernetesClustersPermission'; +import { kubernetesClustersPermission } from '@backstage/plugin-kubernetes-common'; const ContentGrid = () => { const { error } = useKubernetesClusterError(); @@ -57,7 +59,15 @@ const ContentGrid = () => { * @public */ export const KubernetesClusterContent = () => { - return ( + const hasKubernetesClustersPermission = useKubernetesClustersPermission(); + + return !hasKubernetesClustersPermission ? ( + + ) : ( diff --git a/plugins/kubernetes-cluster/src/hooks/useKubernetesClustersPermission.ts b/plugins/kubernetes-cluster/src/hooks/useKubernetesClustersPermission.ts new file mode 100644 index 0000000000..4b71d381f6 --- /dev/null +++ b/plugins/kubernetes-cluster/src/hooks/useKubernetesClustersPermission.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { kubernetesClustersPermission } from '@backstage/plugin-kubernetes-common'; +import { usePermission } from '@backstage/plugin-permission-react'; + +export const useKubernetesClustersPermission = () => { + const kubernetesClustersPermissionResult = usePermission({ + permission: kubernetesClustersPermission, + }); + + return kubernetesClustersPermissionResult.allowed; +}; diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index c42471e331..9dbc2c2c6a 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -66,6 +66,7 @@ "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-kubernetes-common": "workspace:^", "@backstage/plugin-kubernetes-react": "workspace:^", + "@backstage/plugin-permission-react": "workspace:^", "@kubernetes-models/apimachinery": "^2.0.0", "@kubernetes-models/base": "^5.0.0", "@kubernetes/client-node": "1.0.0-rc7", diff --git a/plugins/kubernetes/src/KubernetesContent.tsx b/plugins/kubernetes/src/KubernetesContent.tsx index acd1ecc521..f9bde67f62 100644 --- a/plugins/kubernetes/src/KubernetesContent.tsx +++ b/plugins/kubernetes/src/KubernetesContent.tsx @@ -28,13 +28,18 @@ import { import { DetectedError, detectErrors, + kubernetesClustersPermission, + kubernetesResourcesPermission, } from '@backstage/plugin-kubernetes-common'; import { Content, EmptyState, Page, Progress, + WarningPanel, } from '@backstage/core-components'; +import { useKubernetesClustersPermission } from './hooks/useKubernetesClustersPermission'; +import { useKubernetesResourcesPermission } from './hooks/useKubernetesResourcesPermission'; type KubernetesContentProps = { entity: Entity; @@ -46,11 +51,27 @@ export const KubernetesContent = ({ entity, refreshIntervalMs, }: KubernetesContentProps) => { + const hasKubernetesClustersPermission = useKubernetesClustersPermission(); + const hasKubernetesResourcesPermission = useKubernetesResourcesPermission(); const { kubernetesObjects, error } = useKubernetesObjects( entity, refreshIntervalMs, ); + if (!hasKubernetesClustersPermission || !hasKubernetesResourcesPermission) { + return ( + + + + + + ); + } + const clusters = kubernetesObjects?.items.map(item => item.cluster) ?? []; const clustersWithErrors = diff --git a/plugins/kubernetes/src/hooks/useKubernetesClustersPermission.ts b/plugins/kubernetes/src/hooks/useKubernetesClustersPermission.ts new file mode 100644 index 0000000000..4b71d381f6 --- /dev/null +++ b/plugins/kubernetes/src/hooks/useKubernetesClustersPermission.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { kubernetesClustersPermission } from '@backstage/plugin-kubernetes-common'; +import { usePermission } from '@backstage/plugin-permission-react'; + +export const useKubernetesClustersPermission = () => { + const kubernetesClustersPermissionResult = usePermission({ + permission: kubernetesClustersPermission, + }); + + return kubernetesClustersPermissionResult.allowed; +}; diff --git a/plugins/kubernetes/src/hooks/useKubernetesResourcesPermission.ts b/plugins/kubernetes/src/hooks/useKubernetesResourcesPermission.ts new file mode 100644 index 0000000000..d5d730f39b --- /dev/null +++ b/plugins/kubernetes/src/hooks/useKubernetesResourcesPermission.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { kubernetesResourcesPermission } from '@backstage/plugin-kubernetes-common'; +import { usePermission } from '@backstage/plugin-permission-react'; + +export const useKubernetesResourcesPermission = () => { + const kubernetesResourcesPermissionResult = usePermission({ + permission: kubernetesResourcesPermission, + }); + + return kubernetesResourcesPermissionResult.allowed; +}; diff --git a/yarn.lock b/yarn.lock index e212f245b1..0cffefeeb9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6740,6 +6740,7 @@ __metadata: "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-kubernetes-common": "workspace:^" "@backstage/plugin-kubernetes-react": "workspace:^" + "@backstage/plugin-permission-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@kubernetes-models/apimachinery": ^2.0.0 "@kubernetes-models/base": ^5.0.0 @@ -6868,6 +6869,7 @@ __metadata: "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-kubernetes-common": "workspace:^" "@backstage/plugin-kubernetes-react": "workspace:^" + "@backstage/plugin-permission-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@kubernetes-models/apimachinery": ^2.0.0 "@kubernetes-models/base": ^5.0.0 From 6f6e6846cbedaba4302fe267d1032fcebfae2963 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Wed, 8 Jan 2025 17:55:27 +0100 Subject: [PATCH 16/69] Update changeset Signed-off-by: Dominika Zemanovicova --- .changeset/real-foxes-film.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.changeset/real-foxes-film.md b/.changeset/real-foxes-film.md index d32585ea7f..e7cb2d141a 100644 --- a/.changeset/real-foxes-film.md +++ b/.changeset/real-foxes-film.md @@ -1,8 +1,10 @@ --- '@backstage/plugin-kubernetes-backend': minor +'@backstage/plugin-kubernetes-cluster': minor '@backstage/plugin-kubernetes-common': minor +'@backstage/plugin-kubernetes': minor --- -The `/clusters` endpoint is now protected by the `kubernetes.clusters` permission. -The `/services/:serviceId` endpoint is now protected by the `kubernetes.resources` permission. -The `/resources` endpoints are now protected by the `kubernetes.resources` permission. +**BREAKING** The `/clusters` endpoint is now protected by the `kubernetes.clusters` permission. +**BREAKING** The `/services/:serviceId` endpoint is now protected by the `kubernetes.resources` permission. +**BREAKING** The `/resources` endpoints are now protected by the `kubernetes.resources` permission. From 2f993dfc9cd186ad1d641ddda11bdf58e3816441 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Thu, 9 Jan 2025 13:39:55 +0100 Subject: [PATCH 17/69] Mock permission Signed-off-by: Dominika Zemanovicova --- plugins/kubernetes/dev/index.tsx | 28 ++++++-- .../kubernetes/src/KubernetesContent.test.tsx | 67 +++++++++++-------- 2 files changed, 61 insertions(+), 34 deletions(-) diff --git a/plugins/kubernetes/dev/index.tsx b/plugins/kubernetes/dev/index.tsx index 91b97cbfc5..e5581ac2ef 100644 --- a/plugins/kubernetes/dev/index.tsx +++ b/plugins/kubernetes/dev/index.tsx @@ -36,7 +36,8 @@ import fixture4 from '../src/__fixtures__/2-cronjobs.json'; import fixture5 from '../src/__fixtures__/1-rollouts.json'; import fixture6 from '../src/__fixtures__/3-ingresses.json'; import fixture7 from '../src/__fixtures__/2-statefulsets.json'; -import { TestApiProvider } from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; +import { permissionApiRef } from '@backstage/plugin-permission-react'; import { StructuredMetadataTable } from '@backstage/core-components'; const mockEntity: Entity = { @@ -149,7 +150,10 @@ createDevApp() title: 'Fixture 1', element: ( @@ -162,7 +166,10 @@ createDevApp() title: 'Fixture 2', element: ( @@ -175,7 +182,10 @@ createDevApp() title: 'Fixture 3', element: ( @@ -188,7 +198,10 @@ createDevApp() title: 'Fixture 4', element: ( @@ -201,7 +214,10 @@ createDevApp() title: 'Fixture 5', element: ( diff --git a/plugins/kubernetes/src/KubernetesContent.test.tsx b/plugins/kubernetes/src/KubernetesContent.test.tsx index 7c21c6f5a7..daeb184275 100644 --- a/plugins/kubernetes/src/KubernetesContent.test.tsx +++ b/plugins/kubernetes/src/KubernetesContent.test.tsx @@ -16,11 +16,16 @@ import React from 'react'; import { screen } from '@testing-library/react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { + mockApis, + renderInTestApp, + TestApiProvider, +} from '@backstage/test-utils'; import { KubernetesContent } from './KubernetesContent'; import { useKubernetesObjects } from '@backstage/plugin-kubernetes-react'; import * as oneDeployment from './__fixtures__/1-deployments.json'; import * as twoDeployments from './__fixtures__/2-deployments.json'; +import { permissionApiRef } from '@backstage/plugin-permission-react'; jest.mock('@backstage/plugin-kubernetes-react', () => ({ ...jest.requireActual('@backstage/plugin-kubernetes-react'), @@ -39,15 +44,17 @@ describe('KubernetesContent', () => { error: undefined, }); await renderInTestApp( - , + + + , ); expect(screen.getByText('Your Clusters')).toBeInTheDocument(); // TODO add a prompt for the user to configure their clusters @@ -81,15 +88,17 @@ describe('KubernetesContent', () => { error: undefined, }); await renderInTestApp( - , + + + , ); expect(screen.getByText('cluster-1')).toBeInTheDocument(); @@ -145,15 +154,17 @@ describe('KubernetesContent', () => { error: undefined, }); await renderInTestApp( - , + + + , ); expect(screen.getAllByText('Cluster')).toHaveLength(2); expect(screen.getByText('cluster-a')).toBeInTheDocument(); From 7d7497785f82661d8e77d41bcf4d072932ba5359 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Thu, 9 Jan 2025 19:34:56 +0100 Subject: [PATCH 18/69] Fix permissions loading Signed-off-by: Dominika Zemanovicova --- .../KubernetesClusterContent.tsx | 29 +-- .../hooks/useKubernetesClustersPermission.ts | 26 --- plugins/kubernetes/src/KubernetesContent.tsx | 178 ++++++++---------- .../src/RequireKubernetesPermissions.tsx | 63 +++++++ .../hooks/useKubernetesClustersPermission.ts | 26 --- .../hooks/useKubernetesResourcesPermission.ts | 26 --- 6 files changed, 160 insertions(+), 188 deletions(-) delete mode 100644 plugins/kubernetes-cluster/src/hooks/useKubernetesClustersPermission.ts create mode 100644 plugins/kubernetes/src/RequireKubernetesPermissions.tsx delete mode 100644 plugins/kubernetes/src/hooks/useKubernetesClustersPermission.ts delete mode 100644 plugins/kubernetes/src/hooks/useKubernetesResourcesPermission.ts diff --git a/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx b/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx index d005a6a2aa..d47e28d294 100644 --- a/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx +++ b/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx @@ -24,8 +24,8 @@ import { useKubernetesClusterError, } from '../KubernetesClusterErrorContext/KubernetesClusterErrorContext'; import { WarningPanel } from '@backstage/core-components'; -import { useKubernetesClustersPermission } from '../../hooks/useKubernetesClustersPermission'; import { kubernetesClustersPermission } from '@backstage/plugin-kubernetes-common'; +import { RequirePermission } from '@backstage/plugin-permission-react'; const ContentGrid = () => { const { error } = useKubernetesClusterError(); @@ -59,17 +59,20 @@ const ContentGrid = () => { * @public */ export const KubernetesClusterContent = () => { - const hasKubernetesClustersPermission = useKubernetesClustersPermission(); - - return !hasKubernetesClustersPermission ? ( - - ) : ( - - - + return ( + + } + > + + + + ); }; diff --git a/plugins/kubernetes-cluster/src/hooks/useKubernetesClustersPermission.ts b/plugins/kubernetes-cluster/src/hooks/useKubernetesClustersPermission.ts deleted file mode 100644 index 4b71d381f6..0000000000 --- a/plugins/kubernetes-cluster/src/hooks/useKubernetesClustersPermission.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2025 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { kubernetesClustersPermission } from '@backstage/plugin-kubernetes-common'; -import { usePermission } from '@backstage/plugin-permission-react'; - -export const useKubernetesClustersPermission = () => { - const kubernetesClustersPermissionResult = usePermission({ - permission: kubernetesClustersPermission, - }); - - return kubernetesClustersPermissionResult.allowed; -}; diff --git a/plugins/kubernetes/src/KubernetesContent.tsx b/plugins/kubernetes/src/KubernetesContent.tsx index f9bde67f62..633d6b2a5a 100644 --- a/plugins/kubernetes/src/KubernetesContent.tsx +++ b/plugins/kubernetes/src/KubernetesContent.tsx @@ -28,18 +28,14 @@ import { import { DetectedError, detectErrors, - kubernetesClustersPermission, - kubernetesResourcesPermission, } from '@backstage/plugin-kubernetes-common'; import { Content, EmptyState, Page, Progress, - WarningPanel, } from '@backstage/core-components'; -import { useKubernetesClustersPermission } from './hooks/useKubernetesClustersPermission'; -import { useKubernetesResourcesPermission } from './hooks/useKubernetesResourcesPermission'; +import { RequireKubernetesPermissions } from './RequireKubernetesPermissions'; type KubernetesContentProps = { entity: Entity; @@ -51,27 +47,11 @@ export const KubernetesContent = ({ entity, refreshIntervalMs, }: KubernetesContentProps) => { - const hasKubernetesClustersPermission = useKubernetesClustersPermission(); - const hasKubernetesResourcesPermission = useKubernetesResourcesPermission(); const { kubernetesObjects, error } = useKubernetesObjects( entity, refreshIntervalMs, ); - if (!hasKubernetesClustersPermission || !hasKubernetesResourcesPermission) { - return ( - - - - - - ); - } - const clusters = kubernetesObjects?.items.map(item => item.cluster) ?? []; const clustersWithErrors = @@ -83,89 +63,93 @@ export const KubernetesContent = ({ : new Map(); return ( - - - - {kubernetesObjects === undefined && error === undefined && ( - - )} + + + + + {kubernetesObjects === undefined && error === undefined && ( + + )} - {/* errors retrieved from the kubernetes clusters */} - {clustersWithErrors.length > 0 && ( - - - + {/* errors retrieved from the kubernetes clusters */} + {clustersWithErrors.length > 0 && ( + + + + - - )} + )} - {/* other errors */} - {error !== undefined && ( - - - + {/* other errors */} + {error !== undefined && ( + + + + - - )} + )} - {kubernetesObjects && ( - - - - - - Your Clusters - - - {kubernetesObjects?.items.length <= 0 && ( - - - - - - )} - {kubernetesObjects?.items.length > 0 && - kubernetesObjects?.items.map((item, i) => { - const podsWithErrors = new Set( - detectedErrors - .get(item.cluster.name) - ?.filter(de => de.sourceRef.kind === 'Pod') - .map(de => de.sourceRef.name), - ); - - return ( - - + + + + + Your Clusters + + + {kubernetesObjects?.items.length <= 0 && ( + + + - ); - })} + + )} + {kubernetesObjects?.items.length > 0 && + kubernetesObjects?.items.map((item, i) => { + const podsWithErrors = new Set( + detectedErrors + .get(item.cluster.name) + ?.filter(de => de.sourceRef.kind === 'Pod') + .map(de => de.sourceRef.name), + ); + + return ( + + + + ); + })} + - - )} - - - + )} + + + + ); }; diff --git a/plugins/kubernetes/src/RequireKubernetesPermissions.tsx b/plugins/kubernetes/src/RequireKubernetesPermissions.tsx new file mode 100644 index 0000000000..36f802b127 --- /dev/null +++ b/plugins/kubernetes/src/RequireKubernetesPermissions.tsx @@ -0,0 +1,63 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { ReactNode } from 'react'; +import { + kubernetesClustersPermission, + kubernetesResourcesPermission, +} from '@backstage/plugin-kubernetes-common'; +import { usePermission } from '@backstage/plugin-permission-react'; +import { Content, Page, WarningPanel } from '@backstage/core-components'; + +export type RequireKubernetesPermissionProps = { + children: ReactNode; +}; + +export function RequireKubernetesPermissions( + props: RequireKubernetesPermissionProps, +): JSX.Element | null { + const kubernetesClustersPermissionResult = usePermission({ + permission: kubernetesClustersPermission, + }); + const kubernetesResourcesPermissionResult = usePermission({ + permission: kubernetesResourcesPermission, + }); + + if ( + kubernetesClustersPermissionResult.loading || + kubernetesResourcesPermissionResult.loading + ) { + return null; + } + + if ( + kubernetesClustersPermissionResult.allowed && + kubernetesResourcesPermissionResult.allowed + ) { + return <>{props.children}; + } + + return ( + + + + + + ); +} diff --git a/plugins/kubernetes/src/hooks/useKubernetesClustersPermission.ts b/plugins/kubernetes/src/hooks/useKubernetesClustersPermission.ts deleted file mode 100644 index 4b71d381f6..0000000000 --- a/plugins/kubernetes/src/hooks/useKubernetesClustersPermission.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2025 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { kubernetesClustersPermission } from '@backstage/plugin-kubernetes-common'; -import { usePermission } from '@backstage/plugin-permission-react'; - -export const useKubernetesClustersPermission = () => { - const kubernetesClustersPermissionResult = usePermission({ - permission: kubernetesClustersPermission, - }); - - return kubernetesClustersPermissionResult.allowed; -}; diff --git a/plugins/kubernetes/src/hooks/useKubernetesResourcesPermission.ts b/plugins/kubernetes/src/hooks/useKubernetesResourcesPermission.ts deleted file mode 100644 index d5d730f39b..0000000000 --- a/plugins/kubernetes/src/hooks/useKubernetesResourcesPermission.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2025 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { kubernetesResourcesPermission } from '@backstage/plugin-kubernetes-common'; -import { usePermission } from '@backstage/plugin-permission-react'; - -export const useKubernetesResourcesPermission = () => { - const kubernetesResourcesPermissionResult = usePermission({ - permission: kubernetesResourcesPermission, - }); - - return kubernetesResourcesPermissionResult.allowed; -}; From 10974de8d9daad411ade8aa1e5a3fdc415b33718 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Thu, 9 Jan 2025 20:10:18 +0100 Subject: [PATCH 19/69] Rename permissions Co-authored-by: Vincenzo Scamporlino Signed-off-by: Dominika Zemanovicova --- .changeset/real-foxes-film.md | 6 +++--- .../src/routes/resourcesRoutes.ts | 6 +++--- .../src/service/KubernetesBuilder.test.ts | 4 ++-- .../src/service/KubernetesBuilder.ts | 8 ++++---- .../KubernetesClusterContent.tsx | 6 +++--- plugins/kubernetes-common/report.api.md | 4 ++-- plugins/kubernetes-common/src/index.ts | 4 ++-- plugins/kubernetes-common/src/permissions.ts | 20 +++++++++++-------- .../src/RequireKubernetesPermissions.tsx | 10 +++++----- 9 files changed, 36 insertions(+), 32 deletions(-) diff --git a/.changeset/real-foxes-film.md b/.changeset/real-foxes-film.md index e7cb2d141a..16f0f0cbd9 100644 --- a/.changeset/real-foxes-film.md +++ b/.changeset/real-foxes-film.md @@ -5,6 +5,6 @@ '@backstage/plugin-kubernetes': minor --- -**BREAKING** The `/clusters` endpoint is now protected by the `kubernetes.clusters` permission. -**BREAKING** The `/services/:serviceId` endpoint is now protected by the `kubernetes.resources` permission. -**BREAKING** The `/resources` endpoints are now protected by the `kubernetes.resources` permission. +**BREAKING** The `/clusters` endpoint is now protected by the `kubernetes.clusters.read` permission. +**BREAKING** The `/services/:serviceId` endpoint is now protected by the `kubernetes.resources.read` permission. +**BREAKING** The `/resources` endpoints are now protected by the `kubernetes.resources.read` permission. diff --git a/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts b/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts index 6e737a1e4f..d500d3cb8b 100644 --- a/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts +++ b/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts @@ -25,7 +25,7 @@ import { KubernetesObjectsProvider } from '@backstage/plugin-kubernetes-node'; import { AuthService, HttpAuthService } from '@backstage/backend-plugin-api'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { requirePermission } from '../auth/requirePermission'; -import { kubernetesResourcesPermission } from '@backstage/plugin-kubernetes-common'; +import { kubernetesResourcesReadPermission } from '@backstage/plugin-kubernetes-common'; export const addResourceRoutesToRouter = ( router: express.Router, @@ -68,7 +68,7 @@ export const addResourceRoutesToRouter = ( router.post('/resources/workloads/query', async (req, res) => { await requirePermission( permissionApi, - kubernetesResourcesPermission, + kubernetesResourcesReadPermission, httpAuth, req, ); @@ -86,7 +86,7 @@ export const addResourceRoutesToRouter = ( router.post('/resources/custom/query', async (req, res) => { await requirePermission( permissionApi, - kubernetesResourcesPermission, + kubernetesResourcesReadPermission, httpAuth, req, ); diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index 8814e729a6..3046232bad 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -795,8 +795,8 @@ metadata: expect(response.body).toMatchObject({ permissions: [ { type: 'basic', name: 'kubernetes.proxy', attributes: {} }, - { type: 'basic', name: 'kubernetes.resources', attributes: {} }, - { type: 'basic', name: 'kubernetes.clusters', attributes: {} }, + { type: 'basic', name: 'kubernetes.resources.read', attributes: {} }, + { type: 'basic', name: 'kubernetes.clusters.read', attributes: {} }, ], rules: [], }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index de301b7969..2d2c592f53 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -18,9 +18,9 @@ import { Config } from '@backstage/config'; import { ANNOTATION_KUBERNETES_AUTH_PROVIDER, ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, - kubernetesClustersPermission, + kubernetesClustersReadPermission, kubernetesPermissions, - kubernetesResourcesPermission, + kubernetesResourcesReadPermission, } from '@backstage/plugin-kubernetes-common'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; @@ -398,7 +398,7 @@ export class KubernetesBuilder { router.post('/services/:serviceId', async (req, res) => { await requirePermission( permissionApi, - kubernetesResourcesPermission, + kubernetesResourcesReadPermission, httpAuth, req, ); @@ -424,7 +424,7 @@ export class KubernetesBuilder { router.get('/clusters', async (req, res) => { await requirePermission( permissionApi, - kubernetesClustersPermission, + kubernetesClustersReadPermission, httpAuth, req, ); diff --git a/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx b/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx index d47e28d294..3f53f7022b 100644 --- a/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx +++ b/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx @@ -24,7 +24,7 @@ import { useKubernetesClusterError, } from '../KubernetesClusterErrorContext/KubernetesClusterErrorContext'; import { WarningPanel } from '@backstage/core-components'; -import { kubernetesClustersPermission } from '@backstage/plugin-kubernetes-common'; +import { kubernetesClustersReadPermission } from '@backstage/plugin-kubernetes-common'; import { RequirePermission } from '@backstage/plugin-permission-react'; const ContentGrid = () => { @@ -61,12 +61,12 @@ const ContentGrid = () => { export const KubernetesClusterContent = () => { return ( } > diff --git a/plugins/kubernetes-common/report.api.md b/plugins/kubernetes-common/report.api.md index b7a5e68ea2..cc99e5063d 100644 --- a/plugins/kubernetes-common/report.api.md +++ b/plugins/kubernetes-common/report.api.md @@ -318,7 +318,7 @@ export interface JobsFetchResponse { } // @public -export const kubernetesClustersPermission: BasicPermission; +export const kubernetesClustersReadPermission: BasicPermission; // @public (undocumented) export type KubernetesErrorTypes = @@ -351,7 +351,7 @@ export interface KubernetesRequestBody { } // @public -export const kubernetesResourcesPermission: BasicPermission; +export const kubernetesResourcesReadPermission: BasicPermission; // @public (undocumented) export interface LimitRangeFetchResponse { diff --git a/plugins/kubernetes-common/src/index.ts b/plugins/kubernetes-common/src/index.ts index d8eaaaf083..8378ac8199 100644 --- a/plugins/kubernetes-common/src/index.ts +++ b/plugins/kubernetes-common/src/index.ts @@ -25,8 +25,8 @@ export * from './catalog-entity-constants'; export * from './certificate-authority-constants'; export { kubernetesProxyPermission, - kubernetesClustersPermission, - kubernetesResourcesPermission, + kubernetesClustersReadPermission, + kubernetesResourcesReadPermission, kubernetesPermissions, } from './permissions'; export * from './error-detection'; diff --git a/plugins/kubernetes-common/src/permissions.ts b/plugins/kubernetes-common/src/permissions.ts index 7e57d93c93..718251567c 100644 --- a/plugins/kubernetes-common/src/permissions.ts +++ b/plugins/kubernetes-common/src/permissions.ts @@ -27,17 +27,21 @@ export const kubernetesProxyPermission = createPermission({ /** This permission is used to check access to the /resources and /services/:serviceId endpoints * @public */ -export const kubernetesResourcesPermission = createPermission({ - name: 'kubernetes.resources', - attributes: {}, +export const kubernetesResourcesReadPermission = createPermission({ + name: 'kubernetes.resources.read', + attributes: { + action: 'read', + }, }); /** This permission is used to check access to the /clusters endpoint * @public */ -export const kubernetesClustersPermission = createPermission({ - name: 'kubernetes.clusters', - attributes: {}, +export const kubernetesClustersReadPermission = createPermission({ + name: 'kubernetes.clusters.read', + attributes: { + action: 'read', + }, }); /** @@ -46,6 +50,6 @@ export const kubernetesClustersPermission = createPermission({ */ export const kubernetesPermissions = [ kubernetesProxyPermission, - kubernetesResourcesPermission, - kubernetesClustersPermission, + kubernetesResourcesReadPermission, + kubernetesClustersReadPermission, ]; diff --git a/plugins/kubernetes/src/RequireKubernetesPermissions.tsx b/plugins/kubernetes/src/RequireKubernetesPermissions.tsx index 36f802b127..071a5aa4f9 100644 --- a/plugins/kubernetes/src/RequireKubernetesPermissions.tsx +++ b/plugins/kubernetes/src/RequireKubernetesPermissions.tsx @@ -15,8 +15,8 @@ */ import React, { ReactNode } from 'react'; import { - kubernetesClustersPermission, - kubernetesResourcesPermission, + kubernetesClustersReadPermission, + kubernetesResourcesReadPermission, } from '@backstage/plugin-kubernetes-common'; import { usePermission } from '@backstage/plugin-permission-react'; import { Content, Page, WarningPanel } from '@backstage/core-components'; @@ -29,10 +29,10 @@ export function RequireKubernetesPermissions( props: RequireKubernetesPermissionProps, ): JSX.Element | null { const kubernetesClustersPermissionResult = usePermission({ - permission: kubernetesClustersPermission, + permission: kubernetesClustersReadPermission, }); const kubernetesResourcesPermissionResult = usePermission({ - permission: kubernetesResourcesPermission, + permission: kubernetesResourcesReadPermission, }); if ( @@ -55,7 +55,7 @@ export function RequireKubernetesPermissions( From ba816e45a54d7e1dae266230f5bd4edb4c2829db Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Fri, 10 Jan 2025 10:16:09 +0100 Subject: [PATCH 20/69] Move page and content Signed-off-by: Dominika Zemanovicova --- plugins/kubernetes/src/KubernetesContent.tsx | 20 +++++++++---------- .../src/RequireKubernetesPermissions.tsx | 14 +++++-------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/plugins/kubernetes/src/KubernetesContent.tsx b/plugins/kubernetes/src/KubernetesContent.tsx index 633d6b2a5a..f980e87f0b 100644 --- a/plugins/kubernetes/src/KubernetesContent.tsx +++ b/plugins/kubernetes/src/KubernetesContent.tsx @@ -63,12 +63,12 @@ export const KubernetesContent = ({ : new Map(); return ( - - - - + + + + {kubernetesObjects === undefined && error === undefined && ( )} @@ -147,9 +147,9 @@ export const KubernetesContent = ({ )} - - - - + + + + ); }; diff --git a/plugins/kubernetes/src/RequireKubernetesPermissions.tsx b/plugins/kubernetes/src/RequireKubernetesPermissions.tsx index 071a5aa4f9..7912393303 100644 --- a/plugins/kubernetes/src/RequireKubernetesPermissions.tsx +++ b/plugins/kubernetes/src/RequireKubernetesPermissions.tsx @@ -19,7 +19,7 @@ import { kubernetesResourcesReadPermission, } from '@backstage/plugin-kubernetes-common'; import { usePermission } from '@backstage/plugin-permission-react'; -import { Content, Page, WarningPanel } from '@backstage/core-components'; +import { WarningPanel } from '@backstage/core-components'; export type RequireKubernetesPermissionProps = { children: ReactNode; @@ -50,14 +50,10 @@ export function RequireKubernetesPermissions( } return ( - - - - - + /> ); } From 1457580af2efb9d8a8e306cf4835654c5b6ad606 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Wed, 22 Jan 2025 15:39:05 +0100 Subject: [PATCH 21/69] Specify which admin to contact Signed-off-by: Dominika Zemanovicova --- .../KubernetesClusterContent/KubernetesClusterContent.tsx | 2 +- plugins/kubernetes/src/RequireKubernetesPermissions.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx b/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx index 3f53f7022b..88e0f8b703 100644 --- a/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx +++ b/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx @@ -65,7 +65,7 @@ export const KubernetesClusterContent = () => { errorPage={ } diff --git a/plugins/kubernetes/src/RequireKubernetesPermissions.tsx b/plugins/kubernetes/src/RequireKubernetesPermissions.tsx index 7912393303..65855fc44c 100644 --- a/plugins/kubernetes/src/RequireKubernetesPermissions.tsx +++ b/plugins/kubernetes/src/RequireKubernetesPermissions.tsx @@ -52,7 +52,7 @@ export function RequireKubernetesPermissions( return ( ); From d131384763b52327fec41af950650dcd5804864c Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Wed, 22 Jan 2025 15:39:51 +0100 Subject: [PATCH 22/69] Add docs Signed-off-by: Dominika Zemanovicova --- docs/features/kubernetes/permissions.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 docs/features/kubernetes/permissions.md diff --git a/docs/features/kubernetes/permissions.md b/docs/features/kubernetes/permissions.md new file mode 100644 index 0000000000..fd0ab23418 --- /dev/null +++ b/docs/features/kubernetes/permissions.md @@ -0,0 +1,18 @@ +--- +id: permissions +title: Permissions +description: Configuring permissions for Kubernetes plugin +--- + +The Kubernetes plugin integrates with the permission framework. Administrators can define PermissionPolicies +to restrict access to the `/clusters`, `/services/:serviceId`, `/resources` and `/proxy` endpoints. + +This feature assumes your Backstage instance has enabled the [permissions framework](https://backstage.io/docs/permissions/getting-started). + +### Available permissions + +| Name | Policy | Description | +| ------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------- | +| kubernetes.clusters.read | read | Allows the user to read Kubernetes clusters information | +| kubernetes.resources.read | read | Allows the user to read Kubernetes resources information | +| kubernetes.proxy | | Allows the user to make arbitrary requests to the [REST API](https://kubernetes.io/docs/reference/using-api/api-concepts/) | From 2e1a3b446ef4114cd612bc040bd77e6c3e1bc02a Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Wed, 22 Jan 2025 15:42:41 +0100 Subject: [PATCH 23/69] Specify endpoints Signed-off-by: Dominika Zemanovicova --- docs/features/kubernetes/permissions.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/features/kubernetes/permissions.md b/docs/features/kubernetes/permissions.md index fd0ab23418..5f1475d4b3 100644 --- a/docs/features/kubernetes/permissions.md +++ b/docs/features/kubernetes/permissions.md @@ -11,8 +11,8 @@ This feature assumes your Backstage instance has enabled the [permissions framew ### Available permissions -| Name | Policy | Description | -| ------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------- | -| kubernetes.clusters.read | read | Allows the user to read Kubernetes clusters information | -| kubernetes.resources.read | read | Allows the user to read Kubernetes resources information | -| kubernetes.proxy | | Allows the user to make arbitrary requests to the [REST API](https://kubernetes.io/docs/reference/using-api/api-concepts/) | +| Name | Policy | Description | +| ------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------- | +| kubernetes.clusters.read | read | Allows the user to read Kubernetes clusters information under `/clusters` | +| kubernetes.resources.read | read | Allows the user to read Kubernetes resources information under `/services/:serviceId` and `/resources` | +| kubernetes.proxy | | Allows the user to make arbitrary requests to the [REST API](https://kubernetes.io/docs/reference/using-api/api-concepts/) under `/proxy` | From 61f464e86475fe8686045eded4644bbb33227e7c Mon Sep 17 00:00:00 2001 From: Jessica He Date: Mon, 25 Nov 2024 18:49:51 -0500 Subject: [PATCH 24/69] support user configuration of auth cookie max age Signed-off-by: Jessica He --- .changeset/serious-ads-leave.md | 22 ++++++ docs/auth/atlassian/provider.md | 6 ++ docs/auth/auth0/provider.md | 3 + docs/auth/aws-alb/provider.md | 6 ++ docs/auth/bitbucket/provider.md | 6 ++ docs/auth/bitbucketServer/provider.md | 6 ++ docs/auth/cloudflare/provider.md | 6 ++ docs/auth/github/provider.md | 3 + docs/auth/gitlab/provider.md | 3 + docs/auth/google/provider.md | 6 ++ docs/auth/microsoft/provider.md | 1 + docs/auth/oidc.md | 1 + docs/auth/okta/provider.md | 3 + docs/auth/onelogin/provider.md | 6 ++ docs/auth/vmware-cloud/provider.md | 6 ++ .../config.d.ts | 3 + .../package.json | 1 + .../config.d.ts | 3 + .../package.json | 1 + .../config.d.ts | 3 + .../package.json | 1 + .../config.d.ts | 3 + .../package.json | 1 + .../config.d.ts | 3 + .../package.json | 1 + .../config.d.ts | 1 + .../config.d.ts | 3 + .../config.d.ts | 3 + .../package.json | 1 + .../config.d.ts | 3 + .../package.json | 1 + .../config.d.ts | 3 + .../package.json | 1 + .../config.d.ts | 3 + .../package.json | 1 + .../config.d.ts | 3 + .../package.json | 1 + .../config.d.ts | 3 + .../package.json | 1 + .../config.d.ts | 3 + .../package.json | 1 + .../config.d.ts | 3 + .../package.json | 1 + .../package.json | 1 + .../src/config.d.ts | 3 + .../config.d.ts | 3 + .../package.json | 1 + .../auth-node/src/oauth/OAuthCookieManager.ts | 16 ++--- .../oauth/createOAuthRouteHandlers.test.ts | 69 +++++++++++++++++++ .../src/oauth/createOAuthRouteHandlers.ts | 6 +- yarn.lock | 15 ++++ 51 files changed, 245 insertions(+), 9 deletions(-) create mode 100644 .changeset/serious-ads-leave.md diff --git a/.changeset/serious-ads-leave.md b/.changeset/serious-ads-leave.md new file mode 100644 index 0000000000..9caf1bc461 --- /dev/null +++ b/.changeset/serious-ads-leave.md @@ -0,0 +1,22 @@ +--- +'@backstage/plugin-auth-backend-module-cloudflare-access-provider': minor +'@backstage/plugin-auth-backend-module-bitbucket-server-provider': minor +'@backstage/plugin-auth-backend-module-vmware-cloud-provider': minor +'@backstage/plugin-auth-backend-module-atlassian-provider': minor +'@backstage/plugin-auth-backend-module-bitbucket-provider': minor +'@backstage/plugin-auth-backend-module-microsoft-provider': minor +'@backstage/plugin-auth-backend-module-onelogin-provider': minor +'@backstage/plugin-auth-backend-module-pinniped-provider': minor +'@backstage/plugin-auth-backend-module-aws-alb-provider': minor +'@backstage/plugin-auth-backend-module-gcp-iap-provider': minor +'@backstage/plugin-auth-backend-module-github-provider': minor +'@backstage/plugin-auth-backend-module-gitlab-provider': minor +'@backstage/plugin-auth-backend-module-google-provider': minor +'@backstage/plugin-auth-backend-module-oauth2-provider': minor +'@backstage/plugin-auth-backend-module-auth0-provider': minor +'@backstage/plugin-auth-backend-module-oidc-provider': minor +'@backstage/plugin-auth-backend-module-okta-provider': minor +'@backstage/plugin-auth-node': minor +--- + +Added `auth.providers..sessionDuration` config for auth providers to allow the lifespan of user sessions to be configured. diff --git a/docs/auth/atlassian/provider.md b/docs/auth/atlassian/provider.md index a1009b586e..ffee3454a9 100644 --- a/docs/auth/atlassian/provider.md +++ b/docs/auth/atlassian/provider.md @@ -51,6 +51,8 @@ auth: additionalScopes: - 'read:jira-user' - 'read:jira-work' + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code signIn: resolvers: # See https://backstage.io/docs/auth/atlassian/provider#resolvers for more resolvers @@ -67,6 +69,10 @@ The Atlassian provider is a structure with the following configuration keys: **NOTE:** The scopes `offline_access`, `read:jira-work`, and `read:jira-user` are provided by default. +### Optional + +- `sessionDuration`: Lifespan of the user session. + ### Resolvers This provider includes several resolvers out of the box that you can use: diff --git a/docs/auth/auth0/provider.md b/docs/auth/auth0/provider.md index 2873a2cace..44a5bb0e43 100644 --- a/docs/auth/auth0/provider.md +++ b/docs/auth/auth0/provider.md @@ -44,6 +44,8 @@ auth: audience: ${AUTH_AUTH0_AUDIENCE} connection: ${AUTH_AUTH0_CONNECTION} connectionScope: ${AUTH_AUTH0_CONNECTION_SCOPE} + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code session: secret: ${AUTH_SESSION_SECRET} ``` @@ -66,6 +68,7 @@ Auth0 requires a session, so you need to give the session a secret key. - `audience`: The intended recipients of the token. - `connection`: Social identity provider name. To check the available social connections, please visit [Auth0 Social Connections](https://marketplace.auth0.com/features/social-connections). - `connectionScope`: Additional scopes in the interactive token request. It should always be used in combination with the `connection` parameter. +- `sessionDuration`: Lifespan of the user session. ### Resolvers diff --git a/docs/auth/aws-alb/provider.md b/docs/auth/aws-alb/provider.md index 5380877f86..5f289170d2 100644 --- a/docs/auth/aws-alb/provider.md +++ b/docs/auth/aws-alb/provider.md @@ -23,6 +23,8 @@ auth: signer: 'arn:aws:elasticloadbalancing:us-east-2:123456789012:loadbalancer/app/my-load-balancer/1234567890123456' # this is the region where your ALB instance resides region: 'us-west-2' + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code signIn: resolvers: # See https://backstage.io/docs/auth/aws-alb/provider#resolvers for more resolvers @@ -31,6 +33,10 @@ auth: Ensure that you have set the signer correctly. It is also recommended that you restrict your target groups' security policy to only accept connections from that ALB. +### Optional + +- `sessionDuration`: Lifespan of the user session. + ### Resolvers This provider includes several resolvers out of the box that you can use: diff --git a/docs/auth/bitbucket/provider.md b/docs/auth/bitbucket/provider.md index e58ad622fc..7667c7c02e 100644 --- a/docs/auth/bitbucket/provider.md +++ b/docs/auth/bitbucket/provider.md @@ -37,6 +37,8 @@ auth: development: clientId: ${AUTH_BITBUCKET_CLIENT_ID} clientSecret: ${AUTH_BITBUCKET_CLIENT_SECRET} + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code signIn: resolvers: # See https://backstage.io/docs/auth/bitbucket/provider#resolvers for more resolvers @@ -49,6 +51,10 @@ The Bitbucket provider is a structure with two configuration keys: `b59241722e3c3b4816e2` - `clientSecret`: The Secret tied to the generated Key. +### Optional + +- `sessionDuration`: Lifespan of the user session. + ### Resolvers This provider includes several resolvers out of the box that you can use: diff --git a/docs/auth/bitbucketServer/provider.md b/docs/auth/bitbucketServer/provider.md index c41457d09c..fe828a405a 100644 --- a/docs/auth/bitbucketServer/provider.md +++ b/docs/auth/bitbucketServer/provider.md @@ -34,6 +34,8 @@ auth: host: bitbucket.example.org clientId: ${AUTH_BITBUCKET_SERVER_CLIENT_ID} clientSecret: ${AUTH_BITBUCKET_SERVER_CLIENT_SECRET} + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code ``` The Bitbucket Server provider is a structure with two configuration keys: @@ -41,6 +43,10 @@ The Bitbucket Server provider is a structure with two configuration keys: - `clientId`: The client ID that was generated by Bitbucket, e.g. `b0f868455c15dcdff5c5fb5d173ae684`. - `clientSecret`: The client secret tied to the generated client ID. +### Optional + +- `sessionDuration`: Lifespan of the user session. + ### Resolvers This provider includes several resolvers out of the box that you can use: diff --git a/docs/auth/cloudflare/provider.md b/docs/auth/cloudflare/provider.md index 224a64f056..38f9a3f06a 100644 --- a/docs/auth/cloudflare/provider.md +++ b/docs/auth/cloudflare/provider.md @@ -38,6 +38,8 @@ auth: # You can customize the authorization cookie name, by default # CF_Authorization is used authorizationCookieName: + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code # This picks what sign in resolver(s) you want to use. signIn: resolvers: @@ -47,6 +49,10 @@ auth: This config section must be in place for the provider to load at all. +### Optional + +- `sessionDuration`: Lifespan of the user session. + ### Resolvers This provider includes several resolvers out of the box that you can use: diff --git a/docs/auth/github/provider.md b/docs/auth/github/provider.md index 3b61815017..b4168be18f 100644 --- a/docs/auth/github/provider.md +++ b/docs/auth/github/provider.md @@ -49,6 +49,8 @@ auth: clientSecret: ${AUTH_GITHUB_CLIENT_SECRET} ## uncomment if using GitHub Enterprise # enterpriseInstanceUrl: ${AUTH_GITHUB_ENTERPRISE_INSTANCE_URL} + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code signIn: resolvers: # See https://backstage.io/docs/auth/github/provider#resolvers for more resolvers @@ -66,6 +68,7 @@ The GitHub provider is a structure with these configuration keys: initiating an OAuth flow, e.g. `https://your-intermediate-service.com/handler`. Only needed if Backstage is not the immediate receiver (e.g. one OAuth app for many backstage instances). +- `sessionDuration` (optional): Lifespan of the user session. - `signIn`: The configuration for the sign-in process, including the **resolvers** that should be used to match the user from the auth provider with the user entity in the Backstage catalog (typically a single resolver is sufficient). diff --git a/docs/auth/gitlab/provider.md b/docs/auth/gitlab/provider.md index 5241481463..b18b530b61 100644 --- a/docs/auth/gitlab/provider.md +++ b/docs/auth/gitlab/provider.md @@ -44,6 +44,8 @@ auth: # audience: https://gitlab.company.com ## uncomment if using a custom redirect URI # callbackUrl: https://${BASE_URL}/api/auth/gitlab/handler/frame + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code signIn: resolvers: # See https://backstage.io/docs/auth/gitlab/provider#resolvers for more resolvers @@ -60,6 +62,7 @@ The GitLab provider is a structure with three configuration keys: - `callbackUrl` (optional): The URL matching the Redirect URI registered when creating your GitLab OAuth App, e.g. `https://$backstage.acme.corp/api/auth/gitlab/handler/frame` Note: Due to a peculiarity with GitLab OAuth, ensure there is no trailing `/` after 'frame' in the URL. +- `sessionDuration` (optional): Lifespan of the user session. ### Resolvers diff --git a/docs/auth/google/provider.md b/docs/auth/google/provider.md index e03985d432..9a550d5ee0 100644 --- a/docs/auth/google/provider.md +++ b/docs/auth/google/provider.md @@ -42,6 +42,8 @@ auth: development: clientId: ${AUTH_GOOGLE_CLIENT_ID} clientSecret: ${AUTH_GOOGLE_CLIENT_SECRET} + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code signIn: resolvers: # See https://backstage.io/docs/auth/google/provider#resolvers for more resolvers @@ -54,6 +56,10 @@ The Google provider is a structure with two configuration keys: `10023341500512-beui241gjwwkrdkr2eh7dprewj2pp1q.apps.googleusercontent.com` - `clientSecret`: The client secret tied to the generated client ID. +### Optional + +- `sessionDuration`: Lifespan of the user session. + ### Resolvers This provider includes several resolvers out of the box that you can use: diff --git a/docs/auth/microsoft/provider.md b/docs/auth/microsoft/provider.md index 74416f853e..39eed92cba 100644 --- a/docs/auth/microsoft/provider.md +++ b/docs/auth/microsoft/provider.md @@ -84,6 +84,7 @@ The Microsoft provider is a structure with three mandatory configuration keys: For more details, see [Home Realm Discovery](https://learn.microsoft.com/en-us/azure/active-directory/manage-apps/home-realm-discovery-policy) - `additionalScopes` (optional): List of scopes for the App Registration, to be requested in addition to the required ones. - `skipUserProfile` (optional): If true, skips loading the user profile even if the `User.Read` scope is present. This is a performance optimization during login and can be used with resolvers that only needs the email address in `spec.profile.email` obtained when the `email` OAuth2 scope is present. +- `sessionDuration` (optional): Lifespan of the user session. ### Resolvers diff --git a/docs/auth/oidc.md b/docs/auth/oidc.md index 6d2a937320..48f1973c2e 100644 --- a/docs/auth/oidc.md +++ b/docs/auth/oidc.md @@ -263,6 +263,7 @@ check the App Registration you created: basically the same thing. - `prompt`: Recommended to use `auto` so the browser will request login to the IDP if the user has no active session. +- `sessionDuration` (optional): Lifespan of the user session. Note that for the time being, any change in this yaml file requires a restart of the app, also you need to have the `session.secret` part to use OIDC (some other providers might diff --git a/docs/auth/okta/provider.md b/docs/auth/okta/provider.md index abc6231839..1e88aa4cf6 100644 --- a/docs/auth/okta/provider.md +++ b/docs/auth/okta/provider.md @@ -45,6 +45,8 @@ auth: audience: ${AUTH_OKTA_DOMAIN} authServerId: ${AUTH_OKTA_AUTH_SERVER_ID} # Optional idp: ${AUTH_OKTA_IDP} # Optional + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # Optional: supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code # https://developer.okta.com/docs/reference/api/oidc/#scope-dependent-claims-not-always-returned additionalScopes: ${AUTH_OKTA_ADDITIONAL_SCOPES} # Optional signIn: @@ -62,6 +64,7 @@ The values referenced are found on the Application page on your Okta site. `https://company.okta.com` - `authServerId`: The authorization server ID for the Application - `idp`: The identity provider for the application, e.g. `0oaulob4BFVa4zQvt0g3` +- `sessionDuration`: Lifespan of the user session. `additionalScopes` is an optional value, a string of space separated scopes, that will be combined with the default `scope` value of `openid profile email offline_access` to adjust the `scope` sent to Okta during OAuth. This will have an impact on [the dependent claims returned](https://developer.okta.com/docs/reference/api/oidc/#scope-dependent-claims-not-always-returned). For example, setting the `additionalScopes` value to `groups` will result in the claim returning a list of the groups that the user is a member of that also match the ID token group filter of the client app. diff --git a/docs/auth/onelogin/provider.md b/docs/auth/onelogin/provider.md index edceb1de62..d3f57f0615 100644 --- a/docs/auth/onelogin/provider.md +++ b/docs/auth/onelogin/provider.md @@ -38,6 +38,8 @@ auth: clientId: ${AUTH_ONELOGIN_CLIENT_ID} clientSecret: ${AUTH_ONELOGIN_CLIENT_SECRET} issuer: https://.onelogin.com/oidc/2 + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code signIn: resolvers: # See https://backstage.io/docs/auth/onelogin/provider#resolvers for more resolvers @@ -51,6 +53,10 @@ found on the SSO tab** for the OneLogin Application: - `clientSecret`: The client secret - `issuer`: The issuer URL +### Optional + +- `sessionDuration`: Lifespan of the user session. + ### Resolvers This provider includes several resolvers out of the box that you can use: diff --git a/docs/auth/vmware-cloud/provider.md b/docs/auth/vmware-cloud/provider.md index d654331178..3f747f494e 100644 --- a/docs/auth/vmware-cloud/provider.md +++ b/docs/auth/vmware-cloud/provider.md @@ -47,6 +47,8 @@ auth: development: clientId: ${APP_ID} organizationId: ${ORG_ID} + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code signIn: resolvers: # See https://backstage.io/docs/auth/vmware-cloud/provider#resolvers for more resolvers @@ -68,6 +70,10 @@ key for signing session cookies set by Backstage. ::: +### Optional + +- `sessionDuration`: Lifespan of the user session. + ### Resolvers This provider includes several resolvers out of the box that you can use: diff --git a/plugins/auth-backend-module-atlassian-provider/config.d.ts b/plugins/auth-backend-module-atlassian-provider/config.d.ts index b1edea3d6c..f6433c123e 100644 --- a/plugins/auth-backend-module-atlassian-provider/config.d.ts +++ b/plugins/auth-backend-module-atlassian-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -38,6 +40,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-atlassian-provider/package.json b/plugins/auth-backend-module-atlassian-provider/package.json index ee17153053..cc625dfe30 100644 --- a/plugins/auth-backend-module-atlassian-provider/package.json +++ b/plugins/auth-backend-module-atlassian-provider/package.json @@ -45,6 +45,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/types": "workspace:^", "supertest": "^7.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/auth-backend-module-auth0-provider/config.d.ts b/plugins/auth-backend-module-auth0-provider/config.d.ts index a0ec7d4eac..661409a592 100644 --- a/plugins/auth-backend-module-auth0-provider/config.d.ts +++ b/plugins/auth-backend-module-auth0-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -30,6 +32,7 @@ export interface Config { audience?: string; connection?: string; connectionScope?: string; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-auth0-provider/package.json b/plugins/auth-backend-module-auth0-provider/package.json index 787cce9072..2fb6370095 100644 --- a/plugins/auth-backend-module-auth0-provider/package.json +++ b/plugins/auth-backend-module-auth0-provider/package.json @@ -45,6 +45,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/types": "workspace:^", "@types/passport-auth0": "^1.0.5", "@types/passport-oauth2": "^1.4.15", "supertest": "^7.0.0" diff --git a/plugins/auth-backend-module-aws-alb-provider/config.d.ts b/plugins/auth-backend-module-aws-alb-provider/config.d.ts index 2e38490b28..41c84872de 100644 --- a/plugins/auth-backend-module-aws-alb-provider/config.d.ts +++ b/plugins/auth-backend-module-aws-alb-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -48,6 +50,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-aws-alb-provider/package.json b/plugins/auth-backend-module-aws-alb-provider/package.json index 0e804631dc..5985154eb5 100644 --- a/plugins/auth-backend-module-aws-alb-provider/package.json +++ b/plugins/auth-backend-module-aws-alb-provider/package.json @@ -48,6 +48,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/config": "workspace:^", + "@backstage/types": "workspace:^", "express": "^4.18.2", "msw": "^2.0.8" } diff --git a/plugins/auth-backend-module-bitbucket-provider/config.d.ts b/plugins/auth-backend-module-bitbucket-provider/config.d.ts index 552a66d700..f3d82608bd 100644 --- a/plugins/auth-backend-module-bitbucket-provider/config.d.ts +++ b/plugins/auth-backend-module-bitbucket-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -36,6 +38,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-bitbucket-provider/package.json b/plugins/auth-backend-module-bitbucket-provider/package.json index 9a4f801efa..d8f137d1d0 100644 --- a/plugins/auth-backend-module-bitbucket-provider/package.json +++ b/plugins/auth-backend-module-bitbucket-provider/package.json @@ -45,6 +45,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/types": "workspace:^", "supertest": "^7.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/auth-backend-module-bitbucket-server-provider/config.d.ts b/plugins/auth-backend-module-bitbucket-server-provider/config.d.ts index ee924f47f0..8e63581625 100644 --- a/plugins/auth-backend-module-bitbucket-server-provider/config.d.ts +++ b/plugins/auth-backend-module-bitbucket-server-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -27,6 +29,7 @@ export interface Config { clientSecret: string; host: string; callbackUrl?: string; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-bitbucket-server-provider/package.json b/plugins/auth-backend-module-bitbucket-server-provider/package.json index 6b3a1f95f0..c546d05be9 100644 --- a/plugins/auth-backend-module-bitbucket-server-provider/package.json +++ b/plugins/auth-backend-module-bitbucket-server-provider/package.json @@ -44,6 +44,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/types": "workspace:^", "@types/passport-oauth2": "^1.4.15", "supertest": "^7.0.0" }, diff --git a/plugins/auth-backend-module-cloudflare-access-provider/config.d.ts b/plugins/auth-backend-module-cloudflare-access-provider/config.d.ts index b73c8d87a5..39a4ca3eb6 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/config.d.ts +++ b/plugins/auth-backend-module-cloudflare-access-provider/config.d.ts @@ -43,6 +43,7 @@ export interface Config { * The backstage token expiration. */ backstageTokenExpiration?: HumanDuration | string; + sessionDuration?: HumanDuration | string; }; }; } diff --git a/plugins/auth-backend-module-gcp-iap-provider/config.d.ts b/plugins/auth-backend-module-gcp-iap-provider/config.d.ts index d4fe015b7c..d99d890b50 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/config.d.ts +++ b/plugins/auth-backend-module-gcp-iap-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -43,6 +45,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-github-provider/config.d.ts b/plugins/auth-backend-module-github-provider/config.d.ts index b1bc0dc477..69abfa0d12 100644 --- a/plugins/auth-backend-module-github-provider/config.d.ts +++ b/plugins/auth-backend-module-github-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -38,6 +40,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-github-provider/package.json b/plugins/auth-backend-module-github-provider/package.json index 5a78c46541..ecf8fced7c 100644 --- a/plugins/auth-backend-module-github-provider/package.json +++ b/plugins/auth-backend-module-github-provider/package.json @@ -43,6 +43,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/types": "workspace:^", "supertest": "^7.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/auth-backend-module-gitlab-provider/config.d.ts b/plugins/auth-backend-module-gitlab-provider/config.d.ts index cbb9f01e02..8d47421255 100644 --- a/plugins/auth-backend-module-gitlab-provider/config.d.ts +++ b/plugins/auth-backend-module-gitlab-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -38,6 +40,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-gitlab-provider/package.json b/plugins/auth-backend-module-gitlab-provider/package.json index ec0a967264..4f26d564ee 100644 --- a/plugins/auth-backend-module-gitlab-provider/package.json +++ b/plugins/auth-backend-module-gitlab-provider/package.json @@ -45,6 +45,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/types": "workspace:^", "supertest": "^7.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/auth-backend-module-google-provider/config.d.ts b/plugins/auth-backend-module-google-provider/config.d.ts index 1571c06deb..ec3cd92b31 100644 --- a/plugins/auth-backend-module-google-provider/config.d.ts +++ b/plugins/auth-backend-module-google-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { /** Configuration options for the auth plugin */ auth?: { @@ -38,6 +40,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-google-provider/package.json b/plugins/auth-backend-module-google-provider/package.json index 4c0313ba62..b88106229a 100644 --- a/plugins/auth-backend-module-google-provider/package.json +++ b/plugins/auth-backend-module-google-provider/package.json @@ -47,6 +47,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/types": "workspace:^", "@types/passport-google-oauth20": "^2.0.3", "supertest": "^7.0.0" }, diff --git a/plugins/auth-backend-module-microsoft-provider/config.d.ts b/plugins/auth-backend-module-microsoft-provider/config.d.ts index 070b6300f7..d69637e7a2 100644 --- a/plugins/auth-backend-module-microsoft-provider/config.d.ts +++ b/plugins/auth-backend-module-microsoft-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -40,6 +42,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-microsoft-provider/package.json b/plugins/auth-backend-module-microsoft-provider/package.json index a2d45c1ddd..028fea47ed 100644 --- a/plugins/auth-backend-module-microsoft-provider/package.json +++ b/plugins/auth-backend-module-microsoft-provider/package.json @@ -46,6 +46,7 @@ "@backstage/cli": "workspace:^", "@backstage/config": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/types": "workspace:^", "@types/passport-microsoft": "^1.0.0", "msw": "^1.0.0", "supertest": "^7.0.0" diff --git a/plugins/auth-backend-module-oauth2-provider/config.d.ts b/plugins/auth-backend-module-oauth2-provider/config.d.ts index 5cee3a5407..ba047e3340 100644 --- a/plugins/auth-backend-module-oauth2-provider/config.d.ts +++ b/plugins/auth-backend-module-oauth2-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -42,6 +44,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-oauth2-provider/package.json b/plugins/auth-backend-module-oauth2-provider/package.json index e9c236ee5d..6da2bafc97 100644 --- a/plugins/auth-backend-module-oauth2-provider/package.json +++ b/plugins/auth-backend-module-oauth2-provider/package.json @@ -44,6 +44,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/types": "workspace:^", "supertest": "^7.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/auth-backend-module-oidc-provider/config.d.ts b/plugins/auth-backend-module-oidc-provider/config.d.ts index d59c40697a..409c658c23 100644 --- a/plugins/auth-backend-module-oidc-provider/config.d.ts +++ b/plugins/auth-backend-module-oidc-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -40,6 +42,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-oidc-provider/package.json b/plugins/auth-backend-module-oidc-provider/package.json index f52eddcdcd..b7897f521e 100644 --- a/plugins/auth-backend-module-oidc-provider/package.json +++ b/plugins/auth-backend-module-oidc-provider/package.json @@ -46,6 +46,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/config": "workspace:^", + "@backstage/types": "workspace:^", "cookie-parser": "^1.4.6", "express-promise-router": "^4.1.1", "express-session": "^1.17.3", diff --git a/plugins/auth-backend-module-okta-provider/config.d.ts b/plugins/auth-backend-module-okta-provider/config.d.ts index d2b0c4aa02..3f27e805ec 100644 --- a/plugins/auth-backend-module-okta-provider/config.d.ts +++ b/plugins/auth-backend-module-okta-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -40,6 +42,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-okta-provider/package.json b/plugins/auth-backend-module-okta-provider/package.json index fe1dd2e32e..26e45a0944 100644 --- a/plugins/auth-backend-module-okta-provider/package.json +++ b/plugins/auth-backend-module-okta-provider/package.json @@ -45,6 +45,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/types": "workspace:^", "supertest": "^7.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/auth-backend-module-onelogin-provider/config.d.ts b/plugins/auth-backend-module-onelogin-provider/config.d.ts index da7c312dd3..58f471bafb 100644 --- a/plugins/auth-backend-module-onelogin-provider/config.d.ts +++ b/plugins/auth-backend-module-onelogin-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -37,6 +39,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-onelogin-provider/package.json b/plugins/auth-backend-module-onelogin-provider/package.json index 90e0a745a4..2c1e7de9f7 100644 --- a/plugins/auth-backend-module-onelogin-provider/package.json +++ b/plugins/auth-backend-module-onelogin-provider/package.json @@ -45,6 +45,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/types": "workspace:^", "supertest": "^7.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/auth-backend-module-pinniped-provider/package.json b/plugins/auth-backend-module-pinniped-provider/package.json index c20faccc48..577bc0ce90 100644 --- a/plugins/auth-backend-module-pinniped-provider/package.json +++ b/plugins/auth-backend-module-pinniped-provider/package.json @@ -36,6 +36,7 @@ "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", + "@backstage/types": "workspace:^", "luxon": "^3.4.3", "openid-client": "^5.4.3" }, diff --git a/plugins/auth-backend-module-pinniped-provider/src/config.d.ts b/plugins/auth-backend-module-pinniped-provider/src/config.d.ts index 50685abfb0..242de7b2b9 100644 --- a/plugins/auth-backend-module-pinniped-provider/src/config.d.ts +++ b/plugins/auth-backend-module-pinniped-provider/src/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { /** Configuration options for the auth plugin */ auth?: { @@ -27,6 +29,7 @@ export interface Config { */ clientSecret: string; scope?: string; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts b/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts index 67db735713..9886d69a41 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -34,6 +36,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-vmware-cloud-provider/package.json b/plugins/auth-backend-module-vmware-cloud-provider/package.json index e081c896ac..dd6ce0941b 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/package.json +++ b/plugins/auth-backend-module-vmware-cloud-provider/package.json @@ -47,6 +47,7 @@ "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/types": "workspace:^", "msw": "^2.0.8", "supertest": "^7.0.0" } diff --git a/plugins/auth-node/src/oauth/OAuthCookieManager.ts b/plugins/auth-node/src/oauth/OAuthCookieManager.ts index d594d7e78b..8d1c4787cc 100644 --- a/plugins/auth-node/src/oauth/OAuthCookieManager.ts +++ b/plugins/auth-node/src/oauth/OAuthCookieManager.ts @@ -16,6 +16,7 @@ import { CookieOptions, Request, Response } from 'express'; import { CookieConfigurer } from '../types'; +import { HumanDuration, durationToMilliseconds } from '@backstage/types'; const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; const TEN_MINUTES_MS = 600 * 1000; @@ -55,6 +56,7 @@ export class OAuthCookieManager { private readonly nonceCookie: string; private readonly refreshTokenCookie: string; private readonly grantedScopeCookie: string; + private readonly maxAge: number; constructor( private readonly options: { @@ -63,6 +65,7 @@ export class OAuthCookieManager { baseUrl: string; callbackUrl: string; cookieConfigurer?: CookieConfigurer; + sessionDuration?: HumanDuration; }, ) { this.cookieConfigurer = options.cookieConfigurer ?? defaultCookieConfigurer; @@ -70,6 +73,9 @@ export class OAuthCookieManager { this.nonceCookie = `${options.providerId}-nonce`; this.refreshTokenCookie = `${options.providerId}-refresh-token`; this.grantedScopeCookie = `${options.providerId}-granted-scope`; + this.maxAge = options.sessionDuration + ? durationToMilliseconds(options.sessionDuration) + : THOUSAND_DAYS_MS; } private getConfig(origin?: string, pathSuffix: string = '') { @@ -103,7 +109,7 @@ export class OAuthCookieManager { res, this.refreshTokenCookie, refreshToken, - THOUSAND_DAYS_MS, + this.maxAge, origin, ); } @@ -117,13 +123,7 @@ export class OAuthCookieManager { } setGrantedScopes(res: Response, scope: string, origin?: string): void { - this.setCookie( - res, - this.grantedScopeCookie, - scope, - THOUSAND_DAYS_MS, - origin, - ); + this.setCookie(res, this.grantedScopeCookie, scope, this.maxAge, origin); } getNonce(req: Request): string | undefined { diff --git a/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts index 46fa058b47..5bc9fa2551 100644 --- a/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts +++ b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts @@ -813,6 +813,75 @@ describe('createOAuthRouteHandlers', () => { }, }); }); + + it('should set sessionDuration to configured value', async () => { + const baseConfigWithSessionDuration = { + ...baseConfig, + config: new ConfigReader({ + sessionDuration: { days: 7 }, + }), + }; + + const agent = request.agent( + wrapInApp(createOAuthRouteHandlers(baseConfigWithSessionDuration)), + ); + + agent.jar.setCookie( + 'my-provider-refresh-token=refresh-token', + '127.0.0.1', + '/my-provider', + ); + + mockAuthenticator.refresh.mockImplementation(async ({ scope }) => ({ + fullProfile: { id: 'id' } as PassportProfile, + session: { ...mockSession, scope, refreshToken: 'new-refresh-token' }, + })); + + const res = await agent + .post('/my-provider/refresh') + .set('X-Requested-With', 'XMLHttpRequest'); + + expect(res.status).toBe(200); + const expectedExpirationDate = Date.now() + 7 * 24 * 60 * 60 * 1000; + const cookie = getRefreshTokenCookie(agent); + expect(cookie.expiration_date).toBeGreaterThanOrEqual( + expectedExpirationDate - 1000, + ); + expect(cookie.expiration_date).toBeLessThanOrEqual( + expectedExpirationDate + 1000, + ); + }); + + it('should set sessionDuration to default of 1000 days when not configured', async () => { + const agent = request.agent( + wrapInApp(createOAuthRouteHandlers(baseConfig)), + ); + + agent.jar.setCookie( + 'my-provider-refresh-token=refresh-token', + '127.0.0.1', + '/my-provider', + ); + + mockAuthenticator.refresh.mockImplementation(async ({ scope }) => ({ + fullProfile: { id: 'id' } as PassportProfile, + session: { ...mockSession, scope, refreshToken: 'new-refresh-token' }, + })); + + const res = await agent + .post('/my-provider/refresh') + .set('X-Requested-With', 'XMLHttpRequest'); + + expect(res.status).toBe(200); + const expectedExpirationDate = Date.now() + 1000 * 24 * 60 * 60 * 1000; + const cookie = getRefreshTokenCookie(agent); + expect(cookie.expiration_date).toBeGreaterThanOrEqual( + expectedExpirationDate - 1000, + ); + expect(cookie.expiration_date).toBeLessThanOrEqual( + expectedExpirationDate + 1000, + ); + }); }); describe('logout', () => { diff --git a/plugins/auth-node/src/oauth/createOAuthRouteHandlers.ts b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.ts index 53ae4101d3..72f45d8ab1 100644 --- a/plugins/auth-node/src/oauth/createOAuthRouteHandlers.ts +++ b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.ts @@ -40,7 +40,7 @@ import { SignInResolver, } from '../types'; import { OAuthAuthenticator, OAuthAuthenticatorResult } from './types'; -import { Config } from '@backstage/config'; +import { Config, readDurationFromConfig } from '@backstage/config'; import { CookieScopeManager } from './CookieScopeManager'; /** @public */ @@ -99,6 +99,9 @@ export function createOAuthRouteHandlers( const callbackUrl = config.getOptionalString('callbackUrl') ?? `${baseUrl}/${providerId}/handler/frame`; + const sessionDuration = config.has('sessionDuration') + ? readDurationFromConfig(config, { key: 'sessionDuration' }) + : undefined; const stateTransform = options.stateTransform ?? (state => ({ state })); const profileTransform = @@ -110,6 +113,7 @@ export function createOAuthRouteHandlers( defaultAppOrigin, providerId, cookieConfigurer, + sessionDuration, }); const scopeManager = CookieScopeManager.create({ diff --git a/yarn.lock b/yarn.lock index ab8676ace9..f5d1f71c42 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4960,6 +4960,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" express: ^4.18.2 passport: ^0.7.0 passport-atlassian-oauth2: ^2.1.0 @@ -4977,6 +4978,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" "@types/passport-auth0": ^1.0.5 "@types/passport-oauth2": ^1.4.15 express: ^4.17.1 @@ -4997,6 +4999,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" express: ^4.18.2 jose: ^5.0.0 msw: ^2.0.8 @@ -5032,6 +5035,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" express: ^4.18.2 passport: ^0.7.0 passport-bitbucket-oauth2: ^0.1.2 @@ -5049,6 +5053,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" "@types/passport-oauth2": ^1.4.15 passport: ^0.7.0 passport-oauth2: ^1.6.1 @@ -5102,6 +5107,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" passport-github2: ^0.1.12 supertest: ^7.0.0 languageName: unknown @@ -5117,6 +5123,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" express: ^4.18.2 passport: ^0.7.0 passport-gitlab2: ^5.0.0 @@ -5133,6 +5140,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" "@types/passport-google-oauth20": ^2.0.3 google-auth-library: ^9.0.0 passport-google-oauth20: ^2.0.0 @@ -5167,6 +5175,7 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" "@types/passport-microsoft": ^1.0.0 express: ^4.18.2 jose: ^5.0.0 @@ -5186,6 +5195,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" passport: ^0.7.0 passport-oauth2: ^1.6.1 supertest: ^7.0.0 @@ -5216,6 +5226,7 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" cookie-parser: ^1.4.6 express: ^4.18.2 express-promise-router: ^4.1.1 @@ -5238,6 +5249,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" "@davidzemon/passport-okta-oauth": ^0.0.5 express: ^4.18.2 passport: ^0.7.0 @@ -5255,6 +5267,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" express: ^4.18.2 passport: ^0.7.0 passport-onelogin-oauth: ^0.0.1 @@ -5273,6 +5286,7 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" cookie-parser: ^1.4.6 express: ^4.18.2 express-session: ^1.17.3 @@ -5298,6 +5312,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" "@types/passport-oauth2": ^1.4.15 jose: ^5.0.0 msw: ^2.0.8 From 9545c5f66e3cc37aebf2228ea5cb014e5c37a6e8 Mon Sep 17 00:00:00 2001 From: mpsanchis <33475618+mpsanchis@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:11:28 +0100 Subject: [PATCH 25/69] feat(scaffolder-backend-module-gitlab): createGitlabProjectMigrateAction returns migration id Signed-off-by: mpsanchis <33475618+mpsanchis@users.noreply.github.com> --- .changeset/heavy-olives-thank.md | 5 +++++ .../gitlabProjectMigrate.examples.test.ts | 4 +++- .../src/actions/gitlabProjectMigrate.test.ts | 4 +++- .../src/actions/gitlabProjectMigrate.ts | 20 +++++++++++++------ 4 files changed, 25 insertions(+), 8 deletions(-) create mode 100644 .changeset/heavy-olives-thank.md diff --git a/.changeset/heavy-olives-thank.md b/.changeset/heavy-olives-thank.md new file mode 100644 index 0000000000..984b5cb160 --- /dev/null +++ b/.changeset/heavy-olives-thank.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Action createGitlabProjectMigrateAction writes migration Id to its output diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.examples.test.ts index 869bda30ec..d577adc896 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.examples.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.examples.test.ts @@ -24,7 +24,9 @@ import yaml from 'yaml'; const mockGitlabClient = { Migrations: { - create: jest.fn(), + create: jest.fn(() => ({ + id: 0, + })), }, }; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.test.ts index 8e3bc6560d..e1bf9af138 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.test.ts @@ -22,7 +22,9 @@ import { createMockDirectory } from '@backstage/backend-test-utils'; const mockGitlabClient = { Migrations: { - create: jest.fn(), + create: jest.fn(() => ({ + id: 0, + })), }, }; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.ts index 659215c1c0..8f6e3ed2cc 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.ts @@ -89,6 +89,10 @@ export const createGitlabProjectMigrateAction = (options: { title: 'URL to the newly imported repo', type: 'string', }, + migrationId: { + title: 'Id of the migration that imports the project', + type: 'number', + }, }, }, }, @@ -135,17 +139,21 @@ export const createGitlabProjectMigrateAction = (options: { }; try { - await api.Migrations.create(sourceConfig, migrationEntity); + const { id: migrationId } = await api.Migrations.create( + sourceConfig, + migrationEntity, + ); + + ctx.output( + 'importedRepoUrl', + `${destinationHost}/${destinationNamespace}/${destinationSlug}`, + ); + ctx.output('migrationId', migrationId); } catch (e: any) { throw new InputError( `Failed to transfer repo ${sourceFullPath}. Make sure that ${sourceFullPath} exists in ${sourceUrl}, and token has enough rights.\nError: ${e}`, ); } - - ctx.output( - 'importedRepoUrl', - `${destinationHost}/${destinationNamespace}/${destinationSlug}`, - ); }, }); }; From ace69f981d755213053bac28532faaa17caadad7 Mon Sep 17 00:00:00 2001 From: Severin Wischmann Date: Wed, 29 Jan 2025 15:12:10 +0100 Subject: [PATCH 26/69] Use Gitlab path rather than project name Signed-off-by: Severin Wischmann --- .../src/autocomplete/autocomplete.test.ts | 8 ++++---- .../src/autocomplete/autocomplete.ts | 2 ++ plugins/scaffolder-react/src/api/types.ts | 2 +- .../components/fields/RepoUrlPicker/GitlabRepoPicker.tsx | 2 +- .../src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx | 2 +- .../src/components/fields/RepoUrlPicker/types.ts | 1 + 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.test.ts b/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.test.ts index 4fa906686c..a60d317077 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.test.ts @@ -79,8 +79,8 @@ describe('handleAutocompleteRequest', () => { mockClient.Users.showCurrentUser.mockResolvedValue({ id: 3 }); mockClient.Groups.allProjects.mockResolvedValue([ - { name: 'repo1', id: 1 }, - { name: 'repo2', id: 2 }, + { name: 'Repo 1', path: 'repo-1', id: 1 }, + { name: 'Repo 2', path: 'repo-2', id: 2 }, ]); const result = await handleAutocompleteRequest({ @@ -91,8 +91,8 @@ describe('handleAutocompleteRequest', () => { expect(result).toEqual({ results: [ - { title: 'repo1', id: '1' }, - { title: 'repo2', id: '2' }, + { title: 'Repo 1', path: 'repo-1', id: '1' }, + { title: 'Repo 2', path: 'repo-2', id: '2' }, ], }); }); diff --git a/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.ts b/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.ts index 6e9a7f79c4..d9f2f4351d 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.ts @@ -32,6 +32,7 @@ export function createHandleAutocompleteRequest(options: { }): Promise<{ results: { title?: string; + path?: string; id: string; }[]; }> { @@ -97,6 +98,7 @@ export function createHandleAutocompleteRequest(options: { return { results: response.map(project => ({ title: project.name.trim(), + path: project.path, id: project.id.toString(), })), }; diff --git a/plugins/scaffolder-react/src/api/types.ts b/plugins/scaffolder-react/src/api/types.ts index 8e4d989a55..293a6a55e7 100644 --- a/plugins/scaffolder-react/src/api/types.ts +++ b/plugins/scaffolder-react/src/api/types.ts @@ -245,5 +245,5 @@ export interface ScaffolderApi { provider: string; resource: string; context?: Record; - }): Promise<{ results: { title?: string; id: string }[] }>; + }): Promise<{ results: { title?: string; path?: string; id: string }[] }>; } diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx index d97c9431ee..abd7071f44 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx @@ -98,7 +98,7 @@ export const GitlabRepoPicker = ( .then(({ results }) => { onChange({ availableRepos: results.map(r => { - return { name: r.title!, id: r.id }; + return { name: r.title!, path: r.path, id: r.id }; }), }); }) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index 18da234012..41b00cc642 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -244,7 +244,7 @@ export const RepoUrlPicker = ( onChange={repo => setState(prevState => ({ ...prevState, - repoName: repo.name, + repoName: repo.path || repo.name, id: repo.id || '', })) } diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts index d37e764eae..e5c62aed08 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts @@ -15,6 +15,7 @@ */ export interface AvailableRepositories { name: string; + path?: string; id?: string; } From fe4494616fbe9dd5668ebb9fe793b9c09e465e67 Mon Sep 17 00:00:00 2001 From: Severin Wischmann Date: Wed, 29 Jan 2025 15:14:03 +0100 Subject: [PATCH 27/69] Added changeset Signed-off-by: Severin Wischmann --- .changeset/smart-ligers-sniff.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/smart-ligers-sniff.md diff --git a/.changeset/smart-ligers-sniff.md b/.changeset/smart-ligers-sniff.md new file mode 100644 index 0000000000..2a5ec1fd27 --- /dev/null +++ b/.changeset/smart-ligers-sniff.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +'@backstage/plugin-scaffolder-react': patch +'@backstage/plugin-scaffolder': patch +--- + +Changed field that is used in autocomplete From 45ea6f493674fd36726f067b17713b25ce2b8732 Mon Sep 17 00:00:00 2001 From: Jeff Feng Date: Wed, 29 Jan 2025 12:57:01 -0500 Subject: [PATCH 28/69] styling: Matching text alignment of other pages Changing to left-aligned text on the Community page to match how text is aligned on other microsite pages. Signed-off-by: Jeff Feng --- microsite/src/pages/community/community.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/src/pages/community/community.module.scss b/microsite/src/pages/community/community.module.scss index 3f829b0100..14ff66db37 100644 --- a/microsite/src/pages/community/community.module.scss +++ b/microsite/src/pages/community/community.module.scss @@ -10,7 +10,7 @@ } p { - text-align: justify; + text-align: left; } } From 61fef5f18a90251a41645bf224a81aa8a75f6486 Mon Sep 17 00:00:00 2001 From: Jeff Feng Date: Wed, 29 Jan 2025 13:17:45 -0500 Subject: [PATCH 29/69] styling: Matching text alignment Changing to left-aligned text on the Demo page to match how text is aligned on other microsite pages. Signed-off-by: Jeff Feng --- microsite/src/pages/demos/demos.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/src/pages/demos/demos.module.scss b/microsite/src/pages/demos/demos.module.scss index b99874297e..c8caedbeac 100644 --- a/microsite/src/pages/demos/demos.module.scss +++ b/microsite/src/pages/demos/demos.module.scss @@ -13,7 +13,7 @@ .banner { p { flex: 1; - text-align: justify; + text-align: left; } iframe { From a78d52d1c852b3a147613cb3e91a1a3d272517b6 Mon Sep 17 00:00:00 2001 From: Jeff Feng Date: Wed, 29 Jan 2025 13:24:31 -0500 Subject: [PATCH 30/69] Adding CBA info and styling tweaks Adding description/link to the Certified Backstage Associate (CBA) certification. Also, miscellaneous styling tweaks. Signed-off-by: Jeff Feng --- microsite/src/pages/community/index.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/microsite/src/pages/community/index.tsx b/microsite/src/pages/community/index.tsx index 171a5efcdf..0ab29520e7 100644 --- a/microsite/src/pages/community/index.tsx +++ b/microsite/src/pages/community/index.tsx @@ -33,14 +33,14 @@ const Community = () => { }, { content: 'Subscribe to the', - label: 'Community newsletter', + label: 'Community Newsletter', link: 'https://info.backstage.spotify.com/newsletter_subscribe', }, ]; const officialInitiatives: ICollectionItem[] = [ { - title: 'Community sessions', + title: 'Community Sessions', content: 'Maintainers and adopters meet monthly to share updates, demos, and ideas. You can find recorded session on our YouTube channel!', link: 'https://github.com/backstage/community/tree/main/backstage-community-sessions#backstage-community-sessions', @@ -63,6 +63,13 @@ const Community = () => { link: 'https://training.linuxfoundation.org/training/introduction-to-backstage-developer-portals-made-easy-lfs142x/', label: 'Learn more', }, + { + title: 'Certified Backstage Associate (CBA)', + content: + 'Designed for IT engineers, developers, platform engineers, and other IT professionals, the CBA proves you have the skills and the mindset to work with Backstage.', + link: 'https://www.cncf.io/training/certification/cba/', + label: 'Learn more', + }, ]; const partners: { name: string; url: string; logo: string }[] = [ @@ -125,11 +132,11 @@ const Community = () => { > Join the vibrant community around Backstage through social media and different meetups. To ensure that you have a welcoming - environment, we follow the + environment, we follow the{' '} - {' '} - CNCF Code of Conduct{' '} + CNCF Code of Conduct + {' '} in everything we do. @@ -159,7 +166,7 @@ const Community = () => { -

Offical Backstage initiatives

+

Offical Backstage Initiatives

Stay tuned to the latest developments

From e672838876b9d90c936e2802e63969334f7d3a94 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Thu, 30 Jan 2025 07:53:15 +0100 Subject: [PATCH 31/69] Update createGitlabProjectMigrateAction output format Signed-off-by: blam --- .changeset/heavy-olives-thank.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/heavy-olives-thank.md b/.changeset/heavy-olives-thank.md index 984b5cb160..40ca9e16ed 100644 --- a/.changeset/heavy-olives-thank.md +++ b/.changeset/heavy-olives-thank.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder-backend-module-gitlab': patch --- -Action createGitlabProjectMigrateAction writes migration Id to its output +`createGitlabProjectMigrateAction` can now output the `migrationId` From c68e09060da29ffa6cb370bf22c7d99b1ef4dc08 Mon Sep 17 00:00:00 2001 From: Severin Wischmann Date: Thu, 30 Jan 2025 09:26:13 +0100 Subject: [PATCH 32/69] Added API reports Signed-off-by: Severin Wischmann --- plugins/scaffolder-backend-module-gitlab/report.api.md | 6 +++--- plugins/scaffolder-backend/report.api.md | 2 +- plugins/scaffolder-react/report.api.md | 1 + plugins/scaffolder/report-alpha.api.md | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitlab/report.api.md b/plugins/scaffolder-backend-module-gitlab/report.api.md index 896c0a5899..238aa4b05e 100644 --- a/plugins/scaffolder-backend-module-gitlab/report.api.md +++ b/plugins/scaffolder-backend-module-gitlab/report.api.md @@ -33,8 +33,8 @@ export const createGitlabIssueAction: (options: { projectId: number; labels?: string | undefined; description?: string | undefined; - weight?: number | undefined; token?: string | undefined; + weight?: number | undefined; assignees?: number[] | undefined; createdAt?: string | undefined; confidential?: boolean | undefined; @@ -198,7 +198,7 @@ export const createPublishGitlabMergeRequestAction: (options: { sourcePath?: string | undefined; targetPath?: string | undefined; token?: string | undefined; - commitAction?: 'auto' | 'update' | 'delete' | 'create' | 'skip' | undefined; + commitAction?: 'auto' | 'update' | 'skip' | 'delete' | 'create' | undefined; projectid?: string | undefined; removeSourceBranch?: boolean | undefined; assignee?: string | undefined; @@ -234,8 +234,8 @@ export const editGitlabIssueAction: (options: { title?: string | undefined; labels?: string | undefined; description?: string | undefined; - weight?: number | undefined; token?: string | undefined; + weight?: number | undefined; assignees?: number[] | undefined; addLabels?: string | undefined; confidential?: boolean | undefined; diff --git a/plugins/scaffolder-backend/report.api.md b/plugins/scaffolder-backend/report.api.md index c2e6c18ad8..863d4a6a9e 100644 --- a/plugins/scaffolder-backend/report.api.md +++ b/plugins/scaffolder-backend/report.api.md @@ -355,7 +355,7 @@ export const createPublishGitlabMergeRequestAction: (options: { sourcePath?: string | undefined; targetPath?: string | undefined; token?: string | undefined; - commitAction?: 'auto' | 'update' | 'delete' | 'create' | 'skip' | undefined; + commitAction?: 'auto' | 'update' | 'skip' | 'delete' | 'create' | undefined; projectid?: string | undefined; removeSourceBranch?: boolean | undefined; assignee?: string | undefined; diff --git a/plugins/scaffolder-react/report.api.md b/plugins/scaffolder-react/report.api.md index 8a6b4739a4..d83d64d834 100644 --- a/plugins/scaffolder-react/report.api.md +++ b/plugins/scaffolder-react/report.api.md @@ -215,6 +215,7 @@ export interface ScaffolderApi { }): Promise<{ results: { title?: string; + path?: string; id: string; }[]; }>; diff --git a/plugins/scaffolder/report-alpha.api.md b/plugins/scaffolder/report-alpha.api.md index e272b57655..d96a266fe0 100644 --- a/plugins/scaffolder/report-alpha.api.md +++ b/plugins/scaffolder/report-alpha.api.md @@ -291,10 +291,10 @@ export const scaffolderTranslationRef: TranslationRef< readonly 'ongoingTask.title': 'Run of'; readonly 'ongoingTask.contextMenu.cancel': 'Cancel'; readonly 'ongoingTask.contextMenu.startOver': 'Start Over'; - readonly 'ongoingTask.contextMenu.retry': 'Retry'; readonly 'ongoingTask.contextMenu.hideLogs': 'Hide Logs'; readonly 'ongoingTask.contextMenu.showLogs': 'Show Logs'; readonly 'ongoingTask.contextMenu.hideButtonBar': 'Hide Button Bar'; + readonly 'ongoingTask.contextMenu.retry': 'Retry'; readonly 'ongoingTask.contextMenu.showButtonBar': 'Show Button Bar'; readonly 'ongoingTask.subtitle': 'Task {{taskId}}'; readonly 'ongoingTask.pageTitle.hasTemplateName': 'Run of {{templateName}}'; @@ -364,8 +364,8 @@ export const scaffolderTranslationRef: TranslationRef< readonly 'templateEditorToolbar.addToCatalogDialogTitle': 'Publish changes'; readonly 'templateEditorToolbar.addToCatalogDialogContent.stepsIntroduction': 'Follow the instructions below to create or update a template:'; readonly 'templateEditorToolbar.addToCatalogDialogContent.stepsListItems': 'Save the template files in a local directory\nCreate a pull request to a new or existing git repository\nIf the template already exists, the changes will be reflected in the software catalog once the pull request gets merged\nBut if you are creating a new template, follow the documentation linked below to register the new template repository in software catalog'; - readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationUrl': 'https://backstage.io/docs/features/software-templates/adding-templates/'; readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationButton': 'Go to the documentation'; + readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationUrl': 'https://backstage.io/docs/features/software-templates/adding-templates/'; readonly 'templateEditorToolbarFileMenu.button': 'File'; readonly 'templateEditorToolbarFileMenu.options.openDirectory': 'Open template directory'; readonly 'templateEditorToolbarFileMenu.options.createDirectory': 'Create template directory'; From 5def47e1da7fdcded9667a01ff080fa5e89743bb Mon Sep 17 00:00:00 2001 From: Severin Wischmann Date: Thu, 30 Jan 2025 09:49:28 +0100 Subject: [PATCH 33/69] Added api-reports Signed-off-by: Severin Wischmann --- plugins/scaffolder-backend-module-gitlab/report.api.md | 6 +++--- plugins/scaffolder-backend/report.api.md | 2 +- plugins/scaffolder/report-alpha.api.md | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitlab/report.api.md b/plugins/scaffolder-backend-module-gitlab/report.api.md index 238aa4b05e..896c0a5899 100644 --- a/plugins/scaffolder-backend-module-gitlab/report.api.md +++ b/plugins/scaffolder-backend-module-gitlab/report.api.md @@ -33,8 +33,8 @@ export const createGitlabIssueAction: (options: { projectId: number; labels?: string | undefined; description?: string | undefined; - token?: string | undefined; weight?: number | undefined; + token?: string | undefined; assignees?: number[] | undefined; createdAt?: string | undefined; confidential?: boolean | undefined; @@ -198,7 +198,7 @@ export const createPublishGitlabMergeRequestAction: (options: { sourcePath?: string | undefined; targetPath?: string | undefined; token?: string | undefined; - commitAction?: 'auto' | 'update' | 'skip' | 'delete' | 'create' | undefined; + commitAction?: 'auto' | 'update' | 'delete' | 'create' | 'skip' | undefined; projectid?: string | undefined; removeSourceBranch?: boolean | undefined; assignee?: string | undefined; @@ -234,8 +234,8 @@ export const editGitlabIssueAction: (options: { title?: string | undefined; labels?: string | undefined; description?: string | undefined; - token?: string | undefined; weight?: number | undefined; + token?: string | undefined; assignees?: number[] | undefined; addLabels?: string | undefined; confidential?: boolean | undefined; diff --git a/plugins/scaffolder-backend/report.api.md b/plugins/scaffolder-backend/report.api.md index 863d4a6a9e..c2e6c18ad8 100644 --- a/plugins/scaffolder-backend/report.api.md +++ b/plugins/scaffolder-backend/report.api.md @@ -355,7 +355,7 @@ export const createPublishGitlabMergeRequestAction: (options: { sourcePath?: string | undefined; targetPath?: string | undefined; token?: string | undefined; - commitAction?: 'auto' | 'update' | 'skip' | 'delete' | 'create' | undefined; + commitAction?: 'auto' | 'update' | 'delete' | 'create' | 'skip' | undefined; projectid?: string | undefined; removeSourceBranch?: boolean | undefined; assignee?: string | undefined; diff --git a/plugins/scaffolder/report-alpha.api.md b/plugins/scaffolder/report-alpha.api.md index d96a266fe0..e272b57655 100644 --- a/plugins/scaffolder/report-alpha.api.md +++ b/plugins/scaffolder/report-alpha.api.md @@ -291,10 +291,10 @@ export const scaffolderTranslationRef: TranslationRef< readonly 'ongoingTask.title': 'Run of'; readonly 'ongoingTask.contextMenu.cancel': 'Cancel'; readonly 'ongoingTask.contextMenu.startOver': 'Start Over'; + readonly 'ongoingTask.contextMenu.retry': 'Retry'; readonly 'ongoingTask.contextMenu.hideLogs': 'Hide Logs'; readonly 'ongoingTask.contextMenu.showLogs': 'Show Logs'; readonly 'ongoingTask.contextMenu.hideButtonBar': 'Hide Button Bar'; - readonly 'ongoingTask.contextMenu.retry': 'Retry'; readonly 'ongoingTask.contextMenu.showButtonBar': 'Show Button Bar'; readonly 'ongoingTask.subtitle': 'Task {{taskId}}'; readonly 'ongoingTask.pageTitle.hasTemplateName': 'Run of {{templateName}}'; @@ -364,8 +364,8 @@ export const scaffolderTranslationRef: TranslationRef< readonly 'templateEditorToolbar.addToCatalogDialogTitle': 'Publish changes'; readonly 'templateEditorToolbar.addToCatalogDialogContent.stepsIntroduction': 'Follow the instructions below to create or update a template:'; readonly 'templateEditorToolbar.addToCatalogDialogContent.stepsListItems': 'Save the template files in a local directory\nCreate a pull request to a new or existing git repository\nIf the template already exists, the changes will be reflected in the software catalog once the pull request gets merged\nBut if you are creating a new template, follow the documentation linked below to register the new template repository in software catalog'; - readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationButton': 'Go to the documentation'; readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationUrl': 'https://backstage.io/docs/features/software-templates/adding-templates/'; + readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationButton': 'Go to the documentation'; readonly 'templateEditorToolbarFileMenu.button': 'File'; readonly 'templateEditorToolbarFileMenu.options.openDirectory': 'Open template directory'; readonly 'templateEditorToolbarFileMenu.options.createDirectory': 'Create template directory'; From d0cd6a0dec97eff6587e786738388349e0a9e310 Mon Sep 17 00:00:00 2001 From: Severin Wischmann Date: Thu, 30 Jan 2025 13:36:11 +0100 Subject: [PATCH 34/69] Changes functionality in line with comment Based on [this comment](https://github.com/backstage/backstage/issues/28565#issuecomment-2624141216), the path is now the `id` Signed-off-by: Severin Wischmann --- .../src/autocomplete/autocomplete.test.ts | 16 ++++++++-------- .../src/autocomplete/autocomplete.ts | 4 +--- plugins/scaffolder-react/report.api.md | 1 - plugins/scaffolder-react/src/api/types.ts | 2 +- .../fields/RepoUrlPicker/GitlabRepoPicker.tsx | 2 +- .../fields/RepoUrlPicker/RepoUrlPicker.tsx | 3 +-- .../src/components/fields/RepoUrlPicker/types.ts | 2 -- .../fields/RepoUrlPicker/utils.test.ts | 6 ++---- .../src/components/fields/RepoUrlPicker/utils.ts | 7 +------ 9 files changed, 15 insertions(+), 28 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.test.ts b/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.test.ts index a60d317077..a2e2f18ab2 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.test.ts @@ -79,8 +79,8 @@ describe('handleAutocompleteRequest', () => { mockClient.Users.showCurrentUser.mockResolvedValue({ id: 3 }); mockClient.Groups.allProjects.mockResolvedValue([ - { name: 'Repo 1', path: 'repo-1', id: 1 }, - { name: 'Repo 2', path: 'repo-2', id: 2 }, + { name: 'Repo 1', path: 'repo-1' }, + { name: 'Repo 2', path: 'repo-2' }, ]); const result = await handleAutocompleteRequest({ @@ -91,8 +91,8 @@ describe('handleAutocompleteRequest', () => { expect(result).toEqual({ results: [ - { title: 'Repo 1', path: 'repo-1', id: '1' }, - { title: 'Repo 2', path: 'repo-2', id: '2' }, + { title: 'Repo 1', id: 'repo-1' }, + { title: 'Repo 2', id: 'repo-2' }, ], }); }); @@ -104,8 +104,8 @@ describe('handleAutocompleteRequest', () => { mockClient.Users.showCurrentUser.mockResolvedValue({ id: 1 }); mockClient.Users.allProjects.mockResolvedValue([ - { name: 'repo1', id: 1 }, - { name: 'repo2', id: 2 }, + { name: 'Repo 1', path: 'repo-1' }, + { name: 'Repo 2', path: 'repo-2' }, ]); const result = await handleAutocompleteRequest({ @@ -116,8 +116,8 @@ describe('handleAutocompleteRequest', () => { expect(result).toEqual({ results: [ - { title: 'repo1', id: '1' }, - { title: 'repo2', id: '2' }, + { title: 'Repo 1', id: 'repo-1' }, + { title: 'Repo 2', id: 'repo-2' }, ], }); }); diff --git a/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.ts b/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.ts index d9f2f4351d..4eddd844e5 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.ts @@ -32,7 +32,6 @@ export function createHandleAutocompleteRequest(options: { }): Promise<{ results: { title?: string; - path?: string; id: string; }[]; }> { @@ -98,8 +97,7 @@ export function createHandleAutocompleteRequest(options: { return { results: response.map(project => ({ title: project.name.trim(), - path: project.path, - id: project.id.toString(), + id: project.path, })), }; } diff --git a/plugins/scaffolder-react/report.api.md b/plugins/scaffolder-react/report.api.md index d83d64d834..8a6b4739a4 100644 --- a/plugins/scaffolder-react/report.api.md +++ b/plugins/scaffolder-react/report.api.md @@ -215,7 +215,6 @@ export interface ScaffolderApi { }): Promise<{ results: { title?: string; - path?: string; id: string; }[]; }>; diff --git a/plugins/scaffolder-react/src/api/types.ts b/plugins/scaffolder-react/src/api/types.ts index 293a6a55e7..8e4d989a55 100644 --- a/plugins/scaffolder-react/src/api/types.ts +++ b/plugins/scaffolder-react/src/api/types.ts @@ -245,5 +245,5 @@ export interface ScaffolderApi { provider: string; resource: string; context?: Record; - }): Promise<{ results: { title?: string; path?: string; id: string }[] }>; + }): Promise<{ results: { title?: string; id: string }[] }>; } diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx index abd7071f44..d97c9431ee 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx @@ -98,7 +98,7 @@ export const GitlabRepoPicker = ( .then(({ results }) => { onChange({ availableRepos: results.map(r => { - return { name: r.title!, path: r.path, id: r.id }; + return { name: r.title!, id: r.id }; }), }); }) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index 41b00cc642..ef6654b60c 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -244,8 +244,7 @@ export const RepoUrlPicker = ( onChange={repo => setState(prevState => ({ ...prevState, - repoName: repo.path || repo.name, - id: repo.id || '', + repoName: repo.id || repo.name, })) } rawErrors={rawErrors} diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts index e5c62aed08..74d45fa401 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts @@ -15,7 +15,6 @@ */ export interface AvailableRepositories { name: string; - path?: string; id?: string; } @@ -26,7 +25,6 @@ export interface RepoUrlPickerState { organization?: string; workspace?: string; project?: string; - id?: string; availableRepos?: AvailableRepositories[]; } diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.test.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.test.ts index ce3c938e31..c90b357cf1 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.test.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.test.ts @@ -53,10 +53,9 @@ describe('utils', () => { organization: 'organization', workspace: 'workspace', project: 'backstage', - id: '1234', }), ).toBe( - 'github.com?owner=owner&repo=backstage&organization=organization&workspace=workspace&project=backstage&id=1234', + 'github.com?owner=owner&repo=backstage&organization=organization&workspace=workspace&project=backstage', ); }); }); @@ -65,7 +64,7 @@ describe('utils', () => { it('should parse a complete string', () => { expect( parseRepoPickerUrl( - 'github.com?owner=owner&repo=backstage&organization=organization&workspace=workspace&project=backstage&id=1234', + 'github.com?owner=owner&repo=backstage&organization=organization&workspace=workspace&project=backstage', ), ).toEqual({ host: 'github.com', @@ -74,7 +73,6 @@ describe('utils', () => { organization: 'organization', workspace: 'workspace', project: 'backstage', - id: '1234', }); }); }); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts index 145a3e256f..408b87413b 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts @@ -37,9 +37,6 @@ export function serializeRepoPickerUrl(data: RepoUrlPickerState) { if (data.project) { params.set('project', data.project); } - if (data.id) { - params.set('id', data.id); - } return `${data.host}?${params.toString()}`; } @@ -53,7 +50,6 @@ export function parseRepoPickerUrl( let organization = ''; let workspace = ''; let project = ''; - let id = ''; try { if (url) { @@ -64,10 +60,9 @@ export function parseRepoPickerUrl( organization = parsed.searchParams.get('organization') || ''; workspace = parsed.searchParams.get('workspace') || ''; project = parsed.searchParams.get('project') || ''; - id = parsed.searchParams.get('id') || ''; } } catch { /* ok */ } - return { host, owner, repoName, organization, workspace, project, id }; + return { host, owner, repoName, organization, workspace, project }; } From 930fc3c4aa8d3643fe6b4828a2ca2e7c4ad1d303 Mon Sep 17 00:00:00 2001 From: Jeff Feng Date: Thu, 30 Jan 2025 13:08:44 -0500 Subject: [PATCH 35/69] Fixed prettier issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Got my prettier working again😅. Signed-off-by: Jeff Feng --- microsite/src/pages/community/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/microsite/src/pages/community/index.tsx b/microsite/src/pages/community/index.tsx index 0ab29520e7..1e31edeedc 100644 --- a/microsite/src/pages/community/index.tsx +++ b/microsite/src/pages/community/index.tsx @@ -134,9 +134,8 @@ const Community = () => { and different meetups. To ensure that you have a welcoming environment, we follow the{' '} - CNCF Code of Conduct - - {' '} + CNCF Code of Conduct + {' '} in everything we do. From 47c27c270d19ea4d41a776e3f8a0343c52aeadf9 Mon Sep 17 00:00:00 2001 From: Tomasz Szuba Date: Wed, 22 Jan 2025 17:47:43 +0100 Subject: [PATCH 36/69] Upgrade `dagre` to `@dagrejs/dagre` Signed-off-by: Tomasz Szuba --- .changeset/large-pans-brake.md | 5 ++++ packages/core-components/package.json | 3 +- .../DependencyGraph/DependencyGraph.tsx | 2 +- .../src/components/DependencyGraph/Edge.tsx | 2 +- .../components/DependencyGraph/Node.test.tsx | 2 +- .../src/components/DependencyGraph/Node.tsx | 2 +- yarn.lock | 28 ++++++++++++------- 7 files changed, 28 insertions(+), 16 deletions(-) create mode 100644 .changeset/large-pans-brake.md diff --git a/.changeset/large-pans-brake.md b/.changeset/large-pans-brake.md new file mode 100644 index 0000000000..619e697f46 --- /dev/null +++ b/.changeset/large-pans-brake.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Upgrade `dagre` to `@dagrejs/dagre` diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 6a84d11eee..55b0d72c6a 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -59,6 +59,7 @@ "@backstage/errors": "workspace:^", "@backstage/theme": "workspace:^", "@backstage/version-bridge": "workspace:^", + "@dagrejs/dagre": "^1.1.4", "@date-io/core": "^1.3.13", "@material-table/core": "^3.1.0", "@material-ui/core": "^4.12.2", @@ -72,7 +73,6 @@ "d3-selection": "^3.0.0", "d3-shape": "^3.0.0", "d3-zoom": "^3.0.0", - "dagre": "^0.8.5", "js-yaml": "^4.1.0", "linkify-react": "4.1.3", "linkifyjs": "4.1.3", @@ -106,7 +106,6 @@ "@types/d3-selection": "^3.0.1", "@types/d3-shape": "^3.0.1", "@types/d3-zoom": "^3.0.1", - "@types/dagre": "^0.7.44", "@types/google-protobuf": "^3.7.2", "@types/react": "^18.0.0", "@types/react-helmet": "^6.1.0", diff --git a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx index ee6b74f97a..770f1c9c23 100644 --- a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx +++ b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx @@ -18,7 +18,7 @@ import React from 'react'; import * as d3Zoom from 'd3-zoom'; import * as d3Selection from 'd3-selection'; import useTheme from '@material-ui/core/styles/useTheme'; -import dagre from 'dagre'; +import dagre from '@dagrejs/dagre'; import debounce from 'lodash/debounce'; import { DependencyGraphTypes as Types } from './types'; import { Node } from './Node'; diff --git a/packages/core-components/src/components/DependencyGraph/Edge.tsx b/packages/core-components/src/components/DependencyGraph/Edge.tsx index 6a1f8861bf..cd8dd0642a 100644 --- a/packages/core-components/src/components/DependencyGraph/Edge.tsx +++ b/packages/core-components/src/components/DependencyGraph/Edge.tsx @@ -21,7 +21,7 @@ import makeStyles from '@material-ui/core/styles/makeStyles'; import { DependencyGraphTypes as Types } from './types'; import { ARROW_MARKER_ID, EDGE_TEST_ID, LABEL_TEST_ID } from './constants'; import { DefaultLabel } from './DefaultLabel'; -import dagre from 'dagre'; +import dagre from '@dagrejs/dagre'; /* Based on: https://github.com/dagrejs/dagre/wiki#configuring-the-layout */ export type EdgeProperties = { diff --git a/packages/core-components/src/components/DependencyGraph/Node.test.tsx b/packages/core-components/src/components/DependencyGraph/Node.test.tsx index e1af58e731..a507213495 100644 --- a/packages/core-components/src/components/DependencyGraph/Node.test.tsx +++ b/packages/core-components/src/components/DependencyGraph/Node.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import dagre from 'dagre'; +import dagre from '@dagrejs/dagre'; import { render } from '@testing-library/react'; import { Node } from './Node'; import { DependencyGraphTypes as Types } from './types'; diff --git a/packages/core-components/src/components/DependencyGraph/Node.tsx b/packages/core-components/src/components/DependencyGraph/Node.tsx index 133f45aed9..aac1cd81aa 100644 --- a/packages/core-components/src/components/DependencyGraph/Node.tsx +++ b/packages/core-components/src/components/DependencyGraph/Node.tsx @@ -19,7 +19,7 @@ import makeStyles from '@material-ui/core/styles/makeStyles'; import { DefaultNode } from './DefaultNode'; import { DependencyGraphTypes as Types } from './types'; import { NODE_TEST_ID } from './constants'; -import dagre from 'dagre'; +import dagre from '@dagrejs/dagre'; /** @public */ export type DependencyGraphNodeClassKey = 'node'; diff --git a/yarn.lock b/yarn.lock index 8b6c025c23..8a8b6616e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4333,6 +4333,7 @@ __metadata: "@backstage/errors": ^1.2.4 "@backstage/theme": ^0.6.0 "@backstage/version-bridge": ^1.0.10 + "@dagrejs/dagre": ^1.1.4 "@date-io/core": ^1.3.13 "@material-table/core": ^3.1.0 "@material-ui/core": ^4.12.2 @@ -4345,7 +4346,6 @@ __metadata: d3-selection: ^3.0.0 d3-shape: ^3.0.0 d3-zoom: ^3.0.0 - dagre: ^0.8.5 linkify-react: 4.1.3 linkifyjs: 4.1.3 lodash: ^4.17.21 @@ -4442,6 +4442,7 @@ __metadata: "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" "@backstage/version-bridge": "workspace:^" + "@dagrejs/dagre": ^1.1.4 "@date-io/core": ^1.3.13 "@material-table/core": ^3.1.0 "@material-ui/core": ^4.12.2 @@ -4457,7 +4458,6 @@ __metadata: "@types/d3-selection": ^3.0.1 "@types/d3-shape": ^3.0.1 "@types/d3-zoom": ^3.0.1 - "@types/dagre": ^0.7.44 "@types/google-protobuf": ^3.7.2 "@types/react": ^18.0.0 "@types/react-helmet": ^6.1.0 @@ -4472,7 +4472,6 @@ __metadata: d3-selection: ^3.0.0 d3-shape: ^3.0.0 d3-zoom: ^3.0.0 - dagre: ^0.8.5 history: ^5.0.0 js-yaml: ^4.1.0 linkify-react: 4.1.3 @@ -9375,6 +9374,22 @@ __metadata: languageName: node linkType: hard +"@dagrejs/dagre@npm:^1.1.4": + version: 1.1.4 + resolution: "@dagrejs/dagre@npm:1.1.4" + dependencies: + "@dagrejs/graphlib": 2.2.4 + checksum: 39d29a0ea34d1782fee012a87b0395e1959d81f2f8c9cdf4b236e2b703f88524ae2de3d7c4eccce85758c8d26966e1ee147d1075201af059443f6da967e5bfd2 + languageName: node + linkType: hard + +"@dagrejs/graphlib@npm:2.2.4": + version: 2.2.4 + resolution: "@dagrejs/graphlib@npm:2.2.4" + checksum: 02d2d8df07fd6234bfc82f03f0aebd718f1ffc2ba6579ef67a26e423d9ad25a437256d3342e6342e29ae8a5c5e448a07440d6e218c6e77e390bc57ee73b43d2d + languageName: node + linkType: hard + "@date-io/core@npm:1.x, @date-io/core@npm:^1.3.13": version: 1.3.13 resolution: "@date-io/core@npm:1.3.13" @@ -19548,13 +19563,6 @@ __metadata: languageName: node linkType: hard -"@types/dagre@npm:^0.7.44": - version: 0.7.52 - resolution: "@types/dagre@npm:0.7.52" - checksum: 89e046e73f9f83855fcecc0f79838e0e3e0e42d88b6cc42bb573249364a606909ff7ded5b6a0377246eb0648047b330b5003c8dd975358de3135635ddae0f589 - languageName: node - linkType: hard - "@types/debug@npm:^4.0.0, @types/debug@npm:^4.1.12, @types/debug@npm:^4.1.7": version: 4.1.12 resolution: "@types/debug@npm:4.1.12" From c7609de7025ba83b9b02ca3e8354e0c1037b5d02 Mon Sep 17 00:00:00 2001 From: Tomasz Szuba Date: Fri, 31 Jan 2025 12:08:15 +0100 Subject: [PATCH 37/69] Allow passing ip type to cloud-sql-connector Signed-off-by: Tomasz Szuba --- .changeset/pretty-worms-walk.md | 5 ++ packages/backend-defaults/config.d.ts | 4 ++ .../database/connectors/postgres.test.ts | 57 +++++++++++++++++++ .../database/connectors/postgres.ts | 2 +- 4 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 .changeset/pretty-worms-walk.md diff --git a/.changeset/pretty-worms-walk.md b/.changeset/pretty-worms-walk.md new file mode 100644 index 0000000000..890b0dca5b --- /dev/null +++ b/.changeset/pretty-worms-walk.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Allow passing IP type to use with cloud-sql-connector diff --git a/packages/backend-defaults/config.d.ts b/packages/backend-defaults/config.d.ts index aa97313921..438790a177 100644 --- a/packages/backend-defaults/config.d.ts +++ b/packages/backend-defaults/config.d.ts @@ -429,6 +429,10 @@ export interface Config { * The instance connection name for the cloudsql instance, e.g. `project:region:instance` */ instance: string; + /** + * The ip address type to use for the connection. Defaults to 'PUBLIC' + */ + ipAddressType?: 'PUBLIC' | 'PRIVATE' | 'PSC'; } | { /** diff --git a/packages/backend-defaults/src/entrypoints/database/connectors/postgres.test.ts b/packages/backend-defaults/src/entrypoints/database/connectors/postgres.test.ts index 1a3691c83b..254bdf6c9d 100644 --- a/packages/backend-defaults/src/entrypoints/database/connectors/postgres.test.ts +++ b/packages/backend-defaults/src/entrypoints/database/connectors/postgres.test.ts @@ -221,6 +221,63 @@ describe('postgres', () => { }); }); + it('passes default settings to cloud-sql-connector', async () => { + const { Connector } = jest.requireMock( + '@google-cloud/cloud-sql-connector', + ) as jest.Mocked; + + const mockStream = (): any => {}; + Connector.prototype.getOptions.mockResolvedValue({ stream: mockStream }); + + await buildPgDatabaseConfig( + new ConfigReader({ + client: 'pg', + connection: { + type: 'cloudsql', + user: 'ben@gke.com', + instance: 'project:region:instance', + port: 5423, + }, + }), + { connection: { database: 'other_db' } }, + ); + + expect(Connector.prototype.getOptions).toHaveBeenCalledWith({ + authType: 'IAM', + instanceConnectionName: 'project:region:instance', + ipType: 'PUBLIC', + }); + }); + + it('passes ip settings to cloud-sql-connector', async () => { + const { Connector } = jest.requireMock( + '@google-cloud/cloud-sql-connector', + ) as jest.Mocked; + + const mockStream = (): any => {}; + Connector.prototype.getOptions.mockResolvedValue({ stream: mockStream }); + + await buildPgDatabaseConfig( + new ConfigReader({ + client: 'pg', + connection: { + type: 'cloudsql', + user: 'ben@gke.com', + instance: 'project:region:instance', + ipAddressType: 'PRIVATE', + port: 5423, + }, + }), + { connection: { database: 'other_db' } }, + ); + + expect(Connector.prototype.getOptions).toHaveBeenCalledWith({ + authType: 'IAM', + instanceConnectionName: 'project:region:instance', + ipType: 'PRIVATE', + }); + }); + it('throws an error when the connection type is not supported', async () => { await expect( buildPgDatabaseConfig( diff --git a/packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts b/packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts index 3583049a7d..4adf77a2f6 100644 --- a/packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts +++ b/packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts @@ -107,7 +107,7 @@ export async function buildPgDatabaseConfig( const connector = new CloudSqlConnector(); const clientOpts = await connector.getOptions({ instanceConnectionName: config.connection.instance, - ipType: IpAddressTypes.PUBLIC, + ipType: config.connection.ipAddressType ?? IpAddressTypes.PUBLIC, authType: AuthTypes.IAM, }); From f54eed0a7a229aa7ce55735374b6455151a023a5 Mon Sep 17 00:00:00 2001 From: Harrison Hogg Date: Fri, 31 Jan 2025 14:28:16 +0000 Subject: [PATCH 38/69] Add default feature type information to backstage metadata Signed-off-by: Harrison Hogg --- .changeset/hot-mice-cry.md | 6 +++++ packages/cli-node/report.api.md | 12 ++++++++++ .../cli-node/src/monorepo/PackageGraph.ts | 23 +++++++++++++++++++ packages/cli-node/src/monorepo/index.ts | 2 ++ .../__testUtils__/createFeatureEnvironment.ts | 3 +-- .../cli/src/lib/packager/productionPack.ts | 9 ++++++++ packages/cli/src/lib/typeDistProject.test.ts | 7 ++---- packages/cli/src/lib/typeDistProject.ts | 19 +++++---------- 8 files changed, 61 insertions(+), 20 deletions(-) create mode 100644 .changeset/hot-mice-cry.md diff --git a/.changeset/hot-mice-cry.md b/.changeset/hot-mice-cry.md new file mode 100644 index 0000000000..e35538d2de --- /dev/null +++ b/.changeset/hot-mice-cry.md @@ -0,0 +1,6 @@ +--- +'@backstage/cli-node': patch +'@backstage/cli': patch +--- + +Fixed an issue where default feature type information wasn't being added to package.json/exports before publishing if exports didn't exist beforehand diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index bec25ea251..0c13469866 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -12,6 +12,9 @@ export type BackstagePackage = { packageJson: BackstagePackageJson; }; +// @public (undocumented) +export type BackstagePackageFeatureType = (typeof packageFeatureType)[number]; + // @public export interface BackstagePackageJson { // (undocumented) @@ -22,6 +25,7 @@ export interface BackstagePackageJson { pluginId?: string | null; pluginPackage?: string; pluginPackages?: string[]; + features?: Record; }; // (undocumented) bundled?: boolean; @@ -112,6 +116,14 @@ export type LockfileDiffEntry = { range: string; }; +// @public +export const packageFeatureType: readonly [ + '@backstage/BackendFeature', + '@backstage/BackstagePlugin', + '@backstage/FrontendPlugin', + '@backstage/FrontendModule', +]; + // @public export class PackageGraph extends Map { collectPackageNames( diff --git a/packages/cli-node/src/monorepo/PackageGraph.ts b/packages/cli-node/src/monorepo/PackageGraph.ts index 1da896a057..2f82af0808 100644 --- a/packages/cli-node/src/monorepo/PackageGraph.ts +++ b/packages/cli-node/src/monorepo/PackageGraph.ts @@ -22,6 +22,24 @@ import { GitUtils } from '../git'; import { Lockfile } from './Lockfile'; import { JsonValue } from '@backstage/types'; +/** + * A list of the feature types we want to extract from the project + * and include in the metadata + * + * @public + */ +export const packageFeatureType = [ + '@backstage/BackendFeature', + '@backstage/BackstagePlugin', + '@backstage/FrontendPlugin', + '@backstage/FrontendModule', +] as const; + +/** + * @public + */ +export type BackstagePackageFeatureType = (typeof packageFeatureType)[number]; + /** * Known fields in Backstage package.json files. * @@ -72,6 +90,11 @@ export interface BackstagePackageJson { * All packages that are part of the plugin. Must always and only be set for plugin packages and plugin library packages. */ pluginPackages?: string[]; + + /** + * The feature types exported from the package, indexed by path. + */ + features?: Record; }; exports?: JsonValue; diff --git a/packages/cli-node/src/monorepo/index.ts b/packages/cli-node/src/monorepo/index.ts index fdb8395c16..14630356b9 100644 --- a/packages/cli-node/src/monorepo/index.ts +++ b/packages/cli-node/src/monorepo/index.ts @@ -16,10 +16,12 @@ export { isMonoRepo } from './isMonoRepo'; export { + packageFeatureType, PackageGraph, type PackageGraphNode, type BackstagePackage, type BackstagePackageJson, + type BackstagePackageFeatureType, } from './PackageGraph'; export { Lockfile, diff --git a/packages/cli/src/lib/__testUtils__/createFeatureEnvironment.ts b/packages/cli/src/lib/__testUtils__/createFeatureEnvironment.ts index a12a48c523..054e651f0e 100644 --- a/packages/cli/src/lib/__testUtils__/createFeatureEnvironment.ts +++ b/packages/cli/src/lib/__testUtils__/createFeatureEnvironment.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { PackageRole } from '@backstage/cli-node'; +import { PackageRole, BackstagePackageFeatureType } from '@backstage/cli-node'; import { Project } from 'ts-morph'; -import { BackstagePackageFeatureType } from '../typeDistProject'; const mockEntryPoint = 'dist/index.d.ts'; diff --git a/packages/cli/src/lib/packager/productionPack.ts b/packages/cli/src/lib/packager/productionPack.ts index 9736e97b8d..bfbafc9d27 100644 --- a/packages/cli/src/lib/packager/productionPack.ts +++ b/packages/cli/src/lib/packager/productionPack.ts @@ -188,7 +188,16 @@ async function rewriteEntryPoints( // This ensures that the `backstage` field is at the top of the // `exports` field in the package.json because order is important. // https://nodejs.org/docs/latest-v20.x/api/packages.html#conditional-exports + // + // Adding this to the `exports` field in the package.json is to temporarily + // support any existing behavior that relies on this, however not all packages + // have exports field in their package.json. exp = { backstage: defaultFeatureType, ...exp }; + + // Add the default feature type to the backstage metadata in the package.json + pkg.backstage = pkg.backstage ?? {}; + pkg.backstage.features = pkg.backstage.features ?? {}; + pkg.backstage.features[entryPoint.mount] = defaultFeatureType; } } diff --git a/packages/cli/src/lib/typeDistProject.test.ts b/packages/cli/src/lib/typeDistProject.test.ts index dfe3e68ec0..6eef53816b 100644 --- a/packages/cli/src/lib/typeDistProject.test.ts +++ b/packages/cli/src/lib/typeDistProject.test.ts @@ -14,12 +14,9 @@ * limitations under the License. */ -import { PackageRole } from '@backstage/cli-node'; +import { PackageRole, BackstagePackageFeatureType } from '@backstage/cli-node'; import createFeatureEnvironment from './__testUtils__/createFeatureEnvironment'; -import { - getEntryPointDefaultFeatureType, - BackstagePackageFeatureType, -} from './typeDistProject'; +import { getEntryPointDefaultFeatureType } from './typeDistProject'; describe('typeDistProject', () => { describe('for package role', () => { diff --git a/packages/cli/src/lib/typeDistProject.ts b/packages/cli/src/lib/typeDistProject.ts index 59dc503f22..77d6f4a836 100644 --- a/packages/cli/src/lib/typeDistProject.ts +++ b/packages/cli/src/lib/typeDistProject.ts @@ -13,7 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { PackageRole } from '@backstage/cli-node'; +import { + BackstagePackageFeatureType, + packageFeatureType, + PackageRole, +} from '@backstage/cli-node'; import { resolve as resolvePath } from 'path'; import { Project, SourceFile, SyntaxKind, ts, Type } from 'ts-morph'; import { paths } from './paths'; @@ -35,17 +39,6 @@ const targetPackageRoles: PackageRole[] = [ 'node-library', ]; -// A list of the feature types we want to extract from the project -// and include in the metadata -const targetFeatureTypes = [ - '@backstage/BackendFeature', - '@backstage/BackstagePlugin', - '@backstage/FrontendPlugin', - '@backstage/FrontendModule', -] as const; - -export type BackstagePackageFeatureType = (typeof targetFeatureTypes)[number]; - export const getEntryPointDefaultFeatureType = ( role: PackageRole, packageDir: string, @@ -146,6 +139,6 @@ function isTargetFeatureType( type: string | BackstagePackageFeatureType, ): type is BackstagePackageFeatureType { return ( - !!type && targetFeatureTypes.includes(type as BackstagePackageFeatureType) + !!type && packageFeatureType.includes(type as BackstagePackageFeatureType) ); } From 043c01d55dde3a014a902126b6288568b0aa3ad1 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Sat, 1 Feb 2025 11:15:13 -0500 Subject: [PATCH 39/69] update following sig meeting Signed-off-by: aramissennyeydd --- docs/overview/logos.md | 33 +-------------------------------- docs/overview/threat-model.md | 1 + microsite/docusaurus.config.ts | 4 ++++ microsite/sidebars.js | 31 +++++++++++++++---------------- 4 files changed, 21 insertions(+), 48 deletions(-) diff --git a/docs/overview/logos.md b/docs/overview/logos.md index fe993cc378..3198b38b71 100644 --- a/docs/overview/logos.md +++ b/docs/overview/logos.md @@ -7,36 +7,5 @@ description: Guidelines for how to use the Backstage logos and icons Guidelines for how to use the Backstage logo and icon can be found [here](https://backstage.io/logo_assets/Backstage_Identity_Assets_Overview.pdf). -The assets below are all in `.svg` format. Other formats are available in the -[repository](https://github.com/backstage/backstage/tree/master/microsite/static/logo_assets). -## Backstage logo - - - Backstage White logo - - - - Backstage Teal logo - - - - Backstage Black logo - - -## Backstage icon - - +You can find the assets at https://github.com/cncf/artwork/tree/main/projects/backstage. diff --git a/docs/overview/threat-model.md b/docs/overview/threat-model.md index 2461f002c8..6f06fbd527 100644 --- a/docs/overview/threat-model.md +++ b/docs/overview/threat-model.md @@ -1,6 +1,7 @@ --- id: threat-model title: Backstage Threat Model +sidebar_label: Security & Thread Model description: A document describing the threat model for Backstage. --- diff --git a/microsite/docusaurus.config.ts b/microsite/docusaurus.config.ts index 43bde19e35..38bf582d6b 100644 --- a/microsite/docusaurus.config.ts +++ b/microsite/docusaurus.config.ts @@ -405,6 +405,10 @@ const config: Config = { label: 'GitHub', to: 'https://github.com/backstage/', }, + { + label: 'Assets', + to: 'https://github.com/cncf/artwork/tree/main/projects/backstage' + } ], }, ], diff --git a/microsite/sidebars.js b/microsite/sidebars.js index d1230b9abd..244148fc4f 100644 --- a/microsite/sidebars.js +++ b/microsite/sidebars.js @@ -4,9 +4,12 @@ module.exports = { docs: { Overview: [ 'overview/what-is-backstage', + 'overview/architecture-overview', 'overview/background', 'overview/vision', 'overview/roadmap', + 'overview/threat-model', + 'overview/versioning-policy', ], 'Getting Started': [ 'getting-started/index', @@ -296,7 +299,6 @@ module.exports = { ], Plugins: [ 'plugins/index', - 'overview/architecture-overview', 'plugins/existing-plugins', 'plugins/create-a-plugin', 'plugins/plugin-development', @@ -490,27 +492,31 @@ module.exports = { { Techical: [ 'tutorials/quickstart-app-plugin', - 'tutorials/react-router-stable-migration', - 'tutorials/react18-migration', - 'tutorials/package-role-migration', - 'tutorials/migrating-away-from-core', 'tutorials/configuring-plugin-databases', 'tutorials/switching-sqlite-postgres', 'tutorials/using-backstage-proxy-within-plugin', - 'tutorials/yarn-migration', - 'tutorials/migrate-to-mui5', - 'tutorials/auth-service-migration', 'tutorials/enable-public-entry', 'tutorials/setup-opentelemetry', 'accessibility/index', ], }, + {Migrations: [ + 'tutorials/react-router-stable-migration', + 'tutorials/react18-migration', + 'tutorials/package-role-migration', + 'tutorials/migrating-away-from-core', + 'tutorials/yarn-migration', + 'tutorials/migrate-to-mui5', + 'tutorials/auth-service-migration', + ]} ], - FAQ: ['faq/index', 'faq/technical', 'faq/product'], + FAQ: ['faq/index', 'faq/product','faq/technical'], Contribute: [ 'contribute/index', 'contribute/getting-involved', 'contribute/project-structure', + ], + References: [ { 'Designing for Backstage': [ 'dls/design', @@ -519,8 +525,6 @@ module.exports = { 'dls/figma', ], }, - ], - References: [ { type: 'category', label: 'Architecture Decision Records (ADRs)', @@ -543,15 +547,10 @@ module.exports = { }, 'api/deprecations', 'references/glossary', - { - Security: ['overview/threat-model'], - }, 'api/utility-apis', - 'overview/versioning-policy', 'reference/index', ], }, - Assets: ['overview/logos'], releases: { 'Release Notes': releases.map(release => `releases/${release}`), }, From 92a56f6b9c58dad068064a03cec2184d0d11b0af Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 31 Jan 2025 10:54:30 +0100 Subject: [PATCH 40/69] backend-plugin-api: removed the deprecated featureDiscoveryServiceRef Signed-off-by: Patrik Oldsberg --- .changeset/fluffy-carrots-wink.md | 5 + .changeset/hot-planes-hear.md | 5 + .changeset/many-goats-flash.md | 5 + .changeset/young-plants-report.md | 5 + packages/backend-app-api/package.json | 4 - packages/backend-app-api/report-alpha.api.md | 17 - packages/backend-app-api/src/alpha.ts | 17 - .../featureDiscoveryServiceFactory.test.ts | 328 ------------------ .../alpha/featureDiscoveryServiceFactory.ts | 38 -- .../src/wiring/BackendInitializer.ts | 15 - .../src/PackageDiscoveryService.ts | 3 +- .../package.json | 2 +- .../report.api.md | 8 - .../src/manager/index.ts | 1 - .../src/manager/plugin-manager.ts | 58 +--- .../backend-plugin-api/report-alpha.api.md | 16 - packages/backend-plugin-api/src/alpha.ts | 21 +- 17 files changed, 36 insertions(+), 512 deletions(-) create mode 100644 .changeset/fluffy-carrots-wink.md create mode 100644 .changeset/hot-planes-hear.md create mode 100644 .changeset/many-goats-flash.md create mode 100644 .changeset/young-plants-report.md delete mode 100644 packages/backend-app-api/report-alpha.api.md delete mode 100644 packages/backend-app-api/src/alpha.ts delete mode 100644 packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts delete mode 100644 packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.ts diff --git a/.changeset/fluffy-carrots-wink.md b/.changeset/fluffy-carrots-wink.md new file mode 100644 index 0000000000..a6f09d9558 --- /dev/null +++ b/.changeset/fluffy-carrots-wink.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': minor +--- + +**BREAKING ALPHA**: Removed the deprecated `featureDiscoveryServiceRef` and `FeatureDiscoveryService`. diff --git a/.changeset/hot-planes-hear.md b/.changeset/hot-planes-hear.md new file mode 100644 index 0000000000..6ef29772ef --- /dev/null +++ b/.changeset/hot-planes-hear.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Internal refactor to stop importing the removed `FeatureDiscoveryService` from `@backstage/backend-plugin-api`. diff --git a/.changeset/many-goats-flash.md b/.changeset/many-goats-flash.md new file mode 100644 index 0000000000..f3fec7e79a --- /dev/null +++ b/.changeset/many-goats-flash.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': minor +--- + +**BREAKING ALPHA**: Removed the deprecated `featureDiscoveryServiceFactory`. Existing usage can be replaced with `discoveryFeatureLoader` from `@backstage/backend-defaults`. diff --git a/.changeset/young-plants-report.md b/.changeset/young-plants-report.md new file mode 100644 index 0000000000..b35fb4b472 --- /dev/null +++ b/.changeset/young-plants-report.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-dynamic-feature-service': minor +--- + +**BREAKING**: removed the deprecated `dynamicPluginsFeatureDiscoveryServiceFactory`. diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 071a4a4ba4..5e159be704 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -20,16 +20,12 @@ "license": "Apache-2.0", "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.ts", "./package.json": "./package.json" }, "main": "src/index.ts", "types": "src/index.ts", "typesVersions": { "*": { - "alpha": [ - "src/alpha.ts" - ], "package.json": [ "package.json" ] diff --git a/packages/backend-app-api/report-alpha.api.md b/packages/backend-app-api/report-alpha.api.md deleted file mode 100644 index 163cc325a1..0000000000 --- a/packages/backend-app-api/report-alpha.api.md +++ /dev/null @@ -1,17 +0,0 @@ -## API Report File for "@backstage/backend-app-api" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts -import { FeatureDiscoveryService } from '@backstage/backend-plugin-api/alpha'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; - -// @alpha @deprecated (undocumented) -export const featureDiscoveryServiceFactory: ServiceFactory< - FeatureDiscoveryService, - 'root', - 'singleton' ->; - -// (No @packageDocumentation comment for this package) -``` diff --git a/packages/backend-app-api/src/alpha.ts b/packages/backend-app-api/src/alpha.ts deleted file mode 100644 index a334214738..0000000000 --- a/packages/backend-app-api/src/alpha.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2023 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { featureDiscoveryServiceFactory } from './alpha/featureDiscoveryServiceFactory'; diff --git a/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts b/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts deleted file mode 100644 index 6550a6b1d0..0000000000 --- a/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts +++ /dev/null @@ -1,328 +0,0 @@ -/* - * Copyright 2023 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - startTestBackend, - mockServices, - createMockDirectory, -} from '@backstage/backend-test-utils'; -import { featureDiscoveryServiceFactory } from './featureDiscoveryServiceFactory'; - -const mockDir = createMockDirectory(); -process.argv[1] = mockDir.path; - -const pluginApiPath = require.resolve('@backstage/backend-plugin-api'); - -describe('featureDiscoveryServiceFactory', () => { - beforeEach(() => { - mockDir.setContent({ - 'package.json': JSON.stringify({ - name: 'example-app', - dependencies: { - 'detected-plugin': '0.0.0', - 'detected-module': '0.0.0', - 'detected-plugin-with-alpha': '0.0.0', - 'detected-library': '0.0.0', - }, - }), - 'node_modules/detected-plugin': { - 'package.json': JSON.stringify({ - name: 'detected-plugin', - main: 'index.js', - backstage: { - role: 'backend-plugin', - }, - }), - 'index.js': ` - const { createBackendPlugin, coreServices } = require('${pluginApiPath}'); - exports.default = createBackendPlugin({ - pluginId: 'detected', - register(env) { - env.registerInit({ - deps: { logger: coreServices.rootLogger }, - async init({ logger }) { - logger.warn('detected-plugin'); - }, - }); - }, - }); - `, - }, - 'node_modules/detected-module': { - 'package.json': JSON.stringify({ - name: 'detected-module', - main: 'index.js', - backstage: { - role: 'backend-plugin-module', - }, - }), - 'index.js': ` - const { createBackendModule, coreServices } = require('${pluginApiPath}'); - exports.default = createBackendModule({ - pluginId: 'detected', - moduleId: 'derp', - register(env) { - env.registerInit({ - deps: { logger: coreServices.rootLogger }, - async init({ logger }) { - logger.warn('detected-module'); - }, - }); - }, - }); - `, - }, - 'node_modules/detected-plugin-with-alpha': { - 'package.json': JSON.stringify({ - name: 'detected-plugin-with-alpha', - main: 'index.js', - exports: { - '.': { - default: 'index.js', - }, - './alpha': { - default: 'alpha.js', - }, - './package.json': './package.json', - }, - backstage: { - role: 'backend-plugin', - }, - }), - 'index.js': `exports.default = undefined;`, - 'alpha.js': ` - const { createBackendPlugin, coreServices } = require('${pluginApiPath}'); - exports.default = createBackendPlugin({ - pluginId: 'detected-alpha', - register(env) { - env.registerInit({ - deps: { logger: coreServices.rootLogger }, - async init({ logger }) { - logger.warn('detected-plugin-with-alpha'); - }, - }); - }, - }); - `, - }, - 'node_modules/detected-library': { - 'package.json': JSON.stringify({ - name: 'detected-library', - main: 'index.js', - backstage: { - role: 'node-library', - }, - }), - 'index.js': ` - const { createServiceFactory, createServiceRef, coreServices } = require('${pluginApiPath}'); - exports.default = createServiceFactory({ - service: createServiceRef({ id: 'test', scope: 'root' }), - deps: { logger: coreServices.rootLogger }, - factory({ logger }) { - logger.warn('detected-library'); - return {}; - }, - }); - `, - }, - }); - }); - - it('should detect plugin and module packages when "all" is specified', async () => { - const mock = mockServices.rootLogger.mock({ child: () => mock }); - - await startTestBackend({ - features: [ - mock.factory, - featureDiscoveryServiceFactory, - mockServices.rootConfig.factory({ - data: { backend: { packages: 'all' } }, - }), - ], - }); - - expect(mock.warn).toHaveBeenCalledWith('detected-plugin'); - expect(mock.warn).toHaveBeenCalledWith('detected-module'); - expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha'); - expect(mock.warn).toHaveBeenCalledWith('detected-library'); - }); - - it('detects only the packages that are listed as included', async () => { - const mock = mockServices.rootLogger.mock({ child: () => mock }); - - await startTestBackend({ - features: [ - mock.factory, - featureDiscoveryServiceFactory, - mockServices.rootConfig.factory({ - data: { - backend: { - packages: { - include: [ - 'detected-plugin', - 'detected-plugin-with-alpha', - 'detected-library', - ], - }, - }, - }, - }), - ], - }); - - expect(mock.warn).toHaveBeenCalledWith('detected-plugin'); - expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha'); - expect(mock.warn).toHaveBeenCalledWith('detected-library'); - expect(mock.warn).not.toHaveBeenCalledWith('detected-module'); - }); - - it('does not detect packages when included is an empty list', async () => { - const mock = mockServices.rootLogger.mock({ child: () => mock }); - - await startTestBackend({ - features: [ - mock.factory, - featureDiscoveryServiceFactory, - mockServices.rootConfig.factory({ - data: { - backend: { - packages: { - include: [], - }, - }, - }, - }), - ], - }); - - expect(mock.warn).not.toHaveBeenCalledWith('detected-plugin'); - expect(mock.warn).not.toHaveBeenCalledWith('detected-plugin-with-alpha'); - expect(mock.warn).not.toHaveBeenCalledWith('detected-module'); - expect(mock.warn).not.toHaveBeenCalledWith('detected-library'); - }); - - it('does not detect an excluded packages', async () => { - const mock = mockServices.rootLogger.mock({ child: () => mock }); - - await startTestBackend({ - features: [ - mock.factory, - featureDiscoveryServiceFactory, - mockServices.rootConfig.factory({ - data: { - backend: { - packages: { - exclude: ['detected-plugin', 'detected-module'], - }, - }, - }, - }), - ], - }); - - expect(mock.warn).not.toHaveBeenCalledWith('detected-plugin'); - expect(mock.warn).not.toHaveBeenCalledWith('detected-module'); - expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha'); - expect(mock.warn).toHaveBeenCalledWith('detected-library'); - }); - - it('does not excluded packages when it is an empty list', async () => { - const mock = mockServices.rootLogger.mock({ child: () => mock }); - - await startTestBackend({ - features: [ - mock.factory, - featureDiscoveryServiceFactory, - mockServices.rootConfig.factory({ - data: { - backend: { - packages: { - exclude: [], - }, - }, - }, - }), - ], - }); - - expect(mock.warn).toHaveBeenCalledWith('detected-plugin'); - expect(mock.warn).toHaveBeenCalledWith('detected-module'); - expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha'); - expect(mock.warn).toHaveBeenCalledWith('detected-library'); - }); - - it('does not detect packages that are included and excluded', async () => { - const mock = mockServices.rootLogger.mock({ child: () => mock }); - - await startTestBackend({ - features: [ - mock.factory, - featureDiscoveryServiceFactory, - mockServices.rootConfig.factory({ - data: { - backend: { - packages: { - include: [ - 'detected-plugin', - 'detected-module', - 'detected-plugin-with-alpha', - ], - exclude: ['detected-module'], - }, - }, - }, - }), - ], - }); - - expect(mock.warn).toHaveBeenCalledWith('detected-plugin'); - expect(mock.warn).not.toHaveBeenCalledWith('detected-library'); - expect(mock.warn).not.toHaveBeenCalledWith('detected-module'); - expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha'); - }); - - it('does not detect any packages when "packages" is empty', async () => { - const mock = mockServices.rootLogger.mock({ child: () => mock }); - - await startTestBackend({ - features: [ - mock.factory, - featureDiscoveryServiceFactory, - mockServices.rootConfig.factory({ - data: { backend: { packages: {} } }, - }), - ], - }); - - expect(mock.warn).not.toHaveBeenCalled(); - }); - - it('does not detect any packages when "packages" is not present', async () => { - const mock = mockServices.rootLogger.mock({ child: () => mock }); - - await startTestBackend({ - features: [ - mock.factory, - featureDiscoveryServiceFactory, - mockServices.rootConfig.factory({ - data: { backend: {} }, - }), - ], - }); - - expect(mock.warn).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.ts b/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.ts deleted file mode 100644 index 05de659083..0000000000 --- a/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2023 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { featureDiscoveryServiceRef } from '@backstage/backend-plugin-api/alpha'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { PackageDiscoveryService } from '../../../backend-defaults/src/PackageDiscoveryService'; - -/** - * @alpha - * @deprecated The `featureDiscoveryServiceFactory` is deprecated in favor of using {@link @backstage/backend-defaults#discoveryFeatureLoader} instead. - */ -export const featureDiscoveryServiceFactory = createServiceFactory({ - service: featureDiscoveryServiceRef, - deps: { - config: coreServices.rootConfig, - logger: coreServices.rootLogger, - }, - factory({ config, logger }) { - return new PackageDiscoveryService(config, logger); - }, -}); diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 6bec5439ec..02fad4f217 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -37,7 +37,6 @@ import type { InternalServiceFactory } from '../../../backend-plugin-api/src/ser import { ForwardedError, ConflictError, assertError } from '@backstage/errors'; import { instanceMetadataServiceRef, - featureDiscoveryServiceRef, BackendFeatureMeta, } from '@backstage/backend-plugin-api/alpha'; import { DependencyGraph } from '../lib/DependencyGraph'; @@ -252,20 +251,6 @@ export class BackendInitializer { this.#addFeature(await feature); } - const featureDiscovery = await this.#serviceRegistry.get( - // TODO: Let's leave this in place and remove it once the deprecated service is removed. We can do that post-1.0 since it's alpha - featureDiscoveryServiceRef, - 'root', - ); - - if (featureDiscovery) { - const { features } = await featureDiscovery.getBackendFeatures(); - for (const feature of features) { - this.#addFeature(unwrapFeature(feature)); - } - this.#serviceRegistry.checkForCircularDeps(); - } - await this.#applyBackendFeatureLoaders(this.#registeredFeatureLoaders); this.#serviceRegistry.add( diff --git a/packages/backend-defaults/src/PackageDiscoveryService.ts b/packages/backend-defaults/src/PackageDiscoveryService.ts index d7d127f4fa..90e8d0f59d 100644 --- a/packages/backend-defaults/src/PackageDiscoveryService.ts +++ b/packages/backend-defaults/src/PackageDiscoveryService.ts @@ -22,7 +22,6 @@ import { RootConfigService, RootLoggerService, } from '@backstage/backend-plugin-api'; -import { FeatureDiscoveryService } from '@backstage/backend-plugin-api/alpha'; import { BackstagePackageJson } from '@backstage/cli-node'; const DETECTED_PACKAGE_ROLES = [ @@ -79,7 +78,7 @@ async function findClosestPackageDir( } /** @internal */ -export class PackageDiscoveryService implements FeatureDiscoveryService { +export class PackageDiscoveryService { constructor( private readonly config: RootConfigService, private readonly logger: RootLoggerService, diff --git a/packages/backend-dynamic-feature-service/package.json b/packages/backend-dynamic-feature-service/package.json index ac03cceafa..2464b4cd14 100644 --- a/packages/backend-dynamic-feature-service/package.json +++ b/packages/backend-dynamic-feature-service/package.json @@ -45,7 +45,6 @@ "test": "backstage-cli package test" }, "dependencies": { - "@backstage/backend-app-api": "workspace:^", "@backstage/backend-common": "^0.25.0", "@backstage/backend-defaults": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", @@ -74,6 +73,7 @@ "winston": "^3.2.1" }, "devDependencies": { + "@backstage/backend-app-api": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-app-backend": "workspace:^", diff --git a/packages/backend-dynamic-feature-service/report.api.md b/packages/backend-dynamic-feature-service/report.api.md index e2ad34c84d..b0fa84edbd 100644 --- a/packages/backend-dynamic-feature-service/report.api.md +++ b/packages/backend-dynamic-feature-service/report.api.md @@ -13,7 +13,6 @@ import { DiscoveryService } from '@backstage/backend-plugin-api'; import { EventBroker } from '@backstage/plugin-events-node'; import { EventsBackend } from '@backstage/plugin-events-backend'; import { EventsService } from '@backstage/plugin-events-node'; -import { FeatureDiscoveryService } from '@backstage/backend-plugin-api/alpha'; import { HttpPostIngressOptions } from '@backstage/plugin-events-node'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { IndexBuilder } from '@backstage/plugin-search-backend-node'; @@ -150,13 +149,6 @@ export interface DynamicPluginsFactoryOptions { // @public @deprecated (undocumented) export const dynamicPluginsFeatureDiscoveryLoader: BackendFeature; -// @public @deprecated (undocumented) -export const dynamicPluginsFeatureDiscoveryServiceFactory: ServiceFactory< - FeatureDiscoveryService, - 'root', - 'singleton' ->; - // @public export const dynamicPluginsFeatureLoader: (( options?: DynamicPluginsFeatureLoaderOptions, diff --git a/packages/backend-dynamic-feature-service/src/manager/index.ts b/packages/backend-dynamic-feature-service/src/manager/index.ts index d24a41a7a3..391ea46249 100644 --- a/packages/backend-dynamic-feature-service/src/manager/index.ts +++ b/packages/backend-dynamic-feature-service/src/manager/index.ts @@ -32,7 +32,6 @@ export type { export { DynamicPluginManager, - dynamicPluginsFeatureDiscoveryServiceFactory, dynamicPluginsServiceFactory, dynamicPluginsServiceFactoryWithOptions, dynamicPluginsServiceRef, diff --git a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts index 398e9fb14c..0162cbef2d 100644 --- a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts +++ b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts @@ -37,10 +37,6 @@ import { import { PackageRole, PackageRoles } from '@backstage/cli-node'; import { findPaths } from '@backstage/cli-common'; import * as fs from 'fs'; -import { - FeatureDiscoveryService, - featureDiscoveryServiceRef, -} from '@backstage/backend-plugin-api/alpha'; /** * @public @@ -307,55 +303,27 @@ export const dynamicPluginsServiceFactory = Object.assign( dynamicPluginsServiceFactoryWithOptions(), ); -class DynamicPluginsEnabledFeatureDiscoveryService - implements FeatureDiscoveryService -{ - constructor( - private readonly dynamicPlugins: DynamicPluginProvider, - private readonly featureDiscoveryService?: FeatureDiscoveryService, - ) {} +class DynamicPluginsEnabledFeatureDiscoveryService { + constructor(private readonly dynamicPlugins: DynamicPluginProvider) {} async getBackendFeatures(): Promise<{ features: Array }> { - const staticFeatures = - (await this.featureDiscoveryService?.getBackendFeatures())?.features ?? - []; - return { - features: [ - ...this.dynamicPlugins - .backendPlugins() - .flatMap((plugin): BackendFeature[] => { - if (plugin.installer?.kind === 'new') { - const installed = plugin.installer.install(); - if (Array.isArray(installed)) { - return installed; - } - return [installed]; + features: this.dynamicPlugins + .backendPlugins() + .flatMap((plugin): BackendFeature[] => { + if (plugin.installer?.kind === 'new') { + const installed = plugin.installer.install(); + if (Array.isArray(installed)) { + return installed; } - return []; - }), - ...staticFeatures, - ], + return [installed]; + } + return []; + }), }; } } -/** - * @public - * @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins. - */ -export const dynamicPluginsFeatureDiscoveryServiceFactory = - createServiceFactory({ - service: featureDiscoveryServiceRef, - deps: { - config: coreServices.rootConfig, - dynamicPlugins: dynamicPluginsServiceRef, - }, - factory({ dynamicPlugins }) { - return new DynamicPluginsEnabledFeatureDiscoveryService(dynamicPlugins); - }, - }); - /** * @public * @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins. diff --git a/packages/backend-plugin-api/report-alpha.api.md b/packages/backend-plugin-api/report-alpha.api.md index 8509d1196d..d485e2132e 100644 --- a/packages/backend-plugin-api/report-alpha.api.md +++ b/packages/backend-plugin-api/report-alpha.api.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; // @alpha (undocumented) @@ -18,21 +17,6 @@ export type BackendFeatureMeta = moduleId: string; }; -// @alpha (undocumented) -export interface FeatureDiscoveryService { - // (undocumented) - getBackendFeatures(): Promise<{ - features: Array; - }>; -} - -// @alpha @deprecated -export const featureDiscoveryServiceRef: ServiceRef< - FeatureDiscoveryService, - 'root', - 'singleton' ->; - // @alpha (undocumented) export interface InstanceMetadataService { // (undocumented) diff --git a/packages/backend-plugin-api/src/alpha.ts b/packages/backend-plugin-api/src/alpha.ts index 1a39fe6c23..6e91e10671 100644 --- a/packages/backend-plugin-api/src/alpha.ts +++ b/packages/backend-plugin-api/src/alpha.ts @@ -14,26 +14,7 @@ * limitations under the License. */ -import { - BackendFeature, - createServiceRef, -} from '@backstage/backend-plugin-api'; - -/** @alpha */ -export interface FeatureDiscoveryService { - getBackendFeatures(): Promise<{ features: Array }>; -} - -/** - * An optional service that can be used to dynamically load in additional BackendFeatures at runtime. - * @alpha - * @deprecated The `featureDiscoveryServiceRef` is deprecated in favor of using {@link @backstage/backend-defaults#discoveryFeatureLoader} instead. - */ -export const featureDiscoveryServiceRef = - createServiceRef({ - id: 'core.featureDiscovery', - scope: 'root', - }); +import { createServiceRef } from '@backstage/backend-plugin-api'; /** * EXPERIMENTAL: Instance metadata service. From 45bac6d53e2a7e56c975fa97a424a1897df696f1 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Mon, 3 Feb 2025 16:58:31 +0100 Subject: [PATCH 41/69] Change to patch Signed-off-by: Dominika Zemanovicova --- .changeset/real-foxes-film.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.changeset/real-foxes-film.md b/.changeset/real-foxes-film.md index 16f0f0cbd9..22f1ecead2 100644 --- a/.changeset/real-foxes-film.md +++ b/.changeset/real-foxes-film.md @@ -1,10 +1,10 @@ --- -'@backstage/plugin-kubernetes-backend': minor -'@backstage/plugin-kubernetes-cluster': minor -'@backstage/plugin-kubernetes-common': minor -'@backstage/plugin-kubernetes': minor +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-kubernetes-cluster': patch +'@backstage/plugin-kubernetes-common': patch +'@backstage/plugin-kubernetes': patch --- -**BREAKING** The `/clusters` endpoint is now protected by the `kubernetes.clusters.read` permission. -**BREAKING** The `/services/:serviceId` endpoint is now protected by the `kubernetes.resources.read` permission. -**BREAKING** The `/resources` endpoints are now protected by the `kubernetes.resources.read` permission. +The `/clusters` endpoint is now protected by the `kubernetes.clusters.read` permission. +The `/services/:serviceId` endpoint is now protected by the `kubernetes.resources.read` permission. +The `/resources` endpoints are now protected by the `kubernetes.resources.read` permission. From 91f3313f3755049d03c97282bff0a49360ef3cf0 Mon Sep 17 00:00:00 2001 From: Hellgren Heikki Date: Mon, 3 Feb 2025 14:08:06 +0200 Subject: [PATCH 42/69] feat(scaffolder): experimental flag to wait for task completions Enabling the `EXPERIMENTAL_gracefulShutdown` flag in the scaffolder config will make the scaffolder block the shutdown process until all running tasks have completed. This is useful when there is a need to ensure that all tasks have completed before the scaffolder is shut down. Signed-off-by: Hellgren Heikki --- .changeset/tidy-oranges-allow.md | 12 ++++++++++++ plugins/scaffolder-backend/config.d.ts | 5 +++++ plugins/scaffolder-backend/report.api.md | 3 ++- .../src/scaffolder/tasks/TaskWorker.ts | 11 ++++++++++- plugins/scaffolder-backend/src/service/router.ts | 9 +++++++-- 5 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 .changeset/tidy-oranges-allow.md diff --git a/.changeset/tidy-oranges-allow.md b/.changeset/tidy-oranges-allow.md new file mode 100644 index 0000000000..78d5393cec --- /dev/null +++ b/.changeset/tidy-oranges-allow.md @@ -0,0 +1,12 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Added experimental flag for scaffolder to wait for running tasks to complete on shutdown + +Enabling the `EXPERIMENTAL_gracefulShutdown` flag in the scaffolder config will make the +scaffolder block the shutdown process until all running tasks have completed. This is useful +when there is a need to ensure that all tasks have completed before the scaffolder is shut down. + +Please note, that the `TaskWorker` `stop` method is now asynchronous and awaited for the +tasks to complete when the experimental flag is enabled. diff --git a/plugins/scaffolder-backend/config.d.ts b/plugins/scaffolder-backend/config.d.ts index 90040d68f1..a391f97264 100644 --- a/plugins/scaffolder-backend/config.d.ts +++ b/plugins/scaffolder-backend/config.d.ts @@ -41,6 +41,11 @@ export interface Config { */ concurrentTasksLimit?: number; + /** + * Tries to wait for tasks to finish during SIGTERM before shutting down the TaskWorker. + */ + EXPERIMENTAL_gracefulShutdown?: boolean; + /** * Sets the tasks recoverability on system start up. * diff --git a/plugins/scaffolder-backend/report.api.md b/plugins/scaffolder-backend/report.api.md index f21bcd76f7..9d39f52d85 100644 --- a/plugins/scaffolder-backend/report.api.md +++ b/plugins/scaffolder-backend/report.api.md @@ -414,6 +414,7 @@ export type CreateWorkerOptions = { concurrentTasksLimit?: number; additionalTemplateGlobals?: Record; permissions?: PermissionEvaluator; + gracefulShutdown?: boolean; }; // @public @@ -841,7 +842,7 @@ export class TaskWorker { // (undocumented) start(): void; // (undocumented) - stop(): void; + stop(): Promise; } // @public @deprecated (undocumented) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index 6c77ef3413..55af873083 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -44,6 +44,7 @@ export type TaskWorkerOptions = { permissions?: PermissionEvaluator; logger?: Logger; auditor?: AuditorService; + gracefulShutdown?: boolean; }; /** @@ -74,6 +75,7 @@ export type CreateWorkerOptions = { concurrentTasksLimit?: number; additionalTemplateGlobals?: Record; permissions?: PermissionEvaluator; + gracefulShutdown?: boolean; }; /** @@ -108,6 +110,7 @@ export class TaskWorker { concurrentTasksLimit = 10, // from 1 to Infinity additionalTemplateGlobals, permissions, + gracefulShutdown, } = options; const workflowRunner = new NunjucksWorkflowRunner({ @@ -127,6 +130,7 @@ export class TaskWorker { concurrentTasksLimit, permissions, auditor, + gracefulShutdown, }); } @@ -156,8 +160,13 @@ export class TaskWorker { })(); } - stop() { + async stop() { this.stopWorkers = true; + if (this.options?.gracefulShutdown) { + while (this.taskQueue.size > 0) { + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } } protected onReadyToClaimTask(): Promise { diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index a12e43615e..c39ee29a1e 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -366,6 +366,10 @@ export async function createRouter( const workers: TaskWorker[] = []; if (concurrentTasksLimit !== 0) { + const gracefulShutdown = config.getOptionalBoolean( + 'scaffolder.EXPERIMENTAL_gracefulShutdown', + ); + for (let i = 0; i < (taskWorkers || 1); i++) { const worker = await TaskWorker.create({ taskBroker, @@ -378,6 +382,7 @@ export async function createRouter( additionalTemplateGlobals, concurrentTasksLimit, permissions, + gracefulShutdown, }); workers.push(worker); } @@ -399,8 +404,8 @@ export async function createRouter( const launchWorkers = () => workers.forEach(worker => worker.start()); - const shutdownWorkers = () => { - workers.forEach(worker => worker.stop()); + const shutdownWorkers = async () => { + await Promise.allSettled(workers.map(worker => worker.stop())); }; if (options.lifecycle) { From 4c76f0582683f1b877440a054e1a305b91afb3a5 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Mon, 3 Feb 2025 17:38:35 +0100 Subject: [PATCH 43/69] Change alert message Signed-off-by: Dominika Zemanovicova --- .../KubernetesClusterContent/KubernetesClusterContent.tsx | 2 +- plugins/kubernetes/src/RequireKubernetesPermissions.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx b/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx index 88e0f8b703..11293a41a7 100644 --- a/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx +++ b/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx @@ -65,7 +65,7 @@ export const KubernetesClusterContent = () => { errorPage={ } diff --git a/plugins/kubernetes/src/RequireKubernetesPermissions.tsx b/plugins/kubernetes/src/RequireKubernetesPermissions.tsx index 65855fc44c..92a88e5b9c 100644 --- a/plugins/kubernetes/src/RequireKubernetesPermissions.tsx +++ b/plugins/kubernetes/src/RequireKubernetesPermissions.tsx @@ -52,7 +52,7 @@ export function RequireKubernetesPermissions( return ( ); From cce1c33d520c32d79265d8c3bdb615596defb043 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Mon, 3 Feb 2025 13:07:14 -0500 Subject: [PATCH 44/69] prettier :( Signed-off-by: aramissennyeydd --- microsite/docusaurus.config.ts | 4 ++-- microsite/sidebars.js | 22 ++++++++++++---------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/microsite/docusaurus.config.ts b/microsite/docusaurus.config.ts index 38bf582d6b..e07ff68c18 100644 --- a/microsite/docusaurus.config.ts +++ b/microsite/docusaurus.config.ts @@ -407,8 +407,8 @@ const config: Config = { }, { label: 'Assets', - to: 'https://github.com/cncf/artwork/tree/main/projects/backstage' - } + to: 'https://github.com/cncf/artwork/tree/main/projects/backstage', + }, ], }, ], diff --git a/microsite/sidebars.js b/microsite/sidebars.js index 244148fc4f..d650925c20 100644 --- a/microsite/sidebars.js +++ b/microsite/sidebars.js @@ -500,17 +500,19 @@ module.exports = { 'accessibility/index', ], }, - {Migrations: [ - 'tutorials/react-router-stable-migration', - 'tutorials/react18-migration', - 'tutorials/package-role-migration', - 'tutorials/migrating-away-from-core', - 'tutorials/yarn-migration', - 'tutorials/migrate-to-mui5', - 'tutorials/auth-service-migration', - ]} + { + Migrations: [ + 'tutorials/react-router-stable-migration', + 'tutorials/react18-migration', + 'tutorials/package-role-migration', + 'tutorials/migrating-away-from-core', + 'tutorials/yarn-migration', + 'tutorials/migrate-to-mui5', + 'tutorials/auth-service-migration', + ], + }, ], - FAQ: ['faq/index', 'faq/product','faq/technical'], + FAQ: ['faq/index', 'faq/product', 'faq/technical'], Contribute: [ 'contribute/index', 'contribute/getting-involved', From d92fd526a041f9e0720264a068f69562d7675e14 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Tue, 4 Feb 2025 10:21:57 +0100 Subject: [PATCH 45/69] Fix bug in GitLab autocomplete onChange handler Signed-off-by: blam --- .changeset/smart-ligers-sniff.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.changeset/smart-ligers-sniff.md b/.changeset/smart-ligers-sniff.md index 2a5ec1fd27..42353eca2b 100644 --- a/.changeset/smart-ligers-sniff.md +++ b/.changeset/smart-ligers-sniff.md @@ -1,7 +1,6 @@ --- '@backstage/plugin-scaffolder-backend-module-gitlab': patch -'@backstage/plugin-scaffolder-react': patch '@backstage/plugin-scaffolder': patch --- -Changed field that is used in autocomplete +Fixed bug of passing wrong value to `onChange` handler when using `GitLab` autocomplete From 590ea1559bfab2b5a9f4b24dde5cfa5118c4abb1 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 4 Feb 2025 10:52:19 +0100 Subject: [PATCH 46/69] chore: cleanup the setTimeout usage Signed-off-by: blam --- .../scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index 55af873083..4441a2f0ca 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -29,6 +29,7 @@ import { Logger } from 'winston'; import { TemplateActionRegistry } from '../actions'; import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner'; import { WorkflowRunner } from './types'; +import { setTimeout } from 'timers/promises'; /** * TaskWorkerOptions @@ -145,7 +146,7 @@ export class TaskWorker { start() { (async () => { while (!this.stopWorkers) { - await new Promise(resolve => setTimeout(resolve, 10000)); + await setTimeout(10000); await this.recoverTasks(); } })(); @@ -164,7 +165,7 @@ export class TaskWorker { this.stopWorkers = true; if (this.options?.gracefulShutdown) { while (this.taskQueue.size > 0) { - await new Promise(resolve => setTimeout(resolve, 1000)); + await setTimeout(1000); } } } From aff6b479f1e4af0a39d4fc17781684990cced38d Mon Sep 17 00:00:00 2001 From: JounQin Date: Tue, 19 Nov 2024 17:16:30 +0800 Subject: [PATCH 47/69] feat: add cancel button for WidgetSettingsOverlay Signed-off-by: JounQin --- .changeset/happy-windows-shop.md | 5 +++++ .../CustomHomepage/WidgetSettingsOverlay.tsx | 17 +++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 .changeset/happy-windows-shop.md diff --git a/.changeset/happy-windows-shop.md b/.changeset/happy-windows-shop.md new file mode 100644 index 0000000000..2c8d32a0dc --- /dev/null +++ b/.changeset/happy-windows-shop.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-home': patch +--- + +feat: add cancel button for WidgetSettingsOverlay diff --git a/plugins/home/src/components/CustomHomepage/WidgetSettingsOverlay.tsx b/plugins/home/src/components/CustomHomepage/WidgetSettingsOverlay.tsx index 5fe4f65136..c7cb868eee 100644 --- a/plugins/home/src/components/CustomHomepage/WidgetSettingsOverlay.tsx +++ b/plugins/home/src/components/CustomHomepage/WidgetSettingsOverlay.tsx @@ -26,6 +26,8 @@ import { Widget } from './types'; import { withTheme } from '@rjsf/core'; import { Theme as MuiTheme } from '@rjsf/material-ui'; import validator from '@rjsf/validator-ajv8'; +import Button from '@material-ui/core/Button'; +import Box from '@material-ui/core/Box'; const Form = withTheme(MuiTheme); @@ -64,13 +66,15 @@ export const WidgetSettingsOverlay = (props: WidgetSettingsOverlayProps) => { const [settingsDialogOpen, setSettingsDialogOpen] = React.useState(false); const styles = useStyles(); + const onClose = () => setSettingsDialogOpen(false); + return (
{widget.settingsSchema && ( setSettingsDialogOpen(false)} + onClose={onClose} >
{ experimental_defaultFormStateBehavior={{ allOf: 'populateDefaults', }} - /> + > + + + + +
)} From 6987540baf9bb434344bbfe6e8fd80e5f970fbe3 Mon Sep 17 00:00:00 2001 From: JounQin Date: Wed, 15 Jan 2025 01:29:37 +0800 Subject: [PATCH 48/69] chore: migrate to DialogActions Signed-off-by: JounQin --- .../CustomHomepage/WidgetSettingsOverlay.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/plugins/home/src/components/CustomHomepage/WidgetSettingsOverlay.tsx b/plugins/home/src/components/CustomHomepage/WidgetSettingsOverlay.tsx index c7cb868eee..cfcd47b8f3 100644 --- a/plugins/home/src/components/CustomHomepage/WidgetSettingsOverlay.tsx +++ b/plugins/home/src/components/CustomHomepage/WidgetSettingsOverlay.tsx @@ -13,21 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import Button from '@material-ui/core/Button'; import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; import DialogContent from '@material-ui/core/DialogContent'; import Grid from '@material-ui/core/Grid'; +import IconButton from '@material-ui/core/IconButton'; import Tooltip from '@material-ui/core/Tooltip'; import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; -import IconButton from '@material-ui/core/IconButton'; -import SettingsIcon from '@material-ui/icons/Settings'; import DeleteIcon from '@material-ui/icons/Delete'; -import React from 'react'; -import { Widget } from './types'; +import SettingsIcon from '@material-ui/icons/Settings'; import { withTheme } from '@rjsf/core'; import { Theme as MuiTheme } from '@rjsf/material-ui'; import validator from '@rjsf/validator-ajv8'; -import Button from '@material-ui/core/Button'; -import Box from '@material-ui/core/Box'; +import React from 'react'; + +import { Widget } from './types'; const Form = withTheme(MuiTheme); @@ -95,14 +97,14 @@ export const WidgetSettingsOverlay = (props: WidgetSettingsOverlayProps) => { allOf: 'populateDefaults', }} > - + - + From af8ec8757600b3bbd995334430b2ab0f43ea5fe2 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Tue, 4 Feb 2025 10:39:01 +0100 Subject: [PATCH 49/69] Use translation Signed-off-by: Dominika Zemanovicova --- plugins/kubernetes-cluster/package.json | 19 +++++++-- .../kubernetes-cluster/report-alpha.api.md | 18 +++++++++ plugins/kubernetes-cluster/src/alpha.ts | 17 ++++++++ .../KubernetesClusterContent.tsx | 9 +++-- plugins/kubernetes-cluster/src/translation.ts | 30 ++++++++++++++ plugins/kubernetes/report-alpha.api.md | 40 ++++++++++++------- .../src/RequireKubernetesPermissions.tsx | 8 ++-- plugins/kubernetes/src/alpha/index.ts | 2 +- plugins/kubernetes/src/alpha/translation.ts | 30 ++++++++++++++ 9 files changed, 148 insertions(+), 25 deletions(-) create mode 100644 plugins/kubernetes-cluster/report-alpha.api.md create mode 100644 plugins/kubernetes-cluster/src/alpha.ts create mode 100644 plugins/kubernetes-cluster/src/translation.ts create mode 100644 plugins/kubernetes/src/alpha/translation.ts diff --git a/plugins/kubernetes-cluster/package.json b/plugins/kubernetes-cluster/package.json index 78ff79e43b..cd204eaf9c 100644 --- a/plugins/kubernetes-cluster/package.json +++ b/plugins/kubernetes-cluster/package.json @@ -10,9 +10,7 @@ ] }, "publishConfig": { - "access": "public", - "main": "dist/index.esm.js", - "types": "dist/index.d.ts" + "access": "public" }, "keywords": [ "backstage", @@ -26,8 +24,23 @@ }, "license": "Apache-2.0", "sideEffects": false, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, "main": "src/index.ts", "types": "src/index.ts", + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } + }, "files": [ "dist" ], diff --git a/plugins/kubernetes-cluster/report-alpha.api.md b/plugins/kubernetes-cluster/report-alpha.api.md new file mode 100644 index 0000000000..13314d6c83 --- /dev/null +++ b/plugins/kubernetes-cluster/report-alpha.api.md @@ -0,0 +1,18 @@ +## API Report File for "@backstage/plugin-kubernetes-cluster" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { TranslationRef } from '@backstage/core-plugin-api/alpha'; + +// @alpha (undocumented) +export const kubernetesClusterTranslationRef: TranslationRef< + 'kubernetes-cluster', + { + readonly 'kubernetesClusterContentPage.permissionAlert.message': "To view Kubernetes objects, contact your portal administrator to give you the 'kubernetes.clusters.read' permission."; + readonly 'kubernetesClusterContentPage.permissionAlert.title': 'Permission required'; + } +>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/kubernetes-cluster/src/alpha.ts b/plugins/kubernetes-cluster/src/alpha.ts new file mode 100644 index 0000000000..e49b7ac9c9 --- /dev/null +++ b/plugins/kubernetes-cluster/src/alpha.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { kubernetesClusterTranslationRef } from './translation'; diff --git a/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx b/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx index 11293a41a7..2a824d329a 100644 --- a/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx +++ b/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx @@ -26,6 +26,8 @@ import { import { WarningPanel } from '@backstage/core-components'; import { kubernetesClustersReadPermission } from '@backstage/plugin-kubernetes-common'; import { RequirePermission } from '@backstage/plugin-permission-react'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { kubernetesClusterTranslationRef } from '../../translation'; const ContentGrid = () => { const { error } = useKubernetesClusterError(); @@ -59,14 +61,15 @@ const ContentGrid = () => { * @public */ export const KubernetesClusterContent = () => { + const { t } = useTranslationRef(kubernetesClusterTranslationRef); + return ( } > diff --git a/plugins/kubernetes-cluster/src/translation.ts b/plugins/kubernetes-cluster/src/translation.ts new file mode 100644 index 0000000000..ba99c6744e --- /dev/null +++ b/plugins/kubernetes-cluster/src/translation.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; + +/** @alpha */ +export const kubernetesClusterTranslationRef = createTranslationRef({ + id: 'kubernetes-cluster', + messages: { + kubernetesClusterContentPage: { + permissionAlert: { + title: 'Permission required', + message: + "To view Kubernetes objects, contact your portal administrator to give you the 'kubernetes.clusters.read' permission.", + }, + }, + }, +}); diff --git a/plugins/kubernetes/report-alpha.api.md b/plugins/kubernetes/report-alpha.api.md index 4f779df258..ea04986595 100644 --- a/plugins/kubernetes/report-alpha.api.md +++ b/plugins/kubernetes/report-alpha.api.md @@ -13,6 +13,7 @@ import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { FrontendPlugin } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/frontend-plugin-api'; +import { TranslationRef } from '@backstage/core-plugin-api/alpha'; // @public (undocumented) const _default: FrontendPlugin< @@ -47,21 +48,6 @@ const _default: FrontendPlugin< routeRef?: RouteRef | undefined; }; }>; - 'api:kubernetes': ExtensionDefinition<{ - kind: 'api'; - name: undefined; - config: {}; - configInput: {}; - output: ConfigurableExtensionDataRef< - AnyApiFactory, - 'core.api.factory', - {} - >; - inputs: {}; - params: { - factory: AnyApiFactory; - }; - }>; 'entity-content:kubernetes/kubernetes': ExtensionDefinition<{ kind: 'entity-content'; name: 'kubernetes'; @@ -113,6 +99,21 @@ const _default: FrontendPlugin< filter?: string | ((entity: Entity) => boolean) | undefined; }; }>; + 'api:kubernetes': ExtensionDefinition<{ + kind: 'api'; + name: undefined; + config: {}; + configInput: {}; + output: ConfigurableExtensionDataRef< + AnyApiFactory, + 'core.api.factory', + {} + >; + inputs: {}; + params: { + factory: AnyApiFactory; + }; + }>; 'api:kubernetes/proxy': ExtensionDefinition<{ kind: 'api'; name: 'proxy'; @@ -162,5 +163,14 @@ const _default: FrontendPlugin< >; export default _default; +// @alpha (undocumented) +export const kubernetesTranslationRef: TranslationRef< + 'kubernetes', + { + readonly 'kubernetesContentPage.permissionAlert.message': "To view Kubernetes objects, contact your portal administrator to give you the 'kubernetes.clusters.read' and 'kubernetes.resources.read' permission."; + readonly 'kubernetesContentPage.permissionAlert.title': 'Permission required'; + } +>; + // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/kubernetes/src/RequireKubernetesPermissions.tsx b/plugins/kubernetes/src/RequireKubernetesPermissions.tsx index 92a88e5b9c..efc83e8ef8 100644 --- a/plugins/kubernetes/src/RequireKubernetesPermissions.tsx +++ b/plugins/kubernetes/src/RequireKubernetesPermissions.tsx @@ -20,6 +20,8 @@ import { } from '@backstage/plugin-kubernetes-common'; import { usePermission } from '@backstage/plugin-permission-react'; import { WarningPanel } from '@backstage/core-components'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { kubernetesTranslationRef } from './alpha/translation'; export type RequireKubernetesPermissionProps = { children: ReactNode; @@ -34,6 +36,7 @@ export function RequireKubernetesPermissions( const kubernetesResourcesPermissionResult = usePermission({ permission: kubernetesResourcesReadPermission, }); + const { t } = useTranslationRef(kubernetesTranslationRef); if ( kubernetesClustersPermissionResult.loading || @@ -51,9 +54,8 @@ export function RequireKubernetesPermissions( return ( ); } diff --git a/plugins/kubernetes/src/alpha/index.ts b/plugins/kubernetes/src/alpha/index.ts index 2f137f09ee..23888f968f 100644 --- a/plugins/kubernetes/src/alpha/index.ts +++ b/plugins/kubernetes/src/alpha/index.ts @@ -13,5 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +export { kubernetesTranslationRef } from './translation'; export { default } from './plugin'; diff --git a/plugins/kubernetes/src/alpha/translation.ts b/plugins/kubernetes/src/alpha/translation.ts new file mode 100644 index 0000000000..7f9704af84 --- /dev/null +++ b/plugins/kubernetes/src/alpha/translation.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; + +/** @alpha */ +export const kubernetesTranslationRef = createTranslationRef({ + id: 'kubernetes', + messages: { + kubernetesContentPage: { + permissionAlert: { + title: 'Permission required', + message: + "To view Kubernetes objects, contact your portal administrator to give you the 'kubernetes.clusters.read' and 'kubernetes.resources.read' permission.", + }, + }, + }, +}); From 828b1020ccd239981867d7704078e3ecb7373414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 4 Feb 2025 11:48:24 +0100 Subject: [PATCH 50/69] patch broken tag fetching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .github/workflows/sync_version-packages.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/sync_version-packages.yml b/.github/workflows/sync_version-packages.yml index 36e31565b9..34d34ef5a6 100644 --- a/.github/workflows/sync_version-packages.yml +++ b/.github/workflows/sync_version-packages.yml @@ -3,6 +3,8 @@ on: push: branches: - master + paths: + - '.github/workflows/sync_version-packages.yml' concurrency: group: sync-version-packages @@ -23,6 +25,10 @@ jobs: fetch-depth: 20000 fetch-tags: true token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} + + - name: Fetch tags # See https://github.com/actions/checkout/issues/2041 + run: git fetch --tags + - name: Install Dependencies run: yarn --immutable - name: Create Release Pull Request From f47658612a923e9888be4742a2ada2f8078cff62 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Tue, 4 Feb 2025 11:56:04 +0100 Subject: [PATCH 51/69] Update internationalization kubernetes docs Signed-off-by: Dominika Zemanovicova --- docs/features/kubernetes/configuration.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 1a921103b1..c323d5118a 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -80,11 +80,13 @@ The default value is `false`. #### Internationalization -To customize or translate the **Delete Pod** text, use the following approach: +To customize or translate text in some of the components, use the following approach: ```js import { createTranslationMessages } from '@backstage/core-plugin-api/alpha'; import { kubernetesReactTranslationRef } from '@backstage/plugin-kubernetes-react/alpha'; +import { kubernetesTranslationRef } from '@backstage/plugin-kubernetes/alpha'; +import { kubernetesClusterTranslationRef } from '@backstage/plugin-kubernetes-cluster/alpha'; const app = createApp({ __experimentalTranslations: { @@ -94,7 +96,21 @@ const app = createApp({ messages: { "podDrawer.buttons.delete": 'Restart Pod' } - }) + }), + createTranslationMessages({ + ref: kubernetesTranslationRef, + messages: { + 'kubernetesContentPage.permissionAlert.title': 'Insufficient permissions', + 'kubernetesContentPage.permissionAlert.message': 'You do not have permissions to view Kubernetes objects.', + }, + }), + createTranslationMessages({ + ref: kubernetesClusterTranslationRef, + messages: { + 'kubernetesClusterContentPage.permissionAlert.title': 'Insufficient permissions', + 'kubernetesClusterContentPage.permissionAlert.message': 'You do not have permissions to view Kubernetes objects.', + }, + }), ] }, ... From d93b7fc6fbdb584772f1021e65b5fbc27a6856ef Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 4 Feb 2025 12:43:38 +0100 Subject: [PATCH 52/69] chore: re-order Signed-off-by: blam --- plugins/kubernetes/report-alpha.api.md | 30 +++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/plugins/kubernetes/report-alpha.api.md b/plugins/kubernetes/report-alpha.api.md index ea04986595..19f0554291 100644 --- a/plugins/kubernetes/report-alpha.api.md +++ b/plugins/kubernetes/report-alpha.api.md @@ -48,6 +48,21 @@ const _default: FrontendPlugin< routeRef?: RouteRef | undefined; }; }>; + 'api:kubernetes': ExtensionDefinition<{ + kind: 'api'; + name: undefined; + config: {}; + configInput: {}; + output: ConfigurableExtensionDataRef< + AnyApiFactory, + 'core.api.factory', + {} + >; + inputs: {}; + params: { + factory: AnyApiFactory; + }; + }>; 'entity-content:kubernetes/kubernetes': ExtensionDefinition<{ kind: 'entity-content'; name: 'kubernetes'; @@ -99,21 +114,6 @@ const _default: FrontendPlugin< filter?: string | ((entity: Entity) => boolean) | undefined; }; }>; - 'api:kubernetes': ExtensionDefinition<{ - kind: 'api'; - name: undefined; - config: {}; - configInput: {}; - output: ConfigurableExtensionDataRef< - AnyApiFactory, - 'core.api.factory', - {} - >; - inputs: {}; - params: { - factory: AnyApiFactory; - }; - }>; 'api:kubernetes/proxy': ExtensionDefinition<{ kind: 'api'; name: 'proxy'; From fda96db681f0e02be4d342d98da626774deab84f Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 31 Jan 2025 13:32:19 +0100 Subject: [PATCH 53/69] refactor(scaffolder): split getOctokitOptions repoUrl parameter into host, owner and repo parameters Signed-off-by: Benjamin Janssens --- .../report.api.md | 4 +++- .../src/actions/github.ts | 8 ++++--- .../src/actions/githubActionsDispatch.ts | 6 ++++-- .../src/actions/githubAutolinks.ts | 6 ++++-- .../src/actions/githubBranchProtection.ts | 8 ++++--- .../src/actions/githubDeployKey.ts | 8 ++++--- .../src/actions/githubEnvironment.ts | 8 ++++--- .../src/actions/githubIssuesLabel.ts | 6 ++++-- .../src/actions/githubPagesEnable.ts | 8 ++++--- .../src/actions/githubPullRequest.ts | 8 +++---- .../src/actions/githubRepoCreate.ts | 8 ++++--- .../src/actions/githubRepoPush.ts | 6 ++++-- .../src/actions/githubWebhook.ts | 6 ++++-- .../src/actions/helpers.ts | 21 ++++++++++--------- 14 files changed, 67 insertions(+), 44 deletions(-) diff --git a/plugins/scaffolder-backend-module-github/report.api.md b/plugins/scaffolder-backend-module-github/report.api.md index 935886bf14..782846dec7 100644 --- a/plugins/scaffolder-backend-module-github/report.api.md +++ b/plugins/scaffolder-backend-module-github/report.api.md @@ -477,8 +477,10 @@ export const createPublishGithubPullRequestAction: ( export function getOctokitOptions(options: { integrations: ScmIntegrationRegistry; credentialsProvider?: GithubCredentialsProvider; + host: string; + owner?: string; + repo?: string; token?: string; - repoUrl: string; }): Promise; // @public diff --git a/plugins/scaffolder-backend-module-github/src/actions/github.ts b/plugins/scaffolder-backend-module-github/src/actions/github.ts index e3a9ff3352..da54b7c8cb 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/github.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/github.ts @@ -225,16 +225,18 @@ export function createPublishGithubAction(options: { requiredLinearHistory = false, } = ctx.input; + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + const octokitOptions = await getOctokitOptions({ integrations, credentialsProvider: githubCredentialsProvider, token: providedToken, - repoUrl: repoUrl, + host, + owner, + repo, }); const client = new Octokit(octokitOptions); - const { owner, repo } = parseRepoUrl(repoUrl, integrations); - if (!owner) { throw new InputError('Invalid repository owner provided in repoUrl'); } diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts index 0d24b74927..4c83f538ce 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts @@ -96,7 +96,7 @@ export function createGithubActionsDispatchAction(options: { `Dispatching workflow ${workflowId} for repo ${repoUrl} on ${branchOrTagName}`, ); - const { owner, repo } = parseRepoUrl(repoUrl, integrations); + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); if (!owner) { throw new InputError('Invalid repository owner provided in repoUrl'); @@ -105,7 +105,9 @@ export function createGithubActionsDispatchAction(options: { const client = new Octokit( await getOctokitOptions({ integrations, - repoUrl, + host, + owner, + repo, credentialsProvider: githubCredentialsProvider, token: providedToken, }), diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.ts b/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.ts index 4b0d0e1e1f..2037287fad 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.ts @@ -89,7 +89,7 @@ export function createGithubAutolinksAction(options: { ctx.logger.info(`Creating autolink reference for repo ${repoUrl}`); - const { owner, repo } = parseRepoUrl(repoUrl, integrations); + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); if (!owner) { throw new InputError('Invalid repository owner provided in repoUrl'); @@ -98,7 +98,9 @@ export function createGithubAutolinksAction(options: { const client = new Octokit( await getOctokitOptions({ integrations, - repoUrl, + host, + owner, + repo, credentialsProvider: githubCredentialsProvider, token, }), diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts index a7555d3af1..b8716a3f8f 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts @@ -115,15 +115,17 @@ export function createGithubBranchProtectionAction(options: { token: providedToken, } = ctx.input; + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + const octokitOptions = await getOctokitOptions({ integrations, token: providedToken, - repoUrl: repoUrl, + host, + owner, + repo, }); const client = new Octokit(octokitOptions); - const { owner, repo } = parseRepoUrl(repoUrl, integrations); - if (!owner) { throw new InputError(`No owner provided for repo ${repoUrl}`); } diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts index ba55b6a1fb..c9a881c6b1 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts @@ -107,14 +107,16 @@ export function createGithubDeployKeyAction(options: { token: providedToken, } = ctx.input; + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + const octokitOptions = await getOctokitOptions({ integrations, token: providedToken, - repoUrl: repoUrl, + host, + owner, + repo, }); - const { owner, repo } = parseRepoUrl(repoUrl, integrations); - if (!owner) { throw new InputError(`No owner provided for repo ${repoUrl}`); } diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts index ef59b611fe..736b562d54 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts @@ -175,14 +175,16 @@ export function createGithubEnvironmentAction(options: { // Add a 2-second delay before initiating the steps in this action. await new Promise(resolve => setTimeout(resolve, 2000)); + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + const octokitOptions = await getOctokitOptions({ integrations, token: providedToken, - repoUrl: repoUrl, + host, + owner, + repo, }); - const { owner, repo } = parseRepoUrl(repoUrl, integrations); - if (!owner) { throw new InputError(`No owner provided for repo ${repoUrl}`); } diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.ts index 90f5a89dc7..7814429bd7 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.ts @@ -80,7 +80,7 @@ export function createGithubIssuesLabelAction(options: { async handler(ctx) { const { repoUrl, number, labels, token: providedToken } = ctx.input; - const { owner, repo } = parseRepoUrl(repoUrl, integrations); + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); ctx.logger.info(`Adding labels to ${number} issue on repo ${repo}`); if (!owner) { @@ -91,7 +91,9 @@ export function createGithubIssuesLabelAction(options: { await getOctokitOptions({ integrations, credentialsProvider: githubCredentialsProvider, - repoUrl: repoUrl, + host, + owner, + repo, token: providedToken, }), ); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts index 42b9c47030..f8df207638 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts @@ -92,16 +92,18 @@ export function createGithubPagesEnableAction(options: { token: providedToken, } = ctx.input; + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + const octokitOptions = await getOctokitOptions({ integrations, credentialsProvider: githubCredentialsProvider, token: providedToken, - repoUrl: repoUrl, + host, + owner, + repo, }); const client = new Octokit(octokitOptions); - const { owner, repo } = parseRepoUrl(repoUrl, integrations); - if (!owner) { throw new InputError('Invalid repository owner provided in repoUrl'); } diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts index 7fe9d8e401..63496133f1 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts @@ -49,14 +49,12 @@ export const defaultClientFactory: CreateGithubPullRequestActionOptions['clientF host = 'github.com', token: providedToken, }) => { - const [encodedHost, encodedOwner, encodedRepo] = [host, owner, repo].map( - encodeURIComponent, - ); - const octokitOptions = await getOctokitOptions({ integrations, credentialsProvider: githubCredentialsProvider, - repoUrl: `${encodedHost}?owner=${encodedOwner}&repo=${encodedRepo}`, + host, + owner, + repo, token: providedToken, }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts index b4608e57de..a3a758e005 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts @@ -182,16 +182,18 @@ export function createGithubRepoCreateAction(options: { token: providedToken, } = ctx.input; + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + const octokitOptions = await getOctokitOptions({ integrations, credentialsProvider: githubCredentialsProvider, token: providedToken, - repoUrl: repoUrl, + host, + owner, + repo, }); const client = new Octokit(octokitOptions); - const { owner, repo } = parseRepoUrl(repoUrl, integrations); - if (!owner) { throw new InputError('Invalid repository owner provided in repoUrl'); } diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.ts index 62624057c8..b11c4b8f5c 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.ts @@ -142,7 +142,7 @@ export function createGithubRepoPushAction(options: { requiredLinearHistory = false, } = ctx.input; - const { owner, repo } = parseRepoUrl(repoUrl, integrations); + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); if (!owner) { throw new InputError('Invalid repository owner provided in repoUrl'); @@ -152,7 +152,9 @@ export function createGithubRepoPushAction(options: { integrations, credentialsProvider: githubCredentialsProvider, token: providedToken, - repoUrl, + host, + owner, + repo, }); const client = new Octokit(octokitOptions); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.ts b/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.ts index e188cdf0cb..0289d3c69b 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.ts @@ -134,7 +134,7 @@ export function createGithubWebhookAction(options: { } = ctx.input; ctx.logger.info(`Creating webhook ${webhookUrl} for repo ${repoUrl}`); - const { owner, repo } = parseRepoUrl(repoUrl, integrations); + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); if (!owner) { throw new InputError('Invalid repository owner provided in repoUrl'); @@ -144,7 +144,9 @@ export function createGithubWebhookAction(options: { await getOctokitOptions({ integrations, credentialsProvider: githubCredentialsProvider, - repoUrl: repoUrl, + host, + owner, + repo, token: providedToken, }), ); diff --git a/plugins/scaffolder-backend-module-github/src/actions/helpers.ts b/plugins/scaffolder-backend-module-github/src/actions/helpers.ts index 242f95ce34..45e4229e73 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/helpers.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/helpers.ts @@ -27,7 +27,6 @@ import { Octokit } from 'octokit'; import { getRepoSourceDirectory, initRepoAndPush, - parseRepoUrl, } from '@backstage/plugin-scaffolder-node'; import Sodium from 'libsodium-wrappers'; @@ -47,21 +46,19 @@ const DEFAULT_TIMEOUT_MS = 60_000; export async function getOctokitOptions(options: { integrations: ScmIntegrationRegistry; credentialsProvider?: GithubCredentialsProvider; + host: string; + owner?: string; + repo?: string; token?: string; - repoUrl: string; }): Promise { - const { integrations, credentialsProvider, repoUrl, token } = options; - const { owner, repo, host } = parseRepoUrl(repoUrl, integrations); + const { integrations, credentialsProvider, host, owner, repo, token } = + options; const requestOptions = { // set timeout to 60 seconds timeout: DEFAULT_TIMEOUT_MS, }; - if (!owner) { - throw new InputError(`No owner provided for repo ${repoUrl}`); - } - const integrationConfig = integrations.github.byHost(host)?.config; if (!integrationConfig) { @@ -78,12 +75,16 @@ export async function getOctokitOptions(options: { }; } + if (!owner || !repo) { + throw new InputError( + `No owner and/or owner provided, which is required if a token is not provided`, + ); + } + const githubCredentialsProvider = credentialsProvider ?? DefaultGithubCredentialsProvider.fromIntegrations(integrations); - // TODO(blam): Consider changing this API to take host and repo instead of repoUrl, as we end up parsing in this function - // and then parsing in the `getCredentials` function too the other side const { token: credentialProviderToken } = await githubCredentialsProvider.getCredentials({ url: `https://${host}/${encodeURIComponent(owner)}/${encodeURIComponent( From 95439bbdad7aa08e41a8ef3f8eeae4ca63523a8f Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 31 Jan 2025 13:44:33 +0100 Subject: [PATCH 54/69] refactor(scaffolder): move getOctokitOptions to separate util file Signed-off-by: Benjamin Janssens --- .../src/actions/github.ts | 2 +- .../src/actions/githubActionsDispatch.ts | 2 +- .../src/actions/githubAutolinks.ts | 2 +- .../src/actions/githubBranchProtection.ts | 2 +- .../src/actions/githubDeployKey.ts | 2 +- .../src/actions/githubEnvironment.ts | 2 +- .../githubIssuesLabel.examples.test.ts | 2 +- .../src/actions/githubIssuesLabel.test.ts | 2 +- .../src/actions/githubIssuesLabel.ts | 2 +- .../src/actions/githubPagesEnable.ts | 2 +- .../src/actions/githubPullRequest.ts | 2 +- .../src/actions/githubRepoCreate.ts | 6 +- .../src/actions/githubRepoPush.ts | 3 +- .../src/actions/githubWebhook.ts | 2 +- .../src/actions/helpers.ts | 77 +--------------- .../src/actions/index.ts | 2 - .../src/index.ts | 1 + .../src/util.ts | 92 +++++++++++++++++++ 18 files changed, 110 insertions(+), 95 deletions(-) create mode 100644 plugins/scaffolder-backend-module-github/src/util.ts diff --git a/plugins/scaffolder-backend-module-github/src/actions/github.ts b/plugins/scaffolder-backend-module-github/src/actions/github.ts index da54b7c8cb..4c04087670 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/github.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/github.ts @@ -27,9 +27,9 @@ import { } from '@backstage/plugin-scaffolder-node'; import { createGithubRepoWithCollaboratorsAndTopics, - getOctokitOptions, initRepoPushAndProtect, } from './helpers'; +import { getOctokitOptions } from '../util'; import * as inputProps from './inputProperties'; import * as outputProps from './outputProperties'; import { examples } from './github.examples'; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts index 4c83f538ce..93f89a565c 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts @@ -24,7 +24,7 @@ import { parseRepoUrl, } from '@backstage/plugin-scaffolder-node'; import { Octokit } from 'octokit'; -import { getOctokitOptions } from './helpers'; +import { getOctokitOptions } from '../util'; import { examples } from './githubActionsDispatch.examples'; /** diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.ts b/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.ts index 2037287fad..b55811630b 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.ts @@ -25,7 +25,7 @@ import { } from '@backstage/plugin-scaffolder-node'; import { Octokit } from 'octokit'; import { examples } from './githubAutolinks.examples'; -import { getOctokitOptions } from './helpers'; +import { getOctokitOptions } from '../util'; /** * Create an autolink reference for a repository diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts index b8716a3f8f..5d5366a7b1 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts @@ -22,7 +22,7 @@ import { import { ScmIntegrationRegistry } from '@backstage/integration'; import { examples } from './githubBranchProtection.examples'; import * as inputProps from './inputProperties'; -import { getOctokitOptions } from './helpers'; +import { getOctokitOptions } from '../util'; import { Octokit } from 'octokit'; import { enableBranchProtectionOnDefaultRepoBranch } from './gitHelpers'; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts index c9a881c6b1..6a7a2f86c6 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts @@ -20,7 +20,7 @@ import { parseRepoUrl, } from '@backstage/plugin-scaffolder-node'; import { ScmIntegrationRegistry } from '@backstage/integration'; -import { getOctokitOptions } from './helpers'; +import { getOctokitOptions } from '../util'; import { Octokit } from 'octokit'; import Sodium from 'libsodium-wrappers'; import { examples } from './githubDeployKey.examples'; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts index 736b562d54..5d3817ee6d 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts @@ -20,7 +20,7 @@ import { parseRepoUrl, } from '@backstage/plugin-scaffolder-node'; import { ScmIntegrationRegistry } from '@backstage/integration'; -import { getOctokitOptions } from './helpers'; +import { getOctokitOptions } from '../util'; import { Octokit } from 'octokit'; import Sodium from 'libsodium-wrappers'; import { examples } from './gitHubEnvironment.examples'; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts index 75ac6025ce..8230053fda 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts @@ -25,7 +25,7 @@ import { import { createGithubIssuesLabelAction } from './githubIssuesLabel'; import yaml from 'yaml'; import { examples } from './githubIssuesLabel.examples'; -import { getOctokitOptions } from './helpers'; +import { getOctokitOptions } from '../util'; jest.mock('./helpers', () => { return { diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts index 13c7df68cb..1340a28d14 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts @@ -23,7 +23,7 @@ import { import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ConfigReader } from '@backstage/config'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { getOctokitOptions } from './helpers'; +import { getOctokitOptions } from '../util'; jest.mock('./helpers', () => { return { diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.ts index 7814429bd7..0ce6642f39 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.ts @@ -24,7 +24,7 @@ import { } from '@backstage/plugin-scaffolder-node'; import { assertError, InputError } from '@backstage/errors'; import { Octokit } from 'octokit'; -import { getOctokitOptions } from './helpers'; +import { getOctokitOptions } from '../util'; import { examples } from './githubIssuesLabel.examples'; /** diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts index f8df207638..54bcfbcc43 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts @@ -25,7 +25,7 @@ import { parseRepoUrl, } from '@backstage/plugin-scaffolder-node'; import { examples } from './githubPagesEnable.examples'; -import { getOctokitOptions } from '@backstage/plugin-scaffolder-backend-module-github'; +import { getOctokitOptions } from '../util'; /** * Creates a new action that enables GitHub Pages for a repository. diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts index 63496133f1..44830b3047 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts @@ -28,7 +28,7 @@ import { import { Octokit } from 'octokit'; import { CustomErrorBase, InputError } from '@backstage/errors'; import { createPullRequest } from 'octokit-plugin-create-pull-request'; -import { getOctokitOptions } from './helpers'; +import { getOctokitOptions } from '../util'; import { examples } from './githubPullRequest.examples'; import { LoggerService, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts index a3a758e005..b103dffb15 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts @@ -24,10 +24,8 @@ import { createTemplateAction, parseRepoUrl, } from '@backstage/plugin-scaffolder-node'; -import { - createGithubRepoWithCollaboratorsAndTopics, - getOctokitOptions, -} from './helpers'; +import { createGithubRepoWithCollaboratorsAndTopics } from './helpers'; +import { getOctokitOptions } from '../util'; import * as inputProps from './inputProperties'; import * as outputProps from './outputProperties'; import { examples } from './githubRepoCreate.examples'; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.ts index b11c4b8f5c..dcb159806c 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.ts @@ -25,7 +25,8 @@ import { createTemplateAction, parseRepoUrl, } from '@backstage/plugin-scaffolder-node'; -import { getOctokitOptions, initRepoPushAndProtect } from './helpers'; +import { initRepoPushAndProtect } from './helpers'; +import { getOctokitOptions } from '../util'; import * as inputProps from './inputProperties'; import * as outputProps from './outputProperties'; import { examples } from './githubRepoPush.examples'; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.ts b/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.ts index 0289d3c69b..13200a56d0 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.ts @@ -25,7 +25,7 @@ import { import { emitterEventNames } from '@octokit/webhooks'; import { assertError, InputError } from '@backstage/errors'; import { Octokit } from 'octokit'; -import { getOctokitOptions } from './helpers'; +import { getOctokitOptions } from '../util'; import { examples } from './githubWebhook.examples'; /** diff --git a/plugins/scaffolder-backend-module-github/src/actions/helpers.ts b/plugins/scaffolder-backend-module-github/src/actions/helpers.ts index 45e4229e73..8e6e521c29 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/helpers.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/helpers.ts @@ -15,13 +15,7 @@ */ import { Config } from '@backstage/config'; -import { assertError, InputError, NotFoundError } from '@backstage/errors'; -import { - DefaultGithubCredentialsProvider, - GithubCredentialsProvider, - ScmIntegrationRegistry, -} from '@backstage/integration'; -import { OctokitOptions } from '@octokit/core/dist-types/types'; +import { assertError, NotFoundError } from '@backstage/errors'; import { Octokit } from 'octokit'; import { @@ -36,75 +30,6 @@ import { } from './gitHelpers'; import { LoggerService } from '@backstage/backend-plugin-api'; -const DEFAULT_TIMEOUT_MS = 60_000; - -/** - * Helper for generating octokit configuration options for given repoUrl. - * If no token is provided, it will attempt to get a token from the credentials provider. - * @public - */ -export async function getOctokitOptions(options: { - integrations: ScmIntegrationRegistry; - credentialsProvider?: GithubCredentialsProvider; - host: string; - owner?: string; - repo?: string; - token?: string; -}): Promise { - const { integrations, credentialsProvider, host, owner, repo, token } = - options; - - const requestOptions = { - // set timeout to 60 seconds - timeout: DEFAULT_TIMEOUT_MS, - }; - - const integrationConfig = integrations.github.byHost(host)?.config; - - if (!integrationConfig) { - throw new InputError(`No integration for host ${host}`); - } - - // short circuit the `githubCredentialsProvider` if there is a token provided by the caller already - if (token) { - return { - auth: token, - baseUrl: integrationConfig.apiBaseUrl, - previews: ['nebula-preview'], - request: requestOptions, - }; - } - - if (!owner || !repo) { - throw new InputError( - `No owner and/or owner provided, which is required if a token is not provided`, - ); - } - - const githubCredentialsProvider = - credentialsProvider ?? - DefaultGithubCredentialsProvider.fromIntegrations(integrations); - - const { token: credentialProviderToken } = - await githubCredentialsProvider.getCredentials({ - url: `https://${host}/${encodeURIComponent(owner)}/${encodeURIComponent( - repo, - )}`, - }); - - if (!credentialProviderToken) { - throw new InputError( - `No token available for host: ${host}, with owner ${owner}, and repo ${repo}. Make sure GitHub auth is configured correctly. See https://backstage.io/docs/auth/github/provider for more details.`, - ); - } - - return { - auth: credentialProviderToken, - baseUrl: integrationConfig.apiBaseUrl, - previews: ['nebula-preview'], - }; -} - export async function createGithubRepoWithCollaboratorsAndTopics( client: Octokit, repo: string, diff --git a/plugins/scaffolder-backend-module-github/src/actions/index.ts b/plugins/scaffolder-backend-module-github/src/actions/index.ts index c758415e1d..7cf4109e6b 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/index.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/index.ts @@ -29,5 +29,3 @@ export { createPublishGithubAction } from './github'; export { createGithubAutolinksAction } from './githubAutolinks'; export { createGithubPagesEnableAction } from './githubPagesEnable'; export { createGithubBranchProtectionAction } from './githubBranchProtection'; - -export { getOctokitOptions } from './helpers'; diff --git a/plugins/scaffolder-backend-module-github/src/index.ts b/plugins/scaffolder-backend-module-github/src/index.ts index 219ba54dbb..cf0bc003cb 100644 --- a/plugins/scaffolder-backend-module-github/src/index.ts +++ b/plugins/scaffolder-backend-module-github/src/index.ts @@ -22,3 +22,4 @@ export * from './actions'; export { githubModule as default } from './module'; +export { getOctokitOptions } from './util'; diff --git a/plugins/scaffolder-backend-module-github/src/util.ts b/plugins/scaffolder-backend-module-github/src/util.ts new file mode 100644 index 0000000000..575ad8c6ed --- /dev/null +++ b/plugins/scaffolder-backend-module-github/src/util.ts @@ -0,0 +1,92 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputError } from '@backstage/errors'; +import { + DefaultGithubCredentialsProvider, + GithubCredentialsProvider, + ScmIntegrationRegistry, +} from '@backstage/integration'; +import { OctokitOptions } from '@octokit/core/dist-types/types'; + +const DEFAULT_TIMEOUT_MS = 60_000; + +/** + * Helper for generating octokit configuration options for given repoUrl. + * If no token is provided, it will attempt to get a token from the credentials provider. + * @public + */ +export async function getOctokitOptions(options: { + integrations: ScmIntegrationRegistry; + credentialsProvider?: GithubCredentialsProvider; + host: string; + owner?: string; + repo?: string; + token?: string; +}): Promise { + const { integrations, credentialsProvider, host, owner, repo, token } = + options; + + const requestOptions = { + // set timeout to 60 seconds + timeout: DEFAULT_TIMEOUT_MS, + }; + + const integrationConfig = integrations.github.byHost(host)?.config; + + if (!integrationConfig) { + throw new InputError(`No integration for host ${host}`); + } + + // short circuit the `githubCredentialsProvider` if there is a token provided by the caller already + if (token) { + return { + auth: token, + baseUrl: integrationConfig.apiBaseUrl, + previews: ['nebula-preview'], + request: requestOptions, + }; + } + + if (!owner || !repo) { + throw new InputError( + `No owner and/or owner provided, which is required if a token is not provided`, + ); + } + + const githubCredentialsProvider = + credentialsProvider ?? + DefaultGithubCredentialsProvider.fromIntegrations(integrations); + + const { token: credentialProviderToken } = + await githubCredentialsProvider.getCredentials({ + url: `https://${host}/${encodeURIComponent(owner)}/${encodeURIComponent( + repo, + )}`, + }); + + if (!credentialProviderToken) { + throw new InputError( + `No token available for host: ${host}, with owner ${owner}, and repo ${repo}. Make sure GitHub auth is configured correctly. See https://backstage.io/docs/auth/github/provider for more details.`, + ); + } + + return { + auth: credentialProviderToken, + baseUrl: integrationConfig.apiBaseUrl, + previews: ['nebula-preview'], + }; +} From 02445b1364989d349ea74b469fc407d117ad63a6 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 31 Jan 2025 14:30:23 +0100 Subject: [PATCH 55/69] feat(scaffolder): add support for autocompleting GitHub owners to scaffolder Signed-off-by: Benjamin Janssens --- .../src/autocomplete/autocomplete.ts | 58 ++++++++++++++++++ .../src/module.ts | 14 ++++- .../fields/RepoUrlPicker/GithubRepoPicker.tsx | 61 ++++++++++++++++--- .../fields/RepoUrlPicker/RepoUrlPicker.tsx | 4 ++ 4 files changed, 125 insertions(+), 12 deletions(-) create mode 100644 plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.ts diff --git a/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.ts b/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.ts new file mode 100644 index 0000000000..a015067f52 --- /dev/null +++ b/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputError } from '@backstage/errors'; +import { getOctokitOptions } from '../util'; +import { Octokit } from 'octokit'; +import { ScmIntegrationRegistry } from '@backstage/integration'; + +export function createHandleAutocompleteRequest(options: { + integrations: ScmIntegrationRegistry; +}) { + return async function handleAutocompleteRequest({ + resource, + token, + context, + }: { + resource: string; + token: string; + context: Record; + }): Promise<{ results: { title?: string; id: string }[] }> { + const { integrations } = options; + const octokitOptions = await getOctokitOptions({ + integrations, + token, + host: context.host ?? 'github.com', + }); + const client = new Octokit(octokitOptions); + + switch (resource) { + case 'owners': { + const organizations = await client.paginate( + client.rest.orgs.listForAuthenticatedUser, + ); + + const results = organizations.map(organization => ({ + id: organization.login, + })); + + return { results }; + } + default: + throw new InputError(`Invalid resource: ${resource}`); + } + }; +} diff --git a/plugins/scaffolder-backend-module-github/src/module.ts b/plugins/scaffolder-backend-module-github/src/module.ts index 0387473fdb..02dfc2d7f3 100644 --- a/plugins/scaffolder-backend-module-github/src/module.ts +++ b/plugins/scaffolder-backend-module-github/src/module.ts @@ -17,7 +17,10 @@ import { coreServices, createBackendModule, } from '@backstage/backend-plugin-api'; -import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha'; +import { + scaffolderActionsExtensionPoint, + scaffolderAutocompleteExtensionPoint, +} from '@backstage/plugin-scaffolder-node/alpha'; import { createGithubActionsDispatchAction, createGithubAutolinksAction, @@ -37,6 +40,7 @@ import { ScmIntegrations, } from '@backstage/integration'; import { CatalogClient } from '@backstage/catalog-client'; +import { createHandleAutocompleteRequest } from './autocomplete/autocomplete'; /** * @public @@ -52,8 +56,9 @@ export const githubModule = createBackendModule({ config: coreServices.rootConfig, discovery: coreServices.discovery, auth: coreServices.auth, + autocomplete: scaffolderAutocompleteExtensionPoint, }, - async init({ scaffolder, config, discovery, auth }) { + async init({ scaffolder, config, discovery, auth, autocomplete }) { const integrations = ScmIntegrations.fromConfig(config); const githubCredentialsProvider = DefaultGithubCredentialsProvider.fromIntegrations(integrations); @@ -109,6 +114,11 @@ export const githubModule = createBackendModule({ integrations, }), ); + + autocomplete.addAutocompleteProvider({ + id: 'github', + handler: createHandleAutocompleteRequest({ integrations }), + }); }, }); }, diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx index 6f6cbfb355..09850d85c0 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; +import React, { useCallback, useState } from 'react'; import FormControl from '@material-ui/core/FormControl'; import FormHelperText from '@material-ui/core/FormHelperText'; import TextField from '@material-ui/core/TextField'; @@ -21,13 +21,18 @@ import { Select, SelectItem } from '@backstage/core-components'; import { BaseRepoUrlPickerProps } from './types'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import { scaffolderTranslationRef } from '../../../translation'; +import { useApi } from '@backstage/core-plugin-api'; +import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; +import useDebounce from 'react-use/esm/useDebounce'; +import Autocomplete from '@material-ui/lab/Autocomplete'; export const GithubRepoPicker = ( props: BaseRepoUrlPickerProps<{ allowedOwners?: string[]; + accessToken?: string; }>, ) => { - const { allowedOwners = [], rawErrors, state, onChange } = props; + const { allowedOwners = [], rawErrors, state, onChange, accessToken } = props; const { t } = useTranslationRef(scaffolderTranslationRef); const ownerItems: SelectItem[] = allowedOwners ? allowedOwners.map(i => ({ label: i, value: i })) @@ -35,6 +40,33 @@ export const GithubRepoPicker = ( const { owner } = state; + const scaffolderApi = useApi(scaffolderApiRef); + + const [availableOwners, setAvailableOwners] = useState([]); + + // Update available owners when client is available + const updateAvailableOwners = useCallback(() => { + if (!scaffolderApi.autocomplete || !accessToken) { + setAvailableOwners([]); + return; + } + + scaffolderApi + .autocomplete({ + token: accessToken, + resource: 'owners', + provider: 'github', + }) + .then(({ results }) => { + setAvailableOwners(results.map(r => r.id)); + }) + .catch(() => { + setAvailableOwners([]); + }); + }, [scaffolderApi, accessToken]); + + useDebounce(updateAvailableOwners, 500, [updateAvailableOwners]); + return ( <> - - {t('fields.githubRepoPicker.owner.description')} - ) : ( - onChange({ owner: e.target.value })} - helperText={t('fields.githubRepoPicker.owner.description')} + { + onChange({ owner: newValue || '' }); + }} + options={availableOwners} + renderInput={params => ( + + )} + freeSolo + autoSelect /> )} + + {t('fields.githubRepoPicker.owner.description')} + ); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index ef6654b60c..3c6eba1695 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -186,6 +186,10 @@ export const RepoUrlPicker = ( onChange={updateLocalState} rawErrors={rawErrors} state={state} + accessToken={ + uiSchema?.['ui:options']?.requestUserCredentials?.secretsKey && + secrets[uiSchema['ui:options'].requestUserCredentials.secretsKey] + } /> )} {hostType === 'gitea' && ( From 9f7b6ac9b1b6be5a08493bba271b34f38ee2c620 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 13:51:07 +0100 Subject: [PATCH 56/69] feat(scaffolder): add support for autocompleting GitHub repositories to scaffolder Signed-off-by: Benjamin Janssens --- .../src/autocomplete/autocomplete.ts | 10 ++--- .../fields/RepoUrlPicker/GithubRepoPicker.tsx | 45 +++++++++++++++---- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.ts b/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.ts index a015067f52..08041640d9 100644 --- a/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.ts +++ b/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.ts @@ -40,14 +40,12 @@ export function createHandleAutocompleteRequest(options: { const client = new Octokit(octokitOptions); switch (resource) { - case 'owners': { - const organizations = await client.paginate( - client.rest.orgs.listForAuthenticatedUser, + case 'repositoriesWithOwner': { + const repositoriesWithOwner = await client.paginate( + client.rest.repos.listForAuthenticatedUser, ); - const results = organizations.map(organization => ({ - id: organization.login, - })); + const results = repositoriesWithOwner.map(r => ({ id: r.full_name })); return { results }; } diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx index 09850d85c0..4aa9cc057b 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import FormControl from '@material-ui/core/FormControl'; import FormHelperText from '@material-ui/core/FormHelperText'; import TextField from '@material-ui/core/TextField'; @@ -25,6 +25,8 @@ import { useApi } from '@backstage/core-plugin-api'; import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; import useDebounce from 'react-use/esm/useDebounce'; import Autocomplete from '@material-ui/lab/Autocomplete'; +import uniq from 'lodash/uniq'; +import map from 'lodash/map'; export const GithubRepoPicker = ( props: BaseRepoUrlPickerProps<{ @@ -42,30 +44,55 @@ export const GithubRepoPicker = ( const scaffolderApi = useApi(scaffolderApiRef); - const [availableOwners, setAvailableOwners] = useState([]); + const [availableRepositoriesWithOwner, setAvailableRepositoriesWithOwner] = + useState<{ owner: string; name: string }[]>([]); - // Update available owners when client is available - const updateAvailableOwners = useCallback(() => { + // Update available repositories with owner when client is available + const updateAvailableRepositoriesWithOwner = useCallback(() => { if (!scaffolderApi.autocomplete || !accessToken) { - setAvailableOwners([]); + setAvailableRepositoriesWithOwner([]); return; } scaffolderApi .autocomplete({ token: accessToken, - resource: 'owners', + resource: 'repositoriesWithOwner', provider: 'github', }) .then(({ results }) => { - setAvailableOwners(results.map(r => r.id)); + setAvailableRepositoriesWithOwner( + results.map(r => { + const [rOwner, rName] = r.id.split('/'); + return { owner: rOwner, name: rName }; + }), + ); }) .catch(() => { - setAvailableOwners([]); + setAvailableRepositoriesWithOwner([]); }); }, [scaffolderApi, accessToken]); - useDebounce(updateAvailableOwners, 500, [updateAvailableOwners]); + useDebounce(updateAvailableRepositoriesWithOwner, 500, [ + updateAvailableRepositoriesWithOwner, + ]); + + // Update available owners when available repositories with owner change + const availableOwners = useMemo( + () => uniq(map(availableRepositoriesWithOwner, 'owner')), + [availableRepositoriesWithOwner], + ); + + // Update available repositories when available repositories with owner change or when owner changes + const updateAvailableRepositories = useCallback(() => { + const availableRepos = availableRepositoriesWithOwner.flatMap(r => + r.owner === owner ? [{ name: r.name }] : [], + ); + + onChange({ availableRepos }); + }, [availableRepositoriesWithOwner, owner, onChange]); + + useDebounce(updateAvailableRepositories, 500, [updateAvailableRepositories]); return ( <> From 19fb7261da7856acfda1a8c5441b609c7e502ad5 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 14:22:45 +0100 Subject: [PATCH 57/69] test(scaffolder): add tests for handleAutocompleteRequest Signed-off-by: Benjamin Janssens --- .../src/autocomplete/autocomplete.test.ts | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.test.ts diff --git a/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.test.ts b/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.test.ts new file mode 100644 index 0000000000..a107117129 --- /dev/null +++ b/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.test.ts @@ -0,0 +1,97 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputError } from '@backstage/errors'; +import { createHandleAutocompleteRequest } from './autocomplete'; +import { ScmIntegrationRegistry } from '@backstage/integration'; + +jest.mock('../util', () => { + return { + getOctokitOptions: jest.fn(), + }; +}); + +const mockOctokit = { + paginate: async (fn: any) => (await fn()).data, + rest: { + repos: { + listForAuthenticatedUser: jest.fn(), + }, + }, +}; +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + +describe('handleAutocompleteRequest', () => { + const mockIntegrations = {} as ScmIntegrationRegistry; + + it('should return repositories with owner', async () => { + const handleAutocompleteRequest = createHandleAutocompleteRequest({ + integrations: mockIntegrations, + }); + + mockOctokit.rest.repos.listForAuthenticatedUser.mockResolvedValue({ + data: [ + { + full_name: 'backstage/backstage', + }, + ], + }); + + const result = await handleAutocompleteRequest({ + resource: 'repositoriesWithOwner', + token: 'token', + context: {}, + }); + + expect(result).toEqual({ + results: [{ id: 'backstage/backstage' }], + }); + }); + + it('should throw an error for invalid resource', async () => { + const handleAutocompleteRequest = createHandleAutocompleteRequest({ + integrations: mockIntegrations, + }); + + await expect( + handleAutocompleteRequest({ + resource: 'invalid', + token: 'token', + context: {}, + }), + ).rejects.toThrow(InputError); + }); + + it('should throw an error if context id is missing for repositories', async () => { + const handleAutocompleteRequest = createHandleAutocompleteRequest({ + integrations: mockIntegrations, + }); + + await expect( + handleAutocompleteRequest({ + resource: 'repositories', + token: 'token', + context: {}, + }), + ).rejects.toThrow(InputError); + }); +}); From fc0eef2b67373de58acaaef397726b9078ebdba1 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 14:45:27 +0100 Subject: [PATCH 58/69] test(scaffolder): fix tests for GithubRepoPicker Signed-off-by: Benjamin Janssens --- .../RepoUrlPicker/GithubRepoPicker.test.tsx | 75 ++++++++++++------- 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx index 3a01c6de7c..a3e233c908 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx @@ -14,22 +14,35 @@ * limitations under the License. */ -import React from 'react'; +import React, { act } from 'react'; import { GithubRepoPicker } from './GithubRepoPicker'; import { fireEvent } from '@testing-library/react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + ScaffolderApi, + scaffolderApiRef, +} from '@backstage/plugin-scaffolder-react'; describe('GithubRepoPicker', () => { + const scaffolderApiMock: Partial = { + autocomplete: jest.fn().mockImplementation(opts => + Promise.resolve({ + results: [{ title: `${opts.resource}_example` }], + }), + ), + }; describe('owner field', () => { it('renders a select if there is a list of allowed owners', async () => { const allowedOwners = ['owner1', 'owner2']; const { findByText } = await renderInTestApp( - , + + + , ); expect(await findByText('owner1')).toBeInTheDocument(); @@ -40,12 +53,14 @@ describe('GithubRepoPicker', () => { const onChange = jest.fn(); const allowedOwners = ['owner1', 'owner2']; const { getByRole } = await renderInTestApp( - , + + + , ); await fireEvent.change(getByRole('combobox'), { @@ -59,12 +74,14 @@ describe('GithubRepoPicker', () => { const onChange = jest.fn(); const allowedOwners = ['owner1']; const { getByRole } = await renderInTestApp( - , + + + , ); expect(getByRole('combobox')).toBeDisabled(); @@ -73,14 +90,20 @@ describe('GithubRepoPicker', () => { it('should display free text if no allowed owners are passed', async () => { const onChange = jest.fn(); const { getAllByRole } = await renderInTestApp( - , + + + , ); const ownerField = getAllByRole('textbox')[0]; - fireEvent.change(ownerField, { target: { value: 'my-mock-owner' } }); + act(() => { + ownerField.focus(); + fireEvent.change(ownerField, { target: { value: 'my-mock-owner' } }); + ownerField.blur(); + }); expect(onChange).toHaveBeenCalledWith({ owner: 'my-mock-owner' }); }); From c3f746ca731662c0b5473e8843298dd4bacaf8d2 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 15:18:19 +0100 Subject: [PATCH 59/69] test(scaffolder): fix more existing tests Signed-off-by: Benjamin Janssens --- .../src/actions/githubIssuesLabel.examples.test.ts | 2 +- .../src/actions/githubIssuesLabel.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts index 8230053fda..4aee57130c 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts @@ -27,7 +27,7 @@ import yaml from 'yaml'; import { examples } from './githubIssuesLabel.examples'; import { getOctokitOptions } from '../util'; -jest.mock('./helpers', () => { +jest.mock('../util', () => { return { getOctokitOptions: jest.fn(), }; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts index 1340a28d14..af1d7d552c 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts @@ -25,7 +25,7 @@ import { ConfigReader } from '@backstage/config'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { getOctokitOptions } from '../util'; -jest.mock('./helpers', () => { +jest.mock('../util', () => { return { getOctokitOptions: jest.fn(), }; From 5cab47abae58552b59e4891360be402c7b781f23 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 15:18:47 +0100 Subject: [PATCH 60/69] test(scaffolder): add autocompletion tests for GithubRepoPicker Signed-off-by: Benjamin Janssens --- .../RepoUrlPicker/GithubRepoPicker.test.tsx | 66 ++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx index a3e233c908..e9a7041fc5 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx @@ -16,18 +16,26 @@ import React, { act } from 'react'; import { GithubRepoPicker } from './GithubRepoPicker'; -import { fireEvent } from '@testing-library/react'; +import { fireEvent, waitFor } from '@testing-library/react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { ScaffolderApi, scaffolderApiRef, } from '@backstage/plugin-scaffolder-react'; +import userEvent from '@testing-library/user-event'; describe('GithubRepoPicker', () => { const scaffolderApiMock: Partial = { autocomplete: jest.fn().mockImplementation(opts => Promise.resolve({ - results: [{ title: `${opts.resource}_example` }], + results: [ + { + id: + opts.resource === 'repositoriesWithOwner' + ? 'spotify/backstage' + : `${opts.resource}_example`, + }, + ], }), ), }; @@ -108,4 +116,58 @@ describe('GithubRepoPicker', () => { expect(onChange).toHaveBeenCalledWith({ owner: 'my-mock-owner' }); }); }); + + describe('autocompletion', () => { + it('should populate owners if accessToken is provided', async () => { + const onChange = jest.fn(); + + const { getAllByRole, getByText } = await renderInTestApp( + + + , + ); + + // Open the Autcomplete dropdown + const ownerInput = getAllByRole('textbox')[0]; + await userEvent.click(ownerInput); + + // Verify that the available owners are shown + await waitFor(() => expect(getByText('spotify')).toBeInTheDocument()); + + // Verify that selecting an option calls onChange + await userEvent.click(getByText('spotify')); + expect(onChange).toHaveBeenCalledWith({ + owner: 'spotify', + }); + }); + + it('should populate owners if owner and accessToken are provided', async () => { + const onChange = jest.fn(); + + await renderInTestApp( + + + , + ); + + // Verify that the available repos are updated + await waitFor( + () => + expect(onChange).toHaveBeenCalledWith({ + availableRepos: [{ name: 'backstage' }], + }), + { timeout: 1500 }, + ); + }); + }); }); From 8e67e4a6ee644e952b5387b175dec3ce943d9d27 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 15:22:22 +0100 Subject: [PATCH 61/69] chore(scaffolder): add changeset Signed-off-by: Benjamin Janssens --- .changeset/wet-bees-heal.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/wet-bees-heal.md diff --git a/.changeset/wet-bees-heal.md b/.changeset/wet-bees-heal.md new file mode 100644 index 0000000000..6708b42d16 --- /dev/null +++ b/.changeset/wet-bees-heal.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder': minor +'@backstage/plugin-scaffolder-backend-module-github': patch +--- + +Added support for autocompletion to GithubRepoPicker component From 2e296c403a6df6a8b663d14b290f3a5f596d16ab Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 16:10:20 +0100 Subject: [PATCH 62/69] test(scaffolder): remove invalid test Signed-off-by: Benjamin Janssens --- .../src/autocomplete/autocomplete.test.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.test.ts b/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.test.ts index a107117129..8556fc9428 100644 --- a/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.test.ts +++ b/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.test.ts @@ -80,18 +80,4 @@ describe('handleAutocompleteRequest', () => { }), ).rejects.toThrow(InputError); }); - - it('should throw an error if context id is missing for repositories', async () => { - const handleAutocompleteRequest = createHandleAutocompleteRequest({ - integrations: mockIntegrations, - }); - - await expect( - handleAutocompleteRequest({ - resource: 'repositories', - token: 'token', - context: {}, - }), - ).rejects.toThrow(InputError); - }); }); From 38543fa980dabef4a77cb382b1fb9a2905bcd295 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 17:01:20 +0100 Subject: [PATCH 63/69] chore(scaffolder): use host in GitHub autocompletion Signed-off-by: Benjamin Janssens --- .../fields/RepoUrlPicker/GithubRepoPicker.test.tsx | 6 +++--- .../components/fields/RepoUrlPicker/GithubRepoPicker.tsx | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx index e9a7041fc5..d5657606a7 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx @@ -126,7 +126,7 @@ describe('GithubRepoPicker', () => { , @@ -146,7 +146,7 @@ describe('GithubRepoPicker', () => { }); }); - it('should populate owners if owner and accessToken are provided', async () => { + it('should populate repositories if owner and accessToken are provided', async () => { const onChange = jest.fn(); await renderInTestApp( @@ -154,7 +154,7 @@ describe('GithubRepoPicker', () => { , diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx index 4aa9cc057b..66a662b530 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx @@ -40,7 +40,7 @@ export const GithubRepoPicker = ( ? allowedOwners.map(i => ({ label: i, value: i })) : [{ label: 'Loading...', value: 'loading' }]; - const { owner } = state; + const { host, owner } = state; const scaffolderApi = useApi(scaffolderApiRef); @@ -49,7 +49,7 @@ export const GithubRepoPicker = ( // Update available repositories with owner when client is available const updateAvailableRepositoriesWithOwner = useCallback(() => { - if (!scaffolderApi.autocomplete || !accessToken) { + if (!scaffolderApi.autocomplete || !accessToken || !host) { setAvailableRepositoriesWithOwner([]); return; } @@ -59,6 +59,7 @@ export const GithubRepoPicker = ( token: accessToken, resource: 'repositoriesWithOwner', provider: 'github', + context: { host }, }) .then(({ results }) => { setAvailableRepositoriesWithOwner( @@ -71,7 +72,7 @@ export const GithubRepoPicker = ( .catch(() => { setAvailableRepositoriesWithOwner([]); }); - }, [scaffolderApi, accessToken]); + }, [scaffolderApi, accessToken, host]); useDebounce(updateAvailableRepositoriesWithOwner, 500, [ updateAvailableRepositoriesWithOwner, From 0a762d998bf9fda79faf8616f20cf733cccb9749 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 4 Feb 2025 13:34:25 +0100 Subject: [PATCH 64/69] refactor(scaffolder): re-add previous signature for getOctokitOptions and make it deprecated Signed-off-by: Benjamin Janssens --- .../report.api.md | 8 +++++ .../src/util.ts | 34 ++++++++++++++++--- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder-backend-module-github/report.api.md b/plugins/scaffolder-backend-module-github/report.api.md index 782846dec7..476e8fde1e 100644 --- a/plugins/scaffolder-backend-module-github/report.api.md +++ b/plugins/scaffolder-backend-module-github/report.api.md @@ -477,10 +477,18 @@ export const createPublishGithubPullRequestAction: ( export function getOctokitOptions(options: { integrations: ScmIntegrationRegistry; credentialsProvider?: GithubCredentialsProvider; + token?: string; host: string; owner?: string; repo?: string; +}): Promise; + +// @public @deprecated +export function getOctokitOptions(options: { + integrations: ScmIntegrationRegistry; + credentialsProvider?: GithubCredentialsProvider; token?: string; + repoUrl: string; }): Promise; // @public diff --git a/plugins/scaffolder-backend-module-github/src/util.ts b/plugins/scaffolder-backend-module-github/src/util.ts index 575ad8c6ed..c25ca99f5f 100644 --- a/plugins/scaffolder-backend-module-github/src/util.ts +++ b/plugins/scaffolder-backend-module-github/src/util.ts @@ -20,32 +20,58 @@ import { GithubCredentialsProvider, ScmIntegrationRegistry, } from '@backstage/integration'; +import { parseRepoUrl } from '@backstage/plugin-scaffolder-node'; import { OctokitOptions } from '@octokit/core/dist-types/types'; const DEFAULT_TIMEOUT_MS = 60_000; /** - * Helper for generating octokit configuration options for given repoUrl. + * Helper for generating octokit configuration options. * If no token is provided, it will attempt to get a token from the credentials provider. * @public */ export async function getOctokitOptions(options: { integrations: ScmIntegrationRegistry; credentialsProvider?: GithubCredentialsProvider; + token?: string; host: string; owner?: string; repo?: string; +}): Promise; + +/** + * Helper for generating octokit configuration options for given repoUrl. + * If no token is provided, it will attempt to get a token from the credentials provider. + * @public + * @deprecated Use options `host`, `owner` and `repo` instead of `repoUrl`. + */ +export async function getOctokitOptions(options: { + integrations: ScmIntegrationRegistry; + credentialsProvider?: GithubCredentialsProvider; token?: string; + repoUrl: string; +}): Promise; + +export async function getOctokitOptions(options: { + integrations: ScmIntegrationRegistry; + credentialsProvider?: GithubCredentialsProvider; + token?: string; + host?: string; + owner?: string; + repo?: string; + repoUrl?: string; }): Promise { - const { integrations, credentialsProvider, host, owner, repo, token } = - options; + const { integrations, credentialsProvider, token, repoUrl } = options; + const { host, owner, repo } = repoUrl + ? parseRepoUrl(repoUrl, integrations) + : options; const requestOptions = { // set timeout to 60 seconds timeout: DEFAULT_TIMEOUT_MS, }; - const integrationConfig = integrations.github.byHost(host)?.config; + const integrationConfig = integrations.github.byHost(host!)?.config; if (!integrationConfig) { throw new InputError(`No integration for host ${host}`); From 9e0bd0a7883b54f5815cc6c7bb03053e985fb6b7 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 4 Feb 2025 13:49:18 +0100 Subject: [PATCH 65/69] chore(scaffolder): improve GitHub scaffolder actions code Signed-off-by: Benjamin Janssens --- .../src/actions/github.ts | 8 ++++---- .../src/actions/githubBranchProtection.ts | 8 ++++---- .../src/actions/githubDeployKey.ts | 8 ++++---- .../src/actions/githubEnvironment.ts | 8 ++++---- .../src/actions/githubPagesEnable.ts | 8 ++++---- .../src/actions/githubRepoCreate.ts | 8 ++++---- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/plugins/scaffolder-backend-module-github/src/actions/github.ts b/plugins/scaffolder-backend-module-github/src/actions/github.ts index 4c04087670..6c47a2dade 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/github.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/github.ts @@ -227,6 +227,10 @@ export function createPublishGithubAction(options: { const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); + } + const octokitOptions = await getOctokitOptions({ integrations, credentialsProvider: githubCredentialsProvider, @@ -237,10 +241,6 @@ export function createPublishGithubAction(options: { }); const client = new Octokit(octokitOptions); - if (!owner) { - throw new InputError('Invalid repository owner provided in repoUrl'); - } - const newRepo = await createGithubRepoWithCollaboratorsAndTopics( client, repo, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts index 5d5366a7b1..6de447541b 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts @@ -117,6 +117,10 @@ export function createGithubBranchProtectionAction(options: { const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + if (!owner) { + throw new InputError(`No owner provided for repo ${repoUrl}`); + } + const octokitOptions = await getOctokitOptions({ integrations, token: providedToken, @@ -126,10 +130,6 @@ export function createGithubBranchProtectionAction(options: { }); const client = new Octokit(octokitOptions); - if (!owner) { - throw new InputError(`No owner provided for repo ${repoUrl}`); - } - const repository = await client.rest.repos.get({ owner: owner, repo: repo, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts index 6a7a2f86c6..a91ac8ba5e 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts @@ -109,6 +109,10 @@ export function createGithubDeployKeyAction(options: { const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + if (!owner) { + throw new InputError(`No owner provided for repo ${repoUrl}`); + } + const octokitOptions = await getOctokitOptions({ integrations, token: providedToken, @@ -117,10 +121,6 @@ export function createGithubDeployKeyAction(options: { repo, }); - if (!owner) { - throw new InputError(`No owner provided for repo ${repoUrl}`); - } - const client = new Octokit(octokitOptions); await client.rest.repos.createDeployKey({ diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts index 5d3817ee6d..49bd96db9d 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts @@ -177,6 +177,10 @@ export function createGithubEnvironmentAction(options: { const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + if (!owner) { + throw new InputError(`No owner provided for repo ${repoUrl}`); + } + const octokitOptions = await getOctokitOptions({ integrations, token: providedToken, @@ -185,10 +189,6 @@ export function createGithubEnvironmentAction(options: { repo, }); - if (!owner) { - throw new InputError(`No owner provided for repo ${repoUrl}`); - } - const client = new Octokit(octokitOptions); const repository = await client.rest.repos.get({ owner: owner, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts index 54bcfbcc43..9212f77338 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts @@ -94,6 +94,10 @@ export function createGithubPagesEnableAction(options: { const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); + } + const octokitOptions = await getOctokitOptions({ integrations, credentialsProvider: githubCredentialsProvider, @@ -104,10 +108,6 @@ export function createGithubPagesEnableAction(options: { }); const client = new Octokit(octokitOptions); - if (!owner) { - throw new InputError('Invalid repository owner provided in repoUrl'); - } - ctx.logger.info( `Attempting to enable GitHub Pages for ${owner}/${repo} with "${buildType}" build type, on source branch "${sourceBranch}" and source path "${sourcePath}"`, ); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts index b103dffb15..d32f7447a6 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts @@ -182,6 +182,10 @@ export function createGithubRepoCreateAction(options: { const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); + } + const octokitOptions = await getOctokitOptions({ integrations, credentialsProvider: githubCredentialsProvider, @@ -192,10 +196,6 @@ export function createGithubRepoCreateAction(options: { }); const client = new Octokit(octokitOptions); - if (!owner) { - throw new InputError('Invalid repository owner provided in repoUrl'); - } - const newRepo = await createGithubRepoWithCollaboratorsAndTopics( client, repo, From 5c187f9313510d55b9eba0b65fc18dd198cea44c Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 4 Feb 2025 14:08:58 +0100 Subject: [PATCH 66/69] chore(scaffolder): add changeset for getOctokitOptions function signature change Signed-off-by: Benjamin Janssens --- .changeset/fast-rabbits-unite.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .changeset/fast-rabbits-unite.md diff --git a/.changeset/fast-rabbits-unite.md b/.changeset/fast-rabbits-unite.md new file mode 100644 index 0000000000..f94e68449c --- /dev/null +++ b/.changeset/fast-rabbits-unite.md @@ -0,0 +1,17 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': patch +--- + +The `getOctokitOptions` function signature with `repoUrl` option has been deprecated in favour of a function signature with individual `host`, `owner`, and `repo` parameters: + +```diff + const octokitOptions = await getOctokitOptions({ + integrations, + credentialsProvider, + token, +- repoUrl, ++ host, ++ owner, ++ repo, + }); +``` From af0a23d8b4070e9baec6fb9a0bda94397db86dc3 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 4 Feb 2025 14:33:27 +0100 Subject: [PATCH 67/69] chore(scaffolder): update API reports Signed-off-by: Benjamin Janssens --- plugins/scaffolder-backend/report.api.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/report.api.md b/plugins/scaffolder-backend/report.api.md index 9d39f52d85..9bbe3cfec1 100644 --- a/plugins/scaffolder-backend/report.api.md +++ b/plugins/scaffolder-backend/report.api.md @@ -332,7 +332,11 @@ export const createPublishGithubPullRequestAction: ( commitMessage?: string | undefined; update?: boolean | undefined; forceFork?: boolean | undefined; - gitAuthorName?: string | undefined; + gitAuthorName?: string | undefined + /** + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-server` instead + */; gitAuthorEmail?: string | undefined; forceEmptyGitAuthor?: boolean | undefined; }, From 301ead8650a0cbbb093ad67e8932440838b82710 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 4 Feb 2025 14:42:44 +0100 Subject: [PATCH 68/69] chore: fixing issue with api-extractor reports Signed-off-by: blam --- plugins/scaffolder-backend/report.api.md | 66 +++++++------- .../src/scaffolder/actions/deprecated.ts | 87 +++++++++++-------- 2 files changed, 88 insertions(+), 65 deletions(-) diff --git a/plugins/scaffolder-backend/report.api.md b/plugins/scaffolder-backend/report.api.md index 9bbe3cfec1..8fe17d26f8 100644 --- a/plugins/scaffolder-backend/report.api.md +++ b/plugins/scaffolder-backend/report.api.md @@ -9,14 +9,27 @@ import { ActionContext as ActionContext_2 } from '@backstage/plugin-scaffolder-n import { AuditorService } from '@backstage/backend-plugin-api'; import { AuthService } from '@backstage/backend-plugin-api'; import { AutocompleteHandler } from '@backstage/plugin-scaffolder-node/alpha'; -import * as azure from '@backstage/plugin-scaffolder-backend-module-azure'; import { BackendFeature } from '@backstage/backend-plugin-api'; import { BackstageCredentials } from '@backstage/backend-plugin-api'; -import * as bitbucket from '@backstage/plugin-scaffolder-backend-module-bitbucket'; -import * as bitbucketCloud from '@backstage/plugin-scaffolder-backend-module-bitbucket-cloud'; -import * as bitbucketServer from '@backstage/plugin-scaffolder-backend-module-bitbucket-server'; import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; +import { createGithubActionsDispatchAction as createGithubActionsDispatchAction_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { createGithubDeployKeyAction as createGithubDeployKeyAction_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { createGithubEnvironmentAction as createGithubEnvironmentAction_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { createGithubIssuesLabelAction as createGithubIssuesLabelAction_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { CreateGithubPullRequestActionOptions as CreateGithubPullRequestActionOptions_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { createGithubRepoCreateAction as createGithubRepoCreateAction_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { createGithubRepoPushAction as createGithubRepoPushAction_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { createGithubWebhookAction as createGithubWebhookAction_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { createPublishAzureAction as createPublishAzureAction_2 } from '@backstage/plugin-scaffolder-backend-module-azure'; +import { createPublishBitbucketAction as createPublishBitbucketAction_2 } from '@backstage/plugin-scaffolder-backend-module-bitbucket'; +import { createPublishBitbucketCloudAction as createPublishBitbucketCloudAction_2 } from '@backstage/plugin-scaffolder-backend-module-bitbucket-cloud'; +import { createPublishBitbucketServerAction as createPublishBitbucketServerAction_2 } from '@backstage/plugin-scaffolder-backend-module-bitbucket-server'; +import { createPublishBitbucketServerPullRequestAction as createPublishBitbucketServerPullRequestAction_2 } from '@backstage/plugin-scaffolder-backend-module-bitbucket-server'; +import { createPublishGerritAction as createPublishGerritAction_2 } from '@backstage/plugin-scaffolder-backend-module-gerrit'; +import { createPublishGerritReviewAction as createPublishGerritReviewAction_2 } from '@backstage/plugin-scaffolder-backend-module-gerrit'; +import { createPublishGithubAction as createPublishGithubAction_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { createPublishGitlabAction as createPublishGitlabAction_2 } from '@backstage/plugin-scaffolder-backend-module-gitlab'; import { DatabaseService } from '@backstage/backend-plugin-api'; import { DiscoveryService } from '@backstage/backend-plugin-api'; import { Duration } from 'luxon'; @@ -25,9 +38,6 @@ import { executeShellCommand as executeShellCommand_2 } from '@backstage/plugin- import { ExecuteShellCommandOptions } from '@backstage/plugin-scaffolder-node'; import express from 'express'; import { fetchContents as fetchContents_2 } from '@backstage/plugin-scaffolder-node'; -import * as gerrit from '@backstage/plugin-scaffolder-backend-module-gerrit'; -import * as github from '@backstage/plugin-scaffolder-backend-module-github'; -import * as gitlab from '@backstage/plugin-scaffolder-backend-module-gitlab'; import { HttpAuthService } from '@backstage/backend-plugin-api'; import { HumanDuration } from '@backstage/types'; import { IdentityApi } from '@backstage/plugin-auth-node'; @@ -265,57 +275,57 @@ export const createFilesystemRenameAction: () => TemplateAction_2< >; // @public @deprecated (undocumented) -export const createGithubActionsDispatchAction: typeof github.createGithubActionsDispatchAction; +export const createGithubActionsDispatchAction: typeof createGithubActionsDispatchAction_2; // @public @deprecated (undocumented) -export const createGithubDeployKeyAction: typeof github.createGithubDeployKeyAction; +export const createGithubDeployKeyAction: typeof createGithubDeployKeyAction_2; // @public @deprecated (undocumented) -export const createGithubEnvironmentAction: typeof github.createGithubEnvironmentAction; +export const createGithubEnvironmentAction: typeof createGithubEnvironmentAction_2; // @public @deprecated (undocumented) -export const createGithubIssuesLabelAction: typeof github.createGithubIssuesLabelAction; +export const createGithubIssuesLabelAction: typeof createGithubIssuesLabelAction_2; // @public @deprecated (undocumented) export type CreateGithubPullRequestActionOptions = - github.CreateGithubPullRequestActionOptions; + CreateGithubPullRequestActionOptions_2; // @public @deprecated (undocumented) -export const createGithubRepoCreateAction: typeof github.createGithubRepoCreateAction; +export const createGithubRepoCreateAction: typeof createGithubRepoCreateAction_2; // @public @deprecated (undocumented) -export const createGithubRepoPushAction: typeof github.createGithubRepoPushAction; +export const createGithubRepoPushAction: typeof createGithubRepoPushAction_2; // @public @deprecated (undocumented) -export const createGithubWebhookAction: typeof github.createGithubWebhookAction; +export const createGithubWebhookAction: typeof createGithubWebhookAction_2; // @public @deprecated (undocumented) -export const createPublishAzureAction: typeof azure.createPublishAzureAction; +export const createPublishAzureAction: typeof createPublishAzureAction_2; // @public @deprecated (undocumented) -export const createPublishBitbucketAction: typeof bitbucket.createPublishBitbucketAction; +export const createPublishBitbucketAction: typeof createPublishBitbucketAction_2; // @public @deprecated (undocumented) -export const createPublishBitbucketCloudAction: typeof bitbucketCloud.createPublishBitbucketCloudAction; +export const createPublishBitbucketCloudAction: typeof createPublishBitbucketCloudAction_2; // @public @deprecated (undocumented) -export const createPublishBitbucketServerAction: typeof bitbucketServer.createPublishBitbucketServerAction; +export const createPublishBitbucketServerAction: typeof createPublishBitbucketServerAction_2; // @public @deprecated (undocumented) -export const createPublishBitbucketServerPullRequestAction: typeof bitbucketServer.createPublishBitbucketServerPullRequestAction; +export const createPublishBitbucketServerPullRequestAction: typeof createPublishBitbucketServerPullRequestAction_2; // @public @deprecated (undocumented) -export const createPublishGerritAction: typeof gerrit.createPublishGerritAction; +export const createPublishGerritAction: typeof createPublishGerritAction_2; // @public @deprecated (undocumented) -export const createPublishGerritReviewAction: typeof gerrit.createPublishGerritReviewAction; +export const createPublishGerritReviewAction: typeof createPublishGerritReviewAction_2; // @public @deprecated (undocumented) -export const createPublishGithubAction: typeof github.createPublishGithubAction; +export const createPublishGithubAction: typeof createPublishGithubAction_2; // @public @deprecated (undocumented) export const createPublishGithubPullRequestAction: ( - options: github.CreateGithubPullRequestActionOptions, + options: CreateGithubPullRequestActionOptions_2, ) => TemplateAction_2< { title: string; @@ -332,11 +342,7 @@ export const createPublishGithubPullRequestAction: ( commitMessage?: string | undefined; update?: boolean | undefined; forceFork?: boolean | undefined; - gitAuthorName?: string | undefined - /** - * @public - * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-server` instead - */; + gitAuthorName?: string | undefined; gitAuthorEmail?: string | undefined; forceEmptyGitAuthor?: boolean | undefined; }, @@ -344,7 +350,7 @@ export const createPublishGithubPullRequestAction: ( >; // @public @deprecated (undocumented) -export const createPublishGitlabAction: typeof gitlab.createPublishGitlabAction; +export const createPublishGitlabAction: typeof createPublishGitlabAction_2; // @public @deprecated (undocumented) export const createPublishGitlabMergeRequestAction: (options: { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/deprecated.ts b/plugins/scaffolder-backend/src/scaffolder/actions/deprecated.ts index 5d037b44b6..324e9b43e1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/deprecated.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/deprecated.ts @@ -13,134 +13,151 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import * as github from '@backstage/plugin-scaffolder-backend-module-github'; -import * as gitlab from '@backstage/plugin-scaffolder-backend-module-gitlab'; -import * as azure from '@backstage/plugin-scaffolder-backend-module-azure'; -import * as bitbucket from '@backstage/plugin-scaffolder-backend-module-bitbucket'; -import * as bitbucketCloud from '@backstage/plugin-scaffolder-backend-module-bitbucket-cloud'; -import * as bitbucketServer from '@backstage/plugin-scaffolder-backend-module-bitbucket-server'; -import * as gerrit from '@backstage/plugin-scaffolder-backend-module-gerrit'; +import { + createGithubActionsDispatchAction as githubActionsDispatch, + createGithubDeployKeyAction as githubDeployKey, + createGithubEnvironmentAction as githubEnvironment, + createGithubIssuesLabelAction as githubIssuesLabel, + CreateGithubPullRequestActionOptions as GithubPullRequestActionOptions, + createGithubRepoCreateAction as githubRepoCreate, + createGithubRepoPushAction as githubRepoPush, + createGithubWebhookAction as githubWebhook, + createPublishGithubAction as publishGithub, + createPublishGithubPullRequestAction as publishGithubPullRequest, +} from '@backstage/plugin-scaffolder-backend-module-github'; + +import { + createPublishGitlabAction as publishGitlab, + createPublishGitlabMergeRequestAction as publishGitlabMergeRequest, +} from '@backstage/plugin-scaffolder-backend-module-gitlab'; + +import { createPublishAzureAction as publishAzure } from '@backstage/plugin-scaffolder-backend-module-azure'; + +import { createPublishBitbucketAction as publishBitbucket } from '@backstage/plugin-scaffolder-backend-module-bitbucket'; + +import { createPublishBitbucketCloudAction as publishBitbucketCloud } from '@backstage/plugin-scaffolder-backend-module-bitbucket-cloud'; + +import { + createPublishBitbucketServerAction as publishBitbucketServer, + createPublishBitbucketServerPullRequestAction as publishBitbucketServerPullRequest, +} from '@backstage/plugin-scaffolder-backend-module-bitbucket-server'; + +import { + createPublishGerritAction as publishGerrit, + createPublishGerritReviewAction as publishGerritReview, +} from '@backstage/plugin-scaffolder-backend-module-gerrit'; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createGithubActionsDispatchAction = - github.createGithubActionsDispatchAction; +export const createGithubActionsDispatchAction = githubActionsDispatch; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createGithubDeployKeyAction = github.createGithubDeployKeyAction; +export const createGithubDeployKeyAction = githubDeployKey; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createGithubEnvironmentAction = - github.createGithubEnvironmentAction; +export const createGithubEnvironmentAction = githubEnvironment; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createGithubIssuesLabelAction = - github.createGithubIssuesLabelAction; +export const createGithubIssuesLabelAction = githubIssuesLabel; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ export type CreateGithubPullRequestActionOptions = - github.CreateGithubPullRequestActionOptions; + GithubPullRequestActionOptions; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createGithubRepoCreateAction = github.createGithubRepoCreateAction; +export const createGithubRepoCreateAction = githubRepoCreate; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createGithubRepoPushAction = github.createGithubRepoPushAction; +export const createGithubRepoPushAction = githubRepoPush; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createGithubWebhookAction = github.createGithubWebhookAction; +export const createGithubWebhookAction = githubWebhook; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createPublishGithubAction = github.createPublishGithubAction; +export const createPublishGithubAction = publishGithub; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createPublishGithubPullRequestAction = - github.createPublishGithubPullRequestAction; +export const createPublishGithubPullRequestAction = publishGithubPullRequest; /** * @public @deprecated use "createPublishBitbucketCloudAction" from \@backstage/plugin-scaffolder-backend-module-bitbucket-cloud or "createPublishBitbucketServerAction" from \@backstage/plugin-scaffolder-backend-module-bitbucket-server instead */ -export const createPublishBitbucketAction = - bitbucket.createPublishBitbucketAction; +export const createPublishBitbucketAction = publishBitbucket; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-cloud` instead */ -export const createPublishBitbucketCloudAction = - bitbucketCloud.createPublishBitbucketCloudAction; +export const createPublishBitbucketCloudAction = publishBitbucketCloud; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-server` instead */ -export const createPublishBitbucketServerAction = - bitbucketServer.createPublishBitbucketServerAction; +export const createPublishBitbucketServerAction = publishBitbucketServer; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-server` instead */ export const createPublishBitbucketServerPullRequestAction = - bitbucketServer.createPublishBitbucketServerPullRequestAction; + publishBitbucketServerPullRequest; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-azure` instead */ -export const createPublishAzureAction = azure.createPublishAzureAction; +export const createPublishAzureAction = publishAzure; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-gerrit` instead */ -export const createPublishGerritAction = gerrit.createPublishGerritAction; +export const createPublishGerritAction = publishGerrit; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-gerrit` instead */ -export const createPublishGerritReviewAction = - gerrit.createPublishGerritReviewAction; +export const createPublishGerritReviewAction = publishGerritReview; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-gitlab` instead */ -export const createPublishGitlabAction = gitlab.createPublishGitlabAction; +export const createPublishGitlabAction = publishGitlab; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-gitlab` instead */ -export const createPublishGitlabMergeRequestAction = - gitlab.createPublishGitlabMergeRequestAction; +export const createPublishGitlabMergeRequestAction = publishGitlabMergeRequest; From 347835413c886a30279141f292348294ac95630b Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 4 Feb 2025 14:52:34 +0100 Subject: [PATCH 69/69] chore: added deprecation prefix Signed-off-by: blam --- .changeset/fast-rabbits-unite.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fast-rabbits-unite.md b/.changeset/fast-rabbits-unite.md index f94e68449c..f4686b32cc 100644 --- a/.changeset/fast-rabbits-unite.md +++ b/.changeset/fast-rabbits-unite.md @@ -2,7 +2,7 @@ '@backstage/plugin-scaffolder-backend-module-github': patch --- -The `getOctokitOptions` function signature with `repoUrl` option has been deprecated in favour of a function signature with individual `host`, `owner`, and `repo` parameters: +**DEPRECATION**: The `getOctokitOptions` function signature with `repoUrl` option has been deprecated in favour of a function signature with individual `host`, `owner`, and `repo` parameters: ```diff const octokitOptions = await getOctokitOptions({