From e5c2ce9aa0be3a0e73fe585261153f0c382e4620 Mon Sep 17 00:00:00 2001 From: Aditya Kumar Date: Tue, 21 May 2024 08:49:09 +0530 Subject: [PATCH 01/21] Updated the backend-system and frontend-system and plugins documents Signed-off-by: Aditya Kumar --- docs/backend-system/architecture/01-index.md | 6 +++++- docs/backend-system/architecture/03-services.md | 6 +++++- .../building-plugins-and-modules/01-index.md | 8 ++++++-- docs/frontend-system/building-plugins/02-testing.md | 6 +++++- docs/plugins/backend-plugin.md | 6 +++++- 5 files changed, 26 insertions(+), 6 deletions(-) diff --git a/docs/backend-system/architecture/01-index.md b/docs/backend-system/architecture/01-index.md index 681d0fada2..968ff2f001 100644 --- a/docs/backend-system/architecture/01-index.md +++ b/docs/backend-system/architecture/01-index.md @@ -19,7 +19,11 @@ The diagram below provides an overview of the different building blocks, and the ![backend system building blocks diagram](../../assets/backend-system/architecture-building-blocks.drawio.svg) -> NOTE: These are all concepts that existed in our old backend system in one way or another, but they have now all been lifted up to be first class concerns. +:::note Note + +These are all concepts that existed in our old backend system in one way or another, but they have now all been lifted up to be first class concerns. + +::: ### Backend diff --git a/docs/backend-system/architecture/03-services.md b/docs/backend-system/architecture/03-services.md index 573f6612c2..7986dc4064 100644 --- a/docs/backend-system/architecture/03-services.md +++ b/docs/backend-system/architecture/03-services.md @@ -192,7 +192,11 @@ When defining a default factory for a service, it is possible for it to end up w ## Service Factory Options -> NOTE: This pattern is discouraged, only use it when necessary. If possible you should prefer to make services configurable via static configuration instead. +:::note Note + +This pattern is discouraged, only use it when necessary. If possible you should prefer to make services configurable via static configuration instead. + +::: When declaring a service factory it's possible to include an options callback. This allows you to customize the factory through code when installing it in the backend. For example, this is how you install an explicit factory instance in the backend without any options: diff --git a/docs/backend-system/building-plugins-and-modules/01-index.md b/docs/backend-system/building-plugins-and-modules/01-index.md index 1aa8b140ed..379ce19587 100644 --- a/docs/backend-system/building-plugins-and-modules/01-index.md +++ b/docs/backend-system/building-plugins-and-modules/01-index.md @@ -6,8 +6,12 @@ sidebar_label: Overview description: Building backend plugins and modules using the new backend system --- -> NOTE: If you have an existing backend and/or backend plugins that are not yet -> using the new backend system, see [migrating](./08-migrating.md). +:::note Note + +If you have an existing backend and/or backend plugins that are not yet +using the new backend system, see [migrating](./08-migrating.md). + +::: This section covers how to build your own backend [plugins](../architecture/04-plugins.md) and [modules](../architecture/06-modules.md). They are sometimes collectively referred to as diff --git a/docs/frontend-system/building-plugins/02-testing.md b/docs/frontend-system/building-plugins/02-testing.md index fa29918311..26c9503c6d 100644 --- a/docs/frontend-system/building-plugins/02-testing.md +++ b/docs/frontend-system/building-plugins/02-testing.md @@ -10,7 +10,11 @@ description: Testing plugins in the frontend system # Testing Frontend Plugins -> NOTE: The new frontend system is in alpha, and some plugins do not yet fully implement it. +:::note Note + +The new frontend system is in alpha, and some plugins do not yet fully implement it. + +::: Utilities for testing frontend features and components are available in `@backstage/frontend-test-utils`. diff --git a/docs/plugins/backend-plugin.md b/docs/plugins/backend-plugin.md index 7a51cb635c..255ab4f865 100644 --- a/docs/plugins/backend-plugin.md +++ b/docs/plugins/backend-plugin.md @@ -44,7 +44,11 @@ cd plugins/carmen-backend yarn start ``` -> Note: this documentation assumes you are using the latest version of Backstage and the new backend system. If you are not, please upgrade and migrate your backend using the [Migration Guide](../backend-system/building-backends/08-migrating.md) +:::note Note + +This documentation assumes you are using the latest version of Backstage and the new backend system. If you are not, please upgrade and migrate your backend using the [Migration Guide](../backend-system/building-backends/08-migrating.md) + +::: This will think for a bit, and then say `Listening on :7007`. In a different terminal window, now run From 159399107fb8ba4d0e0544ef1603b1a6220d9a61 Mon Sep 17 00:00:00 2001 From: Alex Eftimie Date: Wed, 22 May 2024 09:31:11 +0200 Subject: [PATCH 02/21] Try to switch away from polyfill Signed-off-by: Alex Eftimie --- plugins/techdocs/package.json | 1 + plugins/techdocs/src/alpha.tsx | 5 +-- plugins/techdocs/src/client.ts | 67 +++++++++++----------------------- yarn.lock | 8 ++++ 4 files changed, 31 insertions(+), 50 deletions(-) diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 845b65cd8f..22f3074bea 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -70,6 +70,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", "@material-ui/styles": "^4.10.0", + "@microsoft/fetch-event-source": "^2.0.1", "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", "dompurify": "^3.0.0", "event-source-polyfill": "1.0.25", diff --git a/plugins/techdocs/src/alpha.tsx b/plugins/techdocs/src/alpha.tsx index 88dce3b48e..07fd7667ff 100644 --- a/plugins/techdocs/src/alpha.tsx +++ b/plugins/techdocs/src/alpha.tsx @@ -29,7 +29,6 @@ import { createApiFactory, discoveryApiRef, fetchApiRef, - identityApiRef, } from '@backstage/core-plugin-api'; import { compatWrapper, @@ -55,14 +54,12 @@ const techDocsStorageApi = createApiExtension({ deps: { configApi: configApiRef, discoveryApi: discoveryApiRef, - identityApi: identityApiRef, fetchApi: fetchApiRef, }, - factory: ({ configApi, discoveryApi, identityApi, fetchApi }) => + factory: ({ configApi, discoveryApi, fetchApi }) => new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }), }), diff --git a/plugins/techdocs/src/client.ts b/plugins/techdocs/src/client.ts index 0f11eb4068..76338a8f1e 100644 --- a/plugins/techdocs/src/client.ts +++ b/plugins/techdocs/src/client.ts @@ -16,11 +16,7 @@ import { CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { - DiscoveryApi, - FetchApi, - IdentityApi, -} from '@backstage/core-plugin-api'; +import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api'; import { NotFoundError, ResponseError } from '@backstage/errors'; import { SyncResult, @@ -29,7 +25,7 @@ import { TechDocsMetadata, TechDocsStorageApi, } from '@backstage/plugin-techdocs-react'; -import { EventSourcePolyfill } from 'event-source-polyfill'; +import { fetchEventSource } from '@microsoft/fetch-event-source'; /** * API to talk to `techdocs-backend`. @@ -124,18 +120,15 @@ export class TechDocsClient implements TechDocsApi { export class TechDocsStorageClient implements TechDocsStorageApi { public configApi: Config; public discoveryApi: DiscoveryApi; - public identityApi: IdentityApi; private fetchApi: FetchApi; constructor(options: { configApi: Config; discoveryApi: DiscoveryApi; - identityApi: IdentityApi; fetchApi: FetchApi; }) { this.configApi = options.configApi; this.discoveryApi = options.discoveryApi; - this.identityApi = options.identityApi; this.fetchApi = options.fetchApi; } @@ -213,47 +206,29 @@ export class TechDocsStorageClient implements TechDocsStorageApi { const apiOrigin = await this.getApiOrigin(); const url = `${apiOrigin}/sync/${namespace}/${kind}/${name}`; - const { token } = await this.identityApi.getCredentials(); return new Promise((resolve, reject) => { - // Polyfill is used to add support for custom headers and auth - const source = new EventSourcePolyfill(url, { + fetchEventSource(url, { withCredentials: true, - headers: token ? { Authorization: `Bearer ${token}` } : {}, + fetch: this.fetchApi.fetch, + onmessage(e: any) { + if (e.data) { + logHandler(JSON.parse(e.data)); + } + }, + onclose(e: any) { + let updated: boolean = false; + + if (e.data) { + ({ updated } = JSON.parse(e.data)); + } + + resolve(updated ? 'updated' : 'cached'); + }, + onerror(e: any) { + reject(e); + }, }); - - source.addEventListener('log', (e: any) => { - if (e.data) { - logHandler(JSON.parse(e.data)); - } - }); - - source.addEventListener('finish', (e: any) => { - let updated: boolean = false; - - if (e.data) { - ({ updated } = JSON.parse(e.data)); - } - - resolve(updated ? 'updated' : 'cached'); - }); - - source.onerror = (e: any) => { - source.close(); - - switch (e.status) { - // the endpoint returned a 404 status - case 404: - reject(new NotFoundError(e.message)); - return; - - // also handles the event-stream close. the reject is ignored if the Promise was already - // resolved by a finish event. - default: - reject(new Error(e.data)); - return; - } - }; }); } diff --git a/yarn.lock b/yarn.lock index d006daa23c..b70da3e894 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7535,6 +7535,7 @@ __metadata: "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.61 "@material-ui/styles": ^4.10.0 + "@microsoft/fetch-event-source": ^2.0.1 "@testing-library/dom": ^10.0.0 "@testing-library/jest-dom": ^6.0.0 "@testing-library/react": ^15.0.0 @@ -10507,6 +10508,13 @@ __metadata: languageName: node linkType: hard +"@microsoft/fetch-event-source@npm:^2.0.1": + version: 2.0.1 + resolution: "@microsoft/fetch-event-source@npm:2.0.1" + checksum: a50e1c0f33220206967266d0a4bbba0703e2793b079d9f6e6bfd48f71b2115964a803e14cf6e902c6fab321edc084f26022334f5eaacc2cec87f174715d41852 + languageName: node + linkType: hard + "@microsoft/microsoft-graph-types@npm:^2.6.0": version: 2.40.0 resolution: "@microsoft/microsoft-graph-types@npm:2.40.0" From f1c280b3a27e64ad0e9702d0846871f47f88dd8e Mon Sep 17 00:00:00 2001 From: Alex Eftimie Date: Wed, 22 May 2024 11:37:18 +0200 Subject: [PATCH 03/21] Update unit tests Signed-off-by: Alex Eftimie --- plugins/techdocs/src/client.test.ts | 150 ++++++++++------------------ plugins/techdocs/src/client.ts | 24 ++--- 2 files changed, 64 insertions(+), 110 deletions(-) diff --git a/plugins/techdocs/src/client.test.ts b/plugins/techdocs/src/client.test.ts index dde52cda49..788d7e3fed 100644 --- a/plugins/techdocs/src/client.test.ts +++ b/plugins/techdocs/src/client.test.ts @@ -17,16 +17,15 @@ import { UrlPatternDiscovery } from '@backstage/core-app-api'; import { IdentityApi } from '@backstage/core-plugin-api'; import { NotFoundError } from '@backstage/errors'; -import { EventSourcePolyfill } from 'event-source-polyfill'; +import { fetchEventSource } from '@microsoft/fetch-event-source'; import { MockConfigApi, MockFetchApi } from '@backstage/test-utils'; import { TechDocsStorageClient } from './client'; -const MockedEventSource = EventSourcePolyfill as jest.MockedClass< - typeof EventSourcePolyfill +jest.mock('@microsoft/fetch-event-source'); +const mockFetchEventSource = fetchEventSource as jest.MockedFunction< + typeof fetchEventSource >; -jest.mock('event-source-polyfill'); - const mockEntity = { kind: 'Component', namespace: 'default', @@ -51,7 +50,6 @@ describe('TechDocsStorageClient', () => { const storageApi = new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }); @@ -72,7 +70,6 @@ describe('TechDocsStorageClient', () => { const storageApi = new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }); @@ -84,71 +81,49 @@ describe('TechDocsStorageClient', () => { }); describe('syncEntityDocs', () => { - it('should create eventsource without headers', async () => { + it('should create eventsource with fetch', async () => { const storageApi = new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }); - MockedEventSource.prototype.addEventListener.mockImplementation( - (type, fn) => { - if (type === 'finish' && typeof fn === 'function') { - fn({ data: '{"updated": false}' } as any); - } - }, - ); - + mockFetchEventSource.mockImplementation(async (_url, options) => { + const { onopen, onmessage } = options; + await Promise.resolve(); + await onopen?.({ ok: true } as Response); + await Promise.resolve(); + onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' }); + }); identityApi.getCredentials.mockResolvedValue({}); await storageApi.syncEntityDocs(mockEntity); - expect(MockedEventSource).toHaveBeenCalledWith( + expect(mockFetchEventSource).toHaveBeenCalledWith( 'http://backstage:9191/api/techdocs/sync/default/Component/test-component', - { withCredentials: true, headers: {} }, - ); - }); - - it('should create eventsource with headers', async () => { - const storageApi = new TechDocsStorageClient({ - configApi, - discoveryApi, - identityApi, - fetchApi, - }); - - MockedEventSource.prototype.addEventListener.mockImplementation( - (type, fn) => { - if (type === 'finish' && typeof fn === 'function') { - fn({ data: '{"updated": false}' } as any); - } + { + fetch: fetchApi.fetch, + onerror: expect.any(Function), + onmessage: expect.any(Function), }, ); - - identityApi.getCredentials.mockResolvedValue({ token: 'token' }); - await storageApi.syncEntityDocs(mockEntity); - - expect(MockedEventSource).toHaveBeenCalledWith( - 'http://backstage:9191/api/techdocs/sync/default/Component/test-component', - { withCredentials: true, headers: { Authorization: 'Bearer token' } }, - ); }); it('should resolve to cached', async () => { + const myFetchApi = new MockFetchApi({ + injectIdentityAuth: { identityApi }, + }); const storageApi = new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, - fetchApi, + fetchApi: myFetchApi, + }); + mockFetchEventSource.mockImplementation(async (_url, options) => { + const { onopen, onmessage } = options; + await Promise.resolve(); + await onopen?.({ ok: true } as Response); + await Promise.resolve(); + onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' }); }); - - MockedEventSource.prototype.addEventListener.mockImplementation( - (type, fn) => { - if (type === 'finish' && typeof fn === 'function') { - fn({ data: '{"updated": false}' } as any); - } - }, - ); identityApi.getCredentials.mockResolvedValue({}); await expect(storageApi.syncEntityDocs(mockEntity)).resolves.toEqual( @@ -160,17 +135,15 @@ describe('TechDocsStorageClient', () => { const storageApi = new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }); - - MockedEventSource.prototype.addEventListener.mockImplementation( - (type, fn) => { - if (type === 'finish' && typeof fn === 'function') { - fn({ data: '{"updated": true}' } as any); - } - }, - ); + mockFetchEventSource.mockImplementation(async (_url, options) => { + const { onopen, onmessage } = options; + await Promise.resolve(); + await onopen?.({ ok: true } as Response); + await Promise.resolve(); + onmessage?.({ id: '', event: 'finish', data: '{"updated": true}' }); + }); identityApi.getCredentials.mockResolvedValue({}); await expect(storageApi.syncEntityDocs(mockEntity)).resolves.toEqual( @@ -182,21 +155,18 @@ describe('TechDocsStorageClient', () => { const storageApi = new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }); - MockedEventSource.prototype.addEventListener.mockImplementation( - (type, fn) => { - if (type === 'log' && typeof fn === 'function') { - fn({ data: '"A log message"' } as any); - } - - if (type === 'finish' && typeof fn === 'function') { - fn({ data: '{"updated": false}' } as any); - } - }, - ); + mockFetchEventSource.mockImplementation(async (_url, options) => { + const { onopen, onmessage } = options; + await Promise.resolve(); + await onopen?.({ ok: true } as Response); + await Promise.resolve(); + onmessage?.({ id: '', event: 'log', data: '"A log message"' }); + await Promise.resolve(); + onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' }); + }); identityApi.getCredentials.mockResolvedValue({}); const logHandler = jest.fn(); @@ -212,25 +182,18 @@ describe('TechDocsStorageClient', () => { const storageApi = new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }); + mockFetchEventSource.mockImplementation(async (_url, options) => { + const { onerror } = options; + onerror?.(new NotFoundError('Some not found warning')); + }); + // we await later after we emitted the error identityApi.getCredentials.mockResolvedValue({}); const promise = storageApi.syncEntityDocs(mockEntity).then(); - // flush the event loop - await new Promise(r => setTimeout(r)); - - const instance = MockedEventSource.mock - .instances[0] as jest.Mocked; - - instance.onerror?.({ - status: 404, - message: 'Some not found warning', - } as any); - await expect(promise).rejects.toThrow(NotFoundError); await expect(promise).rejects.toThrow('Some not found warning'); }); @@ -239,7 +202,6 @@ describe('TechDocsStorageClient', () => { const storageApi = new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }); @@ -247,16 +209,10 @@ describe('TechDocsStorageClient', () => { identityApi.getCredentials.mockResolvedValue({}); const promise = storageApi.syncEntityDocs(mockEntity).then(); - // flush the event loop - await new Promise(r => setTimeout(r)); - - const instance = MockedEventSource.mock - .instances[0] as jest.Mocked; - - instance.onerror?.({ - type: 'error', - data: 'Some other error', - } as any); + mockFetchEventSource.mockImplementation(async (_url, options) => { + const { onerror } = options; + onerror?.(new Error('Some other error')); + }); await expect(promise).rejects.toThrow(Error); await expect(promise).rejects.toThrow('Some other error'); diff --git a/plugins/techdocs/src/client.ts b/plugins/techdocs/src/client.ts index 76338a8f1e..b79c140750 100644 --- a/plugins/techdocs/src/client.ts +++ b/plugins/techdocs/src/client.ts @@ -209,24 +209,22 @@ export class TechDocsStorageClient implements TechDocsStorageApi { return new Promise((resolve, reject) => { fetchEventSource(url, { - withCredentials: true, fetch: this.fetchApi.fetch, onmessage(e: any) { - if (e.data) { + if (e.event === 'log') { logHandler(JSON.parse(e.data)); + } else if (e.event === 'finish') { + let updated: boolean = false; + if (e.data) { + ({ updated } = JSON.parse(e.data)); + } + resolve(updated ? 'updated' : 'cached'); + } else if (e.event === 'error') { + reject(new Error(e.data)); } }, - onclose(e: any) { - let updated: boolean = false; - - if (e.data) { - ({ updated } = JSON.parse(e.data)); - } - - resolve(updated ? 'updated' : 'cached'); - }, - onerror(e: any) { - reject(e); + onerror(err) { + reject(err); }, }); }); From db76476e6215d0c812b0ecbf077ed1e71cb2733a Mon Sep 17 00:00:00 2001 From: Alex Eftimie Date: Wed, 22 May 2024 11:45:06 +0200 Subject: [PATCH 04/21] Cleanup and fix build Signed-off-by: Alex Eftimie --- plugins/scaffolder/package.json | 1 + plugins/techdocs/dev/index.tsx | 7 +---- plugins/techdocs/package.json | 2 -- .../techdocs/src/event-source-polyfill.d.ts | 27 ------------------- plugins/techdocs/src/plugin.ts | 5 +--- yarn.lock | 10 +------ 6 files changed, 4 insertions(+), 48 deletions(-) delete mode 100644 plugins/techdocs/src/event-source-polyfill.d.ts diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 73e231644a..e4ba300d27 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -103,6 +103,7 @@ "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^15.0.0", "@testing-library/user-event": "^14.0.0", + "@types/event-source-polyfill": "^1.0.0", "@types/humanize-duration": "^3.18.1", "@types/json-schema": "^7.0.9", "msw": "^1.0.0" diff --git a/plugins/techdocs/dev/index.tsx b/plugins/techdocs/dev/index.tsx index 9cf0364c63..57ed36c4e3 100644 --- a/plugins/techdocs/dev/index.tsx +++ b/plugins/techdocs/dev/index.tsx @@ -25,11 +25,7 @@ import { techdocsStorageApiRef, } from '../src'; -import { - configApiRef, - discoveryApiRef, - identityApiRef, -} from '@backstage/core-plugin-api'; +import { configApiRef, discoveryApiRef } from '@backstage/core-plugin-api'; import { TechDocsReaderPageProvider } from '@backstage/plugin-techdocs-react'; import { Header, Page, TabbedLayout } from '@backstage/core-components'; @@ -135,7 +131,6 @@ createDevApp() deps: { configApi: configApiRef, discoveryApi: discoveryApiRef, - identityApi: identityApiRef, }, factory: () => apiBridge, }) diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 22f3074bea..c0113c0c43 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -73,7 +73,6 @@ "@microsoft/fetch-event-source": "^2.0.1", "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", "dompurify": "^3.0.0", - "event-source-polyfill": "1.0.25", "git-url-parse": "^14.0.0", "jss": "~10.10.0", "lodash": "^4.17.21", @@ -91,7 +90,6 @@ "@testing-library/react": "^15.0.0", "@testing-library/user-event": "^14.0.0", "@types/dompurify": "^3.0.0", - "@types/event-source-polyfill": "^1.0.0", "canvas": "^2.10.2" }, "peerDependencies": { diff --git a/plugins/techdocs/src/event-source-polyfill.d.ts b/plugins/techdocs/src/event-source-polyfill.d.ts deleted file mode 100644 index 8e298bde38..0000000000 --- a/plugins/techdocs/src/event-source-polyfill.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2021 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. - */ - -declare module 'event-source-polyfill' { - export class EventSourcePolyfill extends EventSource { - constructor( - url: string, - options?: { - withCredentials?: boolean; - headers?: HeadersInit; - }, - ); - } -} diff --git a/plugins/techdocs/src/plugin.ts b/plugins/techdocs/src/plugin.ts index bdd4814898..efa6bdfc1f 100644 --- a/plugins/techdocs/src/plugin.ts +++ b/plugins/techdocs/src/plugin.ts @@ -31,7 +31,6 @@ import { createRoutableExtension, discoveryApiRef, fetchApiRef, - identityApiRef, } from '@backstage/core-plugin-api'; import { createSearchResultListItemExtension, @@ -52,14 +51,12 @@ export const techdocsPlugin = createPlugin({ deps: { configApi: configApiRef, discoveryApi: discoveryApiRef, - identityApi: identityApiRef, fetchApi: fetchApiRef, }, - factory: ({ configApi, discoveryApi, identityApi, fetchApi }) => + factory: ({ configApi, discoveryApi, fetchApi }) => new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }), }), diff --git a/yarn.lock b/yarn.lock index b70da3e894..052268febb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6967,6 +6967,7 @@ __metadata: "@testing-library/jest-dom": ^6.0.0 "@testing-library/react": ^15.0.0 "@testing-library/user-event": ^14.0.0 + "@types/event-source-polyfill": ^1.0.0 "@types/humanize-duration": ^3.18.1 "@types/json-schema": ^7.0.9 "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 @@ -7541,11 +7542,9 @@ __metadata: "@testing-library/react": ^15.0.0 "@testing-library/user-event": ^14.0.0 "@types/dompurify": ^3.0.0 - "@types/event-source-polyfill": ^1.0.0 "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 canvas: ^2.10.2 dompurify: ^3.0.0 - event-source-polyfill: 1.0.25 git-url-parse: ^14.0.0 jss: ~10.10.0 lodash: ^4.17.21 @@ -25499,13 +25498,6 @@ __metadata: languageName: node linkType: hard -"event-source-polyfill@npm:1.0.25": - version: 1.0.25 - resolution: "event-source-polyfill@npm:1.0.25" - checksum: ed30428cc80eadfd693d267ba4a72dceaae938174cd116081ce38ad62bfd95f199430be7e8341e6f8f1e29489bbd5cfd4b3f6c8d6d463435623f7f91ae5f71b1 - languageName: node - linkType: hard - "event-source-polyfill@npm:^1.0.31": version: 1.0.31 resolution: "event-source-polyfill@npm:1.0.31" From 147f11f08db08608a7439549198c64791ad7c44c Mon Sep 17 00:00:00 2001 From: Alex Eftimie Date: Wed, 22 May 2024 14:50:07 +0200 Subject: [PATCH 05/21] Fix build. Move scaffolder away from polyfill Signed-off-by: Alex Eftimie --- plugins/scaffolder/package.json | 3 +- plugins/scaffolder/src/api.test.ts | 54 +++++++++++++--------------- plugins/scaffolder/src/api.ts | 41 ++++++++++----------- plugins/scaffolder/src/setupTests.ts | 3 -- plugins/techdocs/api-report.md | 4 --- yarn.lock | 17 +-------- 6 files changed, 46 insertions(+), 76 deletions(-) diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index e4ba300d27..b8f69cebe1 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -70,6 +70,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@microsoft/fetch-event-source": "^2.0.1", "@react-hookz/web": "^24.0.0", "@rjsf/core": "5.18.2", "@rjsf/material-ui": "5.18.2", @@ -78,7 +79,6 @@ "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", "@uiw/react-codemirror": "^4.9.3", "classnames": "^2.2.6", - "event-source-polyfill": "^1.0.31", "git-url-parse": "^14.0.0", "humanize-duration": "^3.25.1", "json-schema": "^0.4.0", @@ -103,7 +103,6 @@ "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^15.0.0", "@testing-library/user-event": "^14.0.0", - "@types/event-source-polyfill": "^1.0.0", "@types/humanize-duration": "^3.18.1", "@types/json-schema": "^7.0.9", "msw": "^1.0.0" diff --git a/plugins/scaffolder/src/api.test.ts b/plugins/scaffolder/src/api.test.ts index b9b1881066..cf482e52f7 100644 --- a/plugins/scaffolder/src/api.test.ts +++ b/plugins/scaffolder/src/api.test.ts @@ -20,14 +20,13 @@ import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { ScaffolderClient } from './api'; -import { EventSourcePolyfill } from 'event-source-polyfill'; +import { fetchEventSource } from '@microsoft/fetch-event-source'; -const MockedEventSource = EventSourcePolyfill as jest.MockedClass< - typeof EventSourcePolyfill +jest.mock('@microsoft/fetch-event-source'); +const mockFetchEventSource = fetchEventSource as jest.MockedFunction< + typeof fetchEventSource >; -jest.mock('event-source-polyfill'); - const server = setupServer(); describe('api', () => { @@ -87,26 +86,23 @@ describe('api', () => { describe('streamEvents', () => { describe('eventsource', () => { it('should work', async () => { - MockedEventSource.prototype.addEventListener.mockImplementation( - (type, fn) => { - if (typeof fn !== 'function') { - return; - } - - if (type === 'log') { - fn({ - data: '{"id":1,"taskId":"a-random-id","type":"log","createdAt":"","body":{"message":"My log message"}}', - } as any); - } else if (type === 'completion') { - fn({ - data: '{"id":2,"taskId":"a-random-id","type":"completion","createdAt":"","body":{"message":"Finished!"}}', - } as any); - } - }, - ); - - const token = 'fake-token'; - identityApi.getCredentials.mockResolvedValue({ token: token }); + mockFetchEventSource.mockImplementation(async (_url, options) => { + const { onopen, onmessage } = options; + await Promise.resolve(); + await onopen?.({ ok: true } as Response); + await Promise.resolve(); + onmessage?.({ + id: '', + event: 'log', + data: '{"id":1,"taskId":"a-random-id","type":"log","createdAt":"","body":{"message":"My log message"}}', + }); + await Promise.resolve(); + onmessage?.({ + id: '', + event: 'completion', + data: '{"id":2,"taskId":"a-random-id","type":"completion","createdAt":"","body":{"message":"Finished!"}}', + }); + }); const next = jest.fn(); @@ -116,14 +112,14 @@ describe('api', () => { .subscribe({ next, complete }); }); - expect(MockedEventSource).toHaveBeenCalledWith( + expect(mockFetchEventSource).toHaveBeenCalledWith( 'http://backstage/api/v2/tasks/a-random-task-id/eventstream', { - withCredentials: true, - headers: { Authorization: `Bearer ${token}` }, + fetch: fetchApi.fetch, + onmessage: expect.any(Function), + onerror: expect.any(Function), }, ); - expect(MockedEventSource.prototype.close).toHaveBeenCalled(); expect(next).toHaveBeenCalledTimes(2); expect(next).toHaveBeenCalledWith({ diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index fe0d7c11d0..8dc11ef296 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -41,7 +41,7 @@ import { } from '@backstage/plugin-scaffolder-react'; import queryString from 'qs'; -import { EventSourcePolyfill } from 'event-source-polyfill'; +import { fetchEventSource } from '@microsoft/fetch-event-source'; /** * An API to interact with the scaffolder backend. @@ -226,11 +226,8 @@ export class ScaffolderClient implements ScaffolderApi { params.set('after', String(Number(after))); } - Promise.all([ - this.discoveryApi.getBaseUrl('scaffolder'), - this.identityApi?.getCredentials(), - ]).then( - ([baseUrl, credentials]) => { + this.discoveryApi.getBaseUrl('scaffolder').then( + baseUrl => { const url = `${baseUrl}/v2/tasks/${encodeURIComponent( taskId, )}/eventstream`; @@ -245,22 +242,22 @@ export class ScaffolderClient implements ScaffolderApi { } }; - const eventSource = new EventSourcePolyfill(url, { - withCredentials: true, - headers: credentials?.token - ? { Authorization: `Bearer ${credentials.token}` } - : {}, - }); - eventSource.addEventListener('log', processEvent); - eventSource.addEventListener('recovered', processEvent); - eventSource.addEventListener('cancelled', processEvent); - eventSource.addEventListener('completion', (event: any) => { - processEvent(event); - eventSource.close(); - subscriber.complete(); - }); - eventSource.addEventListener('error', event => { - subscriber.error(event); + fetchEventSource(url, { + fetch: this.fetchApi.fetch, + onmessage(e: any) { + if (e.event === 'log') { + processEvent(e); + return; + } else if (e.event === 'completion') { + processEvent(e); + subscriber.complete(); + return; + } + processEvent(e); + }, + onerror(err) { + subscriber.error(err); + }, }); }, error => { diff --git a/plugins/scaffolder/src/setupTests.ts b/plugins/scaffolder/src/setupTests.ts index 8608e5b98e..7b25d3706a 100644 --- a/plugins/scaffolder/src/setupTests.ts +++ b/plugins/scaffolder/src/setupTests.ts @@ -16,9 +16,6 @@ import '@testing-library/jest-dom'; -const { EventSourcePolyfill } = jest.requireMock('event-source-polyfill'); -global.EventSource = EventSourcePolyfill; - // Patch jsdom to add feature used by CodeMirror document.createRange = () => { const range = new Range(); diff --git a/plugins/techdocs/api-report.md b/plugins/techdocs/api-report.md index ff43f4309b..582e941507 100644 --- a/plugins/techdocs/api-report.md +++ b/plugins/techdocs/api-report.md @@ -14,7 +14,6 @@ import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { EntityOwnerPickerProps } from '@backstage/plugin-catalog-react'; import { FetchApi } from '@backstage/core-plugin-api'; -import { IdentityApi } from '@backstage/core-plugin-api'; import { JSX as JSX_2 } from 'react'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; @@ -465,7 +464,6 @@ export class TechDocsStorageClient implements TechDocsStorageApi_2 { constructor(options: { configApi: Config; discoveryApi: DiscoveryApi; - identityApi: IdentityApi; fetchApi: FetchApi; }); // (undocumented) @@ -485,8 +483,6 @@ export class TechDocsStorageClient implements TechDocsStorageApi_2 { getEntityDocs(entityId: CompoundEntityRef, path: string): Promise; // (undocumented) getStorageUrl(): Promise; - // (undocumented) - identityApi: IdentityApi; syncEntityDocs( entityId: CompoundEntityRef, logHandler?: (line: string) => void, diff --git a/yarn.lock b/yarn.lock index 052268febb..3ac0d88613 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6958,6 +6958,7 @@ __metadata: "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.61 + "@microsoft/fetch-event-source": ^2.0.1 "@react-hookz/web": ^24.0.0 "@rjsf/core": 5.18.2 "@rjsf/material-ui": 5.18.2 @@ -6967,13 +6968,11 @@ __metadata: "@testing-library/jest-dom": ^6.0.0 "@testing-library/react": ^15.0.0 "@testing-library/user-event": ^14.0.0 - "@types/event-source-polyfill": ^1.0.0 "@types/humanize-duration": ^3.18.1 "@types/json-schema": ^7.0.9 "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 "@uiw/react-codemirror": ^4.9.3 classnames: ^2.2.6 - event-source-polyfill: ^1.0.31 git-url-parse: ^14.0.0 humanize-duration: ^3.25.1 json-schema: ^0.4.0 @@ -17134,13 +17133,6 @@ __metadata: languageName: node linkType: hard -"@types/event-source-polyfill@npm:^1.0.0": - version: 1.0.5 - resolution: "@types/event-source-polyfill@npm:1.0.5" - checksum: f506b68710162f2ade1bccbc5691b8c67e5a703e565df2bc0b7b5be2637ba838ef81ec6c10b03248fe4d054386d95a6e827c7aace6e924986c2b9985f77b55de - languageName: node - linkType: hard - "@types/expect@npm:^1.20.4": version: 1.20.4 resolution: "@types/expect@npm:1.20.4" @@ -25498,13 +25490,6 @@ __metadata: languageName: node linkType: hard -"event-source-polyfill@npm:^1.0.31": - version: 1.0.31 - resolution: "event-source-polyfill@npm:1.0.31" - checksum: 973f226404e2a1b14ed7ef15c718b89e213b41d7cfeeb1c10937fd09229f13904f3d7c3075ab28ccf858c213007559908eecdd577577330352f53a351383dd75 - languageName: node - linkType: hard - "event-target-shim@npm:^5.0.0": version: 5.0.1 resolution: "event-target-shim@npm:5.0.1" From cbebad1483dda205bef36d8533905d1d6ef80315 Mon Sep 17 00:00:00 2001 From: Alex Eftimie Date: Wed, 22 May 2024 14:53:42 +0200 Subject: [PATCH 06/21] Add changeset Signed-off-by: Alex Eftimie --- .changeset/empty-avocados-tease.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/empty-avocados-tease.md diff --git a/.changeset/empty-avocados-tease.md b/.changeset/empty-avocados-tease.md new file mode 100644 index 0000000000..da3aaba0e6 --- /dev/null +++ b/.changeset/empty-avocados-tease.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder': minor +'@backstage/plugin-techdocs': minor +--- + +Use fetch-event-source instead of event-source-polify in order to stop hardcoding the Authorization header and reuse the fetchApi configuration. From 79605f824ea5746657dcd363a49630905858732e Mon Sep 17 00:00:00 2001 From: Alex Eftimie Date: Wed, 22 May 2024 19:23:54 +0200 Subject: [PATCH 07/21] Update .changeset/empty-avocados-tease.md Co-authored-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> Signed-off-by: Alex Eftimie --- .changeset/empty-avocados-tease.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/empty-avocados-tease.md b/.changeset/empty-avocados-tease.md index da3aaba0e6..08f191becd 100644 --- a/.changeset/empty-avocados-tease.md +++ b/.changeset/empty-avocados-tease.md @@ -3,4 +3,4 @@ '@backstage/plugin-techdocs': minor --- -Use fetch-event-source instead of event-source-polify in order to stop hardcoding the Authorization header and reuse the fetchApi configuration. +Internal updates to allow reusing Backstage's `fetchApi` implementation for event source requests. This allows you to for example, override the `Authorization` header. From a15948009f11f9b29ebc0b708735db33ae8b29c0 Mon Sep 17 00:00:00 2001 From: Alex Eftimie Date: Mon, 27 May 2024 16:09:26 +0200 Subject: [PATCH 08/21] address PR comments Signed-off-by: Alex Eftimie --- plugins/scaffolder/src/api.ts | 7 +++++-- plugins/techdocs/api-report.md | 1 + plugins/techdocs/src/client.ts | 6 +++++- plugins/techdocs/src/plugin.ts | 8 +++++++- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 8dc11ef296..25f76c029a 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -41,7 +41,10 @@ import { } from '@backstage/plugin-scaffolder-react'; import queryString from 'qs'; -import { fetchEventSource } from '@microsoft/fetch-event-source'; +import { + EventSourceMessage, + fetchEventSource, +} from '@microsoft/fetch-event-source'; /** * An API to interact with the scaffolder backend. @@ -244,7 +247,7 @@ export class ScaffolderClient implements ScaffolderApi { fetchEventSource(url, { fetch: this.fetchApi.fetch, - onmessage(e: any) { + onmessage(e: EventSourceMessage) { if (e.event === 'log') { processEvent(e); return; diff --git a/plugins/techdocs/api-report.md b/plugins/techdocs/api-report.md index 582e941507..b756bc0ab0 100644 --- a/plugins/techdocs/api-report.md +++ b/plugins/techdocs/api-report.md @@ -465,6 +465,7 @@ export class TechDocsStorageClient implements TechDocsStorageApi_2 { configApi: Config; discoveryApi: DiscoveryApi; fetchApi: FetchApi; + identityApi?: any; }); // (undocumented) configApi: Config; diff --git a/plugins/techdocs/src/client.ts b/plugins/techdocs/src/client.ts index b79c140750..06362825a1 100644 --- a/plugins/techdocs/src/client.ts +++ b/plugins/techdocs/src/client.ts @@ -126,6 +126,8 @@ export class TechDocsStorageClient implements TechDocsStorageApi { configApi: Config; discoveryApi: DiscoveryApi; fetchApi: FetchApi; + // @deprecated Remove identityApiRef + identityApi?: any; }) { this.configApi = options.configApi; this.discoveryApi = options.discoveryApi; @@ -212,7 +214,9 @@ export class TechDocsStorageClient implements TechDocsStorageApi { fetch: this.fetchApi.fetch, onmessage(e: any) { if (e.event === 'log') { - logHandler(JSON.parse(e.data)); + if (e.data) { + logHandler(JSON.parse(e.data)); + } } else if (e.event === 'finish') { let updated: boolean = false; if (e.data) { diff --git a/plugins/techdocs/src/plugin.ts b/plugins/techdocs/src/plugin.ts index efa6bdfc1f..9d4157ba9e 100644 --- a/plugins/techdocs/src/plugin.ts +++ b/plugins/techdocs/src/plugin.ts @@ -31,6 +31,7 @@ import { createRoutableExtension, discoveryApiRef, fetchApiRef, + identityApiRef, } from '@backstage/core-plugin-api'; import { createSearchResultListItemExtension, @@ -51,13 +52,18 @@ export const techdocsPlugin = createPlugin({ deps: { configApi: configApiRef, discoveryApi: discoveryApiRef, + /** + * @deprecated fetchApi is enough + */ + identityApi: identityApiRef, fetchApi: fetchApiRef, }, - factory: ({ configApi, discoveryApi, fetchApi }) => + factory: ({ configApi, discoveryApi, fetchApi, identityApi = null }) => new TechDocsStorageClient({ configApi, discoveryApi, fetchApi, + identityApi, }), }), createApiFactory({ From 83688cdca247017a47c6a8a44f3e1665a920d966 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 16:39:47 +0000 Subject: [PATCH 09/21] fix(deps): update dependency @rollup/plugin-commonjs to v25.0.8 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 33e958cb12..a5914223ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14252,8 +14252,8 @@ __metadata: linkType: hard "@rollup/plugin-commonjs@npm:^25.0.0": - version: 25.0.7 - resolution: "@rollup/plugin-commonjs@npm:25.0.7" + version: 25.0.8 + resolution: "@rollup/plugin-commonjs@npm:25.0.8" dependencies: "@rollup/pluginutils": ^5.0.1 commondir: ^1.0.1 @@ -14266,7 +14266,7 @@ __metadata: peerDependenciesMeta: rollup: optional: true - checksum: 052e11839a9edc556eda5dcc759ab816dcc57e9f0f905a1e6e14fff954eaa6b1e2d0d544f5bd18d863993c5eba43d8ac9c19d9bb53b1c3b1213f32cfc9d50b2e + checksum: dd105ee5625fbcaf832c0cf80be0aaf6a86bbd8fe99ff911f9ac4b78c79f26e9e99442b5aa0cc1136b5ddf89ec0b6c5728e5341ac04d687aef1b53063670b395 languageName: node linkType: hard From b7db436113774855800cae92b424a827edaedece Mon Sep 17 00:00:00 2001 From: Alex Eftimie Date: Tue, 28 May 2024 18:49:35 +0200 Subject: [PATCH 10/21] update pr comments Signed-off-by: Alex Eftimie --- plugins/scaffolder/src/api.ts | 3 +++ plugins/techdocs/api-report.md | 3 ++- plugins/techdocs/src/client.ts | 13 ++++++++++--- plugins/techdocs/src/plugin.ts | 8 +------- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 25f76c029a..6ef5fff588 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -245,8 +245,10 @@ export class ScaffolderClient implements ScaffolderApi { } }; + const ctrl = new AbortController(); fetchEventSource(url, { fetch: this.fetchApi.fetch, + signal: ctrl.signal, onmessage(e: EventSourceMessage) { if (e.event === 'log') { processEvent(e); @@ -254,6 +256,7 @@ export class ScaffolderClient implements ScaffolderApi { } else if (e.event === 'completion') { processEvent(e); subscriber.complete(); + ctrl.abort(); return; } processEvent(e); diff --git a/plugins/techdocs/api-report.md b/plugins/techdocs/api-report.md index b756bc0ab0..ebd6adcd3c 100644 --- a/plugins/techdocs/api-report.md +++ b/plugins/techdocs/api-report.md @@ -14,6 +14,7 @@ import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { EntityOwnerPickerProps } from '@backstage/plugin-catalog-react'; import { FetchApi } from '@backstage/core-plugin-api'; +import { IdentityApi } from '@backstage/core-plugin-api'; import { JSX as JSX_2 } from 'react'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; @@ -465,7 +466,7 @@ export class TechDocsStorageClient implements TechDocsStorageApi_2 { configApi: Config; discoveryApi: DiscoveryApi; fetchApi: FetchApi; - identityApi?: any; + identityApi?: IdentityApi; }); // (undocumented) configApi: Config; diff --git a/plugins/techdocs/src/client.ts b/plugins/techdocs/src/client.ts index 06362825a1..d00be2a01f 100644 --- a/plugins/techdocs/src/client.ts +++ b/plugins/techdocs/src/client.ts @@ -16,7 +16,11 @@ import { CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api'; +import { + DiscoveryApi, + FetchApi, + IdentityApi, +} from '@backstage/core-plugin-api'; import { NotFoundError, ResponseError } from '@backstage/errors'; import { SyncResult, @@ -126,8 +130,8 @@ export class TechDocsStorageClient implements TechDocsStorageApi { configApi: Config; discoveryApi: DiscoveryApi; fetchApi: FetchApi; - // @deprecated Remove identityApiRef - identityApi?: any; + /** @deprecated identityApi is not needed any more */ + identityApi?: IdentityApi; }) { this.configApi = options.configApi; this.discoveryApi = options.discoveryApi; @@ -210,8 +214,10 @@ export class TechDocsStorageClient implements TechDocsStorageApi { const url = `${apiOrigin}/sync/${namespace}/${kind}/${name}`; return new Promise((resolve, reject) => { + const ctrl = new AbortController(); fetchEventSource(url, { fetch: this.fetchApi.fetch, + signal: ctrl.signal, onmessage(e: any) { if (e.event === 'log') { if (e.data) { @@ -228,6 +234,7 @@ export class TechDocsStorageClient implements TechDocsStorageApi { } }, onerror(err) { + ctrl.abort(); reject(err); }, }); diff --git a/plugins/techdocs/src/plugin.ts b/plugins/techdocs/src/plugin.ts index 9d4157ba9e..efa6bdfc1f 100644 --- a/plugins/techdocs/src/plugin.ts +++ b/plugins/techdocs/src/plugin.ts @@ -31,7 +31,6 @@ import { createRoutableExtension, discoveryApiRef, fetchApiRef, - identityApiRef, } from '@backstage/core-plugin-api'; import { createSearchResultListItemExtension, @@ -52,18 +51,13 @@ export const techdocsPlugin = createPlugin({ deps: { configApi: configApiRef, discoveryApi: discoveryApiRef, - /** - * @deprecated fetchApi is enough - */ - identityApi: identityApiRef, fetchApi: fetchApiRef, }, - factory: ({ configApi, discoveryApi, fetchApi, identityApi = null }) => + factory: ({ configApi, discoveryApi, fetchApi }) => new TechDocsStorageClient({ configApi, discoveryApi, fetchApi, - identityApi, }), }), createApiFactory({ From edc180129e23d62ccf219dd87c329216b2484a85 Mon Sep 17 00:00:00 2001 From: Alex Eftimie Date: Thu, 30 May 2024 02:23:35 +0200 Subject: [PATCH 11/21] Update .changeset/empty-avocados-tease.md Co-authored-by: Vincenzo Scamporlino Signed-off-by: Alex Eftimie --- .changeset/empty-avocados-tease.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/empty-avocados-tease.md b/.changeset/empty-avocados-tease.md index 08f191becd..f202af2b82 100644 --- a/.changeset/empty-avocados-tease.md +++ b/.changeset/empty-avocados-tease.md @@ -1,6 +1,6 @@ --- -'@backstage/plugin-scaffolder': minor -'@backstage/plugin-techdocs': minor +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-techdocs': patch --- Internal updates to allow reusing Backstage's `fetchApi` implementation for event source requests. This allows you to for example, override the `Authorization` header. From 9d54ebfb3f2396499f301b4bfd80c4dde955dd4c Mon Sep 17 00:00:00 2001 From: Alex Eftimie Date: Thu, 30 May 2024 08:19:02 +0200 Subject: [PATCH 12/21] Fix unittests Signed-off-by: Alex Eftimie --- plugins/scaffolder/src/api.test.ts | 1 + plugins/techdocs/src/client.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/plugins/scaffolder/src/api.test.ts b/plugins/scaffolder/src/api.test.ts index cf482e52f7..488169719d 100644 --- a/plugins/scaffolder/src/api.test.ts +++ b/plugins/scaffolder/src/api.test.ts @@ -118,6 +118,7 @@ describe('api', () => { fetch: fetchApi.fetch, onmessage: expect.any(Function), onerror: expect.any(Function), + signal: expect.any(AbortSignal), }, ); diff --git a/plugins/techdocs/src/client.test.ts b/plugins/techdocs/src/client.test.ts index 788d7e3fed..62d497d790 100644 --- a/plugins/techdocs/src/client.test.ts +++ b/plugins/techdocs/src/client.test.ts @@ -104,6 +104,7 @@ describe('TechDocsStorageClient', () => { fetch: fetchApi.fetch, onerror: expect.any(Function), onmessage: expect.any(Function), + signal: expect.any(AbortSignal), }, ); }); From 1e88e7a5ba3640676dbb55f462ff702efab4f1c2 Mon Sep 17 00:00:00 2001 From: Ismail Mohammed Date: Sun, 2 Jun 2024 01:04:33 +0300 Subject: [PATCH 13/21] Fixed select field width on wide monitors Signed-off-by: Ismail Mohammed --- packages/core-components/src/components/Select/Select.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core-components/src/components/Select/Select.tsx b/packages/core-components/src/components/Select/Select.tsx index b1a2139b85..924b49f640 100644 --- a/packages/core-components/src/components/Select/Select.tsx +++ b/packages/core-components/src/components/Select/Select.tsx @@ -78,7 +78,6 @@ const useStyles = makeStyles( createStyles({ formControl: { margin: theme.spacing(1, 0), - maxWidth: 300, }, label: { transform: 'initial', From 3e175c8729b17690e44d29280042b153d2cccd7f Mon Sep 17 00:00:00 2001 From: Ismail Mohammed Date: Sun, 2 Jun 2024 01:26:13 +0300 Subject: [PATCH 14/21] Added changeset Signed-off-by: Ismail Mohammed --- .changeset/swift-islands-leave.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/swift-islands-leave.md diff --git a/.changeset/swift-islands-leave.md b/.changeset/swift-islands-leave.md new file mode 100644 index 0000000000..d92d3548cf --- /dev/null +++ b/.changeset/swift-islands-leave.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Made `Select` field bug where it was not in full width on large screens. From c4bf42917efc023f461c536f7aa09d23274c15a7 Mon Sep 17 00:00:00 2001 From: Julien Gedeon Date: Mon, 3 Jun 2024 09:54:19 +0200 Subject: [PATCH 15/21] Add Covestro to ADOPTERS.md Signed-off-by: Julien Gedeon --- ADOPTERS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ADOPTERS.md b/ADOPTERS.md index 2bb76a199e..5fe359bf1e 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -270,3 +270,4 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/ | [OP Financial Group](https://www.op.fi/op-financial-group) | [Heikki Hellgren](https://github.com/drodil), [Jyrki Koistinen](https://github.com/snyvision) | We are using Backstage as a gateway into our internal development platform offering to simplify complexity. | | [Scania](https://www.scania.com) | [Santosh Rangavajjula](https://linkedin.com/in/rsbth) | We are implementing backstage at Scania to consolidate operational information from Gitlab, Jira, Confluence, Artifactory, Servicenow and other tools into one place. | | [Senora.dev](https://senora.dev) | [Shaked Braimok Yosef](https://github.com/ShakedBraimok) | We are using Backstage as a service catalog for our costumers. | +| [Covestro](https://www.covestro.com) | [Julien Gedeon](https://github.com/cvvcv1) | We use Backstage as our Internal developer portal for the Digital R&D department. | From 1c6689f768acb0f83fad4a8301a7e91ab823b27e Mon Sep 17 00:00:00 2001 From: Long Zhang Date: Mon, 3 Jun 2024 10:40:23 +0200 Subject: [PATCH 16/21] add infrawallet to the plugins directory Signed-off-by: Long Zhang --- microsite/data/plugins/infrawallet.yaml | 14 ++++++++++++++ microsite/static/img/infrawallet-logo.png | Bin 0 -> 71598 bytes 2 files changed, 14 insertions(+) create mode 100644 microsite/data/plugins/infrawallet.yaml create mode 100644 microsite/static/img/infrawallet-logo.png diff --git a/microsite/data/plugins/infrawallet.yaml b/microsite/data/plugins/infrawallet.yaml new file mode 100644 index 0000000000..bb6c3de7cf --- /dev/null +++ b/microsite/data/plugins/infrawallet.yaml @@ -0,0 +1,14 @@ +--- +title: InfraWallet +author: Electrolux Group +authorUrl: https://opensource.electrolux.one/ +category: FinOps +description: Control your cloud costs just in the way how you control your bank accounts. +documentation: https://github.com/electrolux-oss/infrawallet/blob/main/README.md +iconUrl: /img/infrawallet-logo.png +npmPackageName: '@electrolux-oss/plugin-infrawallet' +tags: + - finops + - cost + - infrastructure +addedDate: '2024-06-03' diff --git a/microsite/static/img/infrawallet-logo.png b/microsite/static/img/infrawallet-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d6bdbe64482bfb87a86785bd6deac50f438187c8 GIT binary patch literal 71598 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 literal 0 HcmV?d00001 From 9391756a0230f27e074a36a1b93e4f1d181c5f16 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Mon, 3 Jun 2024 11:47:47 +0200 Subject: [PATCH 17/21] Update .changeset/swift-islands-leave.md Signed-off-by: Vincenzo Scamporlino --- .changeset/swift-islands-leave.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/swift-islands-leave.md b/.changeset/swift-islands-leave.md index d92d3548cf..3b0b460733 100644 --- a/.changeset/swift-islands-leave.md +++ b/.changeset/swift-islands-leave.md @@ -2,4 +2,4 @@ '@backstage/core-components': patch --- -Made `Select` field bug where it was not in full width on large screens. +Removed max width from `Select` component. From c797d69730427249afa17138609adab6269ed3b9 Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Mon, 3 Jun 2024 13:39:41 +0300 Subject: [PATCH 18/21] feat: deps check to also check for violations from backend plugin modules relates to #25006 Signed-off-by: Heikki Hellgren --- scripts/verify-local-dependencies.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/scripts/verify-local-dependencies.js b/scripts/verify-local-dependencies.js index e2aaf8c33a..963bc40c9c 100755 --- a/scripts/verify-local-dependencies.js +++ b/scripts/verify-local-dependencies.js @@ -36,11 +36,11 @@ const depTypes = [ const roleRules = [ { sourceRole: ['frontend-plugin', 'web-library'], - targetRole: ['backend-plugin', 'node-library'], + targetRole: ['backend-plugin', 'node-library', 'backend-plugin-module'], message: `Package SOURCE_NAME with frontend role SOURCE_ROLE has a dependency on package TARGET_NAME with backend role TARGET_ROLE, which is not permitted`, }, { - sourceRole: ['backend-plugin', 'node-library'], + sourceRole: ['backend-plugin', 'node-library', 'backend-plugin-module'], targetRole: ['frontend-plugin', 'web-library'], message: `Package SOURCE_NAME with backend role SOURCE_ROLE has a dependency on package TARGET_NAME with frontend role TARGET_ROLE, which is not permitted`, }, @@ -51,8 +51,9 @@ const roleRules = [ 'web-library', 'backend-plugin', 'node-library', + 'backend-plugin-module', ], - message: `Polymorphic package SOURCE_NAME has a dependency on package TARGET_NAME with role TARGET_ROLE, which is not permitted since it's not also polymorphic`, + message: `Polymorphic package SOURCE_NAME with role SOURCE_ROLE has a dependency on package TARGET_NAME with role TARGET_ROLE, which is not permitted since it's not also polymorphic`, }, { sourceRole: ['frontend-plugin', 'web-library'], @@ -62,7 +63,7 @@ const roleRules = [ '@backstage/plugin-api-docs', '@backstage/plugin-techdocs-addons-test-utils', ], - message: `Package SOURCE_NAME with role SOURCE_ROLE has a dependency on another plugin package TARGET_NAME, which is not permitted`, + message: `Package SOURCE_NAME with role SOURCE_ROLE has a dependency on another plugin package TARGET_NAME with role TARGET_ROLE, which is not permitted`, }, { sourceRole: ['frontend-plugin', 'web-library'], @@ -80,7 +81,7 @@ const roleRules = [ '@backstage/plugin-techdocs-addons-test-utils', '@backstage/plugin-user-settings', ], - message: `Plugin package SOURCE_NAME with role SOURCE_ROLE has a runtime dependency on package TARGET_NAME, which is not permitted. If you are using this dependency for dev server purposes, you can move it to devDependencies instead.`, + message: `Plugin package SOURCE_NAME with role SOURCE_ROLE has a runtime dependency on package TARGET_NAME with role TARGET_ROLE, which is not permitted. If you are using this dependency for dev server purposes, you can move it to devDependencies instead.`, }, { sourceRole: ['backend-plugin', 'node-library'], @@ -92,7 +93,7 @@ const roleRules = [ '@backstage/backend-test-utils', '@backstage/backend-dynamic-feature-service', ], - message: `Plugin package SOURCE_NAME with role SOURCE_ROLE has a runtime dependency on package TARGET_NAME, which is not permitted. If you are using this dependency for dev server purposes, you can move it to devDependencies instead.`, + message: `Plugin package SOURCE_NAME with role SOURCE_ROLE has a runtime dependency on package TARGET_NAME with role TARGET_ROLE, which is not permitted. If you are using this dependency for dev server purposes, you can move it to devDependencies instead.`, }, ]; From 78e53648cd6de089826984e720dd533b7ee02d62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 3 Jun 2024 13:19:11 +0200 Subject: [PATCH 19/21] added code example for plugin-to-plugin auth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- docs/auth/service-to-service-auth.md | 50 ++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/docs/auth/service-to-service-auth.md b/docs/auth/service-to-service-auth.md index ee88e45305..1d6de30ce6 100644 --- a/docs/auth/service-to-service-auth.md +++ b/docs/auth/service-to-service-auth.md @@ -27,6 +27,45 @@ any configuration. They generate self-signed tokens automatically for making requests to other Backstage backend plugins, and the receivers use the caller's public key set endpoint to be able to perform verification. +A backend plugin wishing to make a request to another backend plugin acquires +the required token as follows, where `auth` and `httpAuth` are assumed to be +injected from `coreServices.auth` and `coreServices.httpAuth`, respectively: + +```ts +const credentials = await httpAuth.credentials(req); +const { token } = await auth.getPluginRequestToken({ + onBehalfOf: credentials, + targetPluginId: '', // e.g. 'catalog' +}); +``` + +In this example we are assuming that we are in an Express request handler, and +we extract the caller credentials (typically a user or a service) out of its +`req` and make the upstream request on-behalf-of that principal. Prefer to use +this pattern wherever there's an incoming set of credentials to refer to. + +If you want to initiate a request entirely as your own service, not on behalf of +anybody else, you can do so as follows: + +```ts +const { token } = await auth.getPluginRequestToken({ + onBehalfOf: await auth.getOwnServiceCredentials(), + targetPluginId: '', // e.g. 'catalog' +}); +``` + +Callers pass along the tokens verbatim with requests in the `Authorization` +header: + +```yaml +Authorization: Bearer eyJhbG... +``` + +You may occasionally also see some code, e.g. clients to other systems, that +accept a `credentials` argument directly instead of a token. For those, just +pass in the credentials as acquired above, instead of making a token. The client +code will know what to do with those credentials internally. + This flow has only one configuration option to set in your app-config: `backend.auth.dangerouslyDisableDefaultAuthPolicy`, which can be set to `true` if you for some reason need to completely disable both the issuing and @@ -73,8 +112,8 @@ The subjects must be strings without whitespace. They are used for identifying each caller, and become part of the credentials object that request recipient plugins get. -Callers pass along the tokens verbatim with requests in the `Authorization` -header: +Callers must pass along tokens verbatim with requests in the `Authorization` +header when calling Backstage plugins: ```yaml Authorization: Bearer eZv5o+fW3KnR3kVabMW4ZcDNLPl8nmMW @@ -125,6 +164,13 @@ credentials object that the request recipient plugins get. All subjects will hav `external:`, but you can also provide a custom subjectPrefix which will get appended before the subject returned from your JWKS service (ex. `external:custom-prefix:sub`). +Callers must pass along tokens with requests in the `Authorization` header when +calling Backstage plugins: + +```yaml +Authorization: Bearer eyJhbG... +``` + ## Legacy Tokens Plugins and backends that are _not_ on the new backend system use a legacy token From 4f21993c37179373804c959782326e08d04fa62a Mon Sep 17 00:00:00 2001 From: mario ma Date: Mon, 3 Jun 2024 19:58:18 +0800 Subject: [PATCH 20/21] =?UTF-8?q?if=20oidc=20server=20do=20not=20provide?= =?UTF-8?q?=20revocation=5Fendpoint=EF=BC=8Cwe=20should=20not=20call=20rev?= =?UTF-8?q?oke=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: mario ma --- .changeset/orange-chefs-end.md | 5 ++ .../src/authenticator.test.ts | 47 +++++++++++++++++++ .../src/authenticator.ts | 9 +++- 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 .changeset/orange-chefs-end.md diff --git a/.changeset/orange-chefs-end.md b/.changeset/orange-chefs-end.md new file mode 100644 index 0000000000..9ba2a29685 --- /dev/null +++ b/.changeset/orange-chefs-end.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-oidc-provider': patch +--- + +if oidc server do not provide revocation_endpoint,we should not call revoke function diff --git a/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts b/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts index 4f1db097d0..67bfd27b19 100644 --- a/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts @@ -493,5 +493,52 @@ describe('oidcAuthenticator', () => { new Error('Refresh failed'), ); }); + + it('should not revoke refreshToken when issuer revocation_endpoint is undefined', async () => { + const refreshToken = 'revokeRefreshToken2'; + const refreshRequest = { + scope: 'testScope', + refreshToken, + req: {} as express.Request, + }; + const logoutRequest = { + refreshToken, + req: {} as express.Request, + }; + + // override .well-known endpoint response, set revocation_endpoint to undefined + mswServer.use( + rest.get( + 'https://oidc.test/.well-known/openid-configuration', + (_req, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + ...issuerMetadata, + revocation_endpoint: undefined, + }), + ), + ), + ); + + const newImplementation = oidcAuthenticator.initialize({ + callbackUrl: 'https://backstage.test/callback', + config: new ConfigReader({ + metadataUrl: 'https://oidc.test/.well-known/openid-configuration', + clientId: 'clientId', + clientSecret: 'clientSecret', + }), + }); + + await oidcAuthenticator.logout?.(logoutRequest, newImplementation); + + const refreshResponse = await oidcAuthenticator.refresh( + refreshRequest, + newImplementation, + ); + + expect(refreshResponse.session.refreshToken).toBe('refreshToken'); + }); }); }); diff --git a/plugins/auth-backend-module-oidc-provider/src/authenticator.ts b/plugins/auth-backend-module-oidc-provider/src/authenticator.ts index f954d228e2..2b6a9b34f1 100644 --- a/plugins/auth-backend-module-oidc-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-oidc-provider/src/authenticator.ts @@ -202,8 +202,15 @@ export const oidcAuthenticator = createOAuthAuthenticator({ async logout(input, ctx) { const { client } = await ctx.promise; + const issuer = client.issuer; + /** + * https://github.com/panva/node-openid-client/blob/main/lib/client.js#L1398 + * client.revoke will check revocation_endpoint and if undefined throw error。 + * + * if oidc server do not provide revocation_endpoint,we should not call revoke function + */ - if (input.refreshToken) { + if (input.refreshToken && issuer.revocation_endpoint) { await client.revoke(input.refreshToken); } }, From 81507c82de3fa4d3df0499df0d4e96a3e40aa67e Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Mon, 3 Jun 2024 07:47:21 -0500 Subject: [PATCH 21/21] Updated `node-gyp` for `create-app` to v10 Signed-off-by: Andre Wanlin --- .changeset/wise-beers-doubt.md | 5 +++++ packages/create-app/templates/default-app/package.json.hbs | 2 +- .../templates/default-app/packages/backend/package.json.hbs | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/wise-beers-doubt.md diff --git a/.changeset/wise-beers-doubt.md b/.changeset/wise-beers-doubt.md new file mode 100644 index 0000000000..be70f326e0 --- /dev/null +++ b/.changeset/wise-beers-doubt.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Updated `node-gyp` to v10 diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index d0c2467ca9..7841b5d4e6 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -37,7 +37,7 @@ "@spotify/prettier-config": "^12.0.0", "concurrently": "^8.0.0", "lerna": "^7.3.0", - "node-gyp": "^9.0.0", + "node-gyp": "^10.0.0", "prettier": "^2.3.2", "typescript": "~5.4.0" }, diff --git a/packages/create-app/templates/default-app/packages/backend/package.json.hbs b/packages/create-app/templates/default-app/packages/backend/package.json.hbs index 93228e5c8f..049ea1c30c 100644 --- a/packages/create-app/templates/default-app/packages/backend/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/backend/package.json.hbs @@ -41,7 +41,7 @@ "app": "link:../app", "better-sqlite3": "^9.0.0", "dockerode": "^3.3.1", - "node-gyp": "^9.0.0", + "node-gyp": "^10.0.0", "pg": "^8.11.3", "winston": "^3.2.1" },