From f9ea944422ed7d7d1c3036f55a31cdd72a21adc9 Mon Sep 17 00:00:00 2001 From: sblausten Date: Wed, 21 Dec 2022 18:52:30 +0100 Subject: [PATCH 001/247] Add generic Explore tab for entities and refactor existing to use it; Add Systems to default page Signed-off-by: sblausten --- .changeset/itchy-kiwis-unite.md | 5 + plugins/explore/README.md | 7 +- .../DefaultExplorePage/DefaultExplorePage.tsx | 7 +- .../DomainExplorerContent.tsx | 4 +- .../EntityCard.test.tsx} | 6 +- .../EntityCard.tsx} | 4 +- .../{DomainCard => EntityCard}/index.ts | 2 +- .../EntityExplorerContent.test.tsx | 136 ++++++++++++++++++ .../EntityExplorerContent.tsx | 109 ++++++++++++++ .../components/EntityExplorerContent/index.ts | 17 +++ 10 files changed, 285 insertions(+), 12 deletions(-) create mode 100644 .changeset/itchy-kiwis-unite.md rename plugins/explore/src/components/{DomainCard/DomainCard.test.tsx => EntityCard/EntityCard.test.tsx} (93%) rename plugins/explore/src/components/{DomainCard/DomainCard.tsx => EntityCard/EntityCard.tsx} (93%) rename plugins/explore/src/components/{DomainCard => EntityCard}/index.ts (93%) create mode 100644 plugins/explore/src/components/EntityExplorerContent/EntityExplorerContent.test.tsx create mode 100644 plugins/explore/src/components/EntityExplorerContent/EntityExplorerContent.tsx create mode 100644 plugins/explore/src/components/EntityExplorerContent/index.ts diff --git a/.changeset/itchy-kiwis-unite.md b/.changeset/itchy-kiwis-unite.md new file mode 100644 index 0000000000..a17f3072b2 --- /dev/null +++ b/.changeset/itchy-kiwis-unite.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-explore': patch +--- + +Extracted generic EntityExplorerContent component so that is is easy to show any component kinds in their own tab in the Explore page. diff --git a/plugins/explore/README.md b/plugins/explore/README.md index 0204f34714..f60d83eafb 100644 --- a/plugins/explore/README.md +++ b/plugins/explore/README.md @@ -2,7 +2,7 @@ Welcome to the explore plugin! -This plugin helps to visualize the domains and tools in your ecosystem. +This plugin helps to visualize the domains, systems and tools in your ecosystem. ## Setup @@ -116,7 +116,10 @@ export const ExplorePage = () => { subtitle="Browse our ecosystem" > - + + + + diff --git a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.tsx b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.tsx index 54beb59619..49a79f5143 100644 --- a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.tsx +++ b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.tsx @@ -15,11 +15,11 @@ */ import React from 'react'; -import { DomainExplorerContent } from '../DomainExplorerContent'; import { ExploreLayout } from '../ExploreLayout'; import { GroupsExplorerContent } from '../GroupsExplorerContent'; import { ToolExplorerContent } from '../ToolExplorerContent'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import { EntityExplorerContent } from '../EntityExplorerContent'; export const DefaultExplorePage = () => { const configApi = useApi(configApiRef); @@ -32,7 +32,10 @@ export const DefaultExplorePage = () => { subtitle="Discover solutions available in your ecosystem" > - + + + + diff --git a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx index 3cfed5b12b..15c0d650cc 100644 --- a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx +++ b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx @@ -19,7 +19,6 @@ import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { Button } from '@material-ui/core'; import React from 'react'; import useAsync from 'react-use/lib/useAsync'; -import { DomainCard } from '../DomainCard'; import { Content, ContentHeader, @@ -30,6 +29,7 @@ import { WarningPanel, } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; +import { EntityCard } from '../EntityCard'; const Body = () => { const catalogApi = useApi(catalogApiRef); @@ -78,7 +78,7 @@ const Body = () => { return ( {entities.map((entity, index) => ( - + ))} ); diff --git a/plugins/explore/src/components/DomainCard/DomainCard.test.tsx b/plugins/explore/src/components/EntityCard/EntityCard.test.tsx similarity index 93% rename from plugins/explore/src/components/DomainCard/DomainCard.test.tsx rename to plugins/explore/src/components/EntityCard/EntityCard.test.tsx index df9b84820a..ccf5deb875 100644 --- a/plugins/explore/src/components/DomainCard/DomainCard.test.tsx +++ b/plugins/explore/src/components/EntityCard/EntityCard.test.tsx @@ -18,9 +18,9 @@ import { DomainEntity } from '@backstage/catalog-model'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp } from '@backstage/test-utils'; import React from 'react'; -import { DomainCard } from './DomainCard'; +import { EntityCard } from './EntityCard'; -describe('', () => { +describe('', () => { it('renders a domain card', async () => { const entity: DomainEntity = { apiVersion: 'backstage.io/v1alpha1', @@ -35,7 +35,7 @@ describe('', () => { }, }; const { getByText } = await renderInTestApp( - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, diff --git a/plugins/explore/src/components/DomainCard/DomainCard.tsx b/plugins/explore/src/components/EntityCard/EntityCard.tsx similarity index 93% rename from plugins/explore/src/components/DomainCard/DomainCard.tsx rename to plugins/explore/src/components/EntityCard/EntityCard.tsx index a20a5e1f0c..a9ba4f2550 100644 --- a/plugins/explore/src/components/DomainCard/DomainCard.tsx +++ b/plugins/explore/src/components/EntityCard/EntityCard.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { DomainEntity, RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; import { EntityRefLinks, entityRouteParams, @@ -35,7 +35,7 @@ import { Button, ItemCardHeader } from '@backstage/core-components'; import { useRouteRef } from '@backstage/core-plugin-api'; /** @public */ -export const DomainCard = (props: { entity: DomainEntity }) => { +export const EntityCard = (props: { entity: Entity }) => { const { entity } = props; const catalogEntityRoute = useRouteRef(entityRouteRef); diff --git a/plugins/explore/src/components/DomainCard/index.ts b/plugins/explore/src/components/EntityCard/index.ts similarity index 93% rename from plugins/explore/src/components/DomainCard/index.ts rename to plugins/explore/src/components/EntityCard/index.ts index dbae755460..be80fb3f8f 100644 --- a/plugins/explore/src/components/DomainCard/index.ts +++ b/plugins/explore/src/components/EntityCard/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { DomainCard } from './DomainCard'; +export { EntityCard } from './EntityCard'; diff --git a/plugins/explore/src/components/EntityExplorerContent/EntityExplorerContent.test.tsx b/plugins/explore/src/components/EntityExplorerContent/EntityExplorerContent.test.tsx new file mode 100644 index 0000000000..4670a10855 --- /dev/null +++ b/plugins/explore/src/components/EntityExplorerContent/EntityExplorerContent.test.tsx @@ -0,0 +1,136 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DomainEntity } from '@backstage/catalog-model'; +import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { waitFor } from '@testing-library/react'; +import React from 'react'; +import { EntityExplorerContent } from './EntityExplorerContent'; + +describe('', () => { + const catalogApi = { + addLocation: jest.fn(), + getEntities: jest.fn(), + getLocationByRef: jest.fn(), + getLocationById: jest.fn(), + removeLocationById: jest.fn(), + removeEntityByUid: jest.fn(), + getEntityByRef: jest.fn(), + refreshEntity: jest.fn(), + getEntityAncestors: jest.fn(), + getEntityFacets: jest.fn(), + validateEntity: jest.fn(), + }; + + const Wrapper = ({ children }: { children?: React.ReactNode }) => ( + + {children} + + ); + + const mountedRoutes = { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('renders a grid of domains', async () => { + const entities: DomainEntity[] = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Domain', + metadata: { + name: 'playback', + }, + spec: { + owner: 'guest', + }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Domain', + metadata: { + name: 'artists', + }, + spec: { + owner: 'guest', + }, + }, + ]; + catalogApi.getEntities.mockResolvedValue({ items: entities }); + + const { getByText } = await renderInTestApp( + + + , + mountedRoutes, + ); + + await waitFor(() => { + expect(getByText('artists')).toBeInTheDocument(); + expect(getByText('playback')).toBeInTheDocument(); + }); + }); + + it('renders a custom title', async () => { + catalogApi.getEntities.mockResolvedValue({ items: [] }); + + const { getByText } = await renderInTestApp( + + + , + mountedRoutes, + ); + + await waitFor(() => expect(getByText('Our Areas')).toBeInTheDocument()); + }); + + it('renders empty state', async () => { + catalogApi.getEntities.mockResolvedValue({ items: [] }); + + const { getByText } = await renderInTestApp( + + + , + mountedRoutes, + ); + + await waitFor(() => + expect(getByText('No domains to display')).toBeInTheDocument(), + ); + }); + + it('renders a friendly error if it cannot collect domains', async () => { + const catalogError = new Error('Network timeout'); + catalogApi.getEntities.mockRejectedValueOnce(catalogError); + + const { getByText } = await renderInTestApp( + + + , + mountedRoutes, + ); + + await waitFor(() => + expect(getByText(/Could not load domains/)).toBeInTheDocument(), + ); + }); +}); diff --git a/plugins/explore/src/components/EntityExplorerContent/EntityExplorerContent.tsx b/plugins/explore/src/components/EntityExplorerContent/EntityExplorerContent.tsx new file mode 100644 index 0000000000..55ef13d98e --- /dev/null +++ b/plugins/explore/src/components/EntityExplorerContent/EntityExplorerContent.tsx @@ -0,0 +1,109 @@ +/* + * 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. + */ + +import { Entity } from '@backstage/catalog-model'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { Button } from '@material-ui/core'; +import React from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import { EntityCard } from '../EntityCard'; +import { + Content, + ContentHeader, + EmptyState, + ItemCardGrid, + Progress, + SupportButton, + WarningPanel, +} from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; + +const Body = (props: { kind: string }) => { + const { kind } = props; + const kindPlural = `${kind}s`; + const catalogApi = useApi(catalogApiRef); + const { + value: entities, + loading, + error, + } = useAsync(async () => { + const response = await catalogApi.getEntities({ + filter: { kind }, + }); + return response.items as Entity[]; + }, [catalogApi]); + + if (loading) { + return ; + } + + if (error) { + return ( + + {error.message} + + ); + } + + if (!entities?.length) { + return ( + + Read more + + } + /> + ); + } + + return ( + + {entities.map((entity, index) => ( + + ))} + + ); +}; + +/** @public */ +export const EntityExplorerContent = (props: { + tabTitle?: string; + kind: string; +}) => { + const { kind, tabTitle } = props; + return ( + + + + Discover the {kind.toLocaleLowerCase()}s in your ecosystem. + + + + + ); +}; diff --git a/plugins/explore/src/components/EntityExplorerContent/index.ts b/plugins/explore/src/components/EntityExplorerContent/index.ts new file mode 100644 index 0000000000..ab06193939 --- /dev/null +++ b/plugins/explore/src/components/EntityExplorerContent/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { EntityExplorerContent } from './EntityExplorerContent'; From 8465e989a6a83262596f4bc55275e90af8979d21 Mon Sep 17 00:00:00 2001 From: sblausten Date: Wed, 21 Dec 2022 18:58:30 +0100 Subject: [PATCH 002/247] Make changeset a minor Signed-off-by: sblausten --- .changeset/itchy-kiwis-unite.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/itchy-kiwis-unite.md b/.changeset/itchy-kiwis-unite.md index a17f3072b2..5fe13d9339 100644 --- a/.changeset/itchy-kiwis-unite.md +++ b/.changeset/itchy-kiwis-unite.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-explore': patch +'@backstage/plugin-explore': minor --- Extracted generic EntityExplorerContent component so that is is easy to show any component kinds in their own tab in the Explore page. From a651c48edd050ca1c3554b2a306376fe0b359071 Mon Sep 17 00:00:00 2001 From: sblausten Date: Wed, 21 Dec 2022 19:19:27 +0100 Subject: [PATCH 003/247] Fix export and typo Signed-off-by: sblausten --- .changeset/itchy-kiwis-unite.md | 2 +- plugins/explore/src/components/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/itchy-kiwis-unite.md b/.changeset/itchy-kiwis-unite.md index 5fe13d9339..e0bee850eb 100644 --- a/.changeset/itchy-kiwis-unite.md +++ b/.changeset/itchy-kiwis-unite.md @@ -2,4 +2,4 @@ '@backstage/plugin-explore': minor --- -Extracted generic EntityExplorerContent component so that is is easy to show any component kinds in their own tab in the Explore page. +Extracted generic EntityExplorerContent component so that it is easy to show any component kinds in their own tab in the Explore page. diff --git a/plugins/explore/src/components/index.ts b/plugins/explore/src/components/index.ts index d3af1b99c2..4cee9f7143 100644 --- a/plugins/explore/src/components/index.ts +++ b/plugins/explore/src/components/index.ts @@ -14,6 +14,6 @@ * limitations under the License. */ -export * from './DomainCard'; +export * from './EntityExplorerContent'; export * from './ExploreLayout'; export * from './ToolSearchResultListItem'; From 18537fedfe293848dd55bf16d3ad4f83d1de9d04 Mon Sep 17 00:00:00 2001 From: sblausten Date: Wed, 21 Dec 2022 19:21:04 +0100 Subject: [PATCH 004/247] Export more stuff for use Signed-off-by: sblausten --- plugins/explore/src/components/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/explore/src/components/index.ts b/plugins/explore/src/components/index.ts index 4cee9f7143..315bd1732a 100644 --- a/plugins/explore/src/components/index.ts +++ b/plugins/explore/src/components/index.ts @@ -15,5 +15,7 @@ */ export * from './EntityExplorerContent'; +export * from './GroupsExplorerContent'; +export * from './DefaultExplorePage'; export * from './ExploreLayout'; export * from './ToolSearchResultListItem'; From 3373cc3e5e2451641054889efacdd44ead01ffcb Mon Sep 17 00:00:00 2001 From: sblausten Date: Thu, 22 Dec 2022 11:45:59 +0100 Subject: [PATCH 005/247] remove duplicate exports Signed-off-by: sblausten --- plugins/explore/src/components/index.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/explore/src/components/index.ts b/plugins/explore/src/components/index.ts index 315bd1732a..96175770fa 100644 --- a/plugins/explore/src/components/index.ts +++ b/plugins/explore/src/components/index.ts @@ -14,8 +14,5 @@ * limitations under the License. */ -export * from './EntityExplorerContent'; -export * from './GroupsExplorerContent'; -export * from './DefaultExplorePage'; export * from './ExploreLayout'; export * from './ToolSearchResultListItem'; From 7d308d65c7ea15aa7a71b684b7e91e97a2330bc8 Mon Sep 17 00:00:00 2001 From: sblausten Date: Thu, 22 Dec 2022 14:31:21 +0100 Subject: [PATCH 006/247] Fix test and update api-report Signed-off-by: sblausten --- plugins/explore/api-report.md | 4 ---- .../components/DefaultExplorePage/DefaultExplorePage.test.tsx | 3 ++- plugins/explore/src/components/index.ts | 1 + 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/plugins/explore/api-report.md b/plugins/explore/api-report.md index d184ce01a4..d10f5ddf95 100644 --- a/plugins/explore/api-report.md +++ b/plugins/explore/api-report.md @@ -9,7 +9,6 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { default as default_2 } from 'react'; import { DiscoveryApi } from '@backstage/core-plugin-api'; -import { DomainEntity } from '@backstage/catalog-model'; import { ExploreToolsConfig } from '@backstage/plugin-explore-react'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; @@ -31,9 +30,6 @@ export const catalogEntityRouteRef: ExternalRouteRef< true >; -// @public (undocumented) -export const DomainCard: (props: { entity: DomainEntity }) => JSX.Element; - // @public (undocumented) export const DomainExplorerContent: (props: { title?: string | undefined; diff --git a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx index 62d197803f..f2533dbc40 100644 --- a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx +++ b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx @@ -56,8 +56,9 @@ describe('', () => { await waitFor(() => { const elements = getAllByRole('tab'); - expect(elements.length).toBe(3); + expect(elements.length).toBe(4); expect(getByText(elements[0], 'Domains')).toBeInTheDocument(); + expect(getByText(elements[1], 'Systems')).toBeInTheDocument(); expect(getByText(elements[1], 'Groups')).toBeInTheDocument(); expect(getByText(elements[2], 'Tools')).toBeInTheDocument(); }); diff --git a/plugins/explore/src/components/index.ts b/plugins/explore/src/components/index.ts index 96175770fa..4cee9f7143 100644 --- a/plugins/explore/src/components/index.ts +++ b/plugins/explore/src/components/index.ts @@ -14,5 +14,6 @@ * limitations under the License. */ +export * from './EntityExplorerContent'; export * from './ExploreLayout'; export * from './ToolSearchResultListItem'; From b3e213629cf1b965e3da36d75887f1070f775664 Mon Sep 17 00:00:00 2001 From: sblausten Date: Tue, 3 Jan 2023 15:11:57 +0100 Subject: [PATCH 007/247] Fix test Signed-off-by: sblausten --- .../components/DefaultExplorePage/DefaultExplorePage.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx index f2533dbc40..c4c6b3011e 100644 --- a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx +++ b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx @@ -59,8 +59,8 @@ describe('', () => { expect(elements.length).toBe(4); expect(getByText(elements[0], 'Domains')).toBeInTheDocument(); expect(getByText(elements[1], 'Systems')).toBeInTheDocument(); - expect(getByText(elements[1], 'Groups')).toBeInTheDocument(); - expect(getByText(elements[2], 'Tools')).toBeInTheDocument(); + expect(getByText(elements[2], 'Groups')).toBeInTheDocument(); + expect(getByText(elements[3], 'Tools')).toBeInTheDocument(); }); }); }); From 6a5dc558f69d521ac63fc0820b8631262cc45673 Mon Sep 17 00:00:00 2001 From: sblausten Date: Tue, 3 Jan 2023 15:47:53 +0100 Subject: [PATCH 008/247] Build api reports Signed-off-by: sblausten --- packages/backend-app-api/api-report.md | 5 - packages/backend-common/api-report.md | 87 ++++++++++++----- packages/backend-plugin-api/api-report.md | 94 ++----------------- packages/catalog-client/api-report.md | 12 --- plugins/apollo-explorer/api-report.md | 2 +- plugins/bazaar/api-report.md | 10 -- .../api-report.md | 8 +- plugins/code-coverage-backend/api-report.md | 3 - plugins/explore/api-report.md | 4 + plugins/rollbar-backend/api-report.md | 6 +- plugins/scaffolder-backend/api-report.md | 40 +++----- plugins/scaffolder/api-report.md | 34 ++----- plugins/techdocs-react/api-report.md | 3 - 13 files changed, 99 insertions(+), 209 deletions(-) diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index ef88d9a9d0..33e732cf05 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -83,11 +83,6 @@ export const permissionsFactory: ( options?: undefined, ) => ServiceFactory; -// @public -export const rootLifecycleFactory: ( - options?: undefined, -) => ServiceFactory; - // @public (undocumented) export const rootLoggerFactory: ( options?: undefined, diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index e8b41e21bb..784f3295c6 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -30,23 +30,12 @@ import { KubeConfig } from '@kubernetes/client-node'; import { LoadConfigOptionsRemote } from '@backstage/config-loader'; import { Logger } from 'winston'; import { MergeResult } from 'isomorphic-git'; -import { DiscoveryService as PluginEndpointDiscovery } from '@backstage/backend-plugin-api'; import { PushResult } from 'isomorphic-git'; import { Readable } from 'stream'; import { ReadCommitResult } from 'isomorphic-git'; -import { ReadTreeOptions } from '@backstage/backend-plugin-api'; -import { ReadTreeResponse } from '@backstage/backend-plugin-api'; -import { ReadTreeResponseDirOptions } from '@backstage/backend-plugin-api'; -import { ReadTreeResponseFile } from '@backstage/backend-plugin-api'; -import { ReadUrlOptions } from '@backstage/backend-plugin-api'; -import { ReadUrlResponse } from '@backstage/backend-plugin-api'; import { RequestHandler } from 'express'; import { Router } from 'express'; -import { SearchOptions } from '@backstage/backend-plugin-api'; -import { SearchResponse } from '@backstage/backend-plugin-api'; -import { SearchResponseFile } from '@backstage/backend-plugin-api'; import { Server } from 'http'; -import { UrlReaderService as UrlReader } from '@backstage/backend-plugin-api'; import { V1PodTemplateSpec } from '@kubernetes/client-node'; import * as winston from 'winston'; import { Writable } from 'stream'; @@ -531,7 +520,11 @@ export interface PluginDatabaseManager { }; } -export { PluginEndpointDiscovery }; +// @public +export type PluginEndpointDiscovery = { + getBaseUrl(pluginId: string): Promise; + getExternalBaseUrl(pluginId: string): Promise; +}; // @public export type ReaderFactory = (options: { @@ -540,11 +533,30 @@ export type ReaderFactory = (options: { treeResponseFactory: ReadTreeResponseFactory; }) => UrlReaderPredicateTuple[]; -export { ReadTreeOptions }; +// @public +export type ReadTreeOptions = { + filter?( + path: string, + info?: { + size: number; + }, + ): boolean; + etag?: string; + signal?: AbortSignal; +}; -export { ReadTreeResponse }; +// @public +export type ReadTreeResponse = { + files(): Promise; + archive(): Promise; + dir(options?: ReadTreeResponseDirOptions): Promise; + etag: string; +}; -export { ReadTreeResponseDirOptions }; +// @public +export type ReadTreeResponseDirOptions = { + targetDir?: string; +}; // @public export interface ReadTreeResponseFactory { @@ -575,11 +587,24 @@ export type ReadTreeResponseFactoryOptions = { ) => boolean; }; -export { ReadTreeResponseFile }; +// @public +export type ReadTreeResponseFile = { + path: string; + content(): Promise; +}; -export { ReadUrlOptions }; +// @public +export type ReadUrlOptions = { + etag?: string; + signal?: AbortSignal; +}; -export { ReadUrlResponse }; +// @public +export type ReadUrlResponse = { + buffer(): Promise; + stream?(): Readable; + etag?: string; +}; // @public export class ReadUrlResponseFactory { @@ -627,11 +652,23 @@ export type RunContainerOptions = { pullImage?: boolean; }; -export { SearchOptions }; +// @public +export type SearchOptions = { + etag?: string; + signal?: AbortSignal; +}; -export { SearchResponse }; +// @public +export type SearchResponse = { + files: SearchResponseFile[]; + etag: string; +}; -export { SearchResponseFile }; +// @public +export type SearchResponseFile = { + url: string; + content(): Promise; +}; // @public export class ServerTokenManager implements TokenManager { @@ -718,7 +755,13 @@ export interface TokenManager { }>; } -export { UrlReader }; +// @public +export type UrlReader = { + read(url: string): Promise; + readUrl(url: string, options?: ReadUrlOptions): Promise; + readTree(url: string, options?: ReadTreeOptions): Promise; + search(url: string, options?: SearchOptions): Promise; +}; // @public export type UrlReaderPredicateTuple = { diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index e0fa44eb20..545728b386 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -3,8 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// - import { Config } from '@backstage/config'; import { Handler } from 'express'; import { Logger } from 'winston'; @@ -12,10 +10,11 @@ import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { PluginCacheManager } from '@backstage/backend-common'; import { PluginDatabaseManager } from '@backstage/backend-common'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; -import { Readable } from 'stream'; import { TokenManager } from '@backstage/backend-common'; import { TransportStreamOptions } from 'winston-transport'; +import { UrlReader } from '@backstage/backend-common'; // @public (undocumented) export interface BackendFeature { @@ -90,7 +89,6 @@ declare namespace coreServices { tokenManagerServiceRef as tokenManager, permissionsServiceRef as permissions, schedulerServiceRef as scheduler, - rootLifecycleServiceRef as rootLifecycle, rootLoggerServiceRef as rootLogger, pluginMetadataServiceRef as pluginMetadata, lifecycleServiceRef as lifecycle, @@ -168,14 +166,11 @@ export type DatabaseService = PluginDatabaseManager; // @public (undocumented) const databaseServiceRef: ServiceRef; -// @public -export type DiscoveryService = { - getBaseUrl(pluginId: string): Promise; - getExternalBaseUrl(pluginId: string): Promise; -}; +// @public (undocumented) +export type DiscoveryService = PluginEndpointDiscovery; // @public (undocumented) -const discoveryServiceRef: ServiceRef; +const discoveryServiceRef: ServiceRef; // @public export type ExtensionPoint = { @@ -205,7 +200,6 @@ const lifecycleServiceRef: ServiceRef; // @public (undocumented) export type LifecycleServiceShutdownHook = { fn: () => void | Promise; - labels?: Record; }; // @public (undocumented) @@ -251,56 +245,6 @@ export interface PluginMetadataService { // @public (undocumented) const pluginMetadataServiceRef: ServiceRef; -// @public -export type ReadTreeOptions = { - filter?( - path: string, - info?: { - size: number; - }, - ): boolean; - etag?: string; - signal?: AbortSignal; -}; - -// @public -export type ReadTreeResponse = { - files(): Promise; - archive(): Promise; - dir(options?: ReadTreeResponseDirOptions): Promise; - etag: string; -}; - -// @public -export type ReadTreeResponseDirOptions = { - targetDir?: string; -}; - -// @public -export type ReadTreeResponseFile = { - path: string; - content(): Promise; -}; - -// @public -export type ReadUrlOptions = { - etag?: string; - signal?: AbortSignal; -}; - -// @public -export type ReadUrlResponse = { - buffer(): Promise; - stream?(): Readable; - etag?: string; -}; - -// @public (undocumented) -export type RootLifecycleService = LifecycleService; - -// @public (undocumented) -const rootLifecycleServiceRef: ServiceRef; - // @public (undocumented) export type RootLoggerService = LoggerService; @@ -313,24 +257,6 @@ export type SchedulerService = PluginTaskScheduler; // @public (undocumented) const schedulerServiceRef: ServiceRef; -// @public -export type SearchOptions = { - etag?: string; - signal?: AbortSignal; -}; - -// @public -export type SearchResponse = { - files: SearchResponseFile[]; - etag: string; -}; - -// @public -export type SearchResponseFile = { - url: string; - content(): Promise; -}; - // @public (undocumented) export type ServiceFactory = | { @@ -381,13 +307,9 @@ export type TypesToServiceRef = { [key in keyof T]: ServiceRef; }; -// @public -export type UrlReaderService = { - readUrl(url: string, options?: ReadUrlOptions): Promise; - readTree(url: string, options?: ReadTreeOptions): Promise; - search(url: string, options?: SearchOptions): Promise; -}; +// @public (undocumented) +export type UrlReaderService = UrlReader; // @public (undocumented) -const urlReaderServiceRef: ServiceRef; +const urlReaderServiceRef: ServiceRef; ``` diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md index a81314308b..2f76be79e2 100644 --- a/packages/catalog-client/api-report.md +++ b/packages/catalog-client/api-report.md @@ -161,17 +161,6 @@ export type EntityFilterQuery = | Record[] | Record; -// @public -export type EntityOrderQuery = - | { - field: string; - order: 'asc' | 'desc'; - } - | Array<{ - field: string; - order: 'asc' | 'desc'; - }>; - // @public export interface GetEntitiesByRefsRequest { entityRefs: string[]; @@ -190,7 +179,6 @@ export interface GetEntitiesRequest { filter?: EntityFilterQuery; limit?: number; offset?: number; - order?: EntityOrderQuery; } // @public diff --git a/plugins/apollo-explorer/api-report.md b/plugins/apollo-explorer/api-report.md index 1d3b41eac1..7272903b0c 100644 --- a/plugins/apollo-explorer/api-report.md +++ b/plugins/apollo-explorer/api-report.md @@ -25,7 +25,7 @@ export const ApolloExplorerPage: (props: { displayOptions: { docsPanelState?: 'closed' | 'open' | undefined; showHeadersAndEnvVars?: boolean | undefined; - theme?: 'dark' | 'light' | undefined; + theme?: 'light' | 'dark' | undefined; }; } | undefined; diff --git a/plugins/bazaar/api-report.md b/plugins/bazaar/api-report.md index afbb7b5504..d7c463888a 100644 --- a/plugins/bazaar/api-report.md +++ b/plugins/bazaar/api-report.md @@ -5,9 +5,7 @@ ```ts /// -import { ApiHolder } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; -import { Entity } from '@backstage/catalog-model'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) @@ -38,14 +36,6 @@ export const bazaarPlugin: BackstagePlugin< // @public (undocumented) export const EntityBazaarInfoCard: () => JSX.Element | null; -// @public (undocumented) -export const isBazaarAvailable: ( - entity: Entity, - context: { - apis: ApiHolder; - }, -) => Promise; - // @public (undocumented) export const SortView: () => JSX.Element; diff --git a/plugins/catalog-backend-module-github/api-report.md b/plugins/catalog-backend-module-github/api-report.md index 78ffd5b018..8ccfdb09ee 100644 --- a/plugins/catalog-backend-module-github/api-report.md +++ b/plugins/catalog-backend-module-github/api-report.md @@ -178,9 +178,7 @@ export class GitHubOrgEntityProvider extends GithubOrgEntityProvider { } // @public -export class GithubOrgEntityProvider - implements EntityProvider, EventSubscriber -{ +export class GithubOrgEntityProvider implements EntityProvider { constructor(options: { id: string; orgUrl: string; @@ -199,11 +197,7 @@ export class GithubOrgEntityProvider ): GithubOrgEntityProvider; // (undocumented) getProviderName(): string; - // (undocumented) - onEvent(params: EventParams): Promise; read(options?: { logger?: Logger }): Promise; - // (undocumented) - supportsEventTopics(): string[]; } // @public @deprecated (undocumented) diff --git a/plugins/code-coverage-backend/api-report.md b/plugins/code-coverage-backend/api-report.md index 440af50714..c33874064b 100644 --- a/plugins/code-coverage-backend/api-report.md +++ b/plugins/code-coverage-backend/api-report.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import express from 'express'; import { Logger } from 'winston'; @@ -16,8 +15,6 @@ export function createRouter(options: RouterOptions): Promise; // @public export interface RouterOptions { - // (undocumented) - catalogApi?: CatalogApi; // (undocumented) config: Config; // (undocumented) diff --git a/plugins/explore/api-report.md b/plugins/explore/api-report.md index d10f5ddf95..d184ce01a4 100644 --- a/plugins/explore/api-report.md +++ b/plugins/explore/api-report.md @@ -9,6 +9,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { default as default_2 } from 'react'; import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { DomainEntity } from '@backstage/catalog-model'; import { ExploreToolsConfig } from '@backstage/plugin-explore-react'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; @@ -30,6 +31,9 @@ export const catalogEntityRouteRef: ExternalRouteRef< true >; +// @public (undocumented) +export const DomainCard: (props: { entity: DomainEntity }) => JSX.Element; + // @public (undocumented) export const DomainExplorerContent: (props: { title?: string | undefined; diff --git a/plugins/rollbar-backend/api-report.md b/plugins/rollbar-backend/api-report.md index 58a5fb0252..9086121306 100644 --- a/plugins/rollbar-backend/api-report.md +++ b/plugins/rollbar-backend/api-report.md @@ -29,8 +29,8 @@ export class RollbarApi { }, ): Promise< { - count: number; timestamp: number; + count: number; }[] >; // (undocumented) @@ -51,8 +51,8 @@ export class RollbarApi { }, ): Promise< { - count: number; timestamp: number; + count: number; }[] >; // (undocumented) @@ -64,8 +64,8 @@ export class RollbarApi { }>; // (undocumented) getProjectItems(projectName: string): Promise<{ - page: number; items: RollbarItem[]; + page: number; totalCount: number; }>; // (undocumented) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 66757a01cd..94377d7720 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -195,12 +195,6 @@ export function createGithubRepoCreateAction(options: { gitAuthorEmail?: string | undefined; allowRebaseMerge?: boolean | undefined; allowSquashMerge?: boolean | undefined; - squashMergeCommitTitle?: 'PR_TITLE' | 'COMMIT_OR_PR_TITLE' | undefined; - squashMergeCommitMessage?: - | 'PR_BODY' - | 'COMMIT_MESSAGES' - | 'BLANK' - | undefined; allowMergeCommit?: boolean | undefined; allowAutoMerge?: boolean | undefined; requireCodeOwnerReviews?: boolean | undefined; @@ -214,16 +208,16 @@ export function createGithubRepoCreateAction(options: { requiredStatusCheckContexts?: string[] | undefined; requireBranchesToBeUpToDate?: boolean | undefined; requiredConversationResolution?: boolean | undefined; - repoVisibility?: 'internal' | 'private' | 'public' | undefined; + repoVisibility?: 'public' | 'private' | 'internal' | undefined; collaborators?: | ( | { user: string; - access: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; } | { team: string; - access: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; } | { username: string; @@ -231,9 +225,6 @@ export function createGithubRepoCreateAction(options: { } )[] | undefined; - hasProjects?: boolean | undefined; - hasWiki?: boolean | undefined; - hasIssues?: boolean | undefined; token?: string | undefined; topics?: string[] | undefined; }>; @@ -307,7 +298,7 @@ export function createPublishBitbucketAction(options: { repoUrl: string; description?: string | undefined; defaultBranch?: string | undefined; - repoVisibility?: 'private' | 'public' | undefined; + repoVisibility?: 'public' | 'private' | undefined; sourcePath?: string | undefined; enableLFS?: boolean | undefined; token?: string | undefined; @@ -324,7 +315,7 @@ export function createPublishBitbucketCloudAction(options: { repoUrl: string; description?: string | undefined; defaultBranch?: string | undefined; - repoVisibility?: 'private' | 'public' | undefined; + repoVisibility?: 'public' | 'private' | undefined; sourcePath?: string | undefined; token?: string | undefined; }>; @@ -337,7 +328,7 @@ export function createPublishBitbucketServerAction(options: { repoUrl: string; description?: string | undefined; defaultBranch?: string | undefined; - repoVisibility?: 'private' | 'public' | undefined; + repoVisibility?: 'public' | 'private' | undefined; sourcePath?: string | undefined; enableLFS?: boolean | undefined; token?: string | undefined; @@ -389,12 +380,6 @@ export function createPublishGithubAction(options: { gitAuthorEmail?: string | undefined; allowRebaseMerge?: boolean | undefined; allowSquashMerge?: boolean | undefined; - squashMergeCommitTitle?: 'PR_TITLE' | 'COMMIT_OR_PR_TITLE' | undefined; - squashMergeCommitMessage?: - | 'PR_BODY' - | 'COMMIT_MESSAGES' - | 'BLANK' - | undefined; allowMergeCommit?: boolean | undefined; allowAutoMerge?: boolean | undefined; sourcePath?: string | undefined; @@ -410,16 +395,16 @@ export function createPublishGithubAction(options: { requiredStatusCheckContexts?: string[] | undefined; requireBranchesToBeUpToDate?: boolean | undefined; requiredConversationResolution?: boolean | undefined; - repoVisibility?: 'internal' | 'private' | 'public' | undefined; + repoVisibility?: 'public' | 'private' | 'internal' | undefined; collaborators?: | ( | { user: string; - access: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; } | { team: string; - access: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; } | { username: string; @@ -427,9 +412,6 @@ export function createPublishGithubAction(options: { } )[] | undefined; - hasProjects?: boolean | undefined; - hasWiki?: boolean | undefined; - hasIssues?: boolean | undefined; token?: string | undefined; topics?: string[] | undefined; }>; @@ -459,7 +441,7 @@ export function createPublishGitlabAction(options: { }): TemplateAction<{ repoUrl: string; defaultBranch?: string | undefined; - repoVisibility?: 'internal' | 'private' | 'public' | undefined; + repoVisibility?: 'public' | 'private' | 'internal' | undefined; sourcePath?: string | undefined; token?: string | undefined; gitCommitMessage?: string | undefined; @@ -480,7 +462,7 @@ export const createPublishGitlabMergeRequestAction: (options: { sourcePath?: string | undefined; targetPath?: string | undefined; token?: string | undefined; - commitAction?: 'update' | 'delete' | 'create' | undefined; + commitAction?: 'create' | 'update' | 'delete' | undefined; projectid?: string | undefined; removeSourceBranch?: boolean | undefined; assignee?: string | undefined; diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 1c4d163123..d936542cfe 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -11,6 +11,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import type { ErrorTransformer } from '@rjsf/utils'; import { Extension } from '@backstage/core-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; @@ -18,8 +19,7 @@ import { FieldProps } from '@rjsf/core'; import { FieldProps as FieldProps_2 } from '@rjsf/utils'; import { FieldValidation } from '@rjsf/core'; import { FieldValidation as FieldValidation_2 } from '@rjsf/utils'; -import type { FormProps as FormProps_2 } from '@rjsf/core'; -import type { FormProps as FormProps_3 } from '@rjsf/core-v5'; +import type { FormProps } from '@rjsf/core'; import { IdentityApi } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; import { JSONSchema7 } from 'json-schema'; @@ -88,10 +88,6 @@ export const EntityPickerFieldExtension: FieldExtensionComponent< defaultNamespace?: string | false | undefined; allowedKinds?: string[] | undefined; allowArbitraryValues?: boolean | undefined; - catalogFilter?: - | Record - | Record[] - | undefined; } >; @@ -103,10 +99,6 @@ export const EntityPickerFieldSchema: FieldSchema< defaultNamespace?: string | false | undefined; allowedKinds?: string[] | undefined; allowArbitraryValues?: boolean | undefined; - catalogFilter?: - | Record - | Record[] - | undefined; } >; @@ -175,12 +167,6 @@ export interface FieldSchema { readonly uiOptionsType: TUiOptions; } -// @alpha -export type FormProps = Pick< - FormProps_3, - 'transformErrors' | 'noHtml5Validate' ->; - // @public export type LayoutComponent<_TInputProps> = () => null; @@ -193,7 +179,7 @@ export interface LayoutOptions

{ } // @public -export type LayoutTemplate = FormProps_2['ObjectFieldTemplate']; +export type LayoutTemplate = FormProps['ObjectFieldTemplate']; // @public export type ListActionsResponse = Array<{ @@ -278,7 +264,7 @@ export type NextRouterProps = { TaskPageComponent?: React_2.ComponentType<{}>; }; groups?: TemplateGroupFilter[]; - FormProps?: FormProps; + transformErrors?: ErrorTransformer; }; // @alpha @@ -324,10 +310,6 @@ export const OwnerPickerFieldExtension: FieldExtensionComponent< defaultNamespace?: string | false | undefined; allowedKinds?: string[] | undefined; allowArbitraryValues?: boolean | undefined; - catalogFilter?: - | Record - | Record[] - | undefined; } >; @@ -338,10 +320,6 @@ export const OwnerPickerFieldSchema: FieldSchema< defaultNamespace?: string | false | undefined; allowedKinds?: string[] | undefined; allowArbitraryValues?: boolean | undefined; - catalogFilter?: - | Record - | Record[] - | undefined; } >; @@ -370,10 +348,10 @@ export const RepoUrlPickerFieldExtension: FieldExtensionComponent< | { additionalScopes?: | { - azure?: string[] | undefined; github?: string[] | undefined; gitlab?: string[] | undefined; bitbucket?: string[] | undefined; + azure?: string[] | undefined; gerrit?: string[] | undefined; } | undefined; @@ -396,10 +374,10 @@ export const RepoUrlPickerFieldSchema: FieldSchema< | { additionalScopes?: | { - azure?: string[] | undefined; github?: string[] | undefined; gitlab?: string[] | undefined; bitbucket?: string[] | undefined; + azure?: string[] | undefined; gerrit?: string[] | undefined; } | undefined; diff --git a/plugins/techdocs-react/api-report.md b/plugins/techdocs-react/api-report.md index f1d6d3f177..09da4e012c 100644 --- a/plugins/techdocs-react/api-report.md +++ b/plugins/techdocs-react/api-report.md @@ -32,9 +32,6 @@ export const SHADOW_DOM_STYLE_LOAD_EVENT = 'TECH_DOCS_SHADOW_DOM_STYLE_LOAD'; // @public export type SyncResult = 'cached' | 'updated'; -// @public -export const TECHDOCS_ADDONS_KEY = 'techdocs.addons.addon.v1'; - // @public export const TECHDOCS_ADDONS_WRAPPER_KEY = 'techdocs.addons.wrapper.v1'; From 268f58aea16d52b1e2173c4a1602ecc81d493b07 Mon Sep 17 00:00:00 2001 From: sblausten Date: Tue, 3 Jan 2023 15:51:55 +0100 Subject: [PATCH 009/247] Revert "Build api reports" This reverts commit 6a5dc558f69d521ac63fc0820b8631262cc45673. Signed-off-by: sblausten --- packages/backend-app-api/api-report.md | 5 + packages/backend-common/api-report.md | 87 +++++------------ packages/backend-plugin-api/api-report.md | 94 +++++++++++++++++-- packages/catalog-client/api-report.md | 12 +++ plugins/apollo-explorer/api-report.md | 2 +- plugins/bazaar/api-report.md | 10 ++ .../api-report.md | 8 +- plugins/code-coverage-backend/api-report.md | 3 + plugins/explore/api-report.md | 4 - plugins/rollbar-backend/api-report.md | 6 +- plugins/scaffolder-backend/api-report.md | 40 +++++--- plugins/scaffolder/api-report.md | 34 +++++-- plugins/techdocs-react/api-report.md | 3 + 13 files changed, 209 insertions(+), 99 deletions(-) diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index 33e732cf05..ef88d9a9d0 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -83,6 +83,11 @@ export const permissionsFactory: ( options?: undefined, ) => ServiceFactory; +// @public +export const rootLifecycleFactory: ( + options?: undefined, +) => ServiceFactory; + // @public (undocumented) export const rootLoggerFactory: ( options?: undefined, diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 784f3295c6..e8b41e21bb 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -30,12 +30,23 @@ import { KubeConfig } from '@kubernetes/client-node'; import { LoadConfigOptionsRemote } from '@backstage/config-loader'; import { Logger } from 'winston'; import { MergeResult } from 'isomorphic-git'; +import { DiscoveryService as PluginEndpointDiscovery } from '@backstage/backend-plugin-api'; import { PushResult } from 'isomorphic-git'; import { Readable } from 'stream'; import { ReadCommitResult } from 'isomorphic-git'; +import { ReadTreeOptions } from '@backstage/backend-plugin-api'; +import { ReadTreeResponse } from '@backstage/backend-plugin-api'; +import { ReadTreeResponseDirOptions } from '@backstage/backend-plugin-api'; +import { ReadTreeResponseFile } from '@backstage/backend-plugin-api'; +import { ReadUrlOptions } from '@backstage/backend-plugin-api'; +import { ReadUrlResponse } from '@backstage/backend-plugin-api'; import { RequestHandler } from 'express'; import { Router } from 'express'; +import { SearchOptions } from '@backstage/backend-plugin-api'; +import { SearchResponse } from '@backstage/backend-plugin-api'; +import { SearchResponseFile } from '@backstage/backend-plugin-api'; import { Server } from 'http'; +import { UrlReaderService as UrlReader } from '@backstage/backend-plugin-api'; import { V1PodTemplateSpec } from '@kubernetes/client-node'; import * as winston from 'winston'; import { Writable } from 'stream'; @@ -520,11 +531,7 @@ export interface PluginDatabaseManager { }; } -// @public -export type PluginEndpointDiscovery = { - getBaseUrl(pluginId: string): Promise; - getExternalBaseUrl(pluginId: string): Promise; -}; +export { PluginEndpointDiscovery }; // @public export type ReaderFactory = (options: { @@ -533,30 +540,11 @@ export type ReaderFactory = (options: { treeResponseFactory: ReadTreeResponseFactory; }) => UrlReaderPredicateTuple[]; -// @public -export type ReadTreeOptions = { - filter?( - path: string, - info?: { - size: number; - }, - ): boolean; - etag?: string; - signal?: AbortSignal; -}; +export { ReadTreeOptions }; -// @public -export type ReadTreeResponse = { - files(): Promise; - archive(): Promise; - dir(options?: ReadTreeResponseDirOptions): Promise; - etag: string; -}; +export { ReadTreeResponse }; -// @public -export type ReadTreeResponseDirOptions = { - targetDir?: string; -}; +export { ReadTreeResponseDirOptions }; // @public export interface ReadTreeResponseFactory { @@ -587,24 +575,11 @@ export type ReadTreeResponseFactoryOptions = { ) => boolean; }; -// @public -export type ReadTreeResponseFile = { - path: string; - content(): Promise; -}; +export { ReadTreeResponseFile }; -// @public -export type ReadUrlOptions = { - etag?: string; - signal?: AbortSignal; -}; +export { ReadUrlOptions }; -// @public -export type ReadUrlResponse = { - buffer(): Promise; - stream?(): Readable; - etag?: string; -}; +export { ReadUrlResponse }; // @public export class ReadUrlResponseFactory { @@ -652,23 +627,11 @@ export type RunContainerOptions = { pullImage?: boolean; }; -// @public -export type SearchOptions = { - etag?: string; - signal?: AbortSignal; -}; +export { SearchOptions }; -// @public -export type SearchResponse = { - files: SearchResponseFile[]; - etag: string; -}; +export { SearchResponse }; -// @public -export type SearchResponseFile = { - url: string; - content(): Promise; -}; +export { SearchResponseFile }; // @public export class ServerTokenManager implements TokenManager { @@ -755,13 +718,7 @@ export interface TokenManager { }>; } -// @public -export type UrlReader = { - read(url: string): Promise; - readUrl(url: string, options?: ReadUrlOptions): Promise; - readTree(url: string, options?: ReadTreeOptions): Promise; - search(url: string, options?: SearchOptions): Promise; -}; +export { UrlReader }; // @public export type UrlReaderPredicateTuple = { diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index 545728b386..e0fa44eb20 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -3,6 +3,8 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// + import { Config } from '@backstage/config'; import { Handler } from 'express'; import { Logger } from 'winston'; @@ -10,11 +12,10 @@ import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { PluginCacheManager } from '@backstage/backend-common'; import { PluginDatabaseManager } from '@backstage/backend-common'; -import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; +import { Readable } from 'stream'; import { TokenManager } from '@backstage/backend-common'; import { TransportStreamOptions } from 'winston-transport'; -import { UrlReader } from '@backstage/backend-common'; // @public (undocumented) export interface BackendFeature { @@ -89,6 +90,7 @@ declare namespace coreServices { tokenManagerServiceRef as tokenManager, permissionsServiceRef as permissions, schedulerServiceRef as scheduler, + rootLifecycleServiceRef as rootLifecycle, rootLoggerServiceRef as rootLogger, pluginMetadataServiceRef as pluginMetadata, lifecycleServiceRef as lifecycle, @@ -166,11 +168,14 @@ export type DatabaseService = PluginDatabaseManager; // @public (undocumented) const databaseServiceRef: ServiceRef; -// @public (undocumented) -export type DiscoveryService = PluginEndpointDiscovery; +// @public +export type DiscoveryService = { + getBaseUrl(pluginId: string): Promise; + getExternalBaseUrl(pluginId: string): Promise; +}; // @public (undocumented) -const discoveryServiceRef: ServiceRef; +const discoveryServiceRef: ServiceRef; // @public export type ExtensionPoint = { @@ -200,6 +205,7 @@ const lifecycleServiceRef: ServiceRef; // @public (undocumented) export type LifecycleServiceShutdownHook = { fn: () => void | Promise; + labels?: Record; }; // @public (undocumented) @@ -245,6 +251,56 @@ export interface PluginMetadataService { // @public (undocumented) const pluginMetadataServiceRef: ServiceRef; +// @public +export type ReadTreeOptions = { + filter?( + path: string, + info?: { + size: number; + }, + ): boolean; + etag?: string; + signal?: AbortSignal; +}; + +// @public +export type ReadTreeResponse = { + files(): Promise; + archive(): Promise; + dir(options?: ReadTreeResponseDirOptions): Promise; + etag: string; +}; + +// @public +export type ReadTreeResponseDirOptions = { + targetDir?: string; +}; + +// @public +export type ReadTreeResponseFile = { + path: string; + content(): Promise; +}; + +// @public +export type ReadUrlOptions = { + etag?: string; + signal?: AbortSignal; +}; + +// @public +export type ReadUrlResponse = { + buffer(): Promise; + stream?(): Readable; + etag?: string; +}; + +// @public (undocumented) +export type RootLifecycleService = LifecycleService; + +// @public (undocumented) +const rootLifecycleServiceRef: ServiceRef; + // @public (undocumented) export type RootLoggerService = LoggerService; @@ -257,6 +313,24 @@ export type SchedulerService = PluginTaskScheduler; // @public (undocumented) const schedulerServiceRef: ServiceRef; +// @public +export type SearchOptions = { + etag?: string; + signal?: AbortSignal; +}; + +// @public +export type SearchResponse = { + files: SearchResponseFile[]; + etag: string; +}; + +// @public +export type SearchResponseFile = { + url: string; + content(): Promise; +}; + // @public (undocumented) export type ServiceFactory = | { @@ -307,9 +381,13 @@ export type TypesToServiceRef = { [key in keyof T]: ServiceRef; }; -// @public (undocumented) -export type UrlReaderService = UrlReader; +// @public +export type UrlReaderService = { + readUrl(url: string, options?: ReadUrlOptions): Promise; + readTree(url: string, options?: ReadTreeOptions): Promise; + search(url: string, options?: SearchOptions): Promise; +}; // @public (undocumented) -const urlReaderServiceRef: ServiceRef; +const urlReaderServiceRef: ServiceRef; ``` diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md index 2f76be79e2..a81314308b 100644 --- a/packages/catalog-client/api-report.md +++ b/packages/catalog-client/api-report.md @@ -161,6 +161,17 @@ export type EntityFilterQuery = | Record[] | Record; +// @public +export type EntityOrderQuery = + | { + field: string; + order: 'asc' | 'desc'; + } + | Array<{ + field: string; + order: 'asc' | 'desc'; + }>; + // @public export interface GetEntitiesByRefsRequest { entityRefs: string[]; @@ -179,6 +190,7 @@ export interface GetEntitiesRequest { filter?: EntityFilterQuery; limit?: number; offset?: number; + order?: EntityOrderQuery; } // @public diff --git a/plugins/apollo-explorer/api-report.md b/plugins/apollo-explorer/api-report.md index 7272903b0c..1d3b41eac1 100644 --- a/plugins/apollo-explorer/api-report.md +++ b/plugins/apollo-explorer/api-report.md @@ -25,7 +25,7 @@ export const ApolloExplorerPage: (props: { displayOptions: { docsPanelState?: 'closed' | 'open' | undefined; showHeadersAndEnvVars?: boolean | undefined; - theme?: 'light' | 'dark' | undefined; + theme?: 'dark' | 'light' | undefined; }; } | undefined; diff --git a/plugins/bazaar/api-report.md b/plugins/bazaar/api-report.md index d7c463888a..afbb7b5504 100644 --- a/plugins/bazaar/api-report.md +++ b/plugins/bazaar/api-report.md @@ -5,7 +5,9 @@ ```ts /// +import { ApiHolder } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { Entity } from '@backstage/catalog-model'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) @@ -36,6 +38,14 @@ export const bazaarPlugin: BackstagePlugin< // @public (undocumented) export const EntityBazaarInfoCard: () => JSX.Element | null; +// @public (undocumented) +export const isBazaarAvailable: ( + entity: Entity, + context: { + apis: ApiHolder; + }, +) => Promise; + // @public (undocumented) export const SortView: () => JSX.Element; diff --git a/plugins/catalog-backend-module-github/api-report.md b/plugins/catalog-backend-module-github/api-report.md index 8ccfdb09ee..78ffd5b018 100644 --- a/plugins/catalog-backend-module-github/api-report.md +++ b/plugins/catalog-backend-module-github/api-report.md @@ -178,7 +178,9 @@ export class GitHubOrgEntityProvider extends GithubOrgEntityProvider { } // @public -export class GithubOrgEntityProvider implements EntityProvider { +export class GithubOrgEntityProvider + implements EntityProvider, EventSubscriber +{ constructor(options: { id: string; orgUrl: string; @@ -197,7 +199,11 @@ export class GithubOrgEntityProvider implements EntityProvider { ): GithubOrgEntityProvider; // (undocumented) getProviderName(): string; + // (undocumented) + onEvent(params: EventParams): Promise; read(options?: { logger?: Logger }): Promise; + // (undocumented) + supportsEventTopics(): string[]; } // @public @deprecated (undocumented) diff --git a/plugins/code-coverage-backend/api-report.md b/plugins/code-coverage-backend/api-report.md index c33874064b..440af50714 100644 --- a/plugins/code-coverage-backend/api-report.md +++ b/plugins/code-coverage-backend/api-report.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import express from 'express'; import { Logger } from 'winston'; @@ -15,6 +16,8 @@ export function createRouter(options: RouterOptions): Promise; // @public export interface RouterOptions { + // (undocumented) + catalogApi?: CatalogApi; // (undocumented) config: Config; // (undocumented) diff --git a/plugins/explore/api-report.md b/plugins/explore/api-report.md index d184ce01a4..d10f5ddf95 100644 --- a/plugins/explore/api-report.md +++ b/plugins/explore/api-report.md @@ -9,7 +9,6 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { default as default_2 } from 'react'; import { DiscoveryApi } from '@backstage/core-plugin-api'; -import { DomainEntity } from '@backstage/catalog-model'; import { ExploreToolsConfig } from '@backstage/plugin-explore-react'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; @@ -31,9 +30,6 @@ export const catalogEntityRouteRef: ExternalRouteRef< true >; -// @public (undocumented) -export const DomainCard: (props: { entity: DomainEntity }) => JSX.Element; - // @public (undocumented) export const DomainExplorerContent: (props: { title?: string | undefined; diff --git a/plugins/rollbar-backend/api-report.md b/plugins/rollbar-backend/api-report.md index 9086121306..58a5fb0252 100644 --- a/plugins/rollbar-backend/api-report.md +++ b/plugins/rollbar-backend/api-report.md @@ -29,8 +29,8 @@ export class RollbarApi { }, ): Promise< { - timestamp: number; count: number; + timestamp: number; }[] >; // (undocumented) @@ -51,8 +51,8 @@ export class RollbarApi { }, ): Promise< { - timestamp: number; count: number; + timestamp: number; }[] >; // (undocumented) @@ -64,8 +64,8 @@ export class RollbarApi { }>; // (undocumented) getProjectItems(projectName: string): Promise<{ - items: RollbarItem[]; page: number; + items: RollbarItem[]; totalCount: number; }>; // (undocumented) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 94377d7720..66757a01cd 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -195,6 +195,12 @@ export function createGithubRepoCreateAction(options: { gitAuthorEmail?: string | undefined; allowRebaseMerge?: boolean | undefined; allowSquashMerge?: boolean | undefined; + squashMergeCommitTitle?: 'PR_TITLE' | 'COMMIT_OR_PR_TITLE' | undefined; + squashMergeCommitMessage?: + | 'PR_BODY' + | 'COMMIT_MESSAGES' + | 'BLANK' + | undefined; allowMergeCommit?: boolean | undefined; allowAutoMerge?: boolean | undefined; requireCodeOwnerReviews?: boolean | undefined; @@ -208,16 +214,16 @@ export function createGithubRepoCreateAction(options: { requiredStatusCheckContexts?: string[] | undefined; requireBranchesToBeUpToDate?: boolean | undefined; requiredConversationResolution?: boolean | undefined; - repoVisibility?: 'public' | 'private' | 'internal' | undefined; + repoVisibility?: 'internal' | 'private' | 'public' | undefined; collaborators?: | ( | { user: string; - access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + access: string; } | { team: string; - access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + access: string; } | { username: string; @@ -225,6 +231,9 @@ export function createGithubRepoCreateAction(options: { } )[] | undefined; + hasProjects?: boolean | undefined; + hasWiki?: boolean | undefined; + hasIssues?: boolean | undefined; token?: string | undefined; topics?: string[] | undefined; }>; @@ -298,7 +307,7 @@ export function createPublishBitbucketAction(options: { repoUrl: string; description?: string | undefined; defaultBranch?: string | undefined; - repoVisibility?: 'public' | 'private' | undefined; + repoVisibility?: 'private' | 'public' | undefined; sourcePath?: string | undefined; enableLFS?: boolean | undefined; token?: string | undefined; @@ -315,7 +324,7 @@ export function createPublishBitbucketCloudAction(options: { repoUrl: string; description?: string | undefined; defaultBranch?: string | undefined; - repoVisibility?: 'public' | 'private' | undefined; + repoVisibility?: 'private' | 'public' | undefined; sourcePath?: string | undefined; token?: string | undefined; }>; @@ -328,7 +337,7 @@ export function createPublishBitbucketServerAction(options: { repoUrl: string; description?: string | undefined; defaultBranch?: string | undefined; - repoVisibility?: 'public' | 'private' | undefined; + repoVisibility?: 'private' | 'public' | undefined; sourcePath?: string | undefined; enableLFS?: boolean | undefined; token?: string | undefined; @@ -380,6 +389,12 @@ export function createPublishGithubAction(options: { gitAuthorEmail?: string | undefined; allowRebaseMerge?: boolean | undefined; allowSquashMerge?: boolean | undefined; + squashMergeCommitTitle?: 'PR_TITLE' | 'COMMIT_OR_PR_TITLE' | undefined; + squashMergeCommitMessage?: + | 'PR_BODY' + | 'COMMIT_MESSAGES' + | 'BLANK' + | undefined; allowMergeCommit?: boolean | undefined; allowAutoMerge?: boolean | undefined; sourcePath?: string | undefined; @@ -395,16 +410,16 @@ export function createPublishGithubAction(options: { requiredStatusCheckContexts?: string[] | undefined; requireBranchesToBeUpToDate?: boolean | undefined; requiredConversationResolution?: boolean | undefined; - repoVisibility?: 'public' | 'private' | 'internal' | undefined; + repoVisibility?: 'internal' | 'private' | 'public' | undefined; collaborators?: | ( | { user: string; - access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + access: string; } | { team: string; - access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + access: string; } | { username: string; @@ -412,6 +427,9 @@ export function createPublishGithubAction(options: { } )[] | undefined; + hasProjects?: boolean | undefined; + hasWiki?: boolean | undefined; + hasIssues?: boolean | undefined; token?: string | undefined; topics?: string[] | undefined; }>; @@ -441,7 +459,7 @@ export function createPublishGitlabAction(options: { }): TemplateAction<{ repoUrl: string; defaultBranch?: string | undefined; - repoVisibility?: 'public' | 'private' | 'internal' | undefined; + repoVisibility?: 'internal' | 'private' | 'public' | undefined; sourcePath?: string | undefined; token?: string | undefined; gitCommitMessage?: string | undefined; @@ -462,7 +480,7 @@ export const createPublishGitlabMergeRequestAction: (options: { sourcePath?: string | undefined; targetPath?: string | undefined; token?: string | undefined; - commitAction?: 'create' | 'update' | 'delete' | undefined; + commitAction?: 'update' | 'delete' | 'create' | undefined; projectid?: string | undefined; removeSourceBranch?: boolean | undefined; assignee?: string | undefined; diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index d936542cfe..1c4d163123 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -11,7 +11,6 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; -import type { ErrorTransformer } from '@rjsf/utils'; import { Extension } from '@backstage/core-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; @@ -19,7 +18,8 @@ import { FieldProps } from '@rjsf/core'; import { FieldProps as FieldProps_2 } from '@rjsf/utils'; import { FieldValidation } from '@rjsf/core'; import { FieldValidation as FieldValidation_2 } from '@rjsf/utils'; -import type { FormProps } from '@rjsf/core'; +import type { FormProps as FormProps_2 } from '@rjsf/core'; +import type { FormProps as FormProps_3 } from '@rjsf/core-v5'; import { IdentityApi } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; import { JSONSchema7 } from 'json-schema'; @@ -88,6 +88,10 @@ export const EntityPickerFieldExtension: FieldExtensionComponent< defaultNamespace?: string | false | undefined; allowedKinds?: string[] | undefined; allowArbitraryValues?: boolean | undefined; + catalogFilter?: + | Record + | Record[] + | undefined; } >; @@ -99,6 +103,10 @@ export const EntityPickerFieldSchema: FieldSchema< defaultNamespace?: string | false | undefined; allowedKinds?: string[] | undefined; allowArbitraryValues?: boolean | undefined; + catalogFilter?: + | Record + | Record[] + | undefined; } >; @@ -167,6 +175,12 @@ export interface FieldSchema { readonly uiOptionsType: TUiOptions; } +// @alpha +export type FormProps = Pick< + FormProps_3, + 'transformErrors' | 'noHtml5Validate' +>; + // @public export type LayoutComponent<_TInputProps> = () => null; @@ -179,7 +193,7 @@ export interface LayoutOptions

{ } // @public -export type LayoutTemplate = FormProps['ObjectFieldTemplate']; +export type LayoutTemplate = FormProps_2['ObjectFieldTemplate']; // @public export type ListActionsResponse = Array<{ @@ -264,7 +278,7 @@ export type NextRouterProps = { TaskPageComponent?: React_2.ComponentType<{}>; }; groups?: TemplateGroupFilter[]; - transformErrors?: ErrorTransformer; + FormProps?: FormProps; }; // @alpha @@ -310,6 +324,10 @@ export const OwnerPickerFieldExtension: FieldExtensionComponent< defaultNamespace?: string | false | undefined; allowedKinds?: string[] | undefined; allowArbitraryValues?: boolean | undefined; + catalogFilter?: + | Record + | Record[] + | undefined; } >; @@ -320,6 +338,10 @@ export const OwnerPickerFieldSchema: FieldSchema< defaultNamespace?: string | false | undefined; allowedKinds?: string[] | undefined; allowArbitraryValues?: boolean | undefined; + catalogFilter?: + | Record + | Record[] + | undefined; } >; @@ -348,10 +370,10 @@ export const RepoUrlPickerFieldExtension: FieldExtensionComponent< | { additionalScopes?: | { + azure?: string[] | undefined; github?: string[] | undefined; gitlab?: string[] | undefined; bitbucket?: string[] | undefined; - azure?: string[] | undefined; gerrit?: string[] | undefined; } | undefined; @@ -374,10 +396,10 @@ export const RepoUrlPickerFieldSchema: FieldSchema< | { additionalScopes?: | { + azure?: string[] | undefined; github?: string[] | undefined; gitlab?: string[] | undefined; bitbucket?: string[] | undefined; - azure?: string[] | undefined; gerrit?: string[] | undefined; } | undefined; diff --git a/plugins/techdocs-react/api-report.md b/plugins/techdocs-react/api-report.md index 09da4e012c..f1d6d3f177 100644 --- a/plugins/techdocs-react/api-report.md +++ b/plugins/techdocs-react/api-report.md @@ -32,6 +32,9 @@ export const SHADOW_DOM_STYLE_LOAD_EVENT = 'TECH_DOCS_SHADOW_DOM_STYLE_LOAD'; // @public export type SyncResult = 'cached' | 'updated'; +// @public +export const TECHDOCS_ADDONS_KEY = 'techdocs.addons.addon.v1'; + // @public export const TECHDOCS_ADDONS_WRAPPER_KEY = 'techdocs.addons.wrapper.v1'; From 26b4bbd8b22fe2104952becdab630ab839321089 Mon Sep 17 00:00:00 2001 From: sblausten Date: Tue, 3 Jan 2023 16:02:08 +0100 Subject: [PATCH 010/247] Api report Signed-off-by: sblausten --- plugins/explore/api-report.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/explore/api-report.md b/plugins/explore/api-report.md index d10f5ddf95..d184ce01a4 100644 --- a/plugins/explore/api-report.md +++ b/plugins/explore/api-report.md @@ -9,6 +9,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { default as default_2 } from 'react'; import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { DomainEntity } from '@backstage/catalog-model'; import { ExploreToolsConfig } from '@backstage/plugin-explore-react'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; @@ -30,6 +31,9 @@ export const catalogEntityRouteRef: ExternalRouteRef< true >; +// @public (undocumented) +export const DomainCard: (props: { entity: DomainEntity }) => JSX.Element; + // @public (undocumented) export const DomainExplorerContent: (props: { title?: string | undefined; From a70d460887d6f0487384985aa6ceda9900edd687 Mon Sep 17 00:00:00 2001 From: sblausten Date: Mon, 30 Jan 2023 09:53:47 +0100 Subject: [PATCH 011/247] Fix api report Signed-off-by: sblausten --- plugins/explore/api-report.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/explore/api-report.md b/plugins/explore/api-report.md index e7adde4b77..b6418fa3e9 100644 --- a/plugins/explore/api-report.md +++ b/plugins/explore/api-report.md @@ -9,7 +9,6 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { default as default_2 } from 'react'; import { DiscoveryApi } from '@backstage/core-plugin-api'; -import { DomainEntity } from '@backstage/catalog-model'; import { ExploreToolsConfig } from '@backstage/plugin-explore-react'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; @@ -31,14 +30,17 @@ export const catalogEntityRouteRef: ExternalRouteRef< true >; -// @public (undocumented) -export const DomainCard: (props: { entity: DomainEntity }) => JSX.Element; - // @public (undocumented) export const DomainExplorerContent: (props: { title?: string | undefined; }) => JSX.Element; +// @public (undocumented) +export const EntityExplorerContent: (props: { + tabTitle?: string; + kind: string; +}) => JSX.Element; + // @public export interface ExploreApi { getTools(request?: GetExploreToolsRequest): Promise; From 2e49348062694062ecb39cc5da29ff54d15ccb65 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Wed, 1 Mar 2023 18:21:31 -0500 Subject: [PATCH 012/247] Fixing techdocs linking issue during dom link overwrites. Signed-off-by: Aramis Sennyey --- .changeset/tender-terms-return.md | 6 + packages/core-app-api/api-report.md | 3 + packages/core-app-api/src/routing/index.ts | 2 + .../src/routing/useNavigateUrl.test.tsx | 120 ++++++++++++++++++ .../src/routing/useNavigateUrl.tsx | 71 +++++++++++ plugins/techdocs/package.json | 2 +- .../TechDocsReaderPage.test.tsx | 14 +- .../TechDocsReaderPageContent/dom.tsx | 13 +- 8 files changed, 222 insertions(+), 9 deletions(-) create mode 100644 .changeset/tender-terms-return.md create mode 100644 packages/core-app-api/src/routing/useNavigateUrl.test.tsx create mode 100644 packages/core-app-api/src/routing/useNavigateUrl.tsx diff --git a/.changeset/tender-terms-return.md b/.changeset/tender-terms-return.md new file mode 100644 index 0000000000..3e215b213f --- /dev/null +++ b/.changeset/tender-terms-return.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-app-api': minor +'@backstage/plugin-techdocs': patch +--- + +Fix a bug in sub-path navigation due to double addition of a sub-path if one was set up in `app.baseUrl`. diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 37f7a98de0..266cebb1f1 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -576,6 +576,9 @@ export class UrlPatternDiscovery implements DiscoveryApi { getBaseUrl(pluginId: string): Promise; } +// @public +export function useNavigateUrl(): (to: string) => void; + // @public export class WebStorage implements StorageApi { constructor(namespace: string, errorApi: ErrorApi); diff --git a/packages/core-app-api/src/routing/index.ts b/packages/core-app-api/src/routing/index.ts index 46b5a6f9b4..008657d135 100644 --- a/packages/core-app-api/src/routing/index.ts +++ b/packages/core-app-api/src/routing/index.ts @@ -18,3 +18,5 @@ export { FlatRoutes } from './FlatRoutes'; export type { FlatRoutesProps } from './FlatRoutes'; export { FeatureFlagged } from './FeatureFlagged'; export type { FeatureFlaggedProps } from './FeatureFlagged'; + +export { useNavigateUrl } from './useNavigateUrl'; diff --git a/packages/core-app-api/src/routing/useNavigateUrl.test.tsx b/packages/core-app-api/src/routing/useNavigateUrl.test.tsx new file mode 100644 index 0000000000..383ee4d688 --- /dev/null +++ b/packages/core-app-api/src/routing/useNavigateUrl.test.tsx @@ -0,0 +1,120 @@ +/* + * 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 React from 'react'; +import { + MockConfigApi, + renderInTestApp, + TestApiProvider, +} from '@backstage/test-utils'; +import { resolveUrlToRelative, useNavigateUrl } from './useNavigateUrl'; +import { configApiRef } from '@backstage/core-plugin-api'; + +const navigate = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => navigate, +})); + +describe('resolveUrlToRelative', () => { + it('does nothing when app.baseUrl has no subpath', () => { + const url = 'http://localhost:3000/test'; + const baseUrl = 'http://localhost:3000'; + expect(resolveUrlToRelative(url, baseUrl)).toBe('/test'); + }); + + it('removes the app.baseUrl subpath when present', () => { + const url = 'http://localhost:3000/instance/test'; + const baseUrl = 'http://localhost:3000/instance'; + expect(resolveUrlToRelative(url, baseUrl)).toBe('/test'); + }); + + it('removes trailing slashes on the URL when present', () => { + const url = 'http://localhost:3000/test//'; + const baseUrl = 'http://localhost:3000'; + expect(resolveUrlToRelative(url, baseUrl)).toBe('/test'); + }); +}); + +const Component = ({ to }: { to: string }) => { + const navigateTo = useNavigateUrl(); + return <>{navigateTo(to)}; +}; + +describe('useNavigateUrl', () => { + beforeEach(() => { + navigate.mockReset(); + }); + it('navigates to the desired page as expected', async () => { + const baseUrl = 'http://localhost:3000'; + await renderInTestApp( + + + , + ); + expect(navigate).toHaveBeenCalledWith('/test'); + }); + it('handles app.baseUrl subpaths', async () => { + const baseUrl = 'http://localhost:3000/instance'; + await renderInTestApp( + + + , + ); + expect(navigate).toHaveBeenCalledWith('/test'); + }); + it('handles relative urls', async () => { + const baseUrl = 'http://localhost:3000'; + await renderInTestApp( + + + , + ); + expect(navigate).toHaveBeenCalledWith('/test'); + }); +}); diff --git a/packages/core-app-api/src/routing/useNavigateUrl.tsx b/packages/core-app-api/src/routing/useNavigateUrl.tsx new file mode 100644 index 0000000000..5e1bd42c6b --- /dev/null +++ b/packages/core-app-api/src/routing/useNavigateUrl.tsx @@ -0,0 +1,71 @@ +/* + * 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 { configApiRef, useApi } from '@backstage/core-plugin-api'; +import { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; + +/** + * Resolve a URL to a relative URL given a base URL that may or may not include subpaths. + * @param url - URL to parse into a relative url based on the baseUrl. + * @param baseUrl - Application base url, where the application is currently hosted. + * @returns relative path without any subpaths from website config. + */ +export function resolveUrlToRelative(url: string, baseUrl: string) { + const parsedAppUrl = new URL(baseUrl); + const appUrlPath = `${parsedAppUrl.origin}${parsedAppUrl.pathname.replace( + /\/$/, + '', + )}`; + + const relativeUrl = url + .replace(appUrlPath, '') + // Remove any leading and trailing slashes. + .replace(/\/+$/, '') + .replace(/^\/+/, ''); + const parsedUrl = new URL(`http://localhost/${relativeUrl}`); + return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`; +} + +/** + * A helper hook that allows for full internal website urls to be processed through the navigate + * hook provided by `react-router-dom`. + * + * NOTE: This does not support routing to external URLs. That should be done with a `Link` or `a` + * element instead, or just `window.location.href`. + * + * @returns Navigation function that is a wrapper over `react-router-dom`'s + * to support passing full URLs for navigation. + * + * @public + */ +export function useNavigateUrl() { + const navigate = useNavigate(); + const configApi = useApi(configApiRef); + const appBaseUrl = configApi.getString('app.baseUrl'); + const navigateFn = useCallback( + (to: string) => { + let url = to; + try { + url = resolveUrlToRelative(to, appBaseUrl); + } catch (err) { + // URL passed in was relative. + } + navigate(url); + }, + [navigate, appBaseUrl], + ); + return navigateFn; +} diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 8f15b11152..201dc3e71a 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -35,6 +35,7 @@ "dependencies": { "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", + "@backstage/core-app-api": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/errors": "workspace:^", @@ -65,7 +66,6 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", - "@backstage/core-app-api": "workspace:^", "@backstage/dev-utils": "workspace:^", "@backstage/plugin-techdocs-module-addons-contrib": "workspace:^", "@backstage/test-utils": "workspace:^", diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx index 4e8f331c7f..e11b7b6e81 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx @@ -18,7 +18,11 @@ import { act } from '@testing-library/react'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + MockConfigApi, + renderInTestApp, + TestApiProvider, +} from '@backstage/test-utils'; import { techdocsApiRef, techdocsStorageApiRef } from '../../../api'; @@ -31,6 +35,7 @@ import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; import { FlatRoutes } from '@backstage/core-app-api'; import { Page } from '@backstage/core-components'; +import { configApiRef } from '@backstage/core-plugin-api'; const mockEntityMetadata = { locationMetadata: { @@ -80,11 +85,18 @@ jest.mock('@backstage/core-components', () => ({ Page: jest.fn(), })); +const configApi = new MockConfigApi({ + app: { + baseUrl: 'http://localhost:3000', + }, +}); + const Wrapper = ({ children }: { children: React.ReactNode }) => { return ( { - const navigate = useNavigate(); + const navigate = useNavigateUrl(); const theme = useTheme(); const isMobileMedia = useMediaQuery(MOBILE_MEDIA_QUERY); const sanitizerTransformer = useSanitizerTransformer(); @@ -176,7 +176,6 @@ export const useTechDocsReaderDom = ( // detect if CTRL or META keys are pressed so that links can be opened in a new tab with `window.open` const modifierActive = event.ctrlKey || event.metaKey; const parsedUrl = new URL(url); - const fullPath = `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`; // capture link clicks within documentation const linkText = @@ -187,9 +186,9 @@ export const useTechDocsReaderDom = ( // hash exists when anchor is clicked on secondary sidebar if (parsedUrl.hash) { if (modifierActive) { - window.open(fullPath, '_blank'); + window.open(url, '_blank'); } else { - navigate(fullPath); + navigate(url); // Scroll to hash if it's on the current page transformedElement ?.querySelector(`[id="${parsedUrl.hash.slice(1)}"]`) @@ -197,9 +196,9 @@ export const useTechDocsReaderDom = ( } } else { if (modifierActive) { - window.open(fullPath, '_blank'); + window.open(url, '_blank'); } else { - navigate(fullPath); + navigate(url); } } }, From 3538d9ad2c4d19bef31d4280c56f27d0fed66227 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Thu, 2 Mar 2023 16:19:39 +0000 Subject: [PATCH 013/247] tweak exports to allow decoupling of backend plugins Signed-off-by: Brian Fletcher --- .changeset/tall-meals-dress.md | 5 +++++ packages/backend/src/index.ts | 15 ++++++--------- packages/backend/src/plugins/catalog.ts | 7 ++++--- .../src/plugins/catalogEventBasedProviders.ts | 3 ++- packages/backend/src/plugins/events.ts | 4 +--- packages/backend/src/types.ts | 2 ++ plugins/events-backend/api-report.md | 12 ++++++++++++ plugins/events-backend/src/index.ts | 1 + .../src/service/InMemoryEventBroker.ts | 2 ++ .../service/http/HttpPostIngressEventPublisher.ts | 2 +- 10 files changed, 36 insertions(+), 17 deletions(-) create mode 100644 .changeset/tall-meals-dress.md diff --git a/.changeset/tall-meals-dress.md b/.changeset/tall-meals-dress.md new file mode 100644 index 0000000000..42d50444aa --- /dev/null +++ b/.changeset/tall-meals-dress.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-events-backend': patch +--- + +Export `InMemoryEventBroker` to allow decoupling of the catalog and events backends in the `example-backend`. diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 011be941cf..3c07c3aad4 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -42,7 +42,6 @@ import { metricsInit, metricsHandler } from './metrics'; import auth from './plugins/auth'; import azureDevOps from './plugins/azure-devops'; import catalog from './plugins/catalog'; -import catalogEventBasedProviders from './plugins/catalogEventBasedProviders'; import codeCoverage from './plugins/codecoverage'; import entityFeedback from './plugins/entityFeedback'; import events from './plugins/events'; @@ -68,6 +67,7 @@ import linguist from './plugins/linguist'; import { PluginEnvironment } from './types'; import { ServerPermissionClient } from '@backstage/plugin-permission-node'; import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; +import { InMemoryEventBroker } from '@backstage/plugin-events-backend'; function makeCreateEnv(config: Config) { const root = getRootLogger(); @@ -85,6 +85,8 @@ function makeCreateEnv(config: Config) { discovery, }); + const eventBroker = new InMemoryEventBroker(root.child({ type: 'plugin' })); + root.info(`Created UrlReader ${reader}`); return (plugin: string): PluginEnvironment => { @@ -99,6 +101,7 @@ function makeCreateEnv(config: Config) { database, config, reader, + eventBroker, discovery, tokenManager, permissions, @@ -156,18 +159,12 @@ async function main() { const exploreEnv = useHotMemoize(module, () => createEnv('explore')); const lighthouseEnv = useHotMemoize(module, () => createEnv('lighthouse')); - const eventBasedEntityProviders = await catalogEventBasedProviders( - catalogEnv, - ); const linguistEnv = useHotMemoize(module, () => createEnv('linguist')); const apiRouter = Router(); - apiRouter.use( - '/catalog', - await catalog(catalogEnv, eventBasedEntityProviders), - ); + apiRouter.use('/catalog', await catalog(catalogEnv)); apiRouter.use('/code-coverage', await codeCoverage(codeCoverageEnv)); - apiRouter.use('/events', await events(eventsEnv, eventBasedEntityProviders)); + apiRouter.use('/events', await events(eventsEnv)); apiRouter.use('/rollbar', await rollbar(rollbarEnv)); apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); apiRouter.use('/tech-insights', await techInsights(techInsightsEnv)); diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index f6fe25f86a..cd1a92ad7b 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -15,18 +15,19 @@ */ import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; -import { EntityProvider } from '@backstage/plugin-catalog-node'; import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; +import createCatalogEventBasedProviders from './catalogEventBasedProviders'; export default async function createPlugin( env: PluginEnvironment, - providers?: Array, ): Promise { + const providers = await createCatalogEventBasedProviders(env); const builder = await CatalogBuilder.create(env); builder.addProcessor(new ScaffolderEntitiesProcessor()); - builder.addEntityProvider(providers ?? []); + env.eventBroker.subscribe(providers); + builder.addEntityProvider(providers); const { processingEngine, router } = await builder.build(); await processingEngine.start(); return router; diff --git a/packages/backend/src/plugins/catalogEventBasedProviders.ts b/packages/backend/src/plugins/catalogEventBasedProviders.ts index 346ed40fa6..3f1ea708df 100644 --- a/packages/backend/src/plugins/catalogEventBasedProviders.ts +++ b/packages/backend/src/plugins/catalogEventBasedProviders.ts @@ -29,10 +29,11 @@ class DemoEventBasedEntityProvider implements EntityProvider, EventSubscriber { ) {} async onEvent(params: EventParams): Promise { + const request = params.eventPayload as Request; this.logger.info( `onEvent: topic=${params.topic}, metadata=${JSON.stringify( params.metadata, - )}, payload=${JSON.stringify(params.eventPayload)}`, + )}, payload=${JSON.stringify(request.body)}`, ); } diff --git a/packages/backend/src/plugins/events.ts b/packages/backend/src/plugins/events.ts index 7427d34217..f3ff354240 100644 --- a/packages/backend/src/plugins/events.ts +++ b/packages/backend/src/plugins/events.ts @@ -18,13 +18,11 @@ import { EventsBackend, HttpPostIngressEventPublisher, } from '@backstage/plugin-events-backend'; -import { EventSubscriber } from '@backstage/plugin-events-node'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; export default async function createPlugin( env: PluginEnvironment, - subscribers: EventSubscriber[], ): Promise { const eventsRouter = Router(); @@ -35,8 +33,8 @@ export default async function createPlugin( http.bind(eventsRouter); await new EventsBackend(env.logger) + .setEventBroker(env.eventBroker) .addPublishers(http) - .addSubscribers(subscribers) .start(); return eventsRouter; diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 895581c702..ab1baf0c95 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -26,6 +26,7 @@ import { import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; +import { EventBroker } from '@backstage/plugin-events-node'; export type PluginEnvironment = { logger: Logger; @@ -38,4 +39,5 @@ export type PluginEnvironment = { permissions: PermissionEvaluator; scheduler: PluginTaskScheduler; identity: IdentityApi; + eventBroker: EventBroker; }; diff --git a/plugins/events-backend/api-report.md b/plugins/events-backend/api-report.md index 7d8ede1979..5d520f10c2 100644 --- a/plugins/events-backend/api-report.md +++ b/plugins/events-backend/api-report.md @@ -5,6 +5,7 @@ ```ts import { Config } from '@backstage/config'; import { EventBroker } from '@backstage/plugin-events-node'; +import { EventParams } from '@backstage/plugin-events-node'; import { EventPublisher } from '@backstage/plugin-events-node'; import { EventSubscriber } from '@backstage/plugin-events-node'; import express from 'express'; @@ -42,4 +43,15 @@ export class HttpPostIngressEventPublisher implements EventPublisher { // (undocumented) setEventBroker(eventBroker: EventBroker): Promise; } + +// @public +export class InMemoryEventBroker implements EventBroker { + constructor(logger: Logger); + // (undocumented) + publish(params: EventParams): Promise; + // (undocumented) + subscribe( + ...subscribers: Array> + ): void; +} ``` diff --git a/plugins/events-backend/src/index.ts b/plugins/events-backend/src/index.ts index 4645e9a479..d443eeabca 100644 --- a/plugins/events-backend/src/index.ts +++ b/plugins/events-backend/src/index.ts @@ -22,3 +22,4 @@ export { EventsBackend } from './service/EventsBackend'; export { HttpPostIngressEventPublisher } from './service/http'; +export { InMemoryEventBroker } from './service/InMemoryEventBroker'; diff --git a/plugins/events-backend/src/service/InMemoryEventBroker.ts b/plugins/events-backend/src/service/InMemoryEventBroker.ts index 04be01f658..d800d88dc5 100644 --- a/plugins/events-backend/src/service/InMemoryEventBroker.ts +++ b/plugins/events-backend/src/service/InMemoryEventBroker.ts @@ -25,6 +25,8 @@ import { Logger } from 'winston'; * In-memory event broker which will pass the event to all registered subscribers * interested in it. * Events will not be persisted in any form. + * + * @public */ // TODO(pjungermann): add prom metrics? (see plugins/catalog-backend/src/util/metrics.ts, etc.) export class InMemoryEventBroker implements EventBroker { diff --git a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts index b5a6ccbca1..54dead0afa 100644 --- a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts +++ b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts @@ -107,7 +107,7 @@ export class HttpPostIngressEventPublisher implements EventPublisher { return; } - const eventPayload = request.body; + const eventPayload = request; await this.eventBroker!.publish({ topic, eventPayload, From 6be62344a07f7670cc3eee0d323c55ec6204bfb6 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Thu, 2 Mar 2023 16:31:09 +0000 Subject: [PATCH 014/247] fix Signed-off-by: Brian Fletcher --- packages/backend/src/plugins/catalogEventBasedProviders.ts | 3 +-- .../src/service/http/HttpPostIngressEventPublisher.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/plugins/catalogEventBasedProviders.ts b/packages/backend/src/plugins/catalogEventBasedProviders.ts index 3f1ea708df..346ed40fa6 100644 --- a/packages/backend/src/plugins/catalogEventBasedProviders.ts +++ b/packages/backend/src/plugins/catalogEventBasedProviders.ts @@ -29,11 +29,10 @@ class DemoEventBasedEntityProvider implements EntityProvider, EventSubscriber { ) {} async onEvent(params: EventParams): Promise { - const request = params.eventPayload as Request; this.logger.info( `onEvent: topic=${params.topic}, metadata=${JSON.stringify( params.metadata, - )}, payload=${JSON.stringify(request.body)}`, + )}, payload=${JSON.stringify(params.eventPayload)}`, ); } diff --git a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts index 54dead0afa..b5a6ccbca1 100644 --- a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts +++ b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts @@ -107,7 +107,7 @@ export class HttpPostIngressEventPublisher implements EventPublisher { return; } - const eventPayload = request; + const eventPayload = request.body; await this.eventBroker!.publish({ topic, eventPayload, From 383f30f9d6060e0436eb9a0b41e503441d7dd155 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Thu, 2 Mar 2023 16:50:42 -0500 Subject: [PATCH 015/247] Ensure backwards compatibility. Signed-off-by: Aramis Sennyey --- .../core-app-api/src/routing/useNavigateUrl.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/core-app-api/src/routing/useNavigateUrl.tsx b/packages/core-app-api/src/routing/useNavigateUrl.tsx index 5e1bd42c6b..3f067f475e 100644 --- a/packages/core-app-api/src/routing/useNavigateUrl.tsx +++ b/packages/core-app-api/src/routing/useNavigateUrl.tsx @@ -54,14 +54,20 @@ export function resolveUrlToRelative(url: string, baseUrl: string) { export function useNavigateUrl() { const navigate = useNavigate(); const configApi = useApi(configApiRef); - const appBaseUrl = configApi.getString('app.baseUrl'); + const appBaseUrl = configApi.getOptionalString('app.baseUrl'); const navigateFn = useCallback( (to: string) => { let url = to; - try { - url = resolveUrlToRelative(to, appBaseUrl); - } catch (err) { - // URL passed in was relative. + /** + * This should always be true when running the application, this just allows + * test cases that do not have the configApi set up to run still. + */ + if (appBaseUrl) { + try { + url = resolveUrlToRelative(to, appBaseUrl); + } catch (err) { + // URL passed in was relative. + } } navigate(url); }, From 26ad3398a876d7f683a0855001262359d1d92ce5 Mon Sep 17 00:00:00 2001 From: Kiss Miklos Date: Thu, 19 Jan 2023 10:26:22 +0100 Subject: [PATCH 016/247] add pattern matching Signed-off-by: Kiss Miklos --- .../src/modules/codeowners/CodeOwnersProcessor.ts | 4 +++- plugins/catalog-backend/src/modules/codeowners/lib/read.ts | 3 ++- plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts index 186b732271..f50ef6b922 100644 --- a/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts +++ b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts @@ -79,11 +79,13 @@ export class CodeOwnersProcessor implements CatalogProcessor { if (!scmIntegration) { return entity; } - + const pattern = + entity.metadata?.annotations['backstage.io/managed-by-location']; const owner = await findCodeOwnerByTarget( this.reader, location.target, scmIntegration, + pattern, ); if (!owner) { diff --git a/plugins/catalog-backend/src/modules/codeowners/lib/read.ts b/plugins/catalog-backend/src/modules/codeowners/lib/read.ts index 2987e162cc..eae4e7ce91 100644 --- a/plugins/catalog-backend/src/modules/codeowners/lib/read.ts +++ b/plugins/catalog-backend/src/modules/codeowners/lib/read.ts @@ -52,6 +52,7 @@ export async function findCodeOwnerByTarget( reader: UrlReader, targetUrl: string, scmIntegration: ScmIntegration, + pattern: string, ): Promise { const codeownersPaths = scmCodeOwnersPaths[scmIntegration?.type ?? '']; @@ -70,7 +71,7 @@ export async function findCodeOwnerByTarget( return undefined; } - const owner = resolveCodeOwner(contents); + const owner = resolveCodeOwner(contents, pattern); return owner; } diff --git a/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts b/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts index 00645f0c9d..f9bc19b1d1 100644 --- a/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts +++ b/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts @@ -29,7 +29,7 @@ export function resolveCodeOwner( const owners = codeowners.parse(contents); return pipe( - filter((e: CodeOwnersEntry) => e.pattern === pattern), + filter((e: CodeOwnersEntry) => pattern.includes(e.pattern)), reverse, head, get('owners'), From 54cffaf07c5cee318eaaea9011171891fb21feef Mon Sep 17 00:00:00 2001 From: Kiss Miklos Date: Mon, 6 Feb 2023 17:53:57 +0100 Subject: [PATCH 017/247] add pattern match Signed-off-by: Kiss Miklos --- .../codeowners/CodeOwnersProcessor.test.ts | 27 ++++++++++++++++--- .../modules/codeowners/CodeOwnersProcessor.ts | 4 ++- .../src/modules/codeowners/lib/read.ts | 2 +- .../modules/codeowners/lib/resolve.test.ts | 10 ++++++- .../src/modules/codeowners/lib/resolve.ts | 18 +++++++++---- 5 files changed, 50 insertions(+), 11 deletions(-) diff --git a/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts index 9cb4d414c0..a33ce84e0d 100644 --- a/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts @@ -21,7 +21,7 @@ import { LocationSpec } from '@backstage/plugin-catalog-node'; const mockCodeOwnersText = () => ` * @acme/team-foo @acme/team-bar -/docs @acme/team-bar +/docs @acme/team-docs `; describe('CodeOwnersProcessor', () => { @@ -34,8 +34,12 @@ describe('CodeOwnersProcessor', () => { }); describe('preProcessEntity', () => { - const setupTest = ({ kind = 'Component', spec = {} } = {}) => { - const entity = { kind, spec }; + const setupTest = ({ + kind = 'Component', + spec = {}, + metadata = {}, + } = {}) => { + const entity = { kind, spec, metadata }; const config = new ConfigReader({}); const reader = { read: jest.fn(), @@ -101,5 +105,22 @@ describe('CodeOwnersProcessor', () => { spec: { owner: 'team-foo' }, }); }); + it('should use the "backstage.io/managed-by-location" annotation as pattern', async () => { + const { entity, processor } = setupTest({ + metadata: { + annotations: { 'backstage.io/managed-by-location': '/docs' }, + }, + }); + + const result = await processor.preProcessEntity( + entity as any, + mockLocation(), + ); + + expect(result).toEqual({ + ...entity, + spec: { owner: 'team-docs' }, + }); + }); }); }); diff --git a/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts index f50ef6b922..9a9c58934c 100644 --- a/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts +++ b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts @@ -79,8 +79,10 @@ export class CodeOwnersProcessor implements CatalogProcessor { if (!scmIntegration) { return entity; } + const pattern = - entity.metadata?.annotations['backstage.io/managed-by-location']; + entity.metadata?.annotations?.['backstage.io/managed-by-location']; + const owner = await findCodeOwnerByTarget( this.reader, location.target, diff --git a/plugins/catalog-backend/src/modules/codeowners/lib/read.ts b/plugins/catalog-backend/src/modules/codeowners/lib/read.ts index eae4e7ce91..cd15a17997 100644 --- a/plugins/catalog-backend/src/modules/codeowners/lib/read.ts +++ b/plugins/catalog-backend/src/modules/codeowners/lib/read.ts @@ -52,7 +52,7 @@ export async function findCodeOwnerByTarget( reader: UrlReader, targetUrl: string, scmIntegration: ScmIntegration, - pattern: string, + pattern?: string, ): Promise { const codeownersPaths = scmCodeOwnersPaths[scmIntegration?.type ?? '']; diff --git a/plugins/catalog-backend/src/modules/codeowners/lib/resolve.test.ts b/plugins/catalog-backend/src/modules/codeowners/lib/resolve.test.ts index 4e6c646073..ec9321284f 100644 --- a/plugins/catalog-backend/src/modules/codeowners/lib/resolve.test.ts +++ b/plugins/catalog-backend/src/modules/codeowners/lib/resolve.test.ts @@ -18,13 +18,21 @@ import { normalizeCodeOwner, resolveCodeOwner } from './resolve'; const mockCodeOwnersText = () => ` * @acme/team-foo @acme/team-bar -/docs @acme/team-bar +/docs @acme/team-docs `; describe('resolveCodeOwner', () => { it('should parse the codeowners file', () => { expect(resolveCodeOwner(mockCodeOwnersText())).toBe('team-foo'); }); + it('should include the codeowners path into the provided pattern', () => { + expect( + resolveCodeOwner( + mockCodeOwnersText(), + 'url:https://github.com/acem/repo/main/docs/catalog-info.yaml', + ), + ).toBe('team-docs'); + }); }); describe('normalizeCodeOwner', () => { diff --git a/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts b/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts index f9bc19b1d1..69865fde21 100644 --- a/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts +++ b/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts @@ -16,7 +16,7 @@ import * as codeowners from 'codeowners-utils'; import { CodeOwnersEntry } from 'codeowners-utils'; -import { filter, get, head, pipe, reverse } from 'lodash/fp'; +import { get, head, pipe, reverse } from 'lodash/fp'; const USER_PATTERN = /^@.*/; const GROUP_PATTERN = /^@.*\/.*/; @@ -24,18 +24,26 @@ const EMAIL_PATTERN = /^.*@.*\..*$/; export function resolveCodeOwner( contents: string, - pattern = '*', + pattern?: string, ): string | undefined { const owners = codeowners.parse(contents); - return pipe( - filter((e: CodeOwnersEntry) => pattern.includes(e.pattern)), + let relevantOwners = owners.filter((e: CodeOwnersEntry) => + pattern?.includes(e.pattern), + ); + if (relevantOwners.length === 0) { + relevantOwners = owners.filter((e: CodeOwnersEntry) => e.pattern === '*'); + } + + const result = pipe( reverse, head, get('owners'), head, normalizeCodeOwner, - )(owners); + )(relevantOwners); + + return result; } export function normalizeCodeOwner(owner: string) { From 92a4590fc3ab0ee02ae8a0ea93d8dbf4aa6411b2 Mon Sep 17 00:00:00 2001 From: Kiss Miklos Date: Mon, 6 Feb 2023 18:17:50 +0100 Subject: [PATCH 018/247] add changeset Signed-off-by: Kiss Miklos --- .changeset/spicy-carrots-argue.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/spicy-carrots-argue.md diff --git a/.changeset/spicy-carrots-argue.md b/.changeset/spicy-carrots-argue.md new file mode 100644 index 0000000000..559335a646 --- /dev/null +++ b/.changeset/spicy-carrots-argue.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Add support to CodeOwnersProcces From f09e37af60f42b24b919585d47199a7785c9d89e Mon Sep 17 00:00:00 2001 From: Kiss Miklos Date: Mon, 6 Feb 2023 18:18:25 +0100 Subject: [PATCH 019/247] add more info to changeset Signed-off-by: Kiss Miklos --- .changeset/spicy-carrots-argue.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/spicy-carrots-argue.md b/.changeset/spicy-carrots-argue.md index 559335a646..b6475b1f7d 100644 --- a/.changeset/spicy-carrots-argue.md +++ b/.changeset/spicy-carrots-argue.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-backend': minor --- -Add support to CodeOwnersProcces +Add monorepo support to CodeOwnersProccesor. From 76df7bd736dc79b42cf7bdff759904c34c188d4b Mon Sep 17 00:00:00 2001 From: Kiss Miklos Date: Mon, 6 Feb 2023 18:19:54 +0100 Subject: [PATCH 020/247] use more descriptive name Signed-off-by: Kiss Miklos --- .../catalog-backend/src/modules/codeowners/lib/resolve.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts b/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts index 69865fde21..139ea881d8 100644 --- a/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts +++ b/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts @@ -28,11 +28,11 @@ export function resolveCodeOwner( ): string | undefined { const owners = codeowners.parse(contents); - let relevantOwners = owners.filter((e: CodeOwnersEntry) => + let filteredOwners = owners.filter((e: CodeOwnersEntry) => pattern?.includes(e.pattern), ); - if (relevantOwners.length === 0) { - relevantOwners = owners.filter((e: CodeOwnersEntry) => e.pattern === '*'); + if (filteredOwners.length === 0) { + filteredOwners = owners.filter((e: CodeOwnersEntry) => e.pattern === '*'); } const result = pipe( @@ -41,7 +41,7 @@ export function resolveCodeOwner( get('owners'), head, normalizeCodeOwner, - )(relevantOwners); + )(filteredOwners); return result; } From c85b7360161c31487b18ddfad73ae8247a566aeb Mon Sep 17 00:00:00 2001 From: Kiss Miklos Date: Mon, 6 Mar 2023 16:27:48 +0100 Subject: [PATCH 021/247] use more robust matching for CODEOWNERS entries Signed-off-by: Kiss Miklos --- .../codeowners/CodeOwnersProcessor.test.ts | 8 ++--- .../modules/codeowners/CodeOwnersProcessor.ts | 4 --- .../src/modules/codeowners/lib/read.ts | 3 +- .../modules/codeowners/lib/resolve.test.ts | 17 +++++++++-- .../src/modules/codeowners/lib/resolve.ts | 29 +++++++------------ 5 files changed, 29 insertions(+), 32 deletions(-) diff --git a/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts index a33ce84e0d..8e1fbca726 100644 --- a/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts @@ -106,15 +106,11 @@ describe('CodeOwnersProcessor', () => { }); }); it('should use the "backstage.io/managed-by-location" annotation as pattern', async () => { - const { entity, processor } = setupTest({ - metadata: { - annotations: { 'backstage.io/managed-by-location': '/docs' }, - }, - }); + const { entity, processor } = setupTest(); const result = await processor.preProcessEntity( entity as any, - mockLocation(), + mockLocation({ basePath: 'docs/' }), ); expect(result).toEqual({ diff --git a/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts index 9a9c58934c..186b732271 100644 --- a/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts +++ b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts @@ -80,14 +80,10 @@ export class CodeOwnersProcessor implements CatalogProcessor { return entity; } - const pattern = - entity.metadata?.annotations?.['backstage.io/managed-by-location']; - const owner = await findCodeOwnerByTarget( this.reader, location.target, scmIntegration, - pattern, ); if (!owner) { diff --git a/plugins/catalog-backend/src/modules/codeowners/lib/read.ts b/plugins/catalog-backend/src/modules/codeowners/lib/read.ts index cd15a17997..6a5234c94f 100644 --- a/plugins/catalog-backend/src/modules/codeowners/lib/read.ts +++ b/plugins/catalog-backend/src/modules/codeowners/lib/read.ts @@ -52,7 +52,6 @@ export async function findCodeOwnerByTarget( reader: UrlReader, targetUrl: string, scmIntegration: ScmIntegration, - pattern?: string, ): Promise { const codeownersPaths = scmCodeOwnersPaths[scmIntegration?.type ?? '']; @@ -71,7 +70,7 @@ export async function findCodeOwnerByTarget( return undefined; } - const owner = resolveCodeOwner(contents, pattern); + const owner = resolveCodeOwner(contents, targetUrl); return owner; } diff --git a/plugins/catalog-backend/src/modules/codeowners/lib/resolve.test.ts b/plugins/catalog-backend/src/modules/codeowners/lib/resolve.test.ts index ec9321284f..f9cdf79530 100644 --- a/plugins/catalog-backend/src/modules/codeowners/lib/resolve.test.ts +++ b/plugins/catalog-backend/src/modules/codeowners/lib/resolve.test.ts @@ -23,16 +23,29 @@ const mockCodeOwnersText = () => ` describe('resolveCodeOwner', () => { it('should parse the codeowners file', () => { - expect(resolveCodeOwner(mockCodeOwnersText())).toBe('team-foo'); + expect( + resolveCodeOwner( + mockCodeOwnersText(), + 'https://github.com/can/be/tree/anything/catalog-info.yaml', + ), + ).toBe('team-foo'); }); it('should include the codeowners path into the provided pattern', () => { expect( resolveCodeOwner( mockCodeOwnersText(), - 'url:https://github.com/acem/repo/main/docs/catalog-info.yaml', + 'https://github.com/acme/repo/tree/main/docs/catalog-info.yaml', ), ).toBe('team-docs'); }); + it('should match only in resource path', () => { + expect( + resolveCodeOwner( + mockCodeOwnersText(), + 'https://github.com/acme/repo/tree/docs/catalog-info.yaml', + ), + ).toBe('team-foo'); + }); }); describe('normalizeCodeOwner', () => { diff --git a/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts b/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts index 139ea881d8..92fa4a0ef6 100644 --- a/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts +++ b/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts @@ -15,8 +15,7 @@ */ import * as codeowners from 'codeowners-utils'; -import { CodeOwnersEntry } from 'codeowners-utils'; -import { get, head, pipe, reverse } from 'lodash/fp'; +import parseGitUrl from 'git-url-parse'; const USER_PATTERN = /^@.*/; const GROUP_PATTERN = /^@.*\/.*/; @@ -24,26 +23,20 @@ const EMAIL_PATTERN = /^.*@.*\..*$/; export function resolveCodeOwner( contents: string, - pattern?: string, + catalogInfoFileUrl: string, ): string | undefined { - const owners = codeowners.parse(contents); + const codeOwnerEntries = codeowners.parse(contents); - let filteredOwners = owners.filter((e: CodeOwnersEntry) => - pattern?.includes(e.pattern), - ); - if (filteredOwners.length === 0) { - filteredOwners = owners.filter((e: CodeOwnersEntry) => e.pattern === '*'); + const { filepath } = parseGitUrl(catalogInfoFileUrl); + const match = codeowners.matchFile(filepath, codeOwnerEntries); + + if (!match) { + throw new Error( + `There is no matching entry for path: ${filepath} in the CODEOWNERS file`, + ); } - const result = pipe( - reverse, - head, - get('owners'), - head, - normalizeCodeOwner, - )(filteredOwners); - - return result; + return normalizeCodeOwner(match.owners[0]); } export function normalizeCodeOwner(owner: string) { From 9d6c429801c2ae1bddad6ae1694b9a2caff01f9e Mon Sep 17 00:00:00 2001 From: Kiss Miklos Date: Mon, 6 Mar 2023 18:45:30 +0100 Subject: [PATCH 022/247] add glob and wildcard tests Signed-off-by: Kiss Miklos --- .../codeowners/CodeOwnersProcessor.test.ts | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts index 8e1fbca726..6c2774a6fb 100644 --- a/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts @@ -17,11 +17,13 @@ import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { CodeOwnersProcessor } from './CodeOwnersProcessor'; -import { LocationSpec } from '@backstage/plugin-catalog-node'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; const mockCodeOwnersText = () => ` -* @acme/team-foo @acme/team-bar -/docs @acme/team-docs +* @acme/team-foo @acme/team-bar +/docs @acme/team-docs +/plugins/catalog-* @backstage/maintainers @backstage/catalog-core +**/logs @logs-owner `; describe('CodeOwnersProcessor', () => { @@ -105,7 +107,7 @@ describe('CodeOwnersProcessor', () => { spec: { owner: 'team-foo' }, }); }); - it('should use the "backstage.io/managed-by-location" annotation as pattern', async () => { + it('should match owner based on the targetUrl', async () => { const { entity, processor } = setupTest(); const result = await processor.preProcessEntity( @@ -118,5 +120,31 @@ describe('CodeOwnersProcessor', () => { spec: { owner: 'team-docs' }, }); }); + it('should match wildcard pattern', async () => { + const { entity, processor } = setupTest(); + + const result = await processor.preProcessEntity( + entity as any, + mockLocation({ basePath: 'plugins/catalog-foo' }), + ); + + expect(result).toEqual({ + ...entity, + spec: { owner: 'maintainers' }, + }); + }); + it('should match glob pattern', async () => { + const { entity, processor } = setupTest(); + + const result = await processor.preProcessEntity( + entity as any, + mockLocation({ basePath: 'plugins/catalog-foo/logs/1.txt' }), + ); + + expect(result).toEqual({ + ...entity, + spec: { owner: 'User:logs-owner' }, + }); + }); }); }); From 90ead6f213d235abf09f8eb66aa56c48c89e0096 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Wed, 8 Mar 2023 09:45:17 +0000 Subject: [PATCH 023/247] addressing code review comments Signed-off-by: Brian Fletcher --- ...dProviders.ts => DemoEventBasedEntityProvider.ts} | 6 ++++-- packages/backend/src/plugins/catalog.ts | 12 ++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) rename packages/backend/src/plugins/{catalogEventBasedProviders.ts => DemoEventBasedEntityProvider.ts} (91%) diff --git a/packages/backend/src/plugins/catalogEventBasedProviders.ts b/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts similarity index 91% rename from packages/backend/src/plugins/catalogEventBasedProviders.ts rename to packages/backend/src/plugins/DemoEventBasedEntityProvider.ts index 346ed40fa6..57f9f05570 100644 --- a/packages/backend/src/plugins/catalogEventBasedProviders.ts +++ b/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts @@ -22,7 +22,9 @@ import { EventParams, EventSubscriber } from '@backstage/plugin-events-node'; import { Logger } from 'winston'; import { PluginEnvironment } from '../types'; -class DemoEventBasedEntityProvider implements EntityProvider, EventSubscriber { +export class DemoEventBasedEntityProvider + implements EntityProvider, EventSubscriber +{ constructor( private readonly logger: Logger, private readonly topics: string[], @@ -55,7 +57,7 @@ export default async function createCatalogEventBasedProviders( const providers: Array< (EntityProvider & EventSubscriber) | Array > = []; - providers.push(new DemoEventBasedEntityProvider(env.logger, ['example'])); + providers.push(); // add your event-based entity providers here return providers.flat(); } diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index cd1a92ad7b..d895b0e85a 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -18,16 +18,20 @@ import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; -import createCatalogEventBasedProviders from './catalogEventBasedProviders'; +import { DemoEventBasedEntityProvider } from './DemoEventBasedEntityProvider'; export default async function createPlugin( env: PluginEnvironment, ): Promise { - const providers = await createCatalogEventBasedProviders(env); + const { eventBroker, logger } = env; + const builder = await CatalogBuilder.create(env); builder.addProcessor(new ScaffolderEntitiesProcessor()); - env.eventBroker.subscribe(providers); - builder.addEntityProvider(providers); + + const demoProvider = new DemoEventBasedEntityProvider(logger, ['example']); + eventBroker.subscribe(demoProvider); + builder.addEntityProvider(demoProvider); + const { processingEngine, router } = await builder.build(); await processingEngine.start(); return router; From c251ef8007d021c1ab060fd143b59ab78595966d Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Wed, 8 Mar 2023 10:30:20 +0000 Subject: [PATCH 024/247] update readme to indicate changes Signed-off-by: Brian Fletcher --- .../plugins/DemoEventBasedEntityProvider.ts | 12 ------- plugins/events-backend/README.md | 34 ++++++++++++------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts b/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts index 57f9f05570..198349cb06 100644 --- a/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts +++ b/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts @@ -20,7 +20,6 @@ import { } from '@backstage/plugin-catalog-node'; import { EventParams, EventSubscriber } from '@backstage/plugin-events-node'; import { Logger } from 'winston'; -import { PluginEnvironment } from '../types'; export class DemoEventBasedEntityProvider implements EntityProvider, EventSubscriber @@ -50,14 +49,3 @@ export class DemoEventBasedEntityProvider return DemoEventBasedEntityProvider.name; } } - -export default async function createCatalogEventBasedProviders( - env: PluginEnvironment, -): Promise> { - const providers: Array< - (EntityProvider & EventSubscriber) | Array - > = []; - providers.push(); - // add your event-based entity providers here - return providers.flat(); -} diff --git a/plugins/events-backend/README.md b/plugins/events-backend/README.md index cef8ac7330..869ee85ea2 100644 --- a/plugins/events-backend/README.md +++ b/plugins/events-backend/README.md @@ -24,6 +24,20 @@ to the used event broker. yarn add --cwd packages/backend @backstage/plugin-events-backend ``` +You will need to add the following to the backend configuration `#makeCreateEnv`. + +```diff +// packages/backend/src/index.ts ++ const eventBroker = new InMemoryEventBroker(root.child({ type: 'plugin' })); +``` + +Then update plugin environment to include the event broker. + +```diff +// packages/backend/src/types.ts ++ eventBroker: EventBroker; +``` + Add a file [`packages/backend/src/plugins/events.ts`](../../packages/backend/src/plugins/events.ts) to your Backstage project. @@ -38,25 +52,20 @@ Additionally, add the events plugin to your backend. // [...] + const eventsEnv = useHotMemoize(module, () => createEnv('events')); // [...] -+ apiRouter.use('/events', await events(eventsEnv, [])); ++ apiRouter.use('/events', await events(eventsEnv)); // [...] ``` ### With Event-based Entity Providers In case you use event-based `EntityProviders`, -you may need something like the following: +you will need to add the subscription to the catalog plugin creation: ```diff -// packages/backend/src/index.ts -- apiRouter.use('/events', await events(eventsEnv, [])); -+ apiRouter.use('/events', await events(eventsEnv, eventBasedEntityProviders)); +// packages/backend/src/plugins/catalog.ts ++ ``` -as well as a file -[`packages/backend/src/plugins/catalogEventBasedProviders.ts`](../../packages/backend/src/plugins/catalogEventBasedProviders.ts) -which contains event-based entity providers. - In case you don't have this dependency added yet: ```bash @@ -67,18 +76,19 @@ yarn add --cwd packages/backend @backstage/plugin-events-backend ```diff // packages/backend/src/plugins/catalog.ts import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; -+import { EntityProvider } from '@backstage/plugin-catalog-node'; ++import { DemoEventBasedEntityProvider } from './DemoEventBasedEntityProvider'; import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; export default async function createPlugin( env: PluginEnvironment, -+ providers?: Array, ): Promise { const builder = await CatalogBuilder.create(env); builder.addProcessor(new ScaffolderEntitiesProcessor()); -+ builder.addEntityProvider(providers ?? []); ++ const demoProvider = new DemoEventBasedEntityProvider(logger, ['example']); ++ env.eventBroker.subscribe(demoProvider); ++ builder.addEntityProvider(demoProvider); const { processingEngine, router } = await builder.build(); await processingEngine.start(); return router; From a444c53a033f20b6563566cf46b531357b35fe9f Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Wed, 8 Mar 2023 15:58:40 +0000 Subject: [PATCH 025/247] address code review comments Signed-off-by: Brian Fletcher --- .changeset/tall-meals-dress.md | 4 +++- packages/backend/src/index.ts | 4 ++-- .../plugins/DemoEventBasedEntityProvider.ts | 23 +++++++++++++++---- packages/backend/src/plugins/catalog.ts | 9 ++++---- plugins/events-backend/README.md | 2 +- plugins/events-backend/api-report.md | 22 +++++++++--------- plugins/events-backend/src/index.ts | 2 +- ...ker.test.ts => DefaultEventBroker.test.ts} | 8 +++---- ...ryEventBroker.ts => DefaultEventBroker.ts} | 4 ++-- .../src/service/EventsBackend.ts | 4 ++-- .../src/service/EventsPlugin.ts | 4 ++-- 11 files changed, 51 insertions(+), 35 deletions(-) rename plugins/events-backend/src/service/{InMemoryEventBroker.test.ts => DefaultEventBroker.test.ts} (94%) rename plugins/events-backend/src/service/{InMemoryEventBroker.ts => DefaultEventBroker.ts} (93%) diff --git a/.changeset/tall-meals-dress.md b/.changeset/tall-meals-dress.md index 42d50444aa..25f67847ae 100644 --- a/.changeset/tall-meals-dress.md +++ b/.changeset/tall-meals-dress.md @@ -2,4 +2,6 @@ '@backstage/plugin-events-backend': patch --- -Export `InMemoryEventBroker` to allow decoupling of the catalog and events backends in the `example-backend`. +Export `DefaultEventBroker` to allow decoupling of the catalog and events backends in the `example-backend`. + +Please look at `plugins/events-backend/README.md` for the currently advised was to set up the event backend and catalog providers. diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 3c07c3aad4..593d30bc3b 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -67,7 +67,7 @@ import linguist from './plugins/linguist'; import { PluginEnvironment } from './types'; import { ServerPermissionClient } from '@backstage/plugin-permission-node'; import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; -import { InMemoryEventBroker } from '@backstage/plugin-events-backend'; +import { DefaultEventBroker } from '@backstage/plugin-events-backend'; function makeCreateEnv(config: Config) { const root = getRootLogger(); @@ -85,7 +85,7 @@ function makeCreateEnv(config: Config) { discovery, }); - const eventBroker = new InMemoryEventBroker(root.child({ type: 'plugin' })); + const eventBroker = new DefaultEventBroker(root.child({ type: 'plugin' })); root.info(`Created UrlReader ${reader}`); diff --git a/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts b/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts index 198349cb06..b793816095 100644 --- a/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts +++ b/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts @@ -18,16 +18,29 @@ import { EntityProvider, EntityProviderConnection, } from '@backstage/plugin-catalog-node'; -import { EventParams, EventSubscriber } from '@backstage/plugin-events-node'; +import { + EventBroker, + EventParams, + EventSubscriber, +} from '@backstage/plugin-events-node'; import { Logger } from 'winston'; export class DemoEventBasedEntityProvider implements EntityProvider, EventSubscriber { - constructor( - private readonly logger: Logger, - private readonly topics: string[], - ) {} + private readonly logger: Logger; + private readonly topics: string[]; + + constructor(opts: { + eventBroker: EventBroker; + logger: Logger; + topics: string[]; + }) { + const { eventBroker, logger, topics } = opts; + eventBroker.subscribe(this); + this.logger = logger; + this.topics = topics; + } async onEvent(params: EventParams): Promise { this.logger.info( diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index d895b0e85a..ae00d5a7ec 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -23,13 +23,14 @@ import { DemoEventBasedEntityProvider } from './DemoEventBasedEntityProvider'; export default async function createPlugin( env: PluginEnvironment, ): Promise { - const { eventBroker, logger } = env; - const builder = await CatalogBuilder.create(env); builder.addProcessor(new ScaffolderEntitiesProcessor()); - const demoProvider = new DemoEventBasedEntityProvider(logger, ['example']); - eventBroker.subscribe(demoProvider); + const demoProvider = new DemoEventBasedEntityProvider({ + logger: env.logger, + topics: ['example'], + eventBroker: env.eventBroker, + }); builder.addEntityProvider(demoProvider); const { processingEngine, router } = await builder.build(); diff --git a/plugins/events-backend/README.md b/plugins/events-backend/README.md index 869ee85ea2..af9812eab7 100644 --- a/plugins/events-backend/README.md +++ b/plugins/events-backend/README.md @@ -28,7 +28,7 @@ You will need to add the following to the backend configuration `#makeCreateEnv` ```diff // packages/backend/src/index.ts -+ const eventBroker = new InMemoryEventBroker(root.child({ type: 'plugin' })); ++ const eventBroker = new DefaultEventBroker(root.child({ type: 'plugin' })); ``` Then update plugin environment to include the event broker. diff --git a/plugins/events-backend/api-report.md b/plugins/events-backend/api-report.md index 5d520f10c2..aeb6f9363d 100644 --- a/plugins/events-backend/api-report.md +++ b/plugins/events-backend/api-report.md @@ -12,6 +12,17 @@ import express from 'express'; import { HttpPostIngressOptions } from '@backstage/plugin-events-node'; import { Logger } from 'winston'; +// @public +export class DefaultEventBroker implements EventBroker { + constructor(logger: Logger); + // (undocumented) + publish(params: EventParams): Promise; + // (undocumented) + subscribe( + ...subscribers: Array> + ): void; +} + // @public export class EventsBackend { constructor(logger: Logger); @@ -43,15 +54,4 @@ export class HttpPostIngressEventPublisher implements EventPublisher { // (undocumented) setEventBroker(eventBroker: EventBroker): Promise; } - -// @public -export class InMemoryEventBroker implements EventBroker { - constructor(logger: Logger); - // (undocumented) - publish(params: EventParams): Promise; - // (undocumented) - subscribe( - ...subscribers: Array> - ): void; -} ``` diff --git a/plugins/events-backend/src/index.ts b/plugins/events-backend/src/index.ts index d443eeabca..be173b677c 100644 --- a/plugins/events-backend/src/index.ts +++ b/plugins/events-backend/src/index.ts @@ -22,4 +22,4 @@ export { EventsBackend } from './service/EventsBackend'; export { HttpPostIngressEventPublisher } from './service/http'; -export { InMemoryEventBroker } from './service/InMemoryEventBroker'; +export { DefaultEventBroker } from './service/DefaultEventBroker'; diff --git a/plugins/events-backend/src/service/InMemoryEventBroker.test.ts b/plugins/events-backend/src/service/DefaultEventBroker.test.ts similarity index 94% rename from plugins/events-backend/src/service/InMemoryEventBroker.test.ts rename to plugins/events-backend/src/service/DefaultEventBroker.test.ts index 861cdda940..99e5f6f72d 100644 --- a/plugins/events-backend/src/service/InMemoryEventBroker.test.ts +++ b/plugins/events-backend/src/service/DefaultEventBroker.test.ts @@ -17,15 +17,15 @@ import { getVoidLogger } from '@backstage/backend-common'; import { TestEventSubscriber } from '@backstage/plugin-events-backend-test-utils'; import { EventParams, EventSubscriber } from '@backstage/plugin-events-node'; -import { InMemoryEventBroker } from './InMemoryEventBroker'; +import { DefaultEventBroker } from './DefaultEventBroker'; const logger = getVoidLogger(); -describe('InMemoryEventBroker', () => { +describe('DefaultEventBroker', () => { it('passes events to interested subscribers', () => { const subscriber1 = new TestEventSubscriber('test1', ['topicA', 'topicB']); const subscriber2 = new TestEventSubscriber('test2', ['topicB', 'topicC']); - const eventBroker = new InMemoryEventBroker(logger); + const eventBroker = new DefaultEventBroker(logger); eventBroker.subscribe(subscriber1); eventBroker.subscribe(subscriber2); @@ -86,7 +86,7 @@ describe('InMemoryEventBroker', () => { })(); const errorSpy = jest.spyOn(logger, 'error'); - const eventBroker = new InMemoryEventBroker(logger); + const eventBroker = new DefaultEventBroker(logger); eventBroker.subscribe(subscriber1); await eventBroker.publish({ topic, eventPayload: '1' }); diff --git a/plugins/events-backend/src/service/InMemoryEventBroker.ts b/plugins/events-backend/src/service/DefaultEventBroker.ts similarity index 93% rename from plugins/events-backend/src/service/InMemoryEventBroker.ts rename to plugins/events-backend/src/service/DefaultEventBroker.ts index d800d88dc5..c3824b3e7d 100644 --- a/plugins/events-backend/src/service/InMemoryEventBroker.ts +++ b/plugins/events-backend/src/service/DefaultEventBroker.ts @@ -22,14 +22,14 @@ import { import { Logger } from 'winston'; /** - * In-memory event broker which will pass the event to all registered subscribers + * In process event broker which will pass the event to all registered subscribers * interested in it. * Events will not be persisted in any form. * * @public */ // TODO(pjungermann): add prom metrics? (see plugins/catalog-backend/src/util/metrics.ts, etc.) -export class InMemoryEventBroker implements EventBroker { +export class DefaultEventBroker implements EventBroker { constructor(private readonly logger: Logger) {} private readonly subscribers: { diff --git a/plugins/events-backend/src/service/EventsBackend.ts b/plugins/events-backend/src/service/EventsBackend.ts index 77b1b538f7..4415b8703a 100644 --- a/plugins/events-backend/src/service/EventsBackend.ts +++ b/plugins/events-backend/src/service/EventsBackend.ts @@ -20,7 +20,7 @@ import { EventSubscriber, } from '@backstage/plugin-events-node'; import { Logger } from 'winston'; -import { InMemoryEventBroker } from './InMemoryEventBroker'; +import { DefaultEventBroker } from './DefaultEventBroker'; /** * A builder that helps wire up all component parts of the event management. @@ -33,7 +33,7 @@ export class EventsBackend { private subscribers: EventSubscriber[] = []; constructor(logger: Logger) { - this.eventBroker = new InMemoryEventBroker(logger); + this.eventBroker = new DefaultEventBroker(logger); } setEventBroker(eventBroker: EventBroker): EventsBackend { diff --git a/plugins/events-backend/src/service/EventsPlugin.ts b/plugins/events-backend/src/service/EventsPlugin.ts index 1ae038a2ac..07f2b11731 100644 --- a/plugins/events-backend/src/service/EventsPlugin.ts +++ b/plugins/events-backend/src/service/EventsPlugin.ts @@ -29,7 +29,7 @@ import { EventSubscriber, HttpPostIngressOptions, } from '@backstage/plugin-events-node'; -import { InMemoryEventBroker } from './InMemoryEventBroker'; +import { DefaultEventBroker } from './DefaultEventBroker'; import Router from 'express-promise-router'; import { HttpPostIngressEventPublisher } from './http'; @@ -113,7 +113,7 @@ export const eventsPlugin = createBackendPlugin({ router.use(eventsRouter); const eventBroker = - extensionPoint.eventBroker ?? new InMemoryEventBroker(winstonLogger); + extensionPoint.eventBroker ?? new DefaultEventBroker(winstonLogger); eventBroker.subscribe(extensionPoint.subscribers); [extensionPoint.publishers, http] From 586e7f08bba81006187dba9a921e8f95fb03c1a4 Mon Sep 17 00:00:00 2001 From: pitwegner Date: Thu, 9 Mar 2023 12:28:37 +0100 Subject: [PATCH 026/247] replace dns validation regex Signed-off-by: pitwegner --- .../src/validation/CommonValidatorFunctions.test.ts | 1 + .../catalog-model/src/validation/CommonValidatorFunctions.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts index a887a9aa2e..5dc3f80694 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts @@ -155,6 +155,7 @@ describe('CommonValidatorFunctions', () => { ['-a-b', false], ['a-b-', false], ['a--b', false], + ['xn--c6h', true], ['a_b', false], [`${'a'.repeat(63)}`, true], [`${'a'.repeat(64)}`, false], diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts index 40a85c5331..4532dd9844 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts @@ -91,7 +91,7 @@ export class CommonValidatorFunctions { typeof value === 'string' && value.length >= 1 && value.length <= 63 && - /^[a-z0-9]+(\-[a-z0-9]+)*$/.test(value) + /^((?!\-))(xn\-\-)?[a-z0-9\-]+[a-z0-9]$/.test(value) ); } From be9c42280736e1ae5e0a93fb3fab0605a3c702e3 Mon Sep 17 00:00:00 2001 From: pitwegner Date: Thu, 9 Mar 2023 12:35:32 +0100 Subject: [PATCH 027/247] generate changeset Signed-off-by: pitwegner --- .changeset/stale-files-sniff.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/stale-files-sniff.md diff --git a/.changeset/stale-files-sniff.md b/.changeset/stale-files-sniff.md new file mode 100644 index 0000000000..6ae2bbd110 --- /dev/null +++ b/.changeset/stale-files-sniff.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': minor +--- + +Modified the regex for dns label validation to support IDN domains From 547726995e5baca2cb305f9092d358519d327d22 Mon Sep 17 00:00:00 2001 From: pitwegner Date: Thu, 9 Mar 2023 13:07:25 +0100 Subject: [PATCH 028/247] fix regex and typo Signed-off-by: pitwegner --- .changeset/stale-files-sniff.md | 2 +- .../catalog-model/src/validation/CommonValidatorFunctions.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/stale-files-sniff.md b/.changeset/stale-files-sniff.md index 6ae2bbd110..b6f35645c4 100644 --- a/.changeset/stale-files-sniff.md +++ b/.changeset/stale-files-sniff.md @@ -2,4 +2,4 @@ '@backstage/catalog-model': minor --- -Modified the regex for dns label validation to support IDN domains +Modified the regex for DNS label validation to support IDN domains diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts index 4532dd9844..22157b616a 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts @@ -91,7 +91,7 @@ export class CommonValidatorFunctions { typeof value === 'string' && value.length >= 1 && value.length <= 63 && - /^((?!\-))(xn\-\-)?[a-z0-9\-]+[a-z0-9]$/.test(value) + /^((?!\-))(xn\-\-)?[a-z0-9\-]*[a-z0-9]$/.test(value) ); } From 675dadc492143248ac64cd099d3c17415cf668c0 Mon Sep 17 00:00:00 2001 From: pitwegner Date: Thu, 9 Mar 2023 13:29:22 +0100 Subject: [PATCH 029/247] simplify regex Signed-off-by: pitwegner --- .../catalog-model/src/validation/CommonValidatorFunctions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts index 22157b616a..18132ba561 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts @@ -91,7 +91,7 @@ export class CommonValidatorFunctions { typeof value === 'string' && value.length >= 1 && value.length <= 63 && - /^((?!\-))(xn\-\-)?[a-z0-9\-]*[a-z0-9]$/.test(value) + /^(?!\-)[a-z0-9\-]*[a-z0-9]$/.test(value) ); } From 651c8b414d100b3531ef4cfdc059f9e8b086f629 Mon Sep 17 00:00:00 2001 From: pitwegner Date: Thu, 9 Mar 2023 14:12:20 +0100 Subject: [PATCH 030/247] fix invalid test cases Signed-off-by: pitwegner --- .../src/validation/CommonValidatorFunctions.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts index 5dc3f80694..88b6e54113 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts @@ -125,7 +125,7 @@ describe('CommonValidatorFunctions', () => { ['a-b', true], ['-a-b', false], ['a-b-', false], - ['a--b', false], + ['a--b', true], ['a_b', false], ['adam.bertil.caesar', true], ['adam.ber-til.caesar', true], @@ -154,7 +154,7 @@ describe('CommonValidatorFunctions', () => { ['a-b', true], ['-a-b', false], ['a-b-', false], - ['a--b', false], + ['a--b', true], ['xn--c6h', true], ['a_b', false], [`${'a'.repeat(63)}`, true], From ed734bdd52439a09539c218d636486d90c56c81e Mon Sep 17 00:00:00 2001 From: pitwegner Date: Thu, 9 Mar 2023 15:35:16 +0100 Subject: [PATCH 031/247] fix more test cases Signed-off-by: pitwegner --- .../src/validation/KubernetesValidatorFunctions.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/catalog-model/src/validation/KubernetesValidatorFunctions.test.ts b/packages/catalog-model/src/validation/KubernetesValidatorFunctions.test.ts index c194774906..dad0631b47 100644 --- a/packages/catalog-model/src/validation/KubernetesValidatorFunctions.test.ts +++ b/packages/catalog-model/src/validation/KubernetesValidatorFunctions.test.ts @@ -31,7 +31,7 @@ describe('KubernetesValidatorFunctions', () => { ['a/a', true], ['a/aAb5C', true], ['a-b.c/v1', true], - ['a--b.c/v1', false], + ['a--b.c/v1', true], [ `${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat( 61, @@ -96,7 +96,7 @@ describe('KubernetesValidatorFunctions', () => { ['a-b', true], ['-a-b', false], ['a-b-', false], - ['a--b', false], + ['a--b', true], ['a_b', false], ['a.b', false], ])(`isValidNamespace %p ? %p`, (value, matches) => { @@ -121,7 +121,7 @@ describe('KubernetesValidatorFunctions', () => { ['a..b', true], ['a/a', true], ['a-b.c/a', true], - ['a--b.c/a', false], + ['a--b.c/a', true], [ `${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat( 61, @@ -178,7 +178,7 @@ describe('KubernetesValidatorFunctions', () => { ['a..b', true], ['a/a', true], ['a-b.c/a', true], - ['a--b.c/a', false], + ['a--b.c/a', true], [ `${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat( 61, From ba8fa5e4e1743ae4176064c700a2f6ef71277869 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Fri, 10 Mar 2023 13:28:05 +0000 Subject: [PATCH 032/247] fixing up the readme Signed-off-by: Brian Fletcher --- plugins/events-backend/README.md | 144 +++++++++++++++++++++---------- 1 file changed, 99 insertions(+), 45 deletions(-) diff --git a/plugins/events-backend/README.md b/plugins/events-backend/README.md index af9812eab7..2288c91f2e 100644 --- a/plugins/events-backend/README.md +++ b/plugins/events-backend/README.md @@ -6,7 +6,7 @@ This plugin provides the wiring of all extension points for managing events as defined by [plugin-events-node](../events-node) including backend plugin `EventsPlugin` and `EventsBackend`. -Additionally, it uses a simple in-memory implementation for +Additionally, it uses a simple in-process implementation for the `EventBroker` by default which you can replace with a more sophisticated implementation of your choice as you need (e.g., via module). @@ -24,7 +24,13 @@ to the used event broker. yarn add --cwd packages/backend @backstage/plugin-events-backend ``` -You will need to add the following to the backend configuration `#makeCreateEnv`. +### Event Broker + +First you will need to add and implementation of the `EventBroker` interface to the backend plugin environment. +This will allow event broker instance any backend plugins to publish and subscribe to events in order to communicate +between them. + +Add the following to `makeCreateEnv` ```diff // packages/backend/src/index.ts @@ -38,10 +44,69 @@ Then update plugin environment to include the event broker. + eventBroker: EventBroker; ``` -Add a file [`packages/backend/src/plugins/events.ts`](../../packages/backend/src/plugins/events.ts) -to your Backstage project. +### Publishing and Subscribing to events with the broker -There, you can add all publishers, subscribers, etc. you want. +Backend plugins are passed the event broker in the plugin environment at startup of the application. The plugin can +make use of this to communicate between parts of the application. + +Here is an example of a plugin publishing a payload to a topic. + +```typescript jsx +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + env.eventBroker.publish({ + topic: 'publish.example', + eventPayload: { message: 'Hello, World!' }, + metadata: {}, + }); +} +``` + +Here is an example of a plugin subscribing to a topic. + +```typescript jsx +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + env.eventBroker.subscribe([ + { + supportsEventTopics: ['publish.example'], + onEvent: async (params: EventParams) => { + env.logger.info(`receieved ${params.topic} event`); + }, + }, + ]); +} +``` + +### Implementing an `EventSubscriber` class + +More complex solutions might warrent the creation of a class that implements the `EventSubscriber` interface. e.g. + +```typescript jsx +import { EventSubscriber } from "./EventSubscriber"; + +class ExampleSubscriber implements EventSubscriber { + ... + + supportsEventTopics() { + return ['publish.example'] + } + + async onEvent(params: EventParams) { + env.logger.info(`receieved ${params.topic} event`) + } +} +``` + +### Events Backend + +The events backend plugin provides a router to handler http events and publish the http requests onto the event +broker. + +To configure it add a file [`packages/backend/src/plugins/events.ts`](../../packages/backend/src/plugins/events.ts) +to your Backstage project. Additionally, add the events plugin to your backend. @@ -56,46 +121,7 @@ Additionally, add the events plugin to your backend. // [...] ``` -### With Event-based Entity Providers - -In case you use event-based `EntityProviders`, -you will need to add the subscription to the catalog plugin creation: - -```diff -// packages/backend/src/plugins/catalog.ts -+ -``` - -In case you don't have this dependency added yet: - -```bash -# From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-events-backend -``` - -```diff -// packages/backend/src/plugins/catalog.ts - import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; -+import { DemoEventBasedEntityProvider } from './DemoEventBasedEntityProvider'; - import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; - import { Router } from 'express'; - import { PluginEnvironment } from '../types'; - - export default async function createPlugin( - env: PluginEnvironment, - ): Promise { - const builder = await CatalogBuilder.create(env); - builder.addProcessor(new ScaffolderEntitiesProcessor()); -+ const demoProvider = new DemoEventBasedEntityProvider(logger, ['example']); -+ env.eventBroker.subscribe(demoProvider); -+ builder.addEntityProvider(demoProvider); - const { processingEngine, router } = await builder.build(); - await processingEngine.start(); - return router; - } -``` - -## Configuration +#### Configuration In order to create HTTP endpoints to receive events for a certain topic, you need to add them at your configuration: @@ -125,6 +151,34 @@ in combination with suitable event subscribers. However, it is not limited to these use cases. +### Event-based Entity Providers + +You can implement the `EventSubscriber` interface on an `EntityProviders` to allow it to handle events from other plugins e.g. the event backend plugin +mentioned above. + +Assuming you have configured the `eventBroker` into the `PluginEnvironment` you can pass the broker to the entity provider for it to subscribe. + +```diff +// packages/backend/src/plugins/catalog.ts + import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; ++import { DemoEventBasedEntityProvider } from './DemoEventBasedEntityProvider'; + import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; + import { Router } from 'express'; + import { PluginEnvironment } from '../types'; + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + const builder = await CatalogBuilder.create(env); + builder.addProcessor(new ScaffolderEntitiesProcessor()); ++ const demoProvider = new DemoEventBasedEntityProvider({ logger: env.logger, topics: ['example'], eventBroker: env.eventBroker }); ++ builder.addEntityProvider(demoProvider); + const { processingEngine, router } = await builder.build(); + await processingEngine.start(); + return router; + } +``` + ## Use Cases ### Custom Event Broker From 2fbaf646110d98c32dc2661973357e69873ecd22 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Fri, 10 Mar 2023 13:33:47 +0000 Subject: [PATCH 033/247] fix typo Signed-off-by: Brian Fletcher --- plugins/events-backend/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/events-backend/README.md b/plugins/events-backend/README.md index 2288c91f2e..d5a321df94 100644 --- a/plugins/events-backend/README.md +++ b/plugins/events-backend/README.md @@ -82,7 +82,7 @@ export default async function createPlugin( ### Implementing an `EventSubscriber` class -More complex solutions might warrent the creation of a class that implements the `EventSubscriber` interface. e.g. +More complex solutions might need the creation of a class that implements the `EventSubscriber` interface. e.g. ```typescript jsx import { EventSubscriber } from "./EventSubscriber"; From 1d9da597be8c9193fcd15af0875ef7a8cae8a360 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Fri, 10 Mar 2023 13:46:33 +0000 Subject: [PATCH 034/247] move review comments Signed-off-by: Brian Fletcher --- .changeset/tall-meals-dress.md | 2 +- docs/integrations/bitbucketCloud/discovery.md | 33 +++++++++---------- docs/integrations/github/discovery.md | 33 ++++++++++--------- docs/integrations/github/org.md | 32 +++++++++--------- 4 files changed, 50 insertions(+), 50 deletions(-) diff --git a/.changeset/tall-meals-dress.md b/.changeset/tall-meals-dress.md index 25f67847ae..3d39c369fe 100644 --- a/.changeset/tall-meals-dress.md +++ b/.changeset/tall-meals-dress.md @@ -4,4 +4,4 @@ Export `DefaultEventBroker` to allow decoupling of the catalog and events backends in the `example-backend`. -Please look at `plugins/events-backend/README.md` for the currently advised was to set up the event backend and catalog providers. +Please look at `plugins/events-backend/README.md` for the currently advised way to set up the event backend and catalog providers. diff --git a/docs/integrations/bitbucketCloud/discovery.md b/docs/integrations/bitbucketCloud/discovery.md index f6ceab9ff7..c5291e3241 100644 --- a/docs/integrations/bitbucketCloud/discovery.md +++ b/docs/integrations/bitbucketCloud/discovery.md @@ -72,30 +72,29 @@ Additionally, you need to decide how you want to receive events from external so Set up your provider ```diff -// packages/backend/src/plugins/catalogEventBasedProviders.ts -+import { CatalogClient } from '@backstage/catalog-client'; +// packages/backend/src/plugins/catalog.ts + import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; +import { BitbucketCloudEntityProvider } from '@backstage/plugin-catalog-backend-module-bitbucket-cloud'; - import { EntityProvider } from '@backstage/plugin-catalog-node'; - import { EventSubscriber } from '@backstage/plugin-events-node'; + import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; + import { Router } from 'express'; import { PluginEnvironment } from '../types'; - export default async function createCatalogEventBasedProviders( -- _: PluginEnvironment, -+ env: PluginEnvironment, - ): Promise> { - const providers: Array< - (EntityProvider & EventSubscriber) | Array - > = []; -- // add your event-based entity providers here -+ providers.push( -+ BitbucketCloudEntityProvider.fromConfig(env.config, { + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + const builder = await CatalogBuilder.create(env); + builder.addProcessor(new ScaffolderEntitiesProcessor()); ++ const bitBucketProvider = BitbucketCloudEntityProvider.fromConfig(env.config, { + catalogApi: new CatalogClient({ discoveryApi: env.discovery }), + logger: env.logger, + scheduler: env.scheduler, + tokenManager: env.tokenManager, -+ }), -+ ); - return providers.flat(); ++ }); ++ env.eventBroker.subscribe(bitBucketProvider); ++ builder.addEntityProvider(bitBucketProvider); + const { processingEngine, router } = await builder.build(); + await processingEngine.start(); + return router; } ``` diff --git a/docs/integrations/github/discovery.md b/docs/integrations/github/discovery.md index bbbe86a834..da4af4b61b 100644 --- a/docs/integrations/github/discovery.md +++ b/docs/integrations/github/discovery.md @@ -68,21 +68,19 @@ Additionally, you need to decide how you want to receive events from external so Set up your provider ```diff -// packages/backend/src/plugins/catalogEventBasedProviders.ts +// packages/backend/src/plugins/catalog.ts + import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; +import { GithubEntityProvider } from '@backstage/plugin-catalog-backend-module-github'; - import { EntityProvider } from '@backstage/plugin-catalog-node'; - import { EventSubscriber } from '@backstage/plugin-events-node'; + import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; + import { Router } from 'express'; import { PluginEnvironment } from '../types'; - export default async function createCatalogEventBasedProviders( -- _: PluginEnvironment, -+ env: PluginEnvironment, - ): Promise> { - const providers: Array< - (EntityProvider & EventSubscriber) | Array - > = []; -- // add your event-based entity providers here -+ providers.push( -+ GithubEntityProvider.fromConfig(env.config, { + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + const builder = await CatalogBuilder.create(env); + builder.addProcessor(new ScaffolderEntitiesProcessor()); ++ const githubProvider = GithubEntityProvider.fromConfig(env.config, { + logger: env.logger, + // optional: alternatively, use scheduler with schedule defined in app-config.yaml + schedule: env.scheduler.createScheduledTaskRunner({ @@ -91,9 +89,12 @@ Set up your provider + }), + // optional: alternatively, use schedule + scheduler: env.scheduler, -+ }), -+ ); - return providers.flat(); ++ }); ++ env.eventBroker.subscribe(githubProvider); ++ builder.addEntityProvider(demoProvider); + const { processingEngine, router } = await builder.build(); + await processingEngine.start(); + return router; } ``` diff --git a/docs/integrations/github/org.md b/docs/integrations/github/org.md index 0aa9997bae..c0b7a74d69 100644 --- a/docs/integrations/github/org.md +++ b/docs/integrations/github/org.md @@ -75,21 +75,19 @@ Additionally, you need to decide how you want to receive events from external so Set up your provider ```diff -// packages/backend/src/plugins/catalogEventBasedProviders.ts +// packages/backend/src/plugins/catalog.ts + import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; +import { GithubOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-github'; - import { EntityProvider } from '@backstage/plugin-catalog-node'; - import { EventSubscriber } from '@backstage/plugin-events-node'; + import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; + import { Router } from 'express'; import { PluginEnvironment } from '../types'; - export default async function createCatalogEventBasedProviders( -- _: PluginEnvironment, -+ env: PluginEnvironment, - ): Promise> { - const providers: Array< - (EntityProvider & EventSubscriber) | Array - > = []; -- // add your event-based entity providers here -+ providers.push( -+ GithubOrgEntityProvider.fromConfig(env.config, { + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + const builder = await CatalogBuilder.create(env); + builder.addProcessor(new ScaffolderEntitiesProcessor()); ++ const githubOrgProvider = GithubOrgEntityProvider.fromConfig(env.config, { + id: 'production', + orgUrl: 'https://github.com/backstage', + logger: env.logger, @@ -97,9 +95,11 @@ Set up your provider + frequency: { minutes: 60 }, + timeout: { minutes: 15 }, + }), -+ }), -+ ); - return providers.flat(); ++ env.eventBroker.subscribe(githubOrgProvider); ++ builder.addEntityProvider(githubOrgProvider); + const { processingEngine, router } = await builder.build(); + await processingEngine.start(); + return router; } ``` From 4fa75c0fd7a8ba4b26a52ad56aba5cc554f9b39f Mon Sep 17 00:00:00 2001 From: pitwegner Date: Fri, 10 Mar 2023 17:07:57 +0100 Subject: [PATCH 035/247] Update packages/catalog-model/src/validation/CommonValidatorFunctions.ts Co-authored-by: Patrik Oldsberg Signed-off-by: pitwegner --- .../catalog-model/src/validation/CommonValidatorFunctions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts index 18132ba561..295c5b8895 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts @@ -91,7 +91,7 @@ export class CommonValidatorFunctions { typeof value === 'string' && value.length >= 1 && value.length <= 63 && - /^(?!\-)[a-z0-9\-]*[a-z0-9]$/.test(value) + /^[a-z0-9]+(?:\-+[a-z0-9]+)*$/.test(value) ); } From f235e4064fc5358faccafb51b73d5f685a99bc8a Mon Sep 17 00:00:00 2001 From: Kiss Miklos Date: Mon, 13 Mar 2023 10:18:46 +0100 Subject: [PATCH 036/247] return undefined if there is no match Signed-off-by: Kiss Miklos --- .../catalog-backend/src/modules/codeowners/lib/resolve.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts b/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts index 92fa4a0ef6..675f6542aa 100644 --- a/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts +++ b/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts @@ -30,13 +30,7 @@ export function resolveCodeOwner( const { filepath } = parseGitUrl(catalogInfoFileUrl); const match = codeowners.matchFile(filepath, codeOwnerEntries); - if (!match) { - throw new Error( - `There is no matching entry for path: ${filepath} in the CODEOWNERS file`, - ); - } - - return normalizeCodeOwner(match.owners[0]); + return match ? normalizeCodeOwner(match.owners[0]) : undefined; } export function normalizeCodeOwner(owner: string) { From 91306c7aa895a356940f62be6e60520d2e192a3f Mon Sep 17 00:00:00 2001 From: sblausten Date: Fri, 17 Mar 2023 15:38:08 +0100 Subject: [PATCH 037/247] Review comments and fixing bug Signed-off-by: sblausten --- .../CatalogKindExploreContent.test.tsx} | 0 .../CatalogKindExploreContent.tsx} | 0 .../index.ts | 0 .../src/components/DomainCard/DomainCard.test.tsx | 15 +++++++++++++++ .../src/components/DomainCard/DomainCard.tsx | 15 +++++++++++++++ .../explore/src/components/DomainCard/index.ts | 15 +++++++++++++++ 6 files changed, 45 insertions(+) rename plugins/explore/src/components/{EntityExplorerContent/EntityExplorerContent.test.tsx => CatalogKindExploreContent/CatalogKindExploreContent.test.tsx} (100%) rename plugins/explore/src/components/{EntityExplorerContent/EntityExplorerContent.tsx => CatalogKindExploreContent/CatalogKindExploreContent.tsx} (100%) rename plugins/explore/src/components/{EntityExplorerContent => CatalogKindExploreContent}/index.ts (100%) create mode 100644 plugins/explore/src/components/DomainCard/DomainCard.test.tsx create mode 100644 plugins/explore/src/components/DomainCard/DomainCard.tsx create mode 100644 plugins/explore/src/components/DomainCard/index.ts diff --git a/plugins/explore/src/components/EntityExplorerContent/EntityExplorerContent.test.tsx b/plugins/explore/src/components/CatalogKindExploreContent/CatalogKindExploreContent.test.tsx similarity index 100% rename from plugins/explore/src/components/EntityExplorerContent/EntityExplorerContent.test.tsx rename to plugins/explore/src/components/CatalogKindExploreContent/CatalogKindExploreContent.test.tsx diff --git a/plugins/explore/src/components/EntityExplorerContent/EntityExplorerContent.tsx b/plugins/explore/src/components/CatalogKindExploreContent/CatalogKindExploreContent.tsx similarity index 100% rename from plugins/explore/src/components/EntityExplorerContent/EntityExplorerContent.tsx rename to plugins/explore/src/components/CatalogKindExploreContent/CatalogKindExploreContent.tsx diff --git a/plugins/explore/src/components/EntityExplorerContent/index.ts b/plugins/explore/src/components/CatalogKindExploreContent/index.ts similarity index 100% rename from plugins/explore/src/components/EntityExplorerContent/index.ts rename to plugins/explore/src/components/CatalogKindExploreContent/index.ts diff --git a/plugins/explore/src/components/DomainCard/DomainCard.test.tsx b/plugins/explore/src/components/DomainCard/DomainCard.test.tsx new file mode 100644 index 0000000000..eb2f165350 --- /dev/null +++ b/plugins/explore/src/components/DomainCard/DomainCard.test.tsx @@ -0,0 +1,15 @@ +/* + * 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. + */ diff --git a/plugins/explore/src/components/DomainCard/DomainCard.tsx b/plugins/explore/src/components/DomainCard/DomainCard.tsx new file mode 100644 index 0000000000..eb2f165350 --- /dev/null +++ b/plugins/explore/src/components/DomainCard/DomainCard.tsx @@ -0,0 +1,15 @@ +/* + * 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. + */ diff --git a/plugins/explore/src/components/DomainCard/index.ts b/plugins/explore/src/components/DomainCard/index.ts new file mode 100644 index 0000000000..eb2f165350 --- /dev/null +++ b/plugins/explore/src/components/DomainCard/index.ts @@ -0,0 +1,15 @@ +/* + * 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. + */ From dfecc7db6b3f6bb69243de4a0a50761dd5c7cf07 Mon Sep 17 00:00:00 2001 From: sblausten Date: Fri, 17 Mar 2023 15:41:10 +0100 Subject: [PATCH 038/247] Review comments and fixing bug Signed-off-by: sblausten --- plugins/explore/api-report.md | 14 ++-- plugins/explore/package.json | 3 +- .../CatalogKindExploreContent.test.tsx | 12 +-- .../CatalogKindExploreContent.tsx | 19 +++-- .../CatalogKindExploreContent/index.ts | 2 +- .../DefaultExplorePage/DefaultExplorePage.tsx | 6 +- .../components/DomainCard/DomainCard.test.tsx | 54 +++++++++++++ .../src/components/DomainCard/DomainCard.tsx | 75 +++++++++++++++++++ .../src/components/DomainCard/index.ts | 17 +++++ .../components/EntityCard/EntityCard.test.tsx | 6 +- plugins/explore/src/components/index.ts | 3 +- 11 files changed, 181 insertions(+), 30 deletions(-) diff --git a/plugins/explore/api-report.md b/plugins/explore/api-report.md index d7bafb7ee0..27748aa8c5 100644 --- a/plugins/explore/api-report.md +++ b/plugins/explore/api-report.md @@ -9,6 +9,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { default as default_2 } from 'react'; import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { DomainEntity } from '@backstage/catalog-model'; import { ExploreToolsConfig } from '@backstage/plugin-explore-react'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; @@ -32,14 +33,17 @@ export const catalogEntityRouteRef: ExternalRouteRef< >; // @public (undocumented) -export const DomainExplorerContent: (props: { - title?: string | undefined; +export const CatalogKindExploreContent: (props: { + tabTitle?: string; + kind: string; }) => JSX.Element; // @public (undocumented) -export const EntityExplorerContent: (props: { - tabTitle?: string; - kind: string; +export const DomainCard: (props: { entity: DomainEntity }) => JSX.Element; + +// @public (undocumented) +export const DomainExplorerContent: (props: { + title?: string | undefined; }) => JSX.Element; // @public diff --git a/plugins/explore/package.json b/plugins/explore/package.json index d14a276ecc..2f5cad2244 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -46,7 +46,8 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "classnames": "^2.2.6", - "react-use": "^17.2.4" + "react-use": "^17.2.4", + "pluralize": "^8.0.0" }, "peerDependencies": { "@types/react": "^16.13.1 || ^17.0.0", diff --git a/plugins/explore/src/components/CatalogKindExploreContent/CatalogKindExploreContent.test.tsx b/plugins/explore/src/components/CatalogKindExploreContent/CatalogKindExploreContent.test.tsx index 4670a10855..6075561ff1 100644 --- a/plugins/explore/src/components/CatalogKindExploreContent/CatalogKindExploreContent.test.tsx +++ b/plugins/explore/src/components/CatalogKindExploreContent/CatalogKindExploreContent.test.tsx @@ -19,9 +19,9 @@ import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; -import { EntityExplorerContent } from './EntityExplorerContent'; +import { CatalogKindExploreContent } from './CatalogKindExploreContent'; -describe('', () => { +describe('', () => { const catalogApi = { addLocation: jest.fn(), getEntities: jest.fn(), @@ -79,7 +79,7 @@ describe('', () => { const { getByText } = await renderInTestApp( - + , mountedRoutes, ); @@ -95,7 +95,7 @@ describe('', () => { const { getByText } = await renderInTestApp( - + , mountedRoutes, ); @@ -108,7 +108,7 @@ describe('', () => { const { getByText } = await renderInTestApp( - + , mountedRoutes, ); @@ -124,7 +124,7 @@ describe('', () => { const { getByText } = await renderInTestApp( - + , mountedRoutes, ); diff --git a/plugins/explore/src/components/CatalogKindExploreContent/CatalogKindExploreContent.tsx b/plugins/explore/src/components/CatalogKindExploreContent/CatalogKindExploreContent.tsx index 55ef13d98e..f3f4ccd103 100644 --- a/plugins/explore/src/components/CatalogKindExploreContent/CatalogKindExploreContent.tsx +++ b/plugins/explore/src/components/CatalogKindExploreContent/CatalogKindExploreContent.tsx @@ -30,10 +30,11 @@ import { WarningPanel, } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; +import pluralize from 'pluralize'; const Body = (props: { kind: string }) => { const { kind } = props; - const kindPlural = `${kind}s`; + const kindPlural = pluralize(kind); const catalogApi = useApi(catalogApiRef); const { value: entities, @@ -44,7 +45,7 @@ const Body = (props: { kind: string }) => { filter: { kind }, }); return response.items as Entity[]; - }, [catalogApi]); + }, [kind]); if (loading) { return ; @@ -87,23 +88,21 @@ const Body = (props: { kind: string }) => { }; /** @public */ -export const EntityExplorerContent = (props: { +export const CatalogKindExploreContent = (props: { tabTitle?: string; kind: string; }) => { const { kind, tabTitle } = props; + const kindLowercase = kind.toLocaleLowerCase(); + const kindCapitalized = `${kind[0].toLocaleUpperCase()}${kind.substring(1)}`; return ( - + - Discover the {kind.toLocaleLowerCase()}s in your ecosystem. + Discover the {pluralize(kindLowercase)} in your ecosystem. - + ); }; diff --git a/plugins/explore/src/components/CatalogKindExploreContent/index.ts b/plugins/explore/src/components/CatalogKindExploreContent/index.ts index ab06193939..af4cafae62 100644 --- a/plugins/explore/src/components/CatalogKindExploreContent/index.ts +++ b/plugins/explore/src/components/CatalogKindExploreContent/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { EntityExplorerContent } from './EntityExplorerContent'; +export { CatalogKindExploreContent } from './CatalogKindExploreContent'; diff --git a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.tsx b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.tsx index 49a79f5143..689da217fe 100644 --- a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.tsx +++ b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.tsx @@ -19,7 +19,7 @@ import { ExploreLayout } from '../ExploreLayout'; import { GroupsExplorerContent } from '../GroupsExplorerContent'; import { ToolExplorerContent } from '../ToolExplorerContent'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; -import { EntityExplorerContent } from '../EntityExplorerContent'; +import { CatalogKindExploreContent } from '../CatalogKindExploreContent'; export const DefaultExplorePage = () => { const configApi = useApi(configApiRef); @@ -32,10 +32,10 @@ export const DefaultExplorePage = () => { subtitle="Discover solutions available in your ecosystem" > - + - + diff --git a/plugins/explore/src/components/DomainCard/DomainCard.test.tsx b/plugins/explore/src/components/DomainCard/DomainCard.test.tsx index eb2f165350..1e4b9b30f1 100644 --- a/plugins/explore/src/components/DomainCard/DomainCard.test.tsx +++ b/plugins/explore/src/components/DomainCard/DomainCard.test.tsx @@ -1,3 +1,57 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DomainEntity } from '@backstage/catalog-model'; +import { entityRouteRef } from '@backstage/plugin-catalog-react'; +import { renderInTestApp } from '@backstage/test-utils'; +import React from 'react'; +import { DomainCard } from './DomainCard'; + +describe('', () => { + it('renders a domain card', async () => { + const entity: DomainEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Domain', + metadata: { + name: 'artists', + description: 'Everything about artists', + tags: ['a-tag'], + }, + spec: { + owner: 'guest', + }, + }; + const { getByText } = await renderInTestApp( + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + expect(getByText('artists')).toBeInTheDocument(); + expect(getByText('Everything about artists')).toBeInTheDocument(); + expect(getByText('a-tag')).toBeInTheDocument(); + expect(getByText('Explore').parentElement).toHaveAttribute( + 'href', + '/catalog/default/domain/artists', + ); + }); +}); /* * Copyright 2023 The Backstage Authors * diff --git a/plugins/explore/src/components/DomainCard/DomainCard.tsx b/plugins/explore/src/components/DomainCard/DomainCard.tsx index eb2f165350..867109f03c 100644 --- a/plugins/explore/src/components/DomainCard/DomainCard.tsx +++ b/plugins/explore/src/components/DomainCard/DomainCard.tsx @@ -1,3 +1,78 @@ +/* + * 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. + */ + +import { DomainEntity, RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { + EntityRefLinks, + entityRouteParams, + getEntityRelations, + entityRouteRef, +} from '@backstage/plugin-catalog-react'; +import { + Box, + Card, + CardActions, + CardContent, + CardMedia, + Chip, +} from '@material-ui/core'; +import React from 'react'; + +import { LinkButton, ItemCardHeader } from '@backstage/core-components'; +import { useRouteRef } from '@backstage/core-plugin-api'; + +/** @public */ +export const DomainCard = (props: { entity: DomainEntity }) => { + const { entity } = props; + + const catalogEntityRoute = useRouteRef(entityRouteRef); + const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY); + const url = catalogEntityRoute(entityRouteParams(entity)); + + const owner = ( + + ); + + return ( + + + + + + {entity.metadata.tags?.length ? ( + + {entity.metadata.tags.map(tag => ( + + ))} + + ) : null} + {entity.metadata.description} + + + + Explore + + + + ); +}; /* * Copyright 2023 The Backstage Authors * diff --git a/plugins/explore/src/components/DomainCard/index.ts b/plugins/explore/src/components/DomainCard/index.ts index eb2f165350..47870e5645 100644 --- a/plugins/explore/src/components/DomainCard/index.ts +++ b/plugins/explore/src/components/DomainCard/index.ts @@ -1,3 +1,20 @@ +/* + * 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. + */ + +export { DomainCard } from './DomainCard'; /* * Copyright 2023 The Backstage Authors * diff --git a/plugins/explore/src/components/EntityCard/EntityCard.test.tsx b/plugins/explore/src/components/EntityCard/EntityCard.test.tsx index ccf5deb875..4b006e347f 100644 --- a/plugins/explore/src/components/EntityCard/EntityCard.test.tsx +++ b/plugins/explore/src/components/EntityCard/EntityCard.test.tsx @@ -14,15 +14,15 @@ * limitations under the License. */ -import { DomainEntity } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp } from '@backstage/test-utils'; import React from 'react'; import { EntityCard } from './EntityCard'; describe('', () => { - it('renders a domain card', async () => { - const entity: DomainEntity = { + it('renders an entity card', async () => { + const entity: Entity = { apiVersion: 'backstage.io/v1alpha1', kind: 'Domain', metadata: { diff --git a/plugins/explore/src/components/index.ts b/plugins/explore/src/components/index.ts index e2f7810216..b3aa82bc79 100644 --- a/plugins/explore/src/components/index.ts +++ b/plugins/explore/src/components/index.ts @@ -14,5 +14,6 @@ * limitations under the License. */ -export * from './EntityExplorerContent'; +export * from './CatalogKindExploreContent'; export * from './ExploreLayout'; +export * from './DomainCard'; From 7137f8f1dc6708a7d01f1bdd1e70aa9752f95a43 Mon Sep 17 00:00:00 2001 From: sblausten Date: Fri, 17 Mar 2023 15:43:22 +0100 Subject: [PATCH 039/247] Change to patch changeset Signed-off-by: sblausten --- .changeset/itchy-kiwis-unite.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/itchy-kiwis-unite.md b/.changeset/itchy-kiwis-unite.md index e0bee850eb..c7bcad6759 100644 --- a/.changeset/itchy-kiwis-unite.md +++ b/.changeset/itchy-kiwis-unite.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-explore': minor +'@backstage/plugin-explore': patch --- Extracted generic EntityExplorerContent component so that it is easy to show any component kinds in their own tab in the Explore page. From 2ad6372e450c769cf948641f785bb30cf09786e5 Mon Sep 17 00:00:00 2001 From: sblausten Date: Fri, 17 Mar 2023 15:44:31 +0100 Subject: [PATCH 040/247] Remove systems tab from defaults Signed-off-by: sblausten --- .../components/DefaultExplorePage/DefaultExplorePage.test.tsx | 1 - .../src/components/DefaultExplorePage/DefaultExplorePage.tsx | 3 --- 2 files changed, 4 deletions(-) diff --git a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx index c4c6b3011e..b04e9335e8 100644 --- a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx +++ b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx @@ -58,7 +58,6 @@ describe('', () => { const elements = getAllByRole('tab'); expect(elements.length).toBe(4); expect(getByText(elements[0], 'Domains')).toBeInTheDocument(); - expect(getByText(elements[1], 'Systems')).toBeInTheDocument(); expect(getByText(elements[2], 'Groups')).toBeInTheDocument(); expect(getByText(elements[3], 'Tools')).toBeInTheDocument(); }); diff --git a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.tsx b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.tsx index 689da217fe..098f73be2e 100644 --- a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.tsx +++ b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.tsx @@ -34,9 +34,6 @@ export const DefaultExplorePage = () => { - - - From d12f28c0f249b0eee7ac71f031de9f42097ad048 Mon Sep 17 00:00:00 2001 From: sblausten Date: Fri, 17 Mar 2023 15:45:42 +0100 Subject: [PATCH 041/247] Ammend readme Signed-off-by: sblausten --- plugins/explore/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/explore/README.md b/plugins/explore/README.md index 27093b69b8..44942ab2a3 100644 --- a/plugins/explore/README.md +++ b/plugins/explore/README.md @@ -2,7 +2,7 @@ Welcome to the explore plugin! -This plugin helps to visualize the domains, systems and tools in your ecosystem. +This plugin helps to visualize the top level entities like domains, groups and tools in your ecosystem. ## Setup From bda537de2c440509dae012207426a40bb7665640 Mon Sep 17 00:00:00 2001 From: sblausten Date: Fri, 17 Mar 2023 15:47:07 +0100 Subject: [PATCH 042/247] Rename prop Signed-off-by: sblausten --- .../CatalogKindExploreContent.test.tsx | 2 +- .../CatalogKindExploreContent/CatalogKindExploreContent.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/explore/src/components/CatalogKindExploreContent/CatalogKindExploreContent.test.tsx b/plugins/explore/src/components/CatalogKindExploreContent/CatalogKindExploreContent.test.tsx index 6075561ff1..4f4f0f034c 100644 --- a/plugins/explore/src/components/CatalogKindExploreContent/CatalogKindExploreContent.test.tsx +++ b/plugins/explore/src/components/CatalogKindExploreContent/CatalogKindExploreContent.test.tsx @@ -95,7 +95,7 @@ describe('', () => { const { getByText } = await renderInTestApp( - + , mountedRoutes, ); diff --git a/plugins/explore/src/components/CatalogKindExploreContent/CatalogKindExploreContent.tsx b/plugins/explore/src/components/CatalogKindExploreContent/CatalogKindExploreContent.tsx index f3f4ccd103..f6f76ce1e0 100644 --- a/plugins/explore/src/components/CatalogKindExploreContent/CatalogKindExploreContent.tsx +++ b/plugins/explore/src/components/CatalogKindExploreContent/CatalogKindExploreContent.tsx @@ -89,15 +89,15 @@ const Body = (props: { kind: string }) => { /** @public */ export const CatalogKindExploreContent = (props: { - tabTitle?: string; + title?: string; kind: string; }) => { - const { kind, tabTitle } = props; + const { kind, title } = props; const kindLowercase = kind.toLocaleLowerCase(); const kindCapitalized = `${kind[0].toLocaleUpperCase()}${kind.substring(1)}`; return ( - + Discover the {pluralize(kindLowercase)} in your ecosystem. From 194013944ad099a43767ccb0e1d174e8127b22f0 Mon Sep 17 00:00:00 2001 From: sblausten Date: Fri, 17 Mar 2023 15:48:19 +0100 Subject: [PATCH 043/247] Update api report Signed-off-by: sblausten --- plugins/explore/api-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/explore/api-report.md b/plugins/explore/api-report.md index 27748aa8c5..70ad479841 100644 --- a/plugins/explore/api-report.md +++ b/plugins/explore/api-report.md @@ -34,7 +34,7 @@ export const catalogEntityRouteRef: ExternalRouteRef< // @public (undocumented) export const CatalogKindExploreContent: (props: { - tabTitle?: string; + title?: string; kind: string; }) => JSX.Element; From 2216940d26db0421fc0b51b0c375747be88d7fde Mon Sep 17 00:00:00 2001 From: sblausten Date: Fri, 17 Mar 2023 15:50:10 +0100 Subject: [PATCH 044/247] deps Signed-off-by: sblausten --- plugins/explore/package.json | 4 ++-- yarn.lock | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 2f5cad2244..8f2e0a6126 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -46,8 +46,8 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "classnames": "^2.2.6", - "react-use": "^17.2.4", - "pluralize": "^8.0.0" + "pluralize": "^8.0.0", + "react-use": "^17.2.4" }, "peerDependencies": { "@types/react": "^16.13.1 || ^17.0.0", diff --git a/yarn.lock b/yarn.lock index ad03a668e0..561e390fd0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6045,6 +6045,7 @@ __metadata: classnames: ^2.2.6 cross-fetch: ^3.1.5 msw: ^1.0.0 + pluralize: ^8.0.0 react-use: ^17.2.4 peerDependencies: "@types/react": ^16.13.1 || ^17.0.0 From 386cfe0fc9f490e4b6011548f635be4ed4af17e6 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Mon, 20 Mar 2023 09:04:23 +0000 Subject: [PATCH 045/247] review comment Signed-off-by: Brian Fletcher --- docs/integrations/bitbucketCloud/discovery.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/integrations/bitbucketCloud/discovery.md b/docs/integrations/bitbucketCloud/discovery.md index e1bfc6ad45..6e1533dea0 100644 --- a/docs/integrations/bitbucketCloud/discovery.md +++ b/docs/integrations/bitbucketCloud/discovery.md @@ -92,7 +92,7 @@ export default async function createPlugin( const builder = await CatalogBuilder.create(env); builder.addProcessor(new ScaffolderEntitiesProcessor()); /* highlight-add-start */ - const bitBucketProvider = BitbucketCloudEntityProvider.fromConfig( + const bitbucketCloudProvider = BitbucketCloudEntityProvider.fromConfig( env.config, { catalogApi: new CatalogClient({ discoveryApi: env.discovery }), @@ -101,8 +101,8 @@ export default async function createPlugin( tokenManager: env.tokenManager, }, ); - env.eventBroker.subscribe(bitBucketProvider); - builder.addEntityProvider(bitBucketProvider); + env.eventBroker.subscribe(bitbucketCloudProvider); + builder.addEntityProvider(bitbucketCloudProvider); /* highlight-add-end */ const { processingEngine, router } = await builder.build(); await processingEngine.start(); From e604552e3d99e5b0cf9ed3fd07514e82276a36bb Mon Sep 17 00:00:00 2001 From: sblausten Date: Mon, 20 Mar 2023 11:31:29 +0100 Subject: [PATCH 046/247] Add test Signed-off-by: sblausten --- .../components/DefaultExplorePage/DefaultExplorePage.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx index b04e9335e8..153bcdb73b 100644 --- a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx +++ b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx @@ -56,7 +56,7 @@ describe('', () => { await waitFor(() => { const elements = getAllByRole('tab'); - expect(elements.length).toBe(4); + expect(elements.length).toBe(3); expect(getByText(elements[0], 'Domains')).toBeInTheDocument(); expect(getByText(elements[2], 'Groups')).toBeInTheDocument(); expect(getByText(elements[3], 'Tools')).toBeInTheDocument(); From 66669634ed00bf5ffd8172aecaf8e6f75d40b83a Mon Sep 17 00:00:00 2001 From: sblausten Date: Mon, 20 Mar 2023 13:47:30 +0100 Subject: [PATCH 047/247] Fix test Signed-off-by: sblausten --- .../components/DefaultExplorePage/DefaultExplorePage.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx index 153bcdb73b..62d197803f 100644 --- a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx +++ b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx @@ -58,8 +58,8 @@ describe('', () => { const elements = getAllByRole('tab'); expect(elements.length).toBe(3); expect(getByText(elements[0], 'Domains')).toBeInTheDocument(); - expect(getByText(elements[2], 'Groups')).toBeInTheDocument(); - expect(getByText(elements[3], 'Tools')).toBeInTheDocument(); + expect(getByText(elements[1], 'Groups')).toBeInTheDocument(); + expect(getByText(elements[2], 'Tools')).toBeInTheDocument(); }); }); }); From 42452f005dccd3bf6530ac90d168c5dbf3d98df9 Mon Sep 17 00:00:00 2001 From: headphonejames Date: Thu, 23 Mar 2023 19:14:28 -0700 Subject: [PATCH 048/247] Enable selecting items in the autocomplete picker component when a values in the query string are already provided in the URL. Fixes https://github.com/backstage/backstage/issues/17021 Signed-off-by: headphonejames --- .../EntityAutocompletePicker/EntityAutocompletePicker.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx index b81d681e6d..20294fa7f1 100644 --- a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx +++ b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx @@ -28,7 +28,6 @@ import { useEntityList, } from '../../hooks/useEntityListProvider'; import { EntityFilter } from '../../types'; -import _ from 'lodash'; type KeysMatchingCondition = T extends V ? K : never; type KeysMatching = { @@ -97,13 +96,10 @@ export function EntityAutocompletePicker< // Set selected options on query parameter updates; this happens at initial page load and from // external updates to the page location useEffect(() => { - if ( - queryParameters.length && - !_.isEqual(selectedOptions, queryParameters) - ) { + if (queryParameters.length) { setSelectedOptions(queryParameters); } - }, [selectedOptions, queryParameters]); + }, [queryParameters]); const availableOptions = Object.keys(availableValues ?? {}); const shouldAddFilter = selectedOptions.length && availableOptions.length; From 333f34386d6111a1d3c8853c19e4cbd09e847c05 Mon Sep 17 00:00:00 2001 From: headphonejames Date: Thu, 23 Mar 2023 19:16:56 -0700 Subject: [PATCH 049/247] Remove lodash library Signed-off-by: headphonejames --- plugins/catalog-react/package.json | 1 - yarn.lock | 1 - 2 files changed, 2 deletions(-) diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 166da09118..60212ecd5e 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -62,7 +62,6 @@ "@material-ui/lab": "4.0.0-alpha.61", "classnames": "^2.2.6", "jwt-decode": "^3.1.0", - "lodash": "^4.17.21", "material-ui-popup-state": "^1.9.3", "qs": "^6.9.4", "react-use": "^17.2.4", diff --git a/yarn.lock b/yarn.lock index 4d2b33b122..fc8d9824a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5509,7 +5509,6 @@ __metadata: classnames: ^2.2.6 cross-fetch: ^3.1.5 jwt-decode: ^3.1.0 - lodash: ^4.17.21 material-ui-popup-state: ^1.9.3 qs: ^6.9.4 react-test-renderer: ^16.13.1 From 77aa3a4d47ee8c3e4d272b51ca8442a7689c9c45 Mon Sep 17 00:00:00 2001 From: Andy Ladjadj Date: Fri, 17 Feb 2023 16:43:12 +0100 Subject: [PATCH 050/247] fix(plugins/adr): use path attribute to fetch file instead of name Signed-off-by: Andy Ladjadj --- .changeset/odd-plums-bake.md | 5 ++++ .../EntityAdrContent/EntityAdrContent.tsx | 27 +++++++++---------- 2 files changed, 17 insertions(+), 15 deletions(-) create mode 100644 .changeset/odd-plums-bake.md diff --git a/.changeset/odd-plums-bake.md b/.changeset/odd-plums-bake.md new file mode 100644 index 0000000000..f06bf4d61d --- /dev/null +++ b/.changeset/odd-plums-bake.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-adr': patch +--- + +use path attribute to fetch files instead of name diff --git a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx index eb5b4aed21..58183b5230 100644 --- a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx +++ b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx @@ -47,7 +47,7 @@ import { import { rootRouteRef } from '../../routes'; import { AdrContentDecorator, AdrReader } from '../AdrReader'; -import { adrApiRef } from '../../api'; +import { adrApiRef, AdrFileInfo } from '../../api'; import useAsync from 'react-use/lib/useAsync'; const useStyles = makeStyles((theme: Theme) => ({ @@ -68,9 +68,7 @@ export const EntityAdrContent = (props: { const classes = useStyles(); const { entity } = useEntity(); const rootLink = useRouteRef(rootRouteRef); - const [adrList, setAdrList] = useState< - { name: string; title?: string; status?: string; date?: string }[] - >([]); + const [adrList, setAdrList] = useState([]); const [searchParams, setSearchParams] = useSearchParams(); const scmIntegrations = useApi(scmIntegrationsApiRef); const adrApi = useApi(adrApiRef); @@ -82,7 +80,8 @@ export const EntityAdrContent = (props: { }, [entity, scmIntegrations]); const selectedAdr = - adrList.find(adr => adr.name === searchParams.get('record'))?.name ?? ''; + adrList.find(adr => adr.name === searchParams.get('record'))?.path ?? ''; + useEffect(() => { if (adrList.length && !selectedAdr) { searchParams.set('record', adrList[0].name); @@ -95,15 +94,13 @@ export const EntityAdrContent = (props: { return; } - const adrs = value.data - .filter( - (item: { type: string; name: string }) => - item.type === 'file' && - (filePathFilterFn - ? filePathFilterFn(item.name) - : madrFilePathFilter(item.name)), - ) - .map(({ name, title, status, date }) => ({ name, title, status, date })); + const adrs: AdrFileInfo[] = value.data.filter( + (item: AdrFileInfo) => + item.type === 'file' && + (filePathFilterFn + ? filePathFilterFn(item.name) + : madrFilePathFilter(item.name)), + ); setAdrList(adrs); }, [filePathFilterFn, value]); @@ -148,7 +145,7 @@ export const EntityAdrContent = (props: { button component={Link} to={`${rootLink()}?record=${adr.name}`} - selected={selectedAdr === adr.name} + selected={selectedAdr === adr.path} > Date: Tue, 14 Mar 2023 17:54:20 +0100 Subject: [PATCH 051/247] feat(plugins/adr): render subdirectories in list Signed-off-by: Andy Ladjadj --- plugins/adr/package.json | 1 + .../EntityAdrContent/EntityAdrContent.tsx | 155 +++++++++++++----- yarn.lock | 1 + 3 files changed, 118 insertions(+), 39 deletions(-) diff --git a/plugins/adr/package.json b/plugins/adr/package.json index 453ff62792..55bcc21d62 100644 --- a/plugins/adr/package.json +++ b/plugins/adr/package.json @@ -34,6 +34,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "lodash": "^4.17.21", "react-markdown": "^8.0.0", "react-use": "^17.2.4", "remark-gfm": "^3.0.1" diff --git a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx index 58183b5230..846678f718 100644 --- a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx +++ b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx @@ -16,9 +16,14 @@ import React, { useEffect, useState } from 'react'; import { Link, useSearchParams } from 'react-router-dom'; +import useAsync from 'react-use/lib/useAsync'; + +import groupBy from 'lodash/groupBy'; + import { Content, ContentHeader, + InfoCard, MissingAnnotationEmptyState, Progress, SupportButton, @@ -35,27 +40,118 @@ import { } from '@backstage/plugin-adr-common'; import { useEntity } from '@backstage/plugin-catalog-react'; import { + Box, Chip, + Collapse, Grid, List, ListItem, + ListItemIcon, ListItemText, makeStyles, Theme, Typography, } from '@material-ui/core'; +import ExpandLess from '@material-ui/icons/ExpandLess'; +import ExpandMore from '@material-ui/icons/ExpandMore'; +import FolderIcon from '@material-ui/icons/Folder'; +import { adrApiRef, AdrFileInfo } from '../../api'; import { rootRouteRef } from '../../routes'; import { AdrContentDecorator, AdrReader } from '../AdrReader'; -import { adrApiRef, AdrFileInfo } from '../../api'; -import useAsync from 'react-use/lib/useAsync'; const useStyles = makeStyles((theme: Theme) => ({ adrMenu: { backgroundColor: theme.palette.background.paper, }, + adrContainerTitle: { + color: theme.palette.grey[700], + marginBottom: theme.spacing(1), + }, + adrChip: { + position: 'absolute', + right: 0, + }, })); +const AdrListContainer = (props: { + adrs: AdrFileInfo[]; + selectedAdr: string; + title: string; +}) => { + const { adrs, selectedAdr, title } = props; + const classes = useStyles(); + const rootLink = useRouteRef(rootRouteRef); + const [open, setOpen] = React.useState(true); + + const getChipColor = (status: string) => { + switch (status) { + case 'accepted': + return 'primary'; + case 'rejected' || 'deprecated': + return 'secondary'; + default: + return 'default'; + } + }; + + const handleClick = () => { + setOpen(!open); + }; + + return ( + <> + {title && ( + + + + + + {open ? : } + + )} + + + {adrs.map((adr, idx) => ( + + + {adr.date} + {adr.status && ( + + )} + + } + /> + + ))} + + + + ); +}; + /** * Component for browsing ADRs on an entity page. * @public @@ -67,7 +163,6 @@ export const EntityAdrContent = (props: { const { contentDecorators, filePathFilterFn } = props; const classes = useStyles(); const { entity } = useEntity(); - const rootLink = useRouteRef(rootRouteRef); const [adrList, setAdrList] = useState([]); const [searchParams, setSearchParams] = useSearchParams(); const scmIntegrations = useApi(scmIntegrationsApiRef); @@ -82,6 +177,10 @@ export const EntityAdrContent = (props: { const selectedAdr = adrList.find(adr => adr.name === searchParams.get('record'))?.path ?? ''; + const adrSubDirectoryFunc = (adr: AdrFileInfo) => { + return adr.path.split('/').slice(0, -1).join('/'); + }; + useEffect(() => { if (adrList.length && !selectedAdr) { searchParams.set('record', adrList[0].name); @@ -105,16 +204,9 @@ export const EntityAdrContent = (props: { setAdrList(adrs); }, [filePathFilterFn, value]); - const getChipColor = (status: string) => { - switch (status) { - case 'accepted': - return 'primary'; - case 'rejected' || 'deprecated': - return 'secondary'; - default: - return 'default'; - } - }; + const adrListGrouped = Object.entries( + groupBy(adrList, adrSubDirectoryFunc), + ).sort(); return ( @@ -138,33 +230,18 @@ export const EntityAdrContent = (props: { (adrList.length ? ( - - {adrList.map((adr, idx) => ( - - + + {adrListGrouped.map(([title, adrs], idx) => ( + - {adr.status && ( - - )} - - ))} - + ))} + + diff --git a/yarn.lock b/yarn.lock index 3f5fbf684b..0c16c829a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4316,6 +4316,7 @@ __metadata: "@types/git-url-parse": ^9.0.0 "@types/node": "*" cross-fetch: ^3.1.5 + lodash: ^4.17.21 msw: ^1.0.0 react-markdown: ^8.0.0 react-use: ^17.2.4 From 668b5733b15715fdfc030d501be7a5f4d9b56655 Mon Sep 17 00:00:00 2001 From: Andy Ladjadj Date: Wed, 15 Mar 2023 09:46:09 +0100 Subject: [PATCH 052/247] fix(plugins/adr): replace url record by the path Signed-off-by: Andy Ladjadj --- plugins/adr-common/src/index.ts | 2 +- .../components/EntityAdrContent/EntityAdrContent.tsx | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/adr-common/src/index.ts b/plugins/adr-common/src/index.ts index c3a048d9d1..25b61d5aa4 100644 --- a/plugins/adr-common/src/index.ts +++ b/plugins/adr-common/src/index.ts @@ -77,7 +77,7 @@ export type AdrFilePathFilterFn = (path: string) => boolean; * @public */ export const madrFilePathFilter: AdrFilePathFilterFn = (path: string) => - /^\d{4}-.+\.md$/.test(path); + /^(?>.+\/)?\d{4}-.+\.md$/.test(path); /** * ADR indexable document interface diff --git a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx index 846678f718..1567306f12 100644 --- a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx +++ b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx @@ -122,7 +122,7 @@ const AdrListContainer = (props: { component={Link} key={idx} selected={selectedAdr === adr.path} - to={`${rootLink()}?record=${adr.name}`} + to={`${rootLink()}?record=${adr.path}`} > adr.name === searchParams.get('record'))?.path ?? ''; + adrList.find(adr => adr.path === searchParams.get('record'))?.path ?? ''; const adrSubDirectoryFunc = (adr: AdrFileInfo) => { return adr.path.split('/').slice(0, -1).join('/'); @@ -183,7 +183,7 @@ export const EntityAdrContent = (props: { useEffect(() => { if (adrList.length && !selectedAdr) { - searchParams.set('record', adrList[0].name); + searchParams.set('record', adrList[0].path); setSearchParams(searchParams, { replace: true }); } }); @@ -197,8 +197,8 @@ export const EntityAdrContent = (props: { (item: AdrFileInfo) => item.type === 'file' && (filePathFilterFn - ? filePathFilterFn(item.name) - : madrFilePathFilter(item.name)), + ? filePathFilterFn(item.path) + : madrFilePathFilter(item.path)), ); setAdrList(adrs); From b012dcefba7cf17ca3b9d290d856f6f1644a9741 Mon Sep 17 00:00:00 2001 From: Andy Ladjadj Date: Wed, 15 Mar 2023 10:05:09 +0100 Subject: [PATCH 053/247] ci: update changeset Signed-off-by: Andy Ladjadj --- .changeset/odd-plums-bake.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/odd-plums-bake.md b/.changeset/odd-plums-bake.md index f06bf4d61d..d2d66e67e2 100644 --- a/.changeset/odd-plums-bake.md +++ b/.changeset/odd-plums-bake.md @@ -2,4 +2,4 @@ '@backstage/plugin-adr': patch --- -use path attribute to fetch files instead of name +use path attribute to fetch files instead of name and update the UI to navigate over subdirectories From 059e14136cdb1b004eb0f3ce69276ff1a3fd7fac Mon Sep 17 00:00:00 2001 From: Andy Ladjadj Date: Wed, 15 Mar 2023 11:03:46 +0100 Subject: [PATCH 054/247] fix(plugins/adr): use javascript regex compliant Signed-off-by: Andy Ladjadj --- plugins/adr-common/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/adr-common/src/index.ts b/plugins/adr-common/src/index.ts index 25b61d5aa4..ffdd9b349a 100644 --- a/plugins/adr-common/src/index.ts +++ b/plugins/adr-common/src/index.ts @@ -77,7 +77,7 @@ export type AdrFilePathFilterFn = (path: string) => boolean; * @public */ export const madrFilePathFilter: AdrFilePathFilterFn = (path: string) => - /^(?>.+\/)?\d{4}-.+\.md$/.test(path); + /^(?:.+\/)?\d{4}-.+\.md$/.test(path); /** * ADR indexable document interface From d07e48212018e2dbd959b0b3136dc6249bddac6d Mon Sep 17 00:00:00 2001 From: Andy Ladjadj Date: Fri, 24 Mar 2023 13:42:11 +0100 Subject: [PATCH 055/247] ci: update changeset to include plugin-adr-common Signed-off-by: Andy Ladjadj --- .changeset/odd-plums-bake.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/odd-plums-bake.md b/.changeset/odd-plums-bake.md index d2d66e67e2..427ab776b5 100644 --- a/.changeset/odd-plums-bake.md +++ b/.changeset/odd-plums-bake.md @@ -1,4 +1,5 @@ --- +'@backstage/plugin-adr-common': patch '@backstage/plugin-adr': patch --- From 0701dfc4b46abfe15e7ef078a069c7407dfc1ffb Mon Sep 17 00:00:00 2001 From: Andy Ladjadj Date: Fri, 24 Mar 2023 13:42:56 +0100 Subject: [PATCH 056/247] fix: update regex to keep retro compatibility Signed-off-by: Andy Ladjadj --- plugins/adr-common/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/adr-common/src/index.ts b/plugins/adr-common/src/index.ts index ffdd9b349a..cf71367cba 100644 --- a/plugins/adr-common/src/index.ts +++ b/plugins/adr-common/src/index.ts @@ -77,7 +77,7 @@ export type AdrFilePathFilterFn = (path: string) => boolean; * @public */ export const madrFilePathFilter: AdrFilePathFilterFn = (path: string) => - /^(?:.+\/)?\d{4}-.+\.md$/.test(path); + /^(?:.*\/)?\d{4}-.+\.md$/.test(path); /** * ADR indexable document interface From 79c2dd6d54331433736ebee106102d1c67c874a4 Mon Sep 17 00:00:00 2001 From: Andy Ladjadj Date: Fri, 24 Mar 2023 16:52:53 +0100 Subject: [PATCH 057/247] Update plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx Co-authored-by: Ben Lambert Signed-off-by: Andy Ladjadj --- .../adr/src/components/EntityAdrContent/EntityAdrContent.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx index 1567306f12..d136de4717 100644 --- a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx +++ b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx @@ -88,7 +88,8 @@ const AdrListContainer = (props: { switch (status) { case 'accepted': return 'primary'; - case 'rejected' || 'deprecated': + case 'rejected': + case 'deprecated': return 'secondary'; default: return 'default'; From d31664d154ab5327975a987c8cbfec6daa978ec2 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Fri, 24 Mar 2023 12:35:15 -0400 Subject: [PATCH 058/247] Remove from public interface and make techdocs specific. Signed-off-by: Aramis Sennyey --- .changeset/tender-terms-return.md | 1 - packages/core-app-api/api-report.md | 3 --- packages/core-app-api/src/routing/index.ts | 2 -- .../src/reader/components/TechDocsReaderPageContent/dom.tsx | 2 +- .../TechDocsReaderPageContent}/useNavigateUrl.test.tsx | 0 .../components/TechDocsReaderPageContent}/useNavigateUrl.tsx | 2 ++ 6 files changed, 3 insertions(+), 7 deletions(-) rename {packages/core-app-api/src/routing => plugins/techdocs/src/reader/components/TechDocsReaderPageContent}/useNavigateUrl.test.tsx (100%) rename {packages/core-app-api/src/routing => plugins/techdocs/src/reader/components/TechDocsReaderPageContent}/useNavigateUrl.tsx (97%) diff --git a/.changeset/tender-terms-return.md b/.changeset/tender-terms-return.md index 3e215b213f..9e8ab6beb7 100644 --- a/.changeset/tender-terms-return.md +++ b/.changeset/tender-terms-return.md @@ -1,5 +1,4 @@ --- -'@backstage/core-app-api': minor '@backstage/plugin-techdocs': patch --- diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 266cebb1f1..37f7a98de0 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -576,9 +576,6 @@ export class UrlPatternDiscovery implements DiscoveryApi { getBaseUrl(pluginId: string): Promise; } -// @public -export function useNavigateUrl(): (to: string) => void; - // @public export class WebStorage implements StorageApi { constructor(namespace: string, errorApi: ErrorApi); diff --git a/packages/core-app-api/src/routing/index.ts b/packages/core-app-api/src/routing/index.ts index 008657d135..46b5a6f9b4 100644 --- a/packages/core-app-api/src/routing/index.ts +++ b/packages/core-app-api/src/routing/index.ts @@ -18,5 +18,3 @@ export { FlatRoutes } from './FlatRoutes'; export type { FlatRoutesProps } from './FlatRoutes'; export { FeatureFlagged } from './FeatureFlagged'; export type { FeatureFlaggedProps } from './FeatureFlagged'; - -export { useNavigateUrl } from './useNavigateUrl'; diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx index 092875609d..cc42c46b4e 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx @@ -46,7 +46,7 @@ import { useSanitizerTransformer, useStylesTransformer, } from '../../transformers'; -import { useNavigateUrl } from '@backstage/core-app-api'; +import { useNavigateUrl } from './useNavigateUrl'; const MOBILE_MEDIA_QUERY = 'screen and (max-width: 76.1875em)'; diff --git a/packages/core-app-api/src/routing/useNavigateUrl.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/useNavigateUrl.test.tsx similarity index 100% rename from packages/core-app-api/src/routing/useNavigateUrl.test.tsx rename to plugins/techdocs/src/reader/components/TechDocsReaderPageContent/useNavigateUrl.test.tsx diff --git a/packages/core-app-api/src/routing/useNavigateUrl.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/useNavigateUrl.tsx similarity index 97% rename from packages/core-app-api/src/routing/useNavigateUrl.tsx rename to plugins/techdocs/src/reader/components/TechDocsReaderPageContent/useNavigateUrl.tsx index 3f067f475e..dfe1058d75 100644 --- a/packages/core-app-api/src/routing/useNavigateUrl.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/useNavigateUrl.tsx @@ -46,6 +46,8 @@ export function resolveUrlToRelative(url: string, baseUrl: string) { * NOTE: This does not support routing to external URLs. That should be done with a `Link` or `a` * element instead, or just `window.location.href`. * + * TODO: Update this to use `useRouteRef` instead of `useApi`. + * * @returns Navigation function that is a wrapper over `react-router-dom`'s * to support passing full URLs for navigation. * From 135b3d9a9c8226d021f7355b275f647fa1053e59 Mon Sep 17 00:00:00 2001 From: headphonejames Date: Fri, 24 Mar 2023 11:19:22 -0700 Subject: [PATCH 059/247] add test for removing tags after query string is provider Signed-off-by: headphonejames --- .../EntityTagPicker/EntityTagPicker.test.tsx | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx index c70217f3a4..bfaa689190 100644 --- a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx @@ -202,6 +202,34 @@ describe('', () => { tags: new EntityTagFilter(['tag2']), }); }); + + it('verify that user can select tags after query string has been set', async () => { + const updateFilters = jest.fn(); + render( + + + + + , + ); + await waitFor(() => + expect(updateFilters).toHaveBeenLastCalledWith({ + tags: new EntityTagFilter(['tag1']), + }), + ); + fireEvent.click(screen.getByTestId('tags-picker-expand')); + fireEvent.click(screen.getByLabelText('tag2')); + expect(screen.getByLabelText('tag2')).toBeChecked(); + expect(updateFilters).toHaveBeenLastCalledWith({ + tags: new EntityTagFilter(['tag1', 'tag2']), + }); + }); + it('removes tags from filters if there are none available', async () => { const updateFilters = jest.fn(); const mockCatalogApiRefNoTags = { From d1f5324dff7089ad8325b2c3e8e720b920ff94eb Mon Sep 17 00:00:00 2001 From: headphonejames Date: Fri, 24 Mar 2023 11:29:53 -0700 Subject: [PATCH 060/247] add changeset Signed-off-by: headphonejames --- .changeset/smart-crabs-dream.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/smart-crabs-dream.md diff --git a/.changeset/smart-crabs-dream.md b/.changeset/smart-crabs-dream.md new file mode 100644 index 0000000000..a08747a6e3 --- /dev/null +++ b/.changeset/smart-crabs-dream.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +Reverted the check if selectedOptions is different than queryParameters before invoking setSelectedOptions. This was preventing updating list items when a query string was already present in the URL when loading the page. From f95a7e2c5d4da5fa69f339da9088432e1c04b68c Mon Sep 17 00:00:00 2001 From: headphonejames Date: Fri, 24 Mar 2023 13:21:17 -0700 Subject: [PATCH 061/247] add changeset Signed-off-by: headphonejames --- .changeset/smart-crabs-dream.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/smart-crabs-dream.md b/.changeset/smart-crabs-dream.md index a08747a6e3..d93c865a23 100644 --- a/.changeset/smart-crabs-dream.md +++ b/.changeset/smart-crabs-dream.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-react': minor --- -Reverted the check if selectedOptions is different than queryParameters before invoking setSelectedOptions. This was preventing updating list items when a query string was already present in the URL when loading the page. +Reverted the check if the selected options list is different than the query parameters list before invoking setSelectedOptions method. This was preventing updating list items when a query string was already present in the URL when loading the page. From 3b0ac0d7e28038e800f7d57e9e2deed4b86d18de Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Fri, 3 Feb 2023 22:47:11 -0500 Subject: [PATCH 062/247] convert module mock to msw This should allow for easier refactoring and adding new microsoft-specific features. Signed-off-by: Jamie Klassen --- .../src/providers/microsoft/provider.test.ts | 146 +++++++++--------- 1 file changed, 69 insertions(+), 77 deletions(-) diff --git a/plugins/auth-backend/src/providers/microsoft/provider.test.ts b/plugins/auth-backend/src/providers/microsoft/provider.test.ts index e4e919004c..f455b0e3f6 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.test.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.test.ts @@ -15,84 +15,60 @@ */ import { MicrosoftAuthProvider } from './provider'; -import * as helpers from '../../lib/passport/PassportStrategyHelper'; -import { OAuthResult } from '../../lib/oauth'; import { getVoidLogger } from '@backstage/backend-common'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { AuthResolverContext } from '../types'; +import express from 'express'; -jest.mock('../../lib/passport/PassportStrategyHelper', () => { - return { - executeFrameHandlerStrategy: jest.fn(), - }; -}); +describe('MicrosoftAuthProvider#handle', () => { + const server = setupServer(); + setupRequestMockHandlers(server); -const mockFrameHandler = jest.spyOn( - helpers, - 'executeFrameHandlerStrategy', -) as unknown as jest.MockedFunction< - () => Promise<{ result: OAuthResult; privateInfo: any }> ->; - -const mockResult = { - result: { - fullProfile: { - emails: [ - { - type: 'work', - value: 'conrad@example.com', + beforeEach(() => { + server.use( + rest.post( + 'https://login.microsoftonline.com/common/oauth2/v2.0/token', + (_, res, ctx) => + res( + ctx.json({ + token_type: 'Bearer', + scope: 'email openid profile User.Read', + expires_in: 123, + ext_expires_in: 123, + access_token: 'accessToken', + refresh_token: 'refreshToken', + id_token: 'idToken', + }), + ), + ), + rest.get('https://graph.microsoft.com/v1.0/me/', (_, res, ctx) => + res( + ctx.json({ + id: 'conrad', + displayName: 'Conrad', + surname: 'Ribas', + givenName: 'Francisco', + mail: 'conrad@example.com', + }), + ), + ), + rest.get( + 'https://graph.microsoft.com/v1.0/me/photos/*', + async (_, res, ctx) => { + const imageBuffer = new Uint8Array([104, 111, 119, 100, 121]).buffer; + return res( + ctx.set('Content-Length', imageBuffer.byteLength.toString()), + ctx.set('Content-Type', 'image/jpeg'), + ctx.body(imageBuffer), + ); }, - ], - displayName: 'Conrad', - name: { - familyName: 'Ribas', - givenName: 'Francisco', - }, - id: 'conrad', - provider: 'microsoft', - photos: [ - { - value: 'some-data', - }, - ], - }, - params: { - id_token: 'idToken', - scope: 'scope', - expires_in: 123, - }, - accessToken: 'accessToken', - }, - privateInfo: { - refreshToken: 'wacka', - }, -}; - -const server = setupServer(); -setupRequestMockHandlers(server); - -const setupHandlers = () => { - server.use( - rest.get( - 'https://graph.microsoft.com/v1.0/me/photos/*', - async (_, res, ctx) => { - const imageBuffer = new Uint8Array([104, 111, 119, 100, 121]).buffer; - return res( - ctx.set('Content-Length', imageBuffer.byteLength.toString()), - ctx.set('Content-Type', 'image/jpeg'), - ctx.body(imageBuffer), - ); - }, - ), - ); -}; - -describe('createMicrosoftProvider', () => { - it('should auth', async () => { - setupHandlers(); + ), + ); + }); + it('returns providerInfo and profile', async () => { const provider = new MicrosoftAuthProvider({ logger: getVoidLogger(), resolverContext: {} as AuthResolverContext, @@ -105,17 +81,25 @@ describe('createMicrosoftProvider', () => { }), clientId: 'mock', clientSecret: 'mock', - callbackUrl: 'mock', + callbackUrl: 'http://backstage.test/api/auth/microsoft/handler/frame', }); - mockFrameHandler.mockResolvedValueOnce(mockResult); - const { response } = await provider.handler({} as any); + const { response } = await provider.handler({ + method: 'GET', + url: 'http://backstage.test/api/auth/microsoft/handler/frame', + query: { + code: 'authorizationcode', + }, + headers: { host: 'backstage.test' }, + connection: {}, + } as unknown as express.Request); + expect(response).toEqual({ providerInfo: { accessToken: 'accessToken', expiresInSeconds: 123, idToken: 'idToken', - scope: 'scope', + scope: 'email openid profile User.Read', }, profile: { email: 'conrad@example.com', @@ -125,8 +109,7 @@ describe('createMicrosoftProvider', () => { }); }); - it('should return the base64 encoded photo data of the profile', async () => { - setupHandlers(); + it('returns base64 encoded photo data', async () => { const provider = new MicrosoftAuthProvider({ logger: getVoidLogger(), resolverContext: {} as AuthResolverContext, @@ -149,8 +132,17 @@ describe('createMicrosoftProvider', () => { }; }, }); - mockFrameHandler.mockResolvedValueOnce(mockResult); - const { response } = await provider.handler({} as any); + + const { response } = await provider.handler({ + method: 'GET', + url: 'http://backstage.test/api/auth/microsoft/handler/frame', + query: { + code: 'authorizationcode', + }, + headers: { host: 'backstage.test' }, + connection: {}, + } as unknown as express.Request); + const overloadedIdentity = response.backstageIdentity as any; const photo = overloadedIdentity.info.result.fullProfile.photos[0]; expect(photo.value).toEqual('data:image/jpeg;base64,aG93ZHk='); From af12902763ca2844b0bc85a5b908ad2944564755 Mon Sep 17 00:00:00 2001 From: headphonejames Date: Fri, 24 Mar 2023 13:55:55 -0700 Subject: [PATCH 063/247] return lodash library Signed-off-by: headphonejames --- plugins/catalog-react/package.json | 1 + yarn.lock | 1 + 2 files changed, 2 insertions(+) diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 60212ecd5e..166da09118 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -62,6 +62,7 @@ "@material-ui/lab": "4.0.0-alpha.61", "classnames": "^2.2.6", "jwt-decode": "^3.1.0", + "lodash": "^4.17.21", "material-ui-popup-state": "^1.9.3", "qs": "^6.9.4", "react-use": "^17.2.4", diff --git a/yarn.lock b/yarn.lock index fc8d9824a1..4d2b33b122 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5509,6 +5509,7 @@ __metadata: classnames: ^2.2.6 cross-fetch: ^3.1.5 jwt-decode: ^3.1.0 + lodash: ^4.17.21 material-ui-popup-state: ^1.9.3 qs: ^6.9.4 react-test-renderer: ^16.13.1 From 646025786e2bc985c61d5368510599364e467c15 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Fri, 3 Feb 2023 22:47:56 -0500 Subject: [PATCH 064/247] construct test subject via factory function Rather than using the MicrosoftAuthProvider constructor directly -- this is more faithful to how this object is instantiated in practice. Also, introduce FakeMicrosoftAPI to simulate negotiating scopes, authorization codes, access tokens, and refresh tokens with Azure. Signed-off-by: Jamie Klassen --- .../microsoft/__testUtils__/fake.test.ts | 90 ++++ .../providers/microsoft/__testUtils__/fake.ts | 126 ++++++ .../src/providers/microsoft/provider.test.ts | 394 ++++++++++++++---- 3 files changed, 525 insertions(+), 85 deletions(-) create mode 100644 plugins/auth-backend/src/providers/microsoft/__testUtils__/fake.test.ts create mode 100644 plugins/auth-backend/src/providers/microsoft/__testUtils__/fake.ts diff --git a/plugins/auth-backend/src/providers/microsoft/__testUtils__/fake.test.ts b/plugins/auth-backend/src/providers/microsoft/__testUtils__/fake.test.ts new file mode 100644 index 0000000000..dee37f0f6b --- /dev/null +++ b/plugins/auth-backend/src/providers/microsoft/__testUtils__/fake.test.ts @@ -0,0 +1,90 @@ +/* + * 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 { FakeMicrosoftAPI } from './fake'; + +describe('FakeMicrosoftAPI', () => { + const api = new FakeMicrosoftAPI(); + + describe('#token', () => { + it('exchanges auth codes', () => { + const { access_token } = api.token( + new URLSearchParams({ + grant_type: 'authorization_code', + code: api.generateAuthCode('User.Read'), + }), + ); + + expect(api.tokenHasScope(access_token, 'User.Read')).toBe(true); + }); + + it('supports scopes for the first requested audience only', () => { + const { access_token } = api.token( + new URLSearchParams({ + grant_type: 'authorization_code', + code: api.generateAuthCode('someaudience/somescope User.Read'), + }), + ); + + expect(api.tokenHasScope(access_token, 'User.Read')).toBe(false); + }); + + it('special openid scopes do not count towards the 1-audience limit', () => { + const { access_token } = api.token( + new URLSearchParams({ + grant_type: 'authorization_code', + code: api.generateAuthCode('openid offline_access User.Read'), + }), + ); + + expect(api.tokenHasScope(access_token, 'User.Read')).toBe(true); + }); + + it('refreshes tokens', () => { + const { access_token } = api.token( + new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: api.generateRefreshToken( + 'email openid profile User.Read', + ), + }), + ); + + expect( + api.tokenHasScope(access_token, 'email openid profile User.Read'), + ).toBe(true); + }); + it('requires `openid` scope for ID token', () => { + const { id_token } = api.token( + new URLSearchParams({ + grant_type: 'authorization_code', + code: api.generateAuthCode('User.Read'), + }), + ); + + expect(id_token).toBeUndefined(); + }); + it('requires `offline_access` scope for refresh token', () => { + const { refresh_token } = api.token( + new URLSearchParams({ + grant_type: 'authorization_code', + code: api.generateAuthCode('User.Read'), + }), + ); + + expect(refresh_token).toBeUndefined(); + }); + }); +}); diff --git a/plugins/auth-backend/src/providers/microsoft/__testUtils__/fake.ts b/plugins/auth-backend/src/providers/microsoft/__testUtils__/fake.ts new file mode 100644 index 0000000000..9b3ca09c57 --- /dev/null +++ b/plugins/auth-backend/src/providers/microsoft/__testUtils__/fake.ts @@ -0,0 +1,126 @@ +/* + * 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 { decodeJwt } from 'jose'; + +type Claims = { aud: string; scp: string }; + +export class FakeMicrosoftAPI { + generateAccessToken(scope: string): string { + return this.tokenWithClaims(this.allClaimsForScope(scope)).access_token; + } + generateAuthCode(scope: string): string { + return this.encodeClaims(this.allClaimsForScope(scope)); + } + generateRefreshToken(scope: string): string { + return this.encodeClaims(this.allClaimsForScope(scope)); + } + token(formData: URLSearchParams): { + access_token: string; + scope: string; + refresh_token?: string; + id_token?: string; + } { + const scopeParameter = formData.get('scope'); + const claims = + (scopeParameter && this.allClaimsForScope(scopeParameter)) ?? + formData.get('grant_type') === 'refresh_token' + ? this.decodeClaims(formData.get('refresh_token')!) + : this.decodeClaims(formData.get('code')!); + return { + ...this.tokenWithClaims(claims), + ...(this.hasScope(claims, 'offline_access') && { + refresh_token: this.encodeClaims(claims), + }), + ...(this.hasScope(claims, 'openid') && { + id_token: 'header.e30K.microsoft', + }), + }; + } + tokenHasScope(token: string, scope: string): boolean { + const { aud, scp } = decodeJwt(token); + return this.hasScope({ aud: aud as string, scp: scp as string }, scope); + } + private tokenWithClaims(claims: Claims): { + access_token: string; + scope: string; + } { + const filteredClaims = { + ...claims, + scp: claims.scp + .split(' ') + .filter(s => s !== 'offline_access') + .join(' '), + }; + return { + access_token: `header.${Buffer.from( + JSON.stringify(filteredClaims), + ).toString('base64')}.signature`, + scope: this.scopeFromClaims(filteredClaims), + }; + } + private allClaimsForScope(scope: string): Claims { + const scopes = scope.split(' ').map(this.parseScope); + const firstAudience = scopes + .map(({ aud }) => aud) + .find(aud => aud !== 'openid'); + return { + aud: firstAudience ?? '00000003-0000-0000-c000-000000000000', + scp: scopes + .filter(({ aud }) => aud === 'openid' || aud === firstAudience) + .map(({ scp }) => scp) + .join(' '), + }; + } + // auth codes and refresh tokens in this fake system are base64-encoded JSON + // strings of claims + private encodeClaims(claims: Claims): string { + return Buffer.from(JSON.stringify(claims)).toString('base64'); + } + private decodeClaims(encoded: string): Claims { + return JSON.parse(Buffer.from(encoded, 'base64').toString()); + } + private hasScope(claims: Claims, scope: string): boolean { + return this.scopeFromClaims(claims).includes(scope); + } + private parseScope(s: string): Claims { + if (s.includes('/')) { + const [aud, scp] = s.split('/'); + return { aud, scp }; + } + switch (s) { + case 'email': + case 'openid': + case 'offline_access': + case 'profile': { + return { aud: 'openid', scp: s }; + } + default: + return { aud: '00000003-0000-0000-c000-000000000000', scp: s }; + } + } + private scopeFromClaims(claims: Claims): string { + return claims.scp + .split(' ') + .map(this.parseScope) + .map(({ aud, scp }) => + aud === 'openid' || + claims.aud === '00000003-0000-0000-c000-000000000000' + ? scp + : `${claims.aud}/${scp}`, + ) + .join(' '); + } +} diff --git a/plugins/auth-backend/src/providers/microsoft/provider.test.ts b/plugins/auth-backend/src/providers/microsoft/provider.test.ts index f455b0e3f6..00ee34bc79 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.test.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.test.ts @@ -14,37 +14,73 @@ * limitations under the License. */ -import { MicrosoftAuthProvider } from './provider'; +import { microsoft } from './provider'; import { getVoidLogger } from '@backstage/backend-common'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { ConfigReader } from '@backstage/config'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { AuthResolverContext } from '../types'; +import { AuthProviderRouteHandlers, AuthResolverContext } from '../types'; import express from 'express'; +import crypto from 'crypto'; +import { FakeMicrosoftAPI } from './__testUtils__/fake'; + +describe('MicrosoftAuthProvider', () => { + const nonce = 'AAAAAAAAAAAAAAAAAAAAAA=='; // 16 bytes of zeros in base64 + const state = Buffer.from( + `nonce=${encodeURIComponent(nonce)}&env=development`, + ).toString('hex'); -describe('MicrosoftAuthProvider#handle', () => { const server = setupServer(); + const microsoftApi = new FakeMicrosoftAPI(); + let provider: AuthProviderRouteHandlers; + let response: jest.Mocked; + setupRequestMockHandlers(server); beforeEach(() => { + provider = microsoft.create()({ + providerId: 'microsoft', + globalConfig: { + baseUrl: 'http://backstage.test/api/auth', + appUrl: 'http://backstage.test', + isOriginAllowed: _ => true, + }, + config: new ConfigReader({ + development: { + tenantId: 'tenantId', + clientId: 'clientId', + clientSecret: 'clientSecret', + }, + }), + logger: getVoidLogger(), + resolverContext: {} as AuthResolverContext, + }); + server.use( rest.post( - 'https://login.microsoftonline.com/common/oauth2/v2.0/token', - (_, res, ctx) => - res( + 'https://login.microsoftonline.com/tenantId/oauth2/v2.0/token', + async (req, res, ctx) => { + return res( ctx.json({ + ...microsoftApi.token(new URLSearchParams(await req.text())), token_type: 'Bearer', - scope: 'email openid profile User.Read', expires_in: 123, ext_expires_in: 123, - access_token: 'accessToken', - refresh_token: 'refreshToken', - id_token: 'idToken', }), - ), + ); + }, ), - rest.get('https://graph.microsoft.com/v1.0/me/', (_, res, ctx) => - res( + rest.get('https://graph.microsoft.com/v1.0/me/', (req, res, ctx) => { + if ( + !microsoftApi.tokenHasScope( + req.headers.get('authorization')!.replace(/^Bearer /, ''), + 'User.Read', + ) + ) { + return res(ctx.status(403)); + } + return res( ctx.json({ id: 'conrad', displayName: 'Conrad', @@ -52,11 +88,19 @@ describe('MicrosoftAuthProvider#handle', () => { givenName: 'Francisco', mail: 'conrad@example.com', }), - ), - ), + ); + }), rest.get( 'https://graph.microsoft.com/v1.0/me/photos/*', - async (_, res, ctx) => { + async (req, res, ctx) => { + if ( + !microsoftApi.tokenHasScope( + req.headers.get('authorization')!.replace(/^Bearer /, ''), + 'User.Read', + ) + ) { + return res(ctx.status(403)); + } const imageBuffer = new Uint8Array([104, 111, 119, 100, 121]).buffer; return res( ctx.set('Content-Length', imageBuffer.byteLength.toString()), @@ -66,85 +110,265 @@ describe('MicrosoftAuthProvider#handle', () => { }, ), ); + response = { + cookie: jest.fn(), + end: jest.fn(), + json: jest.fn(), + setHeader: jest.fn(), + status: jest.fn(), + } as unknown as jest.Mocked; + response.status.mockReturnValue(response); }); - it('returns providerInfo and profile', async () => { - const provider = new MicrosoftAuthProvider({ - logger: getVoidLogger(), - resolverContext: {} as AuthResolverContext, - authHandler: async ({ fullProfile }) => ({ - profile: { - email: fullProfile.emails![0]!.value, - displayName: fullProfile.displayName, - picture: 'http://microsoft.com/lols', - }, - }), - clientId: 'mock', - clientSecret: 'mock', - callbackUrl: 'http://backstage.test/api/auth/microsoft/handler/frame', + describe('#start', () => { + const randomBytes = jest.spyOn( + crypto, + 'randomBytes', + ) as unknown as jest.MockedFunction<(size: number) => Buffer>; + + afterEach(() => { + randomBytes.mockRestore(); }); - const { response } = await provider.handler({ - method: 'GET', - url: 'http://backstage.test/api/auth/microsoft/handler/frame', - query: { - code: 'authorizationcode', - }, - headers: { host: 'backstage.test' }, - connection: {}, - } as unknown as express.Request); + it('redirects to authorize URL', async () => { + randomBytes.mockReturnValue(Buffer.from(nonce, 'base64')); - expect(response).toEqual({ - providerInfo: { - accessToken: 'accessToken', - expiresInSeconds: 123, - idToken: 'idToken', - scope: 'email openid profile User.Read', - }, - profile: { - email: 'conrad@example.com', - displayName: 'Conrad', - picture: 'http://microsoft.com/lols', - }, + await provider.start( + { + query: { + env: 'development', + scope: 'email openid profile User.Read', + }, + } as unknown as express.Request, + response, + ); + + expect(response.setHeader).toHaveBeenCalledWith( + 'Location', + 'https://login.microsoftonline.com/tenantId/oauth2/v2.0/authorize' + + '?response_type=code' + + '&redirect_uri=http%3A%2F%2Fbackstage.test%2Fapi%2Fauth%2Fmicrosoft%2Fhandler%2Fframe' + + '&scope=email%20openid%20profile%20User.Read' + + `&state=${state}` + + '&client_id=clientId', + ); }); }); - it('returns base64 encoded photo data', async () => { - const provider = new MicrosoftAuthProvider({ - logger: getVoidLogger(), - resolverContext: {} as AuthResolverContext, - authHandler: async ({ fullProfile }) => ({ - profile: { - email: fullProfile.emails![0]!.value, - displayName: fullProfile.displayName, - picture: 'http://microsoft.com/lols', - }, - }), - clientId: 'mock', - clientSecret: 'mock', - callbackUrl: 'mock', - // define resolver to return user `info` for photo validation - signInResolver: async (info, _) => { - return { - id: 'user.name', - token: 'token', - info: info, - }; - }, + describe('#handle', () => { + it('returns provider info and profile with photo data', async () => { + await provider.frameHandler( + { + query: { + env: 'development', + code: microsoftApi.generateAuthCode( + 'email openid profile User.Read', + ), + state, + }, + cookies: { + 'microsoft-nonce': nonce, + }, + } as unknown as express.Request, + response, + ); + + expect(response.end).toHaveBeenCalledWith( + expect.stringContaining( + encodeURIComponent( + JSON.stringify({ + type: 'authorization_response', + response: { + providerInfo: { + idToken: 'header.e30K.microsoft', + accessToken: microsoftApi.generateAccessToken( + 'email openid profile User.Read', + ), + scope: 'email openid profile User.Read', + expiresInSeconds: 123, + }, + profile: { + email: 'conrad@example.com', + picture: 'data:image/jpeg;base64,aG93ZHk=', + displayName: 'Conrad', + }, + }, + }), + ), + ), + ); }); - const { response } = await provider.handler({ - method: 'GET', - url: 'http://backstage.test/api/auth/microsoft/handler/frame', - query: { - code: 'authorizationcode', - }, - headers: { host: 'backstage.test' }, - connection: {}, - } as unknown as express.Request); + it('sets refresh token', async () => { + await provider.frameHandler( + { + query: { + env: 'development', + code: microsoftApi.generateAuthCode( + 'email offline_access openid profile User.Read', + ), + state, + }, + cookies: { + 'microsoft-nonce': nonce, + }, + } as unknown as express.Request, + response, + ); - const overloadedIdentity = response.backstageIdentity as any; - const photo = overloadedIdentity.info.result.fullProfile.photos[0]; - expect(photo.value).toEqual('data:image/jpeg;base64,aG93ZHk='); + expect(response.cookie).toHaveBeenCalledWith( + 'microsoft-refresh-token', + microsoftApi.generateRefreshToken( + 'email offline_access openid profile User.Read', + ), + { + domain: 'backstage.test', + httpOnly: true, + maxAge: 86400000000, + path: '/api/auth/microsoft', + sameSite: 'lax', + secure: false, + }, + ); + }); + + it('omits photo data when fetching it fails', async () => { + server.use( + rest.get('https://graph.microsoft.com/v1.0/me/photos/*', (_, res) => + res.networkError('remote hung up'), + ), + ); + + await provider.frameHandler( + { + query: { + env: 'development', + code: microsoftApi.generateAuthCode( + 'email openid profile User.Read', + ), + state, + }, + cookies: { + 'microsoft-nonce': nonce, + }, + } as unknown as express.Request, + response, + ); + + expect(response.end).toHaveBeenCalledWith( + expect.stringContaining( + encodeURIComponent( + JSON.stringify({ + type: 'authorization_response', + response: { + providerInfo: { + idToken: 'header.e30K.microsoft', + accessToken: microsoftApi.generateAccessToken( + 'email openid profile User.Read', + ), + scope: 'email openid profile User.Read', + expiresInSeconds: 123, + }, + profile: { + email: 'conrad@example.com', + displayName: 'Conrad', + }, + }, + }), + ), + ), + ); + }); + }); + + describe('#refresh', () => { + it('returns provider info and profile with photo data', async () => { + await provider.refresh!( + { + query: { + env: 'development', + scope: 'email openid profile User.Read', + }, + header: jest.fn(_ => 'XMLHttpRequest'), + cookies: { + 'microsoft-refresh-token': microsoftApi.generateRefreshToken( + 'email openid profile User.Read', + ), + }, + get: jest.fn(), + } as unknown as express.Request, + response, + ); + + expect(response.json).toHaveBeenCalledWith( + expect.objectContaining({ + providerInfo: { + accessToken: microsoftApi.generateAccessToken( + 'email openid profile User.Read', + ), + expiresInSeconds: 123, + idToken: 'header.e30K.microsoft', + scope: 'email openid profile User.Read', + }, + profile: { + email: 'conrad@example.com', + displayName: 'Conrad', + picture: 'data:image/jpeg;base64,aG93ZHk=', + }, + }), + ); + }); + + it('returns backstage identity when sign-in resolver is configured', async () => { + provider = microsoft.create({ + signIn: { + resolver: _ => + Promise.resolve({ + token: 'protectedheader.e30K.signature', + }), + }, + })({ + providerId: 'microsoft', + globalConfig: { + baseUrl: 'http://backstage.test/api/auth', + appUrl: 'http://backstage.test', + isOriginAllowed: _ => true, + }, + config: new ConfigReader({ + development: { + tenantId: 'tenantId', + clientId: 'clientId', + clientSecret: 'clientSecret', + }, + }), + logger: getVoidLogger(), + resolverContext: {} as AuthResolverContext, + }) as AuthProviderRouteHandlers; + + await provider.refresh!( + { + query: { + env: 'development', + scope: 'email openid profile User.Read', + }, + header: jest.fn(_ => 'XMLHttpRequest'), + cookies: { + 'microsoft-refresh-token': microsoftApi.generateRefreshToken( + 'email openid profile User.Read', + ), + }, + get: jest.fn(), + } as unknown as express.Request, + response, + ); + + expect(response.json).toHaveBeenCalledWith( + expect.objectContaining({ + backstageIdentity: expect.objectContaining({ + token: 'protectedheader.e30K.signature', + }), + }), + ); + }); }); }); From 475abd1dc3fa3cfe41b77d7f5337b23e04147cc0 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Tue, 14 Feb 2023 09:27:08 -0500 Subject: [PATCH 065/247] support non-microsoft graph scopes Signed-off-by: Jamie Klassen --- .changeset/mighty-tips-flow.md | 11 ++ .../src/providers/microsoft/provider.test.ts | 130 +++++++++++++----- .../src/providers/microsoft/provider.ts | 80 +++++++---- 3 files changed, 161 insertions(+), 60 deletions(-) create mode 100644 .changeset/mighty-tips-flow.md diff --git a/.changeset/mighty-tips-flow.md b/.changeset/mighty-tips-flow.md new file mode 100644 index 0000000000..d876942fde --- /dev/null +++ b/.changeset/mighty-tips-flow.md @@ -0,0 +1,11 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +The `microsoft` (i.e. Azure) auth provider now supports negotiating tokens for +Azure resources besides Microsoft Graph (e.g. AKS, Virtual Machines, Machine +Learning Services, etc.). When the `/frame/handler` endpoint is called with an +authorization code for a non-Microsoft Graph scope, the user profile will not be +fetched. Similarly no user profile or photo data will be fetched by the backend +if the `/refresh` endpoint is called with the `scope` query parameter strictly +containing scopes for resources besides Microsoft Graph. diff --git a/plugins/auth-backend/src/providers/microsoft/provider.test.ts b/plugins/auth-backend/src/providers/microsoft/provider.test.ts index 00ee34bc79..6f8df2d4df 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.test.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.test.ts @@ -39,7 +39,11 @@ describe('MicrosoftAuthProvider', () => { setupRequestMockHandlers(server); beforeEach(() => { - provider = microsoft.create()({ + provider = microsoft.create({ + signIn: { + resolver: microsoft.resolvers.emailMatchingUserEntityAnnotation(), + }, + })({ providerId: 'microsoft', globalConfig: { baseUrl: 'http://backstage.test/api/auth', @@ -54,8 +58,15 @@ describe('MicrosoftAuthProvider', () => { }, }), logger: getVoidLogger(), - resolverContext: {} as AuthResolverContext, - }); + resolverContext: { + issueToken: jest.fn(), + findCatalogUser: jest.fn(), + signInWithCatalogUser: _ => + Promise.resolve({ + token: 'header.e30K.backstage', + }), + } as AuthResolverContext, + }) as AuthProviderRouteHandlers; server.use( rest.post( @@ -147,8 +158,10 @@ describe('MicrosoftAuthProvider', () => { 'Location', 'https://login.microsoftonline.com/tenantId/oauth2/v2.0/authorize' + '?response_type=code' + - '&redirect_uri=http%3A%2F%2Fbackstage.test%2Fapi%2Fauth%2Fmicrosoft%2Fhandler%2Fframe' + - '&scope=email%20openid%20profile%20User.Read' + + `&redirect_uri=${encodeURIComponent( + 'http://backstage.test/api/auth/microsoft/handler/frame', + )}` + + `&scope=${encodeURIComponent('email openid profile User.Read')}` + `&state=${state}` + '&client_id=clientId', ); @@ -180,18 +193,58 @@ describe('MicrosoftAuthProvider', () => { type: 'authorization_response', response: { providerInfo: { - idToken: 'header.e30K.microsoft', accessToken: microsoftApi.generateAccessToken( 'email openid profile User.Read', ), scope: 'email openid profile User.Read', expiresInSeconds: 123, + idToken: 'header.e30K.microsoft', }, profile: { email: 'conrad@example.com', picture: 'data:image/jpeg;base64,aG93ZHk=', displayName: 'Conrad', }, + backstageIdentity: { + token: 'header.e30K.backstage', + identity: { type: 'user', ownershipEntityRefs: [] }, + }, + }, + }), + ), + ), + ); + }); + + it('returns access token for non-microsoft graph scope', async () => { + await provider.frameHandler( + { + query: { + env: 'development', + code: microsoftApi.generateAuthCode('aks-audience/user.read'), + state, + }, + cookies: { + 'microsoft-nonce': nonce, + }, + } as unknown as express.Request, + response, + ); + + expect(response.end).toHaveBeenCalledWith( + expect.stringContaining( + encodeURIComponent( + JSON.stringify({ + type: 'authorization_response', + response: { + providerInfo: { + accessToken: microsoftApi.generateAccessToken( + 'aks-audience/user.read', + ), + scope: 'aks-audience/user.read', + expiresInSeconds: 123, + }, + profile: {}, }, }), ), @@ -262,17 +315,21 @@ describe('MicrosoftAuthProvider', () => { type: 'authorization_response', response: { providerInfo: { - idToken: 'header.e30K.microsoft', accessToken: microsoftApi.generateAccessToken( 'email openid profile User.Read', ), scope: 'email openid profile User.Read', expiresInSeconds: 123, + idToken: 'header.e30K.microsoft', }, profile: { email: 'conrad@example.com', displayName: 'Conrad', }, + backstageIdentity: { + token: 'header.e30K.backstage', + identity: { type: 'user', ownershipEntityRefs: [] }, + }, }, }), ), @@ -306,45 +363,50 @@ describe('MicrosoftAuthProvider', () => { accessToken: microsoftApi.generateAccessToken( 'email openid profile User.Read', ), + scope: 'email openid profile User.Read', expiresInSeconds: 123, idToken: 'header.e30K.microsoft', - scope: 'email openid profile User.Read', }, profile: { email: 'conrad@example.com', - displayName: 'Conrad', picture: 'data:image/jpeg;base64,aG93ZHk=', + displayName: 'Conrad', }, }), ); }); - it('returns backstage identity when sign-in resolver is configured', async () => { - provider = microsoft.create({ - signIn: { - resolver: _ => - Promise.resolve({ - token: 'protectedheader.e30K.signature', - }), - }, - })({ - providerId: 'microsoft', - globalConfig: { - baseUrl: 'http://backstage.test/api/auth', - appUrl: 'http://backstage.test', - isOriginAllowed: _ => true, - }, - config: new ConfigReader({ - development: { - tenantId: 'tenantId', - clientId: 'clientId', - clientSecret: 'clientSecret', + it('returns access token for non-microsoft graph scope', async () => { + await provider.refresh!( + { + query: { + env: 'development', + scope: 'aks-audience/user.read', }, - }), - logger: getVoidLogger(), - resolverContext: {} as AuthResolverContext, - }) as AuthProviderRouteHandlers; + header: jest.fn(_ => 'XMLHttpRequest'), + cookies: { + 'microsoft-refresh-token': microsoftApi.generateRefreshToken( + 'aks-audience/user.read', + ), + }, + get: jest.fn(), + } as unknown as express.Request, + response, + ); + expect(response.json).toHaveBeenCalledWith({ + providerInfo: { + accessToken: microsoftApi.generateAccessToken( + 'aks-audience/user.read', + ), + expiresInSeconds: 123, + scope: 'aks-audience/user.read', + }, + profile: {}, + }); + }); + + it('returns backstage identity', async () => { await provider.refresh!( { query: { @@ -365,7 +427,7 @@ describe('MicrosoftAuthProvider', () => { expect(response.json).toHaveBeenCalledWith( expect.objectContaining({ backstageIdentity: expect.objectContaining({ - token: 'protectedheader.e30K.signature', + token: 'header.e30K.backstage', }), }), ); diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index 9b14a3c91b..5d02b7ef61 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -49,6 +49,8 @@ import { } from '../resolvers'; import { Logger } from 'winston'; import fetch from 'node-fetch'; +import { decodeJwt } from 'jose'; +import { Profile as PassportProfile } from 'passport'; const BACKSTAGE_SESSION_EXPIRATION = 3600; @@ -86,6 +88,12 @@ export class MicrosoftAuthProvider implements OAuthHandlers { authorizationURL: options.authorizationUrl, tokenURL: options.tokenUrl, passReqToCallback: false, + skipUserProfile: ( + accessToken: string, + done: (err: unknown, skip: boolean) => void, + ) => { + done(null, this.skipUserProfile(accessToken)); + }, }, ( accessToken: any, @@ -99,6 +107,17 @@ export class MicrosoftAuthProvider implements OAuthHandlers { ); } + private skipUserProfile = (accessToken: string): boolean => { + const { aud, scp } = decodeJwt(accessToken); + const hasGraphReadScope = + aud === '00000003-0000-0000-c000-000000000000' && + (scp as string) + .split(' ') + .map(s => s.toLowerCase()) + .includes('user.read'); + return !hasGraphReadScope; + }; + async start(req: OAuthStartRequest): Promise { return await executeRedirectStrategy(req, this._strategy, { scope: req.scope, @@ -126,53 +145,62 @@ export class MicrosoftAuthProvider implements OAuthHandlers { req.scope, ); - const fullProfile = await executeFetchUserProfileStrategy( - this._strategy, - accessToken, - ); - return { response: await this.handleResult({ - fullProfile, params, accessToken, + ...(!this.skipUserProfile(accessToken) && { + fullProfile: await executeFetchUserProfileStrategy( + this._strategy, + accessToken, + ), + }), }), refreshToken, }; } - private async handleResult(result: OAuthResult) { - const photo = await this.getUserPhoto(result.accessToken); - result.fullProfile.photos = photo ? [{ value: photo }] : undefined; - - const { profile } = await this.authHandler(result, this.resolverContext); + private async handleResult(result: { + fullProfile?: PassportProfile; + params: { + id_token?: string; + scope: string; + expires_in: number; + }; + accessToken: string; + refreshToken?: string; + }): Promise { + let profile = {}; + if (result.fullProfile) { + const photo = await this.getUserPhoto(result.accessToken); + result.fullProfile.photos = photo ? [{ value: photo }] : undefined; + ({ profile } = await this.authHandler( + result as OAuthResult, + this.resolverContext, + )); + } const expiresInSeconds = result.params.expires_in === undefined ? BACKSTAGE_SESSION_EXPIRATION : Math.min(result.params.expires_in, BACKSTAGE_SESSION_EXPIRATION); - const response: OAuthResponse = { + return { providerInfo: { - idToken: result.params.id_token, accessToken: result.accessToken, scope: result.params.scope, expiresInSeconds, + ...{ idToken: result.params.id_token }, }, profile, + ...(result.fullProfile && + this.signInResolver && { + backstageIdentity: await this.signInResolver( + { result: result as OAuthResult, profile }, + this.resolverContext, + ), + }), }; - - if (this.signInResolver) { - response.backstageIdentity = await this.signInResolver( - { - result, - profile, - }, - this.resolverContext, - ); - } - - return response; } private async getUserPhoto(accessToken: string): Promise { @@ -236,7 +264,7 @@ export const microsoft = createAuthProviderIntegration({ const authHandler: AuthHandler = options?.authHandler ? options.authHandler : async ({ fullProfile, params }) => ({ - profile: makeProfileInfo(fullProfile, params.id_token), + profile: makeProfileInfo(fullProfile ?? {}, params.id_token), }); const provider = new MicrosoftAuthProvider({ From c5a12022d88d63e52e78c4a847fabeba02330988 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Wed, 8 Feb 2023 15:26:20 -0500 Subject: [PATCH 066/247] refactor MicrosoftAuth to allow overriding some behaviors in subsequent commits Signed-off-by: Jamie Klassen --- packages/core-app-api/api-report.md | 19 +++++ .../auth/microsoft/MicrosoftAuth.ts | 75 ++++++++++++++++--- 2 files changed, 85 insertions(+), 9 deletions(-) diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index d7e4a22873..f2df03d806 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -442,6 +442,25 @@ export class LocalStorageFeatureFlags implements FeatureFlagsApi { export class MicrosoftAuth { // (undocumented) static create(options: OAuthApiCreateOptions): typeof microsoftAuthApiRef.T; + // (undocumented) + getAccessToken( + scope?: string | string[], + options?: AuthRequestOptions, + ): Promise; + // (undocumented) + getBackstageIdentity( + options?: AuthRequestOptions, + ): Promise; + // (undocumented) + getIdToken(options?: AuthRequestOptions): Promise; + // (undocumented) + getProfile(options?: AuthRequestOptions): Promise; + // (undocumented) + sessionState$(): Observable; + // (undocumented) + signIn(): Promise; + // (undocumented) + signOut(): Promise; } // @public diff --git a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts index 30cfde7c6e..3f08ecaac7 100644 --- a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts @@ -14,7 +14,13 @@ * limitations under the License. */ -import { microsoftAuthApiRef } from '@backstage/core-plugin-api'; +import { + microsoftAuthApiRef, + AuthRequestOptions, + AuthProviderInfo, + DiscoveryApi, + OAuthRequestApi, +} from '@backstage/core-plugin-api'; import { OAuth2 } from '../oauth2'; import { OAuthApiCreateOptions } from '../types'; @@ -30,7 +36,18 @@ const DEFAULT_PROVIDER = { * @public */ export default class MicrosoftAuth { + private oauth2: Record; + private environment: string; + private provider: AuthProviderInfo; + private oauthRequestApi: OAuthRequestApi; + private discoveryApi: DiscoveryApi; + + private static MicrosoftGraphID = '00000003-0000-0000-c000-000000000000'; + static create(options: OAuthApiCreateOptions): typeof microsoftAuthApiRef.T { + return new MicrosoftAuth(options); + } + private constructor(options: OAuthApiCreateOptions) { const { configApi, environment = 'development', @@ -46,13 +63,53 @@ export default class MicrosoftAuth { ], } = options; - return OAuth2.create({ - configApi, - discoveryApi, - oauthRequestApi, - provider, - environment, - defaultScopes, - }); + this.configApi = configApi; + this.environment = environment; + this.provider = provider; + this.oauthRequestApi = oauthRequestApi; + this.discoveryApi = discoveryApi; + + this.oauth2 = { + [MicrosoftAuth.MicrosoftGraphID]: OAuth2.create({ + configApi: this.configApi, + discoveryApi: this.discoveryApi, + oauthRequestApi: this.oauthRequestApi, + provider: this.provider, + environment: this.environment, + defaultScopes, + }), + }; + } + + private microsoftGraph(): OAuth2 { + return this.oauth2[MicrosoftAuth.MicrosoftGraphID]; + } + + getAccessToken(scope?: string | string[], options?: AuthRequestOptions) { + return this.microsoftGraph().getAccessToken(scope, options); + } + + getIdToken(options?: AuthRequestOptions) { + return this.microsoftGraph().getIdToken(options); + } + + getProfile(options?: AuthRequestOptions) { + return this.microsoftGraph().getProfile(options); + } + + getBackstageIdentity(options?: AuthRequestOptions) { + return this.microsoftGraph().getBackstageIdentity(options); + } + + signIn() { + return this.microsoftGraph().signIn(); + } + + signOut() { + return this.microsoftGraph().signOut(); + } + + sessionState$() { + return this.microsoftGraph().sessionState$(); } } From 000cbcc99a448c19bba369c196bef8e6ed9c5dda Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Thu, 9 Feb 2023 18:24:24 -0500 Subject: [PATCH 067/247] can get non-microsoft graph tokens via popup These tokens will need to be requested on demand with a popup every time: Azure does allow you to get a refresh token from its `/token` endpoint for a set of Microsoft Graph scopes and use that refresh token to get access tokens for other resources, but our session manager isn't quite subtle enough to handle use cases like that yet. Signed-off-by: Jamie Klassen --- .../auth/microsoft/MicrosoftAuth.test.ts | 178 ++++++++++++++++++ .../auth/microsoft/MicrosoftAuth.ts | 54 +++++- 2 files changed, 229 insertions(+), 3 deletions(-) create mode 100644 packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts diff --git a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts new file mode 100644 index 0000000000..e2aedbc9b4 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts @@ -0,0 +1,178 @@ +/* + * 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 MicrosoftAuth from './MicrosoftAuth'; +import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; +import { UrlPatternDiscovery } from '../../DiscoveryApi'; +import { ConfigReader } from '@backstage/config'; +import { microsoftAuthApiRef } from '@backstage/core-plugin-api'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupServer } from 'msw/node'; +import { rest } from 'msw'; + +describe('MicrosoftAuth', () => { + const server = setupServer(); + setupRequestMockHandlers(server); + const microsoftAuth = MicrosoftAuth.create({ + configApi: new ConfigReader(undefined), + oauthRequestApi: new MockOAuthApi(), + discoveryApi: UrlPatternDiscovery.compile( + 'http://backstage.test/api/{{ pluginId }}', + ), + }); + + const toHaveJWTClaims = function toHaveJWTClaims( + this: jest.MatcherContext, + received: string, + expected: Record, + ): jest.CustomMatcherResult { + let parsedClaims: Record; + try { + parsedClaims = JSON.parse( + Buffer.from(received.split('.')[1], 'base64').toString(), + ); + } catch (e) { + return { + pass: false, + message: () => + `Expected JWT with claims: ${this.utils.printExpected( + expected, + )}\nReceived invalid JWT ${this.utils.printReceived( + received, + )}\nError: ${e}`, + }; + } + const expectedResult = expect.objectContaining(expected); + return { + pass: this.equals(parsedClaims, expectedResult), + message: () => + `Expected JWT with claims: ${this.utils.printExpected( + expected, + )}\nReceived JWT with claims: ${this.utils.printReceived( + parsedClaims, + )}\n\n${this.utils.diff(expectedResult, parsedClaims)}`, + }; + }; + expect.extend({ toHaveJWTClaims }); + + describe('with a refresh token', () => { + beforeEach(() => { + server.use( + rest.get( + 'http://backstage.test/api/auth/microsoft/refresh', + (req, res, ctx) => { + const scope = + req.url.searchParams.get('scope') || + 'openid profile email User.Read'; + return res( + ctx.json({ + providerInfo: { + accessToken: `header.${Buffer.from( + JSON.stringify({ + aud: '00000003-0000-0000-c000-000000000000', + scp: 'openid profile email User.Read', + }), + ).toString('base64')}.signature`, + scope, + }, + }), + ); + }, + ), + ); + }); + + it('gets access token for Microsoft Graph', async () => { + const accessToken = await microsoftAuth.getAccessToken(); + + expect(accessToken).toHaveJWTClaims({ + aud: '00000003-0000-0000-c000-000000000000', + scp: 'openid profile email User.Read', + }); + }); + }); + + describe('without a refresh token', () => { + let mockRequester: jest.Mock; + let sut: typeof microsoftAuthApiRef.T; + + beforeEach(() => { + mockRequester = jest.fn(); + sut = MicrosoftAuth.create({ + configApi: new ConfigReader(undefined), + oauthRequestApi: { + createAuthRequester: jest.fn().mockReturnValue(mockRequester), + authRequest$: jest.fn(), + }, + discoveryApi: UrlPatternDiscovery.compile( + 'http://backstage.test/api/{{ pluginId }}', + ), + }); + server.use( + rest.get( + 'http://backstage.test/api/auth/microsoft/refresh', + (_, res, ctx) => { + return res( + ctx.status(401), + ctx.json({ + error: { + name: 'AuthenticationError', + message: + 'Refresh failed; caused by InputError: Missing session cookie', + cause: { + name: 'InputError', + message: 'Missing session cookie', + }, + }, + request: { + method: 'GET', + url: '/api/auth/microsoft/refresh?env=development', + }, + response: { + statusCode: 401, + }, + }), + ); + }, + ), + ); + }); + + it('gets access token for Microsoft Graph via popup', async () => { + await sut.getAccessToken(); + + expect(mockRequester).toHaveBeenCalledWith( + new Set(['User.Read', 'email', 'offline_access', 'openid', 'profile']), + ); + }); + + it('gets access token for other azure resources via popup', async () => { + await sut.getAccessToken('azure-resource/scope'); + + expect(mockRequester).toHaveBeenCalledWith( + new Set(['azure-resource/scope']), + ); + }); + + it('requests scopes for resource ID when scope contains a resource URI', async () => { + await sut.getAccessToken('api://customApiClientId/some.scope'); + + expect(mockRequester).toHaveBeenCalledWith( + new Set(['api://customApiClientId/some.scope']), + ); + }); + }); +}); diff --git a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts index 3f08ecaac7..1b2fc20f37 100644 --- a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts @@ -18,6 +18,7 @@ import { microsoftAuthApiRef, AuthRequestOptions, AuthProviderInfo, + ConfigApi, DiscoveryApi, OAuthRequestApi, } from '@backstage/core-plugin-api'; @@ -36,7 +37,8 @@ const DEFAULT_PROVIDER = { * @public */ export default class MicrosoftAuth { - private oauth2: Record; + private oauth2: { [aud: string]: OAuth2 }; + private configApi: ConfigApi | undefined; private environment: string; private provider: AuthProviderInfo; private oauthRequestApi: OAuthRequestApi; @@ -85,8 +87,54 @@ export default class MicrosoftAuth { return this.oauth2[MicrosoftAuth.MicrosoftGraphID]; } - getAccessToken(scope?: string | string[], options?: AuthRequestOptions) { - return this.microsoftGraph().getAccessToken(scope, options); + private static resourceForScopes(scope: string): Promise { + const audience = + scope + .split(' ') + .map(MicrosoftAuth.resourceForScope) + .find(aud => aud !== 'openid') ?? MicrosoftAuth.MicrosoftGraphID; + return Promise.resolve(audience); + } + + private static resourceForScope(scope: string): string { + const groups = scope.match(/^(?.*)\/(?[^\/]*)$/)?.groups; + if (groups) { + const { resourceURI } = groups; + const aud = resourceURI.replace(/^api:\/\//, ''); + return aud; + } + switch (scope) { + case 'email': + case 'openid': + case 'offline_access': + case 'profile': { + return 'openid'; + } + default: + return MicrosoftAuth.MicrosoftGraphID; + } + } + + async getAccessToken( + scope?: string | string[], + options?: AuthRequestOptions, + ): Promise { + const aud = + scope === undefined + ? MicrosoftAuth.MicrosoftGraphID + : await MicrosoftAuth.resourceForScopes( + Array.isArray(scope) ? scope.join(' ') : scope, + ); + if (!(aud in this.oauth2)) { + this.oauth2[aud] = OAuth2.create({ + configApi: this.configApi, + discoveryApi: this.discoveryApi, + oauthRequestApi: this.oauthRequestApi, + provider: this.provider, + environment: this.environment, + }); + } + return this.oauth2[aud].getAccessToken(scope, options); } getIdToken(options?: AuthRequestOptions) { From c15e0cedbe1981ccbe29bba83524d30b5357f74f Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Mon, 13 Feb 2023 11:50:04 -0500 Subject: [PATCH 068/247] can get non-microsoft graph tokens via refresh Next up will be to make it possible to _request_ refreshable tokens via `getAccessToken`, accounting for the fact that the `offline_access` scope does not appear in the auth-backend response when it is requested -- instead the response has a set-cookie header. Signed-off-by: Jamie Klassen --- .changeset/sixty-insects-visit.md | 9 +++ .../auth/microsoft/MicrosoftAuth.test.ts | 66 +++++-------------- .../DefaultAuthConnector.test.ts | 34 +++++----- .../lib/AuthConnector/DefaultAuthConnector.ts | 7 +- .../src/lib/AuthConnector/types.ts | 2 +- .../RefreshingAuthSessionManager.test.ts | 6 +- .../RefreshingAuthSessionManager.ts | 10 +-- 7 files changed, 57 insertions(+), 77 deletions(-) create mode 100644 .changeset/sixty-insects-visit.md diff --git a/.changeset/sixty-insects-visit.md b/.changeset/sixty-insects-visit.md new file mode 100644 index 0000000000..adbe27e0ac --- /dev/null +++ b/.changeset/sixty-insects-visit.md @@ -0,0 +1,9 @@ +--- +'@backstage/core-app-api': minor +--- + +The `AuthConnector` interface now supports specifying a set of scopes when +refreshing a session. The `DefaultAuthConnector` implementation passes the +`scope` query parameter to the auth-backend plugin appropriately. The +`RefreshingAuthSessionManager` passes any scopes in its `GetSessionRequest` +appropriately. diff --git a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts index e2aedbc9b4..61a0a5b64a 100644 --- a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts @@ -34,59 +34,20 @@ describe('MicrosoftAuth', () => { ), }); - const toHaveJWTClaims = function toHaveJWTClaims( - this: jest.MatcherContext, - received: string, - expected: Record, - ): jest.CustomMatcherResult { - let parsedClaims: Record; - try { - parsedClaims = JSON.parse( - Buffer.from(received.split('.')[1], 'base64').toString(), - ); - } catch (e) { - return { - pass: false, - message: () => - `Expected JWT with claims: ${this.utils.printExpected( - expected, - )}\nReceived invalid JWT ${this.utils.printReceived( - received, - )}\nError: ${e}`, - }; - } - const expectedResult = expect.objectContaining(expected); - return { - pass: this.equals(parsedClaims, expectedResult), - message: () => - `Expected JWT with claims: ${this.utils.printExpected( - expected, - )}\nReceived JWT with claims: ${this.utils.printReceived( - parsedClaims, - )}\n\n${this.utils.diff(expectedResult, parsedClaims)}`, - }; - }; - expect.extend({ toHaveJWTClaims }); - describe('with a refresh token', () => { beforeEach(() => { server.use( rest.get( 'http://backstage.test/api/auth/microsoft/refresh', - (req, res, ctx) => { - const scope = - req.url.searchParams.get('scope') || - 'openid profile email User.Read'; + async (req, res, ctx) => { + const scopeParam = req.url.searchParams.get('scope'); return res( ctx.json({ providerInfo: { - accessToken: `header.${Buffer.from( - JSON.stringify({ - aud: '00000003-0000-0000-c000-000000000000', - scp: 'openid profile email User.Read', - }), - ).toString('base64')}.signature`, - scope, + accessToken: scopeParam + ? 'tokenForOtherResource' + : 'tokenForGrantScopes', + scope: scopeParam || 'grant-resource/scope', }, }), ); @@ -95,13 +56,18 @@ describe('MicrosoftAuth', () => { ); }); - it('gets access token for Microsoft Graph', async () => { + it('gets access token with requested scopes for grant', async () => { const accessToken = await microsoftAuth.getAccessToken(); - expect(accessToken).toHaveJWTClaims({ - aud: '00000003-0000-0000-c000-000000000000', - scp: 'openid profile email User.Read', - }); + expect(accessToken).toEqual('tokenForGrantScopes'); + }); + + it('gets access token for other consented scopes besides those directly granted', async () => { + const accessToken = await microsoftAuth.getAccessToken( + 'azure-resource/scope', + ); + + expect(accessToken).toEqual('tokenForOtherResource'); }); }); diff --git a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts index 51da9cbfb5..c46e87581f 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts @@ -59,22 +59,22 @@ describe('DefaultAuthConnector', () => { jest.resetAllMocks(); }); - it('should refresh a session', async () => { + it('should refresh a session with scope', async () => { server.use( - rest.get('*', (_req, res, ctx) => + rest.get('*', (req, res, ctx) => res( ctx.json({ idToken: 'mock-id-token', accessToken: 'mock-access-token', - scopes: 'a b c', + scopes: req.url.searchParams.get('scope') || 'default-scope', expiresInSeconds: '60', }), ), ), ); - const helper = new DefaultAuthConnector(defaultOptions); - const session = await helper.refreshSession(); + const connector = new DefaultAuthConnector(defaultOptions); + const session = await connector.refreshSession(new Set(['a', 'b', 'c'])); expect(session.idToken).toBe('mock-id-token'); expect(session.accessToken).toBe('mock-access-token'); expect(session.scopes).toEqual(new Set(['a', 'b', 'c'])); @@ -89,8 +89,8 @@ describe('DefaultAuthConnector', () => { ), ); - const helper = new DefaultAuthConnector(defaultOptions); - await expect(helper.refreshSession()).rejects.toThrow( + const connector = new DefaultAuthConnector(defaultOptions); + await expect(connector.refreshSession()).rejects.toThrow( 'Auth refresh request failed, Error: Network NOPE', ); }); @@ -98,19 +98,19 @@ describe('DefaultAuthConnector', () => { it('should handle failure response when refreshing session', async () => { server.use(rest.get('*', (_req, res, ctx) => res(ctx.status(401, 'NOPE')))); - const helper = new DefaultAuthConnector(defaultOptions); - await expect(helper.refreshSession()).rejects.toThrow( + const connector = new DefaultAuthConnector(defaultOptions); + await expect(connector.refreshSession()).rejects.toThrow( 'Auth refresh request failed, NOPE', ); }); it('should fail if popup was rejected', async () => { const mockOauth = new MockOAuthApi(); - const helper = new DefaultAuthConnector({ + const connector = new DefaultAuthConnector({ ...defaultOptions, oauthRequestApi: mockOauth, }); - const promise = helper.createSession({ scopes: new Set(['a', 'b']) }); + const promise = connector.createSession({ scopes: new Set(['a', 'b']) }); await mockOauth.rejectAll(); await expect(promise).rejects.toMatchObject({ name: 'RejectedError' }); }); @@ -125,12 +125,12 @@ describe('DefaultAuthConnector', () => { scopes: 'a b', expiresInSeconds: 3600, }); - const helper = new DefaultAuthConnector({ + const connector = new DefaultAuthConnector({ ...defaultOptions, oauthRequestApi: mockOauth, }); - const sessionPromise = helper.createSession({ + const sessionPromise = connector.createSession({ scopes: new Set(['a', 'b']), }); @@ -153,13 +153,13 @@ describe('DefaultAuthConnector', () => { const popupSpy = jest .spyOn(loginPopup, 'showLoginPopup') .mockResolvedValue('my-session'); - const helper = new DefaultAuthConnector({ + const connector = new DefaultAuthConnector({ ...defaultOptions, oauthRequestApi: new MockOAuthApi(), sessionTransform: str => str, }); - const sessionPromise = helper.createSession({ + const sessionPromise = connector.createSession({ scopes: new Set(), instantPopup: true, }); @@ -174,13 +174,13 @@ describe('DefaultAuthConnector', () => { const popupSpy = jest .spyOn(loginPopup, 'showLoginPopup') .mockResolvedValue({ scopes: '' }); - const helper = new DefaultAuthConnector({ + const connector = new DefaultAuthConnector({ ...defaultOptions, joinScopes: scopes => `-${[...scopes].join('')}-`, oauthRequestApi: mockOauth, }); - helper.createSession({ scopes: new Set(['a', 'b']) }); + connector.createSession({ scopes: new Set(['a', 'b']) }); await mockOauth.triggerAll(); diff --git a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts index f2c35b0ba3..728ed3878c 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts @@ -126,9 +126,12 @@ export class DefaultAuthConnector return this.authRequester(options.scopes); } - async refreshSession(): Promise { + async refreshSession(scopes?: Set): Promise { const res = await fetch( - await this.buildUrl('/refresh', { optional: true }), + await this.buildUrl('/refresh', { + optional: true, + ...(scopes && { scope: this.joinScopesFunc(scopes) }), + }), { headers: { 'x-requested-with': 'XMLHttpRequest', diff --git a/packages/core-app-api/src/lib/AuthConnector/types.ts b/packages/core-app-api/src/lib/AuthConnector/types.ts index 464a2ed627..c8e83e1ccc 100644 --- a/packages/core-app-api/src/lib/AuthConnector/types.ts +++ b/packages/core-app-api/src/lib/AuthConnector/types.ts @@ -25,6 +25,6 @@ export type CreateSessionOptions = { */ export type AuthConnector = { createSession(options: CreateSessionOptions): Promise; - refreshSession(): Promise; + refreshSession(scopes?: Set): Promise; removeSession(): Promise; }; diff --git a/packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts b/packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts index a94aca85f8..23432856cb 100644 --- a/packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts +++ b/packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts @@ -47,7 +47,7 @@ describe('RefreshingAuthSessionManager', () => { await manager.getSession({}); expect(createSession).toHaveBeenCalledTimes(1); - expect(refreshSession).toHaveBeenCalledTimes(1); + expect(refreshSession).toHaveBeenCalledWith(undefined); expect(stateSubscriber.mock.calls).toEqual([ [SessionState.SignedOut], [SessionState.SignedIn], @@ -103,7 +103,7 @@ describe('RefreshingAuthSessionManager', () => { await manager.getSession({ scopes: new Set(['a']) }); expect(createSession).toHaveBeenCalledTimes(1); - expect(refreshSession).toHaveBeenCalledTimes(1); + expect(refreshSession).toHaveBeenCalledWith(new Set(['a'])); await manager.getSession({ scopes: new Set(['a']) }); expect(createSession).toHaveBeenCalledTimes(1); @@ -134,7 +134,7 @@ describe('RefreshingAuthSessionManager', () => { expect(await manager.getSession({ optional: true })).toBe(undefined); expect(createSession).toHaveBeenCalledTimes(0); - expect(refreshSession).toHaveBeenCalledTimes(1); + expect(refreshSession).toHaveBeenCalledWith(undefined); }); it('should forward option to instantly show auth popup and not attempt refresh', async () => { diff --git a/packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts b/packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts index 67d83bb36a..c04eb637de 100644 --- a/packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts +++ b/packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts @@ -73,7 +73,9 @@ export class RefreshingAuthSessionManager implements SessionManager { } try { - const refreshedSession = await this.collapsedSessionRefresh(); + const refreshedSession = await this.collapsedSessionRefresh( + options.scopes, + ); const currentScopes = this.sessionScopesFunc(this.currentSession!); const refreshedScopes = this.sessionScopesFunc(refreshedSession); if (hasScopes(refreshedScopes, currentScopes)) { @@ -97,7 +99,7 @@ export class RefreshingAuthSessionManager implements SessionManager { // already had an existing session. if (!this.currentSession && !options.instantPopup) { try { - const newSession = await this.collapsedSessionRefresh(); + const newSession = await this.collapsedSessionRefresh(options.scopes); this.currentSession = newSession; // The session might not have the scopes requested so go back and check again return this.getSession(options); @@ -130,12 +132,12 @@ export class RefreshingAuthSessionManager implements SessionManager { return this.stateTracker.sessionState$(); } - private async collapsedSessionRefresh(): Promise { + private async collapsedSessionRefresh(scopes?: Set): Promise { if (this.refreshPromise) { return this.refreshPromise; } - this.refreshPromise = this.connector.refreshSession(); + this.refreshPromise = this.connector.refreshSession(scopes); try { const session = await this.refreshPromise; From 01e6d459e2906c9b92b83d359e21223e1a34a279 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Mon, 13 Feb 2023 22:13:40 -0500 Subject: [PATCH 069/247] MicrosoftAuth automatically gets refresh tokens Signed-off-by: Jamie Klassen --- .changeset/mighty-tips-flow.md | 5 +++++ packages/core-app-api/api-report.md | 2 +- .../auth/microsoft/MicrosoftAuth.test.ts | 6 +++--- .../implementations/auth/microsoft/MicrosoftAuth.ts | 12 ++++++++---- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/.changeset/mighty-tips-flow.md b/.changeset/mighty-tips-flow.md index d876942fde..514d514588 100644 --- a/.changeset/mighty-tips-flow.md +++ b/.changeset/mighty-tips-flow.md @@ -9,3 +9,8 @@ authorization code for a non-Microsoft Graph scope, the user profile will not be fetched. Similarly no user profile or photo data will be fetched by the backend if the `/refresh` endpoint is called with the `scope` query parameter strictly containing scopes for resources besides Microsoft Graph. + +Furthermore, the `offline_access` scope will be requested by default, even when +it is not mentioned in the argument to `getAccessToken`. This means that any +Azure access token can be automatically refreshed, even if the user has not +signed in via Azure. diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index f2df03d806..4ce4533fe1 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -441,7 +441,7 @@ export class LocalStorageFeatureFlags implements FeatureFlagsApi { // @public export class MicrosoftAuth { // (undocumented) - static create(options: OAuthApiCreateOptions): typeof microsoftAuthApiRef.T; + static create(options: OAuth2CreateOptions): typeof microsoftAuthApiRef.T; // (undocumented) getAccessToken( scope?: string | string[], diff --git a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts index 61a0a5b64a..0d2e016b41 100644 --- a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts @@ -125,11 +125,11 @@ describe('MicrosoftAuth', () => { ); }); - it('gets access token for other azure resources via popup', async () => { + it('gets access + refresh token for other azure resources via popup', async () => { await sut.getAccessToken('azure-resource/scope'); expect(mockRequester).toHaveBeenCalledWith( - new Set(['azure-resource/scope']), + new Set(['azure-resource/scope', 'offline_access']), ); }); @@ -137,7 +137,7 @@ describe('MicrosoftAuth', () => { await sut.getAccessToken('api://customApiClientId/some.scope'); expect(mockRequester).toHaveBeenCalledWith( - new Set(['api://customApiClientId/some.scope']), + new Set(['api://customApiClientId/some.scope', 'offline_access']), ); }); }); diff --git a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts index 1b2fc20f37..fa74979c6b 100644 --- a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts @@ -22,8 +22,7 @@ import { DiscoveryApi, OAuthRequestApi, } from '@backstage/core-plugin-api'; -import { OAuth2 } from '../oauth2'; -import { OAuthApiCreateOptions } from '../types'; +import { OAuth2, OAuth2CreateOptions } from '../oauth2'; const DEFAULT_PROVIDER = { id: 'microsoft', @@ -43,13 +42,14 @@ export default class MicrosoftAuth { private provider: AuthProviderInfo; private oauthRequestApi: OAuthRequestApi; private discoveryApi: DiscoveryApi; + private scopeTransform: (scopes: string[]) => string[]; private static MicrosoftGraphID = '00000003-0000-0000-c000-000000000000'; - static create(options: OAuthApiCreateOptions): typeof microsoftAuthApiRef.T { + static create(options: OAuth2CreateOptions): typeof microsoftAuthApiRef.T { return new MicrosoftAuth(options); } - private constructor(options: OAuthApiCreateOptions) { + private constructor(options: OAuth2CreateOptions) { const { configApi, environment = 'development', @@ -63,6 +63,7 @@ export default class MicrosoftAuth { 'email', 'User.Read', ], + scopeTransform = scopes => scopes.concat('offline_access'), } = options; this.configApi = configApi; @@ -70,6 +71,7 @@ export default class MicrosoftAuth { this.provider = provider; this.oauthRequestApi = oauthRequestApi; this.discoveryApi = discoveryApi; + this.scopeTransform = scopeTransform; this.oauth2 = { [MicrosoftAuth.MicrosoftGraphID]: OAuth2.create({ @@ -78,6 +80,7 @@ export default class MicrosoftAuth { oauthRequestApi: this.oauthRequestApi, provider: this.provider, environment: this.environment, + scopeTransform: this.scopeTransform, defaultScopes, }), }; @@ -132,6 +135,7 @@ export default class MicrosoftAuth { oauthRequestApi: this.oauthRequestApi, provider: this.provider, environment: this.environment, + scopeTransform: this.scopeTransform, }); } return this.oauth2[aud].getAccessToken(scope, options); From 85adf6943b8c756fe44a34fa2577d8109ad81077 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Fri, 17 Mar 2023 17:33:35 -0400 Subject: [PATCH 070/247] fail multi-resource access token requests The emphasis on the word 'erroneously' in [this doc](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/ef0a1ae38249ba2cf6ec5d75731e711a6ae62cba/lib/msal-browser/docs/resources-and-scopes.md?plain=1#L36) makes this behaviour pretty compelling. Signed-off-by: Jamie Klassen --- .../auth/microsoft/MicrosoftAuth.test.ts | 10 ++++++++++ .../auth/microsoft/MicrosoftAuth.ts | 19 ++++++++++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts index 0d2e016b41..c71799ed98 100644 --- a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts @@ -69,6 +69,16 @@ describe('MicrosoftAuth', () => { expect(accessToken).toEqual('tokenForOtherResource'); }); + + it('fails when requesting scopes for multiple resources at once', async () => { + const accessTokenPromise = microsoftAuth.getAccessToken( + 'one-resource/scope other-resource/scope', + ); + + await expect(accessTokenPromise).rejects.toThrow( + 'Requested access token with scopes from multiple Azure resources: one-resource, other-resource. Access tokens can only have a single audience.', + ); + }); }); describe('without a refresh token', () => { diff --git a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts index fa74979c6b..fdffb3fc45 100644 --- a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts @@ -91,11 +91,20 @@ export default class MicrosoftAuth { } private static resourceForScopes(scope: string): Promise { - const audience = - scope - .split(' ') - .map(MicrosoftAuth.resourceForScope) - .find(aud => aud !== 'openid') ?? MicrosoftAuth.MicrosoftGraphID; + const audiences = scope + .split(' ') + .map(MicrosoftAuth.resourceForScope) + .filter(aud => aud !== 'openid'); + if (audiences.length > 1) { + return Promise.reject( + new Error( + `Requested access token with scopes from multiple Azure resources: ${audiences.join( + ', ', + )}. Access tokens can only have a single audience.`, + ), + ); + } + const audience = audiences[0] ?? MicrosoftAuth.MicrosoftGraphID; return Promise.resolve(audience); } From e4badfd959a9e292505fe8437c17f83c4a5c0d78 Mon Sep 17 00:00:00 2001 From: headphonejames Date: Fri, 24 Mar 2023 14:57:43 -0700 Subject: [PATCH 071/247] re-execute checkin tests Signed-off-by: headphonejames --- .../src/components/EntityTagPicker/EntityTagPicker.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx index bfaa689190..5ee8c481e2 100644 --- a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx @@ -222,6 +222,7 @@ describe('', () => { tags: new EntityTagFilter(['tag1']), }), ); + fireEvent.click(screen.getByTestId('tags-picker-expand')); fireEvent.click(screen.getByLabelText('tag2')); expect(screen.getByLabelText('tag2')).toBeChecked(); From 2190368b52fe9f2b47987255f5c0d60c4764cd06 Mon Sep 17 00:00:00 2001 From: headphonejames Date: Sat, 25 Mar 2023 10:49:07 -0700 Subject: [PATCH 072/247] remove react effect for less potential re-rendering Signed-off-by: headphonejames --- .../EntityAutocompletePicker.tsx | 11 ++++++----- .../EntityTagPicker/EntityTagPicker.test.tsx | 1 - 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx index 20294fa7f1..00114a29fc 100644 --- a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx +++ b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx @@ -95,11 +95,12 @@ export function EntityAutocompletePicker< // Set selected options on query parameter updates; this happens at initial page load and from // external updates to the page location - useEffect(() => { - if (queryParameters.length) { - setSelectedOptions(queryParameters); - } - }, [queryParameters]); + const [prevQueryParameters, setQueryParameter] = useState(queryParameters); + // if the query parameter has changed, update the selected options + if (queryParameters !== prevQueryParameters) { + setSelectedOptions(queryParameters); + setQueryParameter(queryParameters); + } const availableOptions = Object.keys(availableValues ?? {}); const shouldAddFilter = selectedOptions.length && availableOptions.length; diff --git a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx index 5ee8c481e2..bfaa689190 100644 --- a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx @@ -222,7 +222,6 @@ describe('', () => { tags: new EntityTagFilter(['tag1']), }), ); - fireEvent.click(screen.getByTestId('tags-picker-expand')); fireEvent.click(screen.getByLabelText('tag2')); expect(screen.getByLabelText('tag2')).toBeChecked(); From a0ef1ec7349f998ac259d7efb265193796dbccbe Mon Sep 17 00:00:00 2001 From: Alex Crome Date: Mon, 27 Mar 2023 00:10:53 +0100 Subject: [PATCH 073/247] Properly expose easy auth provider 16108 added support for Azure easy auth, however the provider wasn't properly exposed so it can't be used. This PR fixes that by properly exposing the provider Signed-off-by: Alex Crome --- .changeset/sour-laws-walk.md | 5 +++++ plugins/auth-backend/src/providers/providers.ts | 3 +++ 2 files changed, 8 insertions(+) create mode 100644 .changeset/sour-laws-walk.md diff --git a/.changeset/sour-laws-walk.md b/.changeset/sour-laws-walk.md new file mode 100644 index 0000000000..073e3dc6a8 --- /dev/null +++ b/.changeset/sour-laws-walk.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Export Azure Easy Auth provider so it can actually be used. diff --git a/plugins/auth-backend/src/providers/providers.ts b/plugins/auth-backend/src/providers/providers.ts index aa630b7481..36a24f4f6c 100644 --- a/plugins/auth-backend/src/providers/providers.ts +++ b/plugins/auth-backend/src/providers/providers.ts @@ -32,6 +32,7 @@ import { onelogin } from './onelogin'; import { saml } from './saml'; import { AuthProviderFactory } from './types'; import { bitbucketServer } from './bitbucketServer'; +import { easyAuth } from './azure-easyauth'; /** * All built-in auth provider integrations. @@ -56,6 +57,7 @@ export const providers = Object.freeze({ okta, onelogin, saml, + easyAuth, }); /** @@ -73,6 +75,7 @@ export const defaultAuthProviderFactories: { okta: okta.create(), auth0: auth0.create(), microsoft: microsoft.create(), + easyAuth: easyAuth.create(), oauth2: oauth2.create(), oidc: oidc.create(), onelogin: onelogin.create(), From 7019734333a0758b34c4c4a68b051fbad2a1afa6 Mon Sep 17 00:00:00 2001 From: Alex Crome Date: Mon, 27 Mar 2023 00:53:19 +0100 Subject: [PATCH 074/247] Fix Api Report Signed-off-by: Alex Crome --- plugins/auth-backend/api-report.md | 19 +++++++++++++++++++ .../src/providers/azure-easyauth/index.ts | 2 +- .../src/providers/azure-easyauth/provider.ts | 1 + plugins/auth-backend/src/providers/index.ts | 1 + 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 7515083c09..81d6512155 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -229,6 +229,12 @@ export const defaultAuthProviderFactories: { [providerId: string]: AuthProviderFactory; }; +// @public (undocumented) +export type EasyAuthResult = { + fullProfile: Profile; + accessToken?: string; +}; + // @public (undocumented) export const encodeState: (state: OAuthState) => string; @@ -694,6 +700,19 @@ export const providers: Readonly<{ nameIdMatchingUserEntityName(): SignInResolver; }>; }>; + easyAuth: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn: { + resolver: SignInResolver; + }; + } + | undefined, + ) => AuthProviderFactory; + resolvers: never; + }>; }>; // @public (undocumented) diff --git a/plugins/auth-backend/src/providers/azure-easyauth/index.ts b/plugins/auth-backend/src/providers/azure-easyauth/index.ts index 698092d73f..73abde5f37 100644 --- a/plugins/auth-backend/src/providers/azure-easyauth/index.ts +++ b/plugins/auth-backend/src/providers/azure-easyauth/index.ts @@ -15,4 +15,4 @@ */ export { easyAuth } from './provider'; -export type { EasyAuthResponse } from './provider'; +export type { EasyAuthResult } from './provider'; diff --git a/plugins/auth-backend/src/providers/azure-easyauth/provider.ts b/plugins/auth-backend/src/providers/azure-easyauth/provider.ts index 405ca9ed59..7da069f6ae 100644 --- a/plugins/auth-backend/src/providers/azure-easyauth/provider.ts +++ b/plugins/auth-backend/src/providers/azure-easyauth/provider.ts @@ -38,6 +38,7 @@ type Options = { resolverContext: AuthResolverContext; }; +/** @public */ export type EasyAuthResult = { fullProfile: Profile; accessToken?: string; diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index a922065fd8..8173a7fbc3 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -15,6 +15,7 @@ */ export type { AwsAlbResult } from './aws-alb'; +export type { EasyAuthResult } from './azure-easyauth'; export type { BitbucketOAuthResult, BitbucketPassportProfile, From 7974080aba537841f8fcbc829ca07fda7ef79467 Mon Sep 17 00:00:00 2001 From: Alex Crome Date: Mon, 27 Mar 2023 00:54:13 +0100 Subject: [PATCH 075/247] Add easy auth to documentation navigation. Also corrected entries so they show up in alphabetical order in the sidebar. Signed-off-by: Alex Crome --- microsite/sidebars.json | 7 ++++--- mkdocs.yml | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 09727685a0..412813263c 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -277,15 +277,16 @@ "items": [ "auth/auth0/provider", "auth/atlassian/provider", - "auth/bitbucket/provider", "auth/microsoft/provider", + "auth/microsoft/easyauth", + "auth/bitbucket/provider", "auth/github/provider", "auth/gitlab/provider", "auth/google/provider", "auth/google/gcp-iap-auth", "auth/okta/provider", - "auth/onelogin/provider", - "auth/oauth2-proxy/provider" + "auth/oauth2-proxy/provider", + "auth/onelogin/provider" ] }, "auth/identity-resolver", diff --git a/mkdocs.yml b/mkdocs.yml index 4b5c2cf7a2..4a75c3f67f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,6 +6,7 @@ edit_uri: edit/master/docs plugins: - techdocs-core +# For sidebar navigation on https://backstage.io/, see `microsite/sidebars.json` nav: - Overview: - What is Backstage?: 'overview/what-is-backstage.md' From 19b5c20aaff14bada9748c2b9366990aa4fb3ba0 Mon Sep 17 00:00:00 2001 From: David Weber Date: Mon, 27 Mar 2023 07:19:04 +0200 Subject: [PATCH 076/247] feat: add api spectral linter plugin to market Signed-off-by: David Weber --- microsite/data/plugins/api-spectral-linter.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 microsite/data/plugins/api-spectral-linter.yaml diff --git a/microsite/data/plugins/api-spectral-linter.yaml b/microsite/data/plugins/api-spectral-linter.yaml new file mode 100644 index 0000000000..292279e324 --- /dev/null +++ b/microsite/data/plugins/api-spectral-linter.yaml @@ -0,0 +1,10 @@ +--- +title: API Spectral Linter +author: dweber019 +authorUrl: https://github.com/dweber019 +category: Linting +description: API Spectral Linter is a quality assurance tool that checks the compliance of API's specifications Spectral rule sets. +documentation: https://github.com/dweber019/backstage-plugin-api-docs-spectral-linter +iconUrl: https://raw.githubusercontent.com/dweber019/backstage-plugin-api-docs-spectral-linter/main/plugins/api-docs-spectral-linter/docs/pluginIcon.png +npmPackageName: '@dweber019/backstage-plugin-api-docs-spectral-linter' +addedDate: '2023-03-27' From 8f0bd33d7a9ac34481fc9bca952634c3a19fc92e Mon Sep 17 00:00:00 2001 From: Phred Date: Mon, 27 Mar 2023 11:59:38 -0500 Subject: [PATCH 077/247] added utility to remove backstage entity refs from entities Signed-off-by: Phred --- .../actions/builtin/helpers.test.ts | 21 ++++++++++++++++++- .../src/scaffolder/actions/builtin/helpers.ts | 4 ++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.test.ts index b5a375d83d..47f2c88b30 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.test.ts @@ -15,7 +15,12 @@ */ import { Git, getVoidLogger } from '@backstage/backend-common'; -import { commitAndPushRepo, initRepoAndPush } from './helpers'; +import { entityNamePickerValidation } from '@backstage/plugin-scaffolder/src/components/fields/EntityNamePicker'; +import { + commitAndPushRepo, + familiarizeEntityName, + initRepoAndPush, +} from './helpers'; jest.mock('@backstage/backend-common', () => ({ Git: { @@ -301,3 +306,17 @@ describe('commitAndPushRepo', () => { }); }); }); + +describe('familiarizeEntityName', () => { + it.each([ + 'user:default/catpants', + 'group:default/catpants', + 'user:catpants', + 'default/catpants', + 'user:custom/catpants', + 'group:custom/catpants', + 'catpants', + ])('should parse: "%s"', (entityName: string) => { + expect(familiarizeEntityName(entityName)).toEqual('catpants'); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts index 8bed279a1b..30b75d9e6f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts @@ -296,3 +296,7 @@ export function getGitCommitMessage( ? gitCommitMessage : config.getOptionalString('scaffolder.defaultCommitMessage'); } + +export function familiarizeEntityName(name: string): string { + return name.replace(/^.*[:/]/g, ''); +} From 57a49da161cfd17dad67c1aa17c7bd346aebc857 Mon Sep 17 00:00:00 2001 From: Phred Date: Mon, 27 Mar 2023 11:59:42 -0500 Subject: [PATCH 078/247] use helper to familiarize entities passed to GitHub Signed-off-by: Phred --- .../actions/builtin/github/helpers.ts | 5 +- .../actions/builtin/publish/github.test.ts | 61 +++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts index 75c8f25d8b..23f6b804b7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts @@ -29,6 +29,7 @@ import { initRepoAndPush, } from '../helpers'; import { getRepoSourceDirectory, parseRepoUrl } from '../publish/util'; +import { familiarizeEntityName } from '../../builtin/helpers'; const DEFAULT_TIMEOUT_MS = 60_000; @@ -218,13 +219,13 @@ export async function createGithubRepoWithCollaboratorsAndTopics( await client.rest.repos.addCollaborator({ owner, repo, - username: collaborator.user, + username: familiarizeEntityName(collaborator.user), permission: collaborator.access, }); } else if ('team' in collaborator) { await client.rest.teams.addOrUpdateRepoPermissionsInOrg({ org: owner, - team_slug: collaborator.team, + team_slug: familiarizeEntityName(collaborator.team), owner, repo, permission: collaborator.access, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts index 94b51f3e94..4bd8ee1652 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts @@ -29,6 +29,7 @@ import { when } from 'jest-when'; import { PassThrough } from 'stream'; import { enableBranchProtectionOnDefaultRepoBranch, + familiarizeEntityName, initRepoAndPush, } from '../helpers'; import { createPublishGithubAction } from './github'; @@ -68,6 +69,8 @@ describe('publish:github', () => { }, }); + const { familiarizeEntityName: realFamiliarizeEntityName } = + jest.requireActual('../helpers'); const integrations = ScmIntegrations.fromConfig(config); let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; @@ -95,6 +98,11 @@ describe('publish:github', () => { config, githubCredentialsProvider, }); + + // restore real implmentation + (familiarizeEntityName as jest.MockedFunction).mockImplementation( + realFamiliarizeEntityName, + ); }); it('should fail to create if the team is not found in the org', async () => { @@ -651,6 +659,59 @@ describe('publish:github', () => { }); }); + it('should familiarize entity names while adding collaborators', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + collaborators: [ + { + access: 'pull', + user: 'user:robot-1', + }, + { + access: 'push', + team: 'group:default/robot-2', + }, + ], + }, + }); + + const commonProperties = { + owner: 'owner', + repo: 'repo', + }; + + expect(mockOctokit.rest.repos.addCollaborator).toHaveBeenCalledWith({ + ...commonProperties, + username: 'robot-1', + permission: 'pull', + }); + + expect( + mockOctokit.rest.teams.addOrUpdateRepoPermissionsInOrg, + ).toHaveBeenCalledWith({ + ...commonProperties, + org: 'owner', + team_slug: 'robot-2', + permission: 'push', + }); + + expect(familiarizeEntityName).toHaveBeenCalledWith('user:robot-1'); + expect(familiarizeEntityName).toHaveBeenCalledWith('group:default/robot-2'); + }); + it('should ignore failures when adding multiple collaborators', async () => { mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, From f37a95adcd8a9a2ad8e5b599706aeba48973a796 Mon Sep 17 00:00:00 2001 From: Phred Date: Mon, 27 Mar 2023 11:59:47 -0500 Subject: [PATCH 079/247] added changeset Signed-off-by: Phred --- .changeset/serious-items-walk.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/serious-items-walk.md diff --git a/.changeset/serious-items-walk.md b/.changeset/serious-items-walk.md new file mode 100644 index 0000000000..6da6d91ac6 --- /dev/null +++ b/.changeset/serious-items-walk.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Removed entity types and namespacing before passing to GitHub API From abbcb55554623639a0425a36c18ba3ad88676db7 Mon Sep 17 00:00:00 2001 From: Phred Date: Mon, 27 Mar 2023 12:25:59 -0500 Subject: [PATCH 080/247] cleaened up tsc issues Signed-off-by: Phred --- .../src/scaffolder/actions/builtin/helpers.test.ts | 1 - .../src/scaffolder/actions/builtin/publish/github.test.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.test.ts index 47f2c88b30..8cde257d64 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.test.ts @@ -15,7 +15,6 @@ */ import { Git, getVoidLogger } from '@backstage/backend-common'; -import { entityNamePickerValidation } from '@backstage/plugin-scaffolder/src/components/fields/EntityNamePicker'; import { commitAndPushRepo, familiarizeEntityName, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts index 4bd8ee1652..82f6948fa8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts @@ -100,7 +100,7 @@ describe('publish:github', () => { }); // restore real implmentation - (familiarizeEntityName as jest.MockedFunction).mockImplementation( + (familiarizeEntityName as jest.Mock).mockImplementation( realFamiliarizeEntityName, ); }); From 88627d957ea85a35b901e63dbe17be7c57f312dc Mon Sep 17 00:00:00 2001 From: Phred Date: Tue, 28 Mar 2023 08:07:50 -0500 Subject: [PATCH 081/247] mocked helper in github repo create tests Signed-off-by: Phred --- .../actions/builtin/github/githubRepoCreate.test.ts | 5 ++++- .../src/scaffolder/actions/builtin/publish/github.test.ts | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts index a9ef8c3daa..58747d57ef 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts @@ -28,6 +28,7 @@ import { import { when } from 'jest-when'; import { PassThrough } from 'stream'; import { createGithubRepoCreateAction } from './githubRepoCreate'; +import { familiarizeEntityName } from '../helpers'; const mockOctokit = { rest: { @@ -83,15 +84,17 @@ describe('github:repo:create', () => { }; beforeEach(() => { - jest.resetAllMocks(); githubCredentialsProvider = DefaultGithubCredentialsProvider.fromIntegrations(integrations); action = createGithubRepoCreateAction({ integrations, githubCredentialsProvider, }); + (familiarizeEntityName as jest.Mock).mockImplementation((s: string) => s); }); + afterEach(jest.resetAllMocks); + it('should call the githubApis with the correct values for createInOrg', async () => { mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'Organization' }, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts index 82f6948fa8..3347ca2e05 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts @@ -90,7 +90,6 @@ describe('publish:github', () => { }; beforeEach(() => { - jest.resetAllMocks(); githubCredentialsProvider = DefaultGithubCredentialsProvider.fromIntegrations(integrations); action = createPublishGithubAction({ @@ -105,6 +104,8 @@ describe('publish:github', () => { ); }); + afterEach(jest.resetAllMocks); + it('should fail to create if the team is not found in the org', async () => { mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'Organization' }, From b3388961885de4bb5e01fdd4f1e34f1ddea753b7 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Tue, 28 Mar 2023 10:46:40 -0600 Subject: [PATCH 082/247] Fix copypasta in events-backend-module-azure Signed-off-by: Tim Hansen --- plugins/events-backend-module-azure/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/events-backend-module-azure/README.md b/plugins/events-backend-module-azure/README.md index d16315b6e7..214c00e2fd 100644 --- a/plugins/events-backend-module-azure/README.md +++ b/plugins/events-backend-module-azure/README.md @@ -34,10 +34,10 @@ yarn add --cwd packages/backend @backstage/plugin-events-backend-module-azure Add the event router to the `EventsBackend`: ```diff -+const githubEventRouter = new AzureDevOpsEventRouter(); ++const azureEventRouter = new AzureDevOpsEventRouter(); EventsBackend -+ .addPublishers(githubEventRouter) -+ .addSubscribers(githubEventRouter); ++ .addPublishers(azureEventRouter) ++ .addSubscribers(azureEventRouter); // [...] ``` From 4933f965d525474a1d61b88b2d43ca33601772d6 Mon Sep 17 00:00:00 2001 From: Phred Date: Tue, 28 Mar 2023 18:03:51 -0400 Subject: [PATCH 083/247] made changeset more explicit Signed-off-by: Phred --- .changeset/serious-items-walk.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/serious-items-walk.md b/.changeset/serious-items-walk.md index 6da6d91ac6..c4cdfe34c4 100644 --- a/.changeset/serious-items-walk.md +++ b/.changeset/serious-items-walk.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder-backend': patch --- -Removed entity types and namespacing before passing to GitHub API +Stripped entity types and namespace before passing to GitHub API From 0579396bcede5f59421b126462ba42ff7aad333e Mon Sep 17 00:00:00 2001 From: "pballandras@coveo.com" Date: Tue, 21 Mar 2023 15:10:17 -0400 Subject: [PATCH 084/247] Add commit message as parameter test as well Signed-off-by: pballandras@coveo.com --- .../builtin/publish/githubPullRequest.test.ts | 52 +++++++++++++++++++ .../builtin/publish/githubPullRequest.ts | 9 +++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts index 70fff20446..7750277728 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts @@ -545,4 +545,56 @@ describe('createPublishGithubPullRequestAction', () => { expect(ctx.output).toHaveBeenCalledWith('pullRequestNumber', 123); }); }); + + describe('with commit message', () => { + let input: GithubPullRequestActionInput; + let ctx: ActionContext; + + beforeEach(() => { + input = { + repoUrl: 'github.com?owner=myorg&repo=myrepo', + title: 'Create my new app', + branchName: 'new-app', + description: 'This PR is really good', + commitMessage: 'Create my new app, but in the commit message', + }; + + mockFs({ + [workspacePath]: { 'file.txt': 'Hello there!' }, + }); + + ctx = { + createTemporaryDirectory: jest.fn(), + output: jest.fn(), + logger: getRootLogger(), + logStream: new Writable(), + input, + workspacePath, + }; + }); + + it('creates a pull request', async () => { + await instance.handler(ctx); + + expect(fakeClient.createPullRequest).toHaveBeenCalledWith({ + owner: 'myorg', + repo: 'myrepo', + title: 'Create my new app', + head: 'new-app', + body: 'This PR is really good', + changes: [ + { + commit: 'Create my new app, but in the commit message', + files: { + 'file.txt': { + content: Buffer.from('Hello there!').toString('base64'), + encoding: 'base64', + mode: '100644', + }, + }, + }, + ], + }); + }); + }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts index b8c0068d70..28fe89318b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts @@ -136,6 +136,7 @@ export const createPublishGithubPullRequestAction = ( token?: string; reviewers?: string[]; teamReviewers?: string[]; + commitMessage?: string; }>({ id: 'publish:github:pull-request', schema: { @@ -202,6 +203,11 @@ export const createPublishGithubPullRequestAction = ( description: 'The teams that will be added as reviewers to the pull request', }, + commitMessage: { + type: 'string', + title: 'Commit Message', + description: 'The commit message for the PR commit', + }, }, }, output: { @@ -233,6 +239,7 @@ export const createPublishGithubPullRequestAction = ( token: providedToken, reviewers, teamReviewers, + commitMessage, } = ctx.input; const { owner, repo, host } = parseRepoUrl(repoUrl, integrations); @@ -298,7 +305,7 @@ export const createPublishGithubPullRequestAction = ( changes: [ { files, - commit: title, + commit: commitMessage || title, }, ], body: description, From d7c8c222e258e59a2b16808d2102e297675d8247 Mon Sep 17 00:00:00 2001 From: "pballandras@coveo.com" Date: Wed, 22 Mar 2023 09:49:09 -0400 Subject: [PATCH 085/247] Add changeset Signed-off-by: pballandras@coveo.com --- .changeset/slow-ravens-destroy.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/slow-ravens-destroy.md diff --git a/.changeset/slow-ravens-destroy.md b/.changeset/slow-ravens-destroy.md new file mode 100644 index 0000000000..a75793f86f --- /dev/null +++ b/.changeset/slow-ravens-destroy.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Allow for a commit message to differ from the PR title when publishing a GitHub pull request. From a761c531585777cdb09731ba9a75280b71d9fcbc Mon Sep 17 00:00:00 2001 From: "pballandras@coveo.com" Date: Wed, 22 Mar 2023 10:48:16 -0400 Subject: [PATCH 086/247] Add api report change Signed-off-by: pballandras@coveo.com --- plugins/scaffolder-backend/api-report.md | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 2faaeca7cc..5b12455d0f 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -559,6 +559,7 @@ export const createPublishGithubPullRequestAction: ( token?: string | undefined; reviewers?: string[] | undefined; teamReviewers?: string[] | undefined; + commitMessage?: string | undefined; }, JsonObject >; From e78f083cb2f6a31f3ecc1ae223f987856ea9181c Mon Sep 17 00:00:00 2001 From: Philippe Ballandras Date: Thu, 30 Mar 2023 13:49:13 -0400 Subject: [PATCH 087/247] Apply suggestions from code review Co-authored-by: Boris Bera Signed-off-by: Philippe Ballandras --- .../scaffolder/actions/builtin/publish/githubPullRequest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts index 28fe89318b..6706ad9c06 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts @@ -206,7 +206,7 @@ export const createPublishGithubPullRequestAction = ( commitMessage: { type: 'string', title: 'Commit Message', - description: 'The commit message for the PR commit', + description: 'The commit message for the pull request commit', }, }, }, @@ -305,7 +305,7 @@ export const createPublishGithubPullRequestAction = ( changes: [ { files, - commit: commitMessage || title, + commit: commitMessage ?? title, }, ], body: description, From 9f09327b28ad322359da2a89731254451a47be30 Mon Sep 17 00:00:00 2001 From: Eric Irwin Date: Thu, 30 Mar 2023 15:17:34 -0600 Subject: [PATCH 088/247] chore(backend-plugin.md) - destructure identity Signed-off-by: Eric Irwin --- docs/plugins/backend-plugin.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/plugins/backend-plugin.md b/docs/plugins/backend-plugin.md index 9ce835f5f5..897a66ae79 100644 --- a/docs/plugins/backend-plugin.md +++ b/docs/plugins/backend-plugin.md @@ -184,6 +184,7 @@ export async function createRouter( options: RouterOptions, ): Promise { const router = Router(); + const { identity } = options; router.post('/example', async (req, res) => { const identity = await identity.getIdentity({ request: req }); From d8cb971216d2c1c22509f387ced6f824192370d2 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Fri, 24 Feb 2023 10:20:27 +0100 Subject: [PATCH 089/247] Remove Tabs component Signed-off-by: Philipp Hugenroth --- .../src/components/Tabs/Tab.test.tsx | 26 --- .../src/components/Tabs/Tab.tsx | 72 -------- .../src/components/Tabs/TabBar.tsx | 60 ------- .../src/components/Tabs/TabIcon.tsx | 66 ------- .../src/components/Tabs/TabPanel.tsx | 37 ---- .../src/components/Tabs/Tabs.stories.tsx | 71 -------- .../src/components/Tabs/Tabs.tsx | 170 ------------------ .../src/components/Tabs/index.ts | 22 --- .../src/components/Tabs/utils.ts | 28 --- .../core-components/src/components/index.ts | 1 - 10 files changed, 553 deletions(-) delete mode 100644 packages/core-components/src/components/Tabs/Tab.test.tsx delete mode 100644 packages/core-components/src/components/Tabs/Tab.tsx delete mode 100644 packages/core-components/src/components/Tabs/TabBar.tsx delete mode 100644 packages/core-components/src/components/Tabs/TabIcon.tsx delete mode 100644 packages/core-components/src/components/Tabs/TabPanel.tsx delete mode 100644 packages/core-components/src/components/Tabs/Tabs.stories.tsx delete mode 100644 packages/core-components/src/components/Tabs/Tabs.tsx delete mode 100644 packages/core-components/src/components/Tabs/index.ts delete mode 100644 packages/core-components/src/components/Tabs/utils.ts diff --git a/packages/core-components/src/components/Tabs/Tab.test.tsx b/packages/core-components/src/components/Tabs/Tab.test.tsx deleted file mode 100644 index d30420ebb6..0000000000 --- a/packages/core-components/src/components/Tabs/Tab.test.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; -import { StyledTab } from './Tab'; - -describe('', () => { - it('renders without exploding', async () => { - const rendered = await renderInTestApp(); - expect(rendered.getByText('test')).toBeInTheDocument(); - }); -}); diff --git a/packages/core-components/src/components/Tabs/Tab.tsx b/packages/core-components/src/components/Tabs/Tab.tsx deleted file mode 100644 index 2922bd9651..0000000000 --- a/packages/core-components/src/components/Tabs/Tab.tsx +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; -import Tab from '@material-ui/core/Tab'; -import { BackstageTheme } from '@backstage/theme'; - -interface StyledTabProps { - label?: string; - icon?: any; // TODO: define type for material-ui icons - isFirstNav?: boolean; - isFirstIndex?: boolean; - value?: any; -} - -const tabMarginLeft = (isFirstNav: boolean, isFirstIndex: boolean) => { - if (isFirstIndex) { - if (isFirstNav) { - return '20px'; - } - return '0'; - } - return '40px'; -}; - -/** @public */ -export type TabClassKey = 'root' | 'selected'; - -const useStyles = makeStyles( - theme => ({ - root: { - textTransform: 'none', - height: '64px', - fontWeight: theme.typography.fontWeightBold, - fontSize: theme.typography.pxToRem(13), - color: theme.palette.textSubtle, - marginLeft: props => - tabMarginLeft( - props.isFirstNav as boolean, - props.isFirstIndex as boolean, - ), - width: '130px', - minWidth: '130px', - '&:hover': { - outline: 'none', - backgroundColor: 'transparent', - color: theme.palette.textSubtle, - }, - }, - }), - { name: 'BackstageTab' }, -); - -export const StyledTab = (props: StyledTabProps) => { - const classes = useStyles(props); - const { isFirstNav, isFirstIndex, ...rest } = props; - return ; -}; diff --git a/packages/core-components/src/components/Tabs/TabBar.tsx b/packages/core-components/src/components/Tabs/TabBar.tsx deleted file mode 100644 index 99be9a7d1d..0000000000 --- a/packages/core-components/src/components/Tabs/TabBar.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { PropsWithChildren } from 'react'; -import { makeStyles } from '@material-ui/core/styles'; -import Tabs from '@material-ui/core/Tabs'; -import Typography from '@material-ui/core/Typography'; -import { BackstageTheme } from '@backstage/theme'; - -interface StyledTabsProps { - value: number | boolean; - selectionFollowsFocus: boolean; - onChange: (event: React.ChangeEvent<{}>, newValue: number) => void; -} - -export type TabBarClassKey = 'indicator' | 'flexContainer' | 'root'; - -const useStyles = makeStyles( - theme => ({ - indicator: { - display: 'flex', - justifyContent: 'center', - backgroundColor: theme.palette.tabbar.indicator, - height: theme.spacing(0.5), - }, - flexContainer: { - alignItems: 'center', - }, - root: { - '&:last-child': { - marginLeft: 'auto', - }, - }, - }), - { name: 'BackstageTabBar' }, -); - -export const StyledTabs = (props: PropsWithChildren) => { - const classes = useStyles(props); - return ( - }} - /> - ); -}; diff --git a/packages/core-components/src/components/Tabs/TabIcon.tsx b/packages/core-components/src/components/Tabs/TabIcon.tsx deleted file mode 100644 index 0e93924de9..0000000000 --- a/packages/core-components/src/components/Tabs/TabIcon.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; -import IconButton from '@material-ui/core/IconButton'; -import { BackstageTheme } from '@backstage/theme'; - -interface StyledIconProps { - ariaLabel: string; - children: any; - isNext?: boolean; - onClick: any; -} - -export type TabIconClassKey = 'root'; - -const useStyles = makeStyles( - theme => ({ - root: { - color: '#6E6E6E', - overflow: 'visible', - fontSize: theme.typography.h5.fontSize, - textAlign: 'center', - borderRadius: '50%', - backgroundColor: '#E6E6E6', - marginLeft: props => (props.isNext ? 'auto' : '0'), - marginRight: props => (props.isNext ? '0' : theme.spacing(1.25)), - '&:hover': { - backgroundColor: '#E6E6E6', - opacity: '1', - }, - }, - }), - { name: 'BackstageTabIcon' }, -); - -export const StyledIcon = (props: StyledIconProps) => { - const classes = useStyles(props); - const { ariaLabel, onClick } = props; - return ( - - {props.children} - - ); -}; diff --git a/packages/core-components/src/components/Tabs/TabPanel.tsx b/packages/core-components/src/components/Tabs/TabPanel.tsx deleted file mode 100644 index 3ecc1ba046..0000000000 --- a/packages/core-components/src/components/Tabs/TabPanel.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import Box from '@material-ui/core/Box'; -import React, { PropsWithChildren } from 'react'; - -export interface TabPanelProps { - value?: any; - index?: number; -} - -export const TabPanel = (props: PropsWithChildren) => { - const { children, value, index, ...other } = props; - - return ( - - ); -}; diff --git a/packages/core-components/src/components/Tabs/Tabs.stories.tsx b/packages/core-components/src/components/Tabs/Tabs.stories.tsx deleted file mode 100644 index 4d4cad8757..0000000000 --- a/packages/core-components/src/components/Tabs/Tabs.stories.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import { Tabs } from './Tabs'; -import AccessAlarmIcon from '@material-ui/icons/AccessAlarm'; - -export default { - title: 'Navigation/Tabs', - component: Tabs, -}; - -const containerStyle = {}; - -export const Default = () => ( -

- ({ - label: `ANOTHER TAB`, - content:
Content {index}
, - }))} - /> -
-); - -export const Expandable = () => ( -
- ({ - label: `ANOTHER TAB`, - content:
Content {index}
, - }))} - /> -
-); - -export const Icons = () => ( -
- ({ - icon: , - content:
Content {index}
, - }))} - /> -
-); - -export const IconsAndLabels = () => ( -
- ({ - icon: , - label: `ANOTHER TAB`, - content:
Content {index}
, - }))} - /> -
-); diff --git a/packages/core-components/src/components/Tabs/Tabs.tsx b/packages/core-components/src/components/Tabs/Tabs.tsx deleted file mode 100644 index 420263f576..0000000000 --- a/packages/core-components/src/components/Tabs/Tabs.tsx +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useRef, useEffect, MutableRefObject, useState } from 'react'; -import { BackstageTheme } from '@backstage/theme'; -import AppBar from '@material-ui/core/AppBar'; -import { makeStyles } from '@material-ui/core/styles'; -import NavigateBeforeIcon from '@material-ui/icons/NavigateBefore'; -import NavigateNextIcon from '@material-ui/icons/NavigateNext'; -import { chunkArray } from './utils'; -import useWindowSize from 'react-use/lib/useWindowSize'; - -/* Import Components */ - -import { TabPanel } from './TabPanel'; -import { StyledIcon } from './TabIcon'; -import { StyledTab } from './Tab'; -import { StyledTabs } from './TabBar'; -import Box from '@material-ui/core/Box'; - -/* Props Types */ - -export interface TabProps { - content: any; - label?: string; - icon?: any; // TODO: define type for material-ui icons -} - -export interface TabsProps { - tabs: TabProps[]; -} - -export type TabsClassKey = 'root' | 'styledTabs' | 'appbar'; - -const useStyles = makeStyles( - theme => ({ - root: { - flexGrow: 1, - width: '100%', - }, - styledTabs: { - backgroundColor: theme.palette.background.paper, - }, - appbar: { - boxShadow: 'none', - backgroundColor: theme.palette.background.paper, - paddingLeft: theme.spacing(1.25), - paddingRight: theme.spacing(1.25), - }, - }), - { name: 'BackstageTabs' }, -); - -export function Tabs(props: TabsProps) { - const { tabs } = props; - const classes = useStyles(); - const [value, setValue] = useState([0, 0]); // [selectedChunkedNavIndex, selectedIndex] - const [navIndex, setNavIndex] = useState(0); - const [numberOfChunkedElement, setNumberOfChunkedElement] = useState(0); - const [chunkedTabs, setChunkedTabs] = useState([[]]); - const wrapper = useRef() as MutableRefObject; - - const { width } = useWindowSize(); - - const handleChange = (_: React.ChangeEvent<{}>, newValue: number) => { - setValue([navIndex, newValue]); - }; - - const navigateToPrevChunk = () => { - setNavIndex(navIndex - 1); - }; - - const navigateToNextChunk = () => { - setNavIndex(navIndex + 1); - }; - - const hasNextNavIndex = () => navIndex + 1 < chunkedTabs.length; - - useEffect(() => { - // Each time the window is resized we calculate how many tabs we can render given the window width - const padding = 20; // The AppBar padding - - const numberOfTabIcons = navIndex === 0 ? 1 : 2; - const wrapperWidth = - wrapper.current.offsetWidth - padding - numberOfTabIcons * 30; - const flattenIndex = value[0] * numberOfChunkedElement + value[1]; - const newChunkedElementSize = Math.floor(wrapperWidth / 170); - - setNumberOfChunkedElement(newChunkedElementSize); - setChunkedTabs(chunkArray(tabs, newChunkedElementSize)); - setValue([ - Math.floor(flattenIndex / newChunkedElementSize), - flattenIndex % newChunkedElementSize, - ]); - // eslint-disable-next-line - }, [width, tabs]); - - const currentIndex = navIndex === value[0] ? value[1] : false; - - return ( - - - - - {navIndex !== 0 && ( - - - - )} - {chunkedTabs[navIndex].map((tab, index) => ( - - ))} - {hasNextNavIndex() && ( - - - - )} - - - - {currentIndex !== false ? ( - chunkedTabs[navIndex].map((tab, index) => ( - - {tab.content} - - )) - ) : ( - // Render if the selected tab index is outside the current rendered chunked array - - {chunkedTabs[value[0]][value[1]].content} - - )} - - ); -} diff --git a/packages/core-components/src/components/Tabs/index.ts b/packages/core-components/src/components/Tabs/index.ts deleted file mode 100644 index 9702c39a48..0000000000 --- a/packages/core-components/src/components/Tabs/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { Tabs } from './Tabs'; -export type { TabsClassKey } from './Tabs'; -export type { TabClassKey } from './Tab'; - -export type { TabBarClassKey } from './TabBar'; -export type { TabIconClassKey } from './TabIcon'; diff --git a/packages/core-components/src/components/Tabs/utils.ts b/packages/core-components/src/components/Tabs/utils.ts deleted file mode 100644 index 80705fdfc0..0000000000 --- a/packages/core-components/src/components/Tabs/utils.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export function chunkArray(array: T[], chunkSize: number): T[][] { - if (chunkSize <= 0) { - return [array]; - } - - const result: T[][] = []; - for (let i = 0; i < array.length; i += chunkSize) { - result.push(array.slice(i, i + chunkSize)); - } - - return result; -} diff --git a/packages/core-components/src/components/index.ts b/packages/core-components/src/components/index.ts index 0057847754..0222b82aa4 100644 --- a/packages/core-components/src/components/index.ts +++ b/packages/core-components/src/components/index.ts @@ -44,6 +44,5 @@ export * from './StructuredMetadataTable'; export * from './SupportButton'; export * from './TabbedLayout'; export * from './Table'; -export * from './Tabs'; export * from './TrendLine'; export * from './WarningPanel'; From 2466406b15977e5d7b883c1e0bdaaa4af04f275e Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Fri, 24 Feb 2023 10:37:12 +0100 Subject: [PATCH 090/247] Generate API Report Signed-off-by: Philipp Hugenroth --- packages/core-components/api-report.md | 24 ------------------- .../src/overridableComponents.ts | 8 ------- 2 files changed, 32 deletions(-) diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 6439be23a7..1e5fa77ed3 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -1318,11 +1318,6 @@ export type Tab = { >; }; -// Warning: (ae-missing-release-tag) "TabBarClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type TabBarClassKey = 'indicator' | 'flexContainer' | 'root'; - // Warning: (ae-forgotten-export) The symbol "Props_18" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "TabbedCard" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1348,14 +1343,6 @@ export namespace TabbedLayout { Route: (props: SubRoute) => null; } -// @public (undocumented) -export type TabClassKey = 'root' | 'selected'; - -// Warning: (ae-missing-release-tag) "TabIconClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type TabIconClassKey = 'root'; - // Warning: (ae-missing-release-tag) "Table" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1438,17 +1425,6 @@ export type TableState = { // @public (undocumented) export type TableToolbarClassKey = 'root' | 'title' | 'searchField'; -// Warning: (ae-forgotten-export) The symbol "TabsProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "Tabs" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export function Tabs(props: TabsProps): JSX.Element; - -// Warning: (ae-missing-release-tag) "TabsClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type TabsClassKey = 'root' | 'styledTabs' | 'appbar'; - // Warning: (ae-missing-release-tag) "TrendLine" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/packages/core-components/src/overridableComponents.ts b/packages/core-components/src/overridableComponents.ts index 49bc162951..c872317049 100644 --- a/packages/core-components/src/overridableComponents.ts +++ b/packages/core-components/src/overridableComponents.ts @@ -60,10 +60,6 @@ import { TableToolbarClassKey, FiltersContainerClassKey, TableClassKey, - TabBarClassKey, - TabIconClassKey, - TabsClassKey, - TabClassKey, WarningPanelClassKey, } from './components'; @@ -141,10 +137,6 @@ type BackstageComponentsNameToClassKey = { BackstageTableToolbar: TableToolbarClassKey; BackstageTableFiltersContainer: FiltersContainerClassKey; BackstageTable: TableClassKey; - BackstageTabBar: TabBarClassKey; - BackstageTabIcon: TabIconClassKey; - BackstageTabs: TabsClassKey; - BackstageTab: TabClassKey; BackstageWarningPanel: WarningPanelClassKey; BackstageBottomLink: BottomLinkClassKey; BackstageBreadcrumbsClickableText: BreadcrumbsClickableTextClassKey; From 01cd4e2575463bf0ab6ae399067e8e91262e9c4a Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Sat, 1 Apr 2023 18:36:59 +0200 Subject: [PATCH 091/247] Add changeset Signed-off-by: Philipp Hugenroth --- .changeset/yellow-candles-kiss.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .changeset/yellow-candles-kiss.md diff --git a/.changeset/yellow-candles-kiss.md b/.changeset/yellow-candles-kiss.md new file mode 100644 index 0000000000..e365be2a0a --- /dev/null +++ b/.changeset/yellow-candles-kiss.md @@ -0,0 +1,21 @@ +--- +'@backstage/core-components': minor +--- + +**BREAKING:** Removing `Tabs` component from `core-components` as it is neither used in the core Backstage app nor in the monorepo plugins. If you are using this component in your instance please consider replacing it with the [Material UI `Tabs`](https://v4.mui.com/components/tabs/#tabs) component like the following: + +```diff +- , +- content:
Label
, +- }]} +- /> + ++ ++ } ++ /> ++ +``` From 9fe2e9987f7a54d0508520496039e2e5db717996 Mon Sep 17 00:00:00 2001 From: Viet Nguyen <19592926+v-ngu@users.noreply.github.com> Date: Sat, 1 Apr 2023 17:44:59 -0400 Subject: [PATCH 092/247] Add Bulletin Board Plugin to marketplace Signed-off-by: Viet Nguyen <19592926+v-ngu@users.noreply.github.com> --- microsite/data/plugins/bulletin-board.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 microsite/data/plugins/bulletin-board.yaml diff --git a/microsite/data/plugins/bulletin-board.yaml b/microsite/data/plugins/bulletin-board.yaml new file mode 100644 index 0000000000..67791f6f84 --- /dev/null +++ b/microsite/data/plugins/bulletin-board.yaml @@ -0,0 +1,10 @@ +--- +title: Bulletin Board +author: v-ngu +authorUrl: https://github.com/v-ngu +category: Discovery +description: Share interesting ideas, news and links with your teammates within Backstage. +documentation: https://github.com/v-ngu/backstage-plugin-bulletin-board +iconUrl: /img/bulletin-board.png +npmPackageName: 'backstage-plugin-bulletin-board' +addedDate: '2023-04-01' From e505e1647ff2edcf83a740f990ff6db72781c1f0 Mon Sep 17 00:00:00 2001 From: Viet Nguyen <19592926+v-ngu@users.noreply.github.com> Date: Sat, 1 Apr 2023 17:45:57 -0400 Subject: [PATCH 093/247] Add Bulletin Board icon for marketplace Signed-off-by: Viet Nguyen <19592926+v-ngu@users.noreply.github.com> --- microsite/static/img/bulletin-board.png | Bin 0 -> 1272 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 microsite/static/img/bulletin-board.png diff --git a/microsite/static/img/bulletin-board.png b/microsite/static/img/bulletin-board.png new file mode 100644 index 0000000000000000000000000000000000000000..19deedd256c0af575f0e676ebd923529aed0609f GIT binary patch literal 1272 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSoCO|{#S9G08$g&*{RsbBprB-l zYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&3=Ax-o-U3d6?5L+b zy+|W)iq6fvM+$xqSn`Cn6quh~bXWAnveokxUbI#<$}LiVu_$9=il~Z17$cWN9`nt> zoRuE2ky|t-K1xa7-qF7B%|)K0-|MxxohP2Z@_weG=42P3Q&2#F=DE-E^0leEIc9(R zcHDYf_6EW_S6dBGSqs~3YzCBYb&C8p$>S$N~tomDei3`7# zv444{5wz_&+X6YWm0LG1+`GbPr@QcNRh^LkFIqIz86=oyI5VK2)3!WUR5{-VT-vob zhyS;~&y+x`y=&#CpR@maTHJYeEsw8{{5T2UVOXcV)Ud)qwriai`U;fZ*A|^o%20+OTyGSyYGud_165Y z-uO25!70WSrQ1BWeA1Ut^>mx!m0Df!_@VoD;nI3#OWk^gCtqc|4mXKkU{g*FerBF2 zQx}(EdQg<9L9xM-rNL6Uy|MB6q=Ku?4cp%4)U3?$UhM3(csuv{M7g(99d173KJLNm z#`Ivfi=FO2@lUs3P4+eF^%ie%OGx?t{E)x_872gp^EY#an^Eu8+~PTIWiB2e`75uB zeto`b^%*VEhKsM`x2$om`xX1{<(&sl{iC)8vtb6@>MMy0^v*pHHMM$`|61qYp>38N z2Uwr%j$Kr~d)sLPCTE6X4kTp3b>Ym+K982u#mreO4#q#PulL>>Y{+8J&S0?JXYW+8 z$y?i}T)TEYj=$;V&GU9gOBVm$+`DPb&LW0m?)oQQ>hDvYICEZSs2bCxMTOt0rp=xI z`Fl)RJ-<{H!_0|OX0`<#U^~M&W3|it=N)W6*G9Ka>-~TEZ~cMKCwG3Y-PkL)Pr=ek zXIoBZs1n1v`%8Xp)$h`^KHHJDO8b{yFZZ2wZ5=BLH~EJcFkCM5d3-T*VQP2Ax^)vi z)at%o<-%|(&!%g|{kNIX_tz#wM>80(C-lYgUl8L#o~)Po2Wt#_&vl;aLw4W8PlpIN#ds7oIH1Uc|GbVdm}@)BK>^zs9N6SD7#T zOX-bT{p;PdcU-lf#mXi-NXuMTJ+J#bBl_H`0}{a4h9hNn#>pFTXta zxb&X%JU{)fc9k)I_P7 Date: Mon, 3 Apr 2023 17:31:38 +0200 Subject: [PATCH 094/247] feat(plugin-vault): use fetchApi instead of raw fetch Signed-off-by: Thomas Cardonne --- .changeset/mean-rice-beg.md | 5 +++++ plugins/vault/src/api.test.ts | 13 +++++++------ plugins/vault/src/api.ts | 18 +++++++++++++++--- .../EntityVaultTable/EntityVaultTable.test.tsx | 5 ++++- plugins/vault/src/plugin.ts | 15 ++++++++++++--- 5 files changed, 43 insertions(+), 13 deletions(-) create mode 100644 .changeset/mean-rice-beg.md diff --git a/.changeset/mean-rice-beg.md b/.changeset/mean-rice-beg.md new file mode 100644 index 0000000000..19cef51292 --- /dev/null +++ b/.changeset/mean-rice-beg.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-vault': patch +--- + +Use `fetchApi` instead of raw `fetch` in order to pass auth header if necessary. diff --git a/plugins/vault/src/api.test.ts b/plugins/vault/src/api.test.ts index 471cadc87e..ec88412303 100644 --- a/plugins/vault/src/api.test.ts +++ b/plugins/vault/src/api.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { VaultSecret, VaultClient } from './api'; @@ -26,6 +26,7 @@ describe('api', () => { const mockBaseUrl = 'https://api-vault.com/api/vault'; const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); + const fetchApi = new MockFetchApi(); const mockSecretsResult: { items: VaultSecret[] } = { items: [ @@ -65,7 +66,7 @@ describe('api', () => { it('should return secrets', async () => { setupHandlers(); - const api = new VaultClient({ discoveryApi }); + const api = new VaultClient({ discoveryApi, fetchApi }); expect(await api.listSecrets('test/success')).toEqual( mockSecretsResult.items, ); @@ -73,19 +74,19 @@ describe('api', () => { it('should return empty secret list', async () => { setupHandlers(); - const api = new VaultClient({ discoveryApi }); + const api = new VaultClient({ discoveryApi, fetchApi }); expect(await api.listSecrets('test/empty')).toEqual([]); }); it('should return all the secrets if no path defined', async () => { setupHandlers(); - const api = new VaultClient({ discoveryApi }); + const api = new VaultClient({ discoveryApi, fetchApi }); expect(await api.listSecrets('')).toEqual(mockSecretsResult.items); }); it('should throw an error if the Vault API responds with an HTTP 404', async () => { setupHandlers(); - const api = new VaultClient({ discoveryApi }); + const api = new VaultClient({ discoveryApi, fetchApi }); await expect(api.listSecrets('test/not-found')).rejects.toThrow( "No secrets found in path 'v1/secrets/test%2Fnot-found'", ); @@ -93,7 +94,7 @@ describe('api', () => { it('should throw an error if the Vault API responds with a non-successful HTTP status code', async () => { setupHandlers(); - const api = new VaultClient({ discoveryApi }); + const api = new VaultClient({ discoveryApi, fetchApi }); await expect(api.listSecrets('test/error')).rejects.toThrow( 'Request failed with 400 Error', ); diff --git a/plugins/vault/src/api.ts b/plugins/vault/src/api.ts index 84843c9176..d924336a2e 100644 --- a/plugins/vault/src/api.ts +++ b/plugins/vault/src/api.ts @@ -13,7 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { DiscoveryApi, createApiRef } from '@backstage/core-plugin-api'; +import { + DiscoveryApi, + createApiRef, + FetchApi, +} from '@backstage/core-plugin-api'; import { NotFoundError, ResponseError } from '@backstage/errors'; /** @@ -52,9 +56,17 @@ export interface VaultApi { */ export class VaultClient implements VaultApi { private readonly discoveryApi: DiscoveryApi; + private readonly fetchApi: FetchApi; - constructor({ discoveryApi }: { discoveryApi: DiscoveryApi }) { + constructor({ + discoveryApi, + fetchApi, + }: { + discoveryApi: DiscoveryApi; + fetchApi: FetchApi; + }) { this.discoveryApi = discoveryApi; + this.fetchApi = fetchApi; } private async callApi( @@ -62,7 +74,7 @@ export class VaultClient implements VaultApi { query: { [key in string]: any }, ): Promise { const apiUrl = `${await this.discoveryApi.getBaseUrl('vault')}`; - const response = await fetch( + const response = await this.fetchApi.fetch( `${apiUrl}/${path}?${new URLSearchParams(query).toString()}`, { headers: { diff --git a/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx b/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx index 27cc0ed91e..e5bd5b43de 100644 --- a/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx +++ b/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx @@ -17,6 +17,7 @@ import React from 'react'; import { setupServer } from 'msw/node'; import { + MockFetchApi, setupRequestMockHandlers, TestApiRegistry, } from '@backstage/test-utils'; @@ -33,6 +34,8 @@ describe('EntityVaultTable', () => { let apis: TestApiRegistry; const mockBaseUrl = 'https://api-vault.com/api/vault'; const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); + const fetchApi = new MockFetchApi(); + const entity = (secretPath: string) => { return { apiVersion: 'backstage.io/v1alpha1', @@ -90,7 +93,7 @@ describe('EntityVaultTable', () => { beforeEach(() => { apis = TestApiRegistry.from([ vaultApiRef, - new VaultClient({ discoveryApi }), + new VaultClient({ discoveryApi, fetchApi }), ]); }); diff --git a/plugins/vault/src/plugin.ts b/plugins/vault/src/plugin.ts index 8275505abb..f62a8a73e4 100644 --- a/plugins/vault/src/plugin.ts +++ b/plugins/vault/src/plugin.ts @@ -19,9 +19,11 @@ import { createPlugin, DiscoveryApi, discoveryApiRef, + FetchApi, + fetchApiRef, } from '@backstage/core-plugin-api'; -import { VaultClient, vaultApiRef } from './api'; +import { vaultApiRef, VaultClient } from './api'; /** * The vault plugin. @@ -32,10 +34,17 @@ export const vaultPlugin = createPlugin({ apis: [ createApiFactory({ api: vaultApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }: { discoveryApi: DiscoveryApi }) => + deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef }, + factory: ({ + discoveryApi, + fetchApi, + }: { + discoveryApi: DiscoveryApi; + fetchApi: FetchApi; + }) => new VaultClient({ discoveryApi, + fetchApi, }), }), ], From 451b7d7ee9a9bd1bf4d6f04ba8b09a55b2968f69 Mon Sep 17 00:00:00 2001 From: danangarbansa Date: Tue, 4 Apr 2023 13:30:14 +1000 Subject: [PATCH 095/247] Fix Select component Signed-off-by: danangarbansa --- .../src/components/Select/Select.test.tsx | 12 ++++++++++++ .../core-components/src/components/Select/Select.tsx | 6 ++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/core-components/src/components/Select/Select.test.tsx b/packages/core-components/src/components/Select/Select.test.tsx index df29983222..edc7058a07 100644 --- a/packages/core-components/src/components/Select/Select.test.tsx +++ b/packages/core-components/src/components/Select/Select.test.tsx @@ -54,4 +54,16 @@ describe(', + ); + + expect(getByTestId('select').textContent).toBe('test 1'); + + rerender(', () => { it('display the placeholder value when selected props updated to undefined', async () => { const { getByTestId, rerender } = render( - , ); expect(getByTestId('select').textContent).toBe('test 1'); From 9aac2e6fa682a03f6483a30c6dd5e726c254ed7e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Apr 2023 05:25:37 +0000 Subject: [PATCH 097/247] fix(deps): update dependency @roadiehq/backstage-plugin-buildkite to v2.1.8 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6490641711..37a5ded750 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13615,8 +13615,8 @@ __metadata: linkType: hard "@roadiehq/backstage-plugin-buildkite@npm:^2.0.8": - version: 2.1.7 - resolution: "@roadiehq/backstage-plugin-buildkite@npm:2.1.7" + version: 2.1.8 + resolution: "@roadiehq/backstage-plugin-buildkite@npm:2.1.8" dependencies: "@backstage/catalog-model": ^1.2.1 "@backstage/core-components": ^0.12.5 @@ -13634,7 +13634,7 @@ __metadata: react-dom: ^16.13.1 || ^17.0.0 react-router: 6.0.0-beta.0 || ^6.3.0 react-router-dom: 6.0.0-beta.0 || ^6.3.0 - checksum: 1b84acf97e22f3b481d4b4fbc1ccb1d184f811ec00998b92cc8ee6f4be95af2acebd9932ad4337c611d17ca15f8c34cd44310a44d775008a1d845fe1a197ae37 + checksum: f35f5bb78cb415a894532e5af5879d47d6b61c2f52bc6f4274fde46bc7bba2bca974845a802fdd361591002d048b8f14a4ce4aee1b6a2d70df531b8bb60ae447 languageName: node linkType: hard From 8899ae66cf067a26f1ede5f6a79991baea77a612 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Apr 2023 07:57:07 +0000 Subject: [PATCH 098/247] fix(deps): update dependency @roadiehq/backstage-plugin-github-insights to v2.3.9 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6490641711..f82ab5b291 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13639,8 +13639,8 @@ __metadata: linkType: hard "@roadiehq/backstage-plugin-github-insights@npm:^2.0.5": - version: 2.3.8 - resolution: "@roadiehq/backstage-plugin-github-insights@npm:2.3.8" + version: 2.3.9 + resolution: "@roadiehq/backstage-plugin-github-insights@npm:2.3.9" dependencies: "@backstage/catalog-model": ^1.2.1 "@backstage/core-components": ^0.12.5 @@ -13662,7 +13662,7 @@ __metadata: react: ^16.13.1 || ^17.0.0 react-dom: ^16.13.1 || ^17.0.0 react-router: 6.0.0-beta.0 || ^6.3.0 - checksum: 0a9027a035d41e46344d88da361ec91fbf43adb018bb9e383e5659d4478b67951ef9e32381b020a2104bf16be5009ae7e97c07182dcbe04721d6385e93ec8ce2 + checksum: 49b95a6724cb1126686dc29fc6d949583209dc47cc299c30a84fa5f9ac951bd74bb15901110856ba410e86dd75027562277ce5a9456d5c07557a67ef04bb9d7f languageName: node linkType: hard From 059e68af3d65a45b00215fd53b2fc999c14d3535 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Apr 2023 08:02:52 +0000 Subject: [PATCH 099/247] fix(deps): update dependency @roadiehq/backstage-plugin-travis-ci to v2.1.8 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 589db99d4e..df265b79ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13697,8 +13697,8 @@ __metadata: linkType: hard "@roadiehq/backstage-plugin-travis-ci@npm:^2.0.5": - version: 2.1.7 - resolution: "@roadiehq/backstage-plugin-travis-ci@npm:2.1.7" + version: 2.1.8 + resolution: "@roadiehq/backstage-plugin-travis-ci@npm:2.1.8" dependencies: "@backstage/catalog-model": ^1.2.1 "@backstage/core-components": ^0.12.5 @@ -13718,7 +13718,7 @@ __metadata: react-dom: ^16.13.1 || ^17.0.0 react-router: 6.0.0-beta.0 || ^6.3.0 react-router-dom: 6.0.0-beta.0 || ^6.3.0 - checksum: 1a00d0128e1bdacac93ffa7b1aa1534ec91b603899d40967ed387e3fd91f35bbc5f9babfc7b811279775d60e28db7747211fd5c0d368db23b5a184b054e918c1 + checksum: b135750b1fbf6f74743dd9142c2fcdec00379633ffd1b3793649eeea9ef6b015165da0dcb85f5442294364794c77592ef8ec1e83ada9f8bbb66ac394729a6f1a languageName: node linkType: hard From 6a67164e09f8726cfaf99d8f90cd7332f7bfeb20 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Apr 2023 09:28:24 +0000 Subject: [PATCH 100/247] fix(deps): update dependency @uiw/react-codemirror to v4.19.11 Signed-off-by: Renovate Bot --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 80c89a35d6..77fa4f7258 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17044,9 +17044,9 @@ __metadata: languageName: node linkType: hard -"@uiw/codemirror-extensions-basic-setup@npm:4.19.10": - version: 4.19.10 - resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.19.10" +"@uiw/codemirror-extensions-basic-setup@npm:4.19.11": + version: 4.19.11 + resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.19.11" dependencies: "@codemirror/autocomplete": ^6.0.0 "@codemirror/commands": ^6.0.0 @@ -17063,19 +17063,19 @@ __metadata: "@codemirror/search": ">=6.0.0" "@codemirror/state": ">=6.0.0" "@codemirror/view": ">=6.0.0" - checksum: 2eca6545f29699bf938238f089a08c7134e04611473a338c7a59377ca1b0ebdfddb73402819921fc718c317fa52c0c473188e4dea20007e5ac68c83caaaa83f0 + checksum: c7bc3498ace8efa29492674ed94feb3c6d941f389302696136c1509ab5e3b55e590ca9887a7883b7e4972cee921d7120d9af7f78354094ccb341a72656ad8ac9 languageName: node linkType: hard "@uiw/react-codemirror@npm:^4.9.3": - version: 4.19.10 - resolution: "@uiw/react-codemirror@npm:4.19.10" + version: 4.19.11 + resolution: "@uiw/react-codemirror@npm:4.19.11" dependencies: "@babel/runtime": ^7.18.6 "@codemirror/commands": ^6.1.0 "@codemirror/state": ^6.1.1 "@codemirror/theme-one-dark": ^6.0.0 - "@uiw/codemirror-extensions-basic-setup": 4.19.10 + "@uiw/codemirror-extensions-basic-setup": 4.19.11 codemirror: ^6.0.0 peerDependencies: "@babel/runtime": ">=7.11.0" @@ -17085,7 +17085,7 @@ __metadata: codemirror: ">=6.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: 3210787e4ec717c1945ef31b8a7f066bc1b326da6e63f295f8bb790aa200359151e791ebc3420afbf9f615c03509a17fae62f4ab7eaeb1dec51f074a26ac9e08 + checksum: 5ff4c7d021b565bc578d0fce2f4b6761b8d55024fa3190288435d287028cc8770b8fe5b4b103a64c452eaf26ab88c8bfd25819782a4b4d7dcfd4361765e307ff languageName: node linkType: hard From 2e622932a4f5f3027589fce740969ffa1bb00113 Mon Sep 17 00:00:00 2001 From: Ruben Vallejo Date: Wed, 8 Mar 2023 16:22:09 -0500 Subject: [PATCH 101/247] Add proxy method to KubernetesBackendClient Signed-off-by: Ruben Vallejo --- plugins/kubernetes/package.json | 1 + .../src/api/KubernetesBackendClient.ts | 69 +++++++++++++++++++ plugins/kubernetes/src/api/types.ts | 5 ++ .../GoogleKubernetesAuthProvider.ts | 5 ++ .../KubernetesAuthProviders.ts | 23 ++++++- .../OidcKubernetesAuthProvider.ts | 4 ++ .../ServerSideAuthProvider.ts | 4 ++ .../src/kubernetes-auth-provider/types.ts | 7 +- plugins/kubernetes/src/plugin.ts | 9 ++- yarn.lock | 1 + 10 files changed, 124 insertions(+), 4 deletions(-) diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 7b3b3d7b25..f7d7430952 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -37,6 +37,7 @@ "@backstage/config": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", + "@backstage/errors": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-kubernetes-common": "workspace:^", "@backstage/theme": "workspace:^", diff --git a/plugins/kubernetes/src/api/KubernetesBackendClient.ts b/plugins/kubernetes/src/api/KubernetesBackendClient.ts index bffc966333..b54046fbcd 100644 --- a/plugins/kubernetes/src/api/KubernetesBackendClient.ts +++ b/plugins/kubernetes/src/api/KubernetesBackendClient.ts @@ -23,17 +23,22 @@ import { } from '@backstage/plugin-kubernetes-common'; import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; import { stringifyEntityRef } from '@backstage/catalog-model'; +import { KubernetesAuthProvidersApi } from '../kubernetes-auth-provider'; +import { NotFoundError } from '@backstage/errors'; export class KubernetesBackendClient implements KubernetesApi { private readonly discoveryApi: DiscoveryApi; private readonly identityApi: IdentityApi; + private readonly kubernetesAuthProvidersApi: KubernetesAuthProvidersApi; constructor(options: { discoveryApi: DiscoveryApi; identityApi: IdentityApi; + kubernetesAuthProvidersApi: KubernetesAuthProvidersApi; }) { this.discoveryApi = options.discoveryApi; this.identityApi = options.identityApi; + this.kubernetesAuthProvidersApi = options.kubernetesAuthProvidersApi; } private async handleResponse(response: Response): Promise { @@ -110,4 +115,68 @@ export class KubernetesBackendClient implements KubernetesApi { return (await this.handleResponse(response)).items; } + + async proxy( + clusterName: string, + path: string, + init?: RequestInit, + ): Promise { + const { authProvider } = await this.getCluster(clusterName); + const token = await this.getBearerToken(authProvider); + + const url = `${await this.discoveryApi.getBaseUrl( + 'kubernetes', + )}/proxy${path}`; + const headers = { + Authorization: `Bearer ${token}`, + ...init?.headers, + [`X-Kubernetes-Cluster`]: clusterName, + }; + const response = await fetch(url, { ...init, headers }); + return this.handleResponse(response); + } + + private async getCluster( + clusterName: string, + ): Promise<{ name: string; authProvider: string }> { + const cluster = await this.getClusters().then(clusters => + clusters.find(c => c.name === clusterName), + ); + if (!cluster) { + throw new NotFoundError(`Cluster ${clusterName} not found`); + } + return cluster; + } + + private async getBearerToken(authProvider: string): Promise { + // const { auth } = + // await this.kubernetesAuthProvidersApi.decorateRequestBodyForAuth( + // authProvider, + // { + // entity: { + // apiVersion: 'v1', + // kind: 'Pods', + // metadata: { + // name: 'podName', + // annotations: { + // 'backstage.io/kubernetes-label-selector': 'k8s-app=kube-dns', + // }, + // }, + // }, + // }, + // ); + return await this.kubernetesAuthProvidersApi.getBearerToken(authProvider); + + // if (auth) { + // if (authProvider === 'google' && auth.google) { + // return auth.google; + // } else if (authProvider.startsWith('oidc.') && auth.oidc) { + // const oidcTokenProvider = authProvider.replace(/^oidc\./, ''); + // return auth.oidc[oidcTokenProvider]; + // } else { + // throw new Error(`invalid auth provider '${authProvider}'`); + // } + // } + // return ''; + } } diff --git a/plugins/kubernetes/src/api/types.ts b/plugins/kubernetes/src/api/types.ts index 0005a8d069..22e0644461 100644 --- a/plugins/kubernetes/src/api/types.ts +++ b/plugins/kubernetes/src/api/types.ts @@ -43,4 +43,9 @@ export interface KubernetesApi { getCustomObjectsByEntity( request: CustomObjectsByEntityRequest, ): Promise; + proxy( + clusterName: string, + path: string, + init?: RequestInit, + ): Promise; } diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts index 058ecf07a3..171ebc9960 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts @@ -38,4 +38,9 @@ export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider { } return requestBody; } + async getBearerToken(): Promise { + return await this.authProvider.getAccessToken( + 'https://www.googleapis.com/auth/cloud-platform', + ); + } } diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts index eb3ea456ea..65dc45e131 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; +import { + KubernetesRequestAuth, + KubernetesRequestBody, +} from '@backstage/plugin-kubernetes-common'; import { KubernetesAuthProvider, KubernetesAuthProvidersApi } from './types'; import { GoogleKubernetesAuthProvider } from './GoogleKubernetesAuthProvider'; import { ServerSideKubernetesAuthProvider } from './ServerSideAuthProvider'; @@ -91,4 +94,22 @@ export class KubernetesAuthProviders implements KubernetesAuthProvidersApi { `authProvider "${authProvider}" has no KubernetesAuthProvider defined for it`, ); } + + async getBearerToken(authProvider: string): Promise { + const kubernetesAuthProvider: KubernetesAuthProvider | undefined = + this.kubernetesAuthProviderMap.get(authProvider); + + if (kubernetesAuthProvider) { + return await kubernetesAuthProvider.getBearerToken(); + } + + if (authProvider.startsWith('oidc.')) { + throw new Error( + `KubernetesAuthProviders has no oidcProvider configured for ${authProvider}`, + ); + } + throw new Error( + `authProvider "${authProvider}" has no KubernetesAuthProvider defined for it`, + ); + } } diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/OidcKubernetesAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/OidcKubernetesAuthProvider.ts index f86df5e73e..b992c71391 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/OidcKubernetesAuthProvider.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/OidcKubernetesAuthProvider.ts @@ -40,4 +40,8 @@ export class OidcKubernetesAuthProvider implements KubernetesAuthProvider { requestBody.auth = auth; return requestBody; } + + async getBearerToken(): Promise { + return await this.authProvider.getIdToken(); + } } diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/ServerSideAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/ServerSideAuthProvider.ts index 1c475b9568..9290a2eda3 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/ServerSideAuthProvider.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/ServerSideAuthProvider.ts @@ -31,4 +31,8 @@ export class ServerSideKubernetesAuthProvider // No-op, auth will be taken care of on the server-side return requestBody; } + + async getBearerToken(): Promise { + return ''; + } } diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/types.ts b/plugins/kubernetes/src/kubernetes-auth-provider/types.ts index ffa0fd5961..de9f91c44e 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/types.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/types.ts @@ -14,13 +14,17 @@ * limitations under the License. */ -import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; +import { + KubernetesRequestAuth, + KubernetesRequestBody, +} from '@backstage/plugin-kubernetes-common'; import { createApiRef } from '@backstage/core-plugin-api'; export interface KubernetesAuthProvider { decorateRequestBodyForAuth( requestBody: KubernetesRequestBody, ): Promise; + getBearerToken(): Promise; } export const kubernetesAuthProvidersApiRef = @@ -33,4 +37,5 @@ export interface KubernetesAuthProvidersApi { authProvider: string, requestBody: KubernetesRequestBody, ): Promise; + getBearerToken(authProvider: string): Promise; } diff --git a/plugins/kubernetes/src/plugin.ts b/plugins/kubernetes/src/plugin.ts index e2b13f43fe..cb85cf06fb 100644 --- a/plugins/kubernetes/src/plugin.ts +++ b/plugins/kubernetes/src/plugin.ts @@ -43,9 +43,14 @@ export const kubernetesPlugin = createPlugin({ deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef, + kubernetesAuthProvidersApi: kubernetesAuthProvidersApiRef, }, - factory: ({ discoveryApi, identityApi }) => - new KubernetesBackendClient({ discoveryApi, identityApi }), + factory: ({ discoveryApi, identityApi, kubernetesAuthProvidersApi }) => + new KubernetesBackendClient({ + discoveryApi, + identityApi, + kubernetesAuthProvidersApi, + }), }), createApiFactory({ api: kubernetesAuthProvidersApiRef, diff --git a/yarn.lock b/yarn.lock index 75f4a41905..9a4399e5ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7153,6 +7153,7 @@ __metadata: "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" + "@backstage/errors": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-kubernetes-common": "workspace:^" "@backstage/test-utils": "workspace:^" From faac72e5def585d8fe7e96dedab228d483f4ed7c Mon Sep 17 00:00:00 2001 From: Ruben Vallejo Date: Thu, 16 Mar 2023 11:02:28 -0400 Subject: [PATCH 102/247] add proxy method with tests for kubernetesbackendclient Signed-off-by: Ruben Vallejo --- plugins/kubernetes/api-report.md | 21 ++ plugins/kubernetes/dev/index.tsx | 10 + .../src/api/KubernetesBackendClient.test.ts | 266 ++++++++++++++++++ .../src/api/KubernetesBackendClient.ts | 102 +++---- plugins/kubernetes/src/api/types.ts | 10 +- .../GoogleKubernetesAuthProvider.ts | 4 +- .../KubernetesAuthProviders.ts | 5 +- .../OidcKubernetesAuthProvider.ts | 2 +- .../src/kubernetes-auth-provider/types.ts | 5 +- plugins/kubernetes/src/setupTests.ts | 1 + 10 files changed, 348 insertions(+), 78 deletions(-) create mode 100644 plugins/kubernetes/src/api/KubernetesBackendClient.test.ts diff --git a/plugins/kubernetes/api-report.md b/plugins/kubernetes/api-report.md index 2cc2674a9e..7c150689b4 100644 --- a/plugins/kubernetes/api-report.md +++ b/plugins/kubernetes/api-report.md @@ -184,6 +184,8 @@ export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider { decorateRequestBodyForAuth( requestBody: KubernetesRequestBody, ): Promise; + // (undocumented) + getBearerToken(): Promise; } // Warning: (ae-missing-release-tag) "GroupedResponses" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -259,6 +261,12 @@ export interface KubernetesApi { getWorkloadsByEntity( request: WorkloadsByEntityRequest, ): Promise; + // (undocumented) + proxy(options: { + clusterName: string; + path: string; + init?: RequestInit; + }): Promise; } // Warning: (ae-missing-release-tag) "kubernetesApiRef" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -281,6 +289,8 @@ export class KubernetesAuthProviders implements KubernetesAuthProvidersApi { authProvider: string, requestBody: KubernetesRequestBody, ): Promise; + // (undocumented) + getBearerToken(authProvider: string): Promise; } // Warning: (ae-missing-release-tag) "KubernetesAuthProvidersApi" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -292,6 +302,8 @@ export interface KubernetesAuthProvidersApi { authProvider: string, requestBody: KubernetesRequestBody, ): Promise; + // (undocumented) + getBearerToken(authProvider: string): Promise; } // Warning: (ae-missing-release-tag) "kubernetesAuthProvidersApiRef" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -306,6 +318,7 @@ export class KubernetesBackendClient implements KubernetesApi { constructor(options: { discoveryApi: DiscoveryApi; identityApi: IdentityApi; + kubernetesAuthProvidersApi: KubernetesAuthProvidersApi; }); // (undocumented) getClusters(): Promise< @@ -326,6 +339,12 @@ export class KubernetesBackendClient implements KubernetesApi { getWorkloadsByEntity( request: WorkloadsByEntityRequest, ): Promise; + // (undocumented) + proxy(options: { + clusterName: string; + path: string; + init?: RequestInit; + }): Promise; } // Warning: (ae-forgotten-export) The symbol "KubernetesContentProps" needs to be exported by the entry point index.d.ts @@ -416,6 +435,8 @@ export class ServerSideKubernetesAuthProvider decorateRequestBodyForAuth( requestBody: KubernetesRequestBody, ): Promise; + // (undocumented) + getBearerToken(): Promise; } // Warning: (ae-forgotten-export) The symbol "ServicesAccordionsProps" needs to be exported by the entry point index.d.ts diff --git a/plugins/kubernetes/dev/index.tsx b/plugins/kubernetes/dev/index.tsx index 77708f1287..5780d649fb 100644 --- a/plugins/kubernetes/dev/index.tsx +++ b/plugins/kubernetes/dev/index.tsx @@ -106,6 +106,16 @@ class MockKubernetesClient implements KubernetesApi { async getClusters(): Promise<{ name: string; authProvider: string }[]> { return [{ name: 'mock-cluster', authProvider: 'serviceAccount' }]; } + + async proxy(_options: { clusterName: String; path: String }): Promise { + return { + kind: 'Namespace', + apiVersion: 'v1', + metadata: { + name: 'mock-ns', + }, + }; + } } createDevApp() diff --git a/plugins/kubernetes/src/api/KubernetesBackendClient.test.ts b/plugins/kubernetes/src/api/KubernetesBackendClient.test.ts new file mode 100644 index 0000000000..ec18dd50bb --- /dev/null +++ b/plugins/kubernetes/src/api/KubernetesBackendClient.test.ts @@ -0,0 +1,266 @@ +/* + * 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 { KubernetesAuthProvidersApi } from '../kubernetes-auth-provider'; +import { KubernetesBackendClient } from './KubernetesBackendClient'; +import { rest } from 'msw'; +import { UrlPatternDiscovery } from '@backstage/core-app-api'; +import { setupServer } from 'msw/node'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { + CustomObjectsByEntityRequest, + KubernetesRequestBody, + ObjectsByEntityResponse, + WorkloadsByEntityRequest, +} from '@backstage/plugin-kubernetes-common'; +import { NotFoundError } from '@backstage/errors'; + +describe('KubernetesBackendClient', () => { + let backendClient: KubernetesBackendClient; + const kubernetesAuthProvidersApi: jest.Mocked = { + decorateRequestBodyForAuth: jest.fn(), + getBearerToken: jest.fn(), + }; + let mockResponse: ObjectsByEntityResponse; + const worker = setupServer(); + setupRequestMockHandlers(worker); + + const identityApi = { + getCredentials: jest.fn(), + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + signOut: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + backendClient = new KubernetesBackendClient({ + discoveryApi: UrlPatternDiscovery.compile( + 'http://localhost:1234/api/{{ pluginId }}', + ), + identityApi, + kubernetesAuthProvidersApi, + }); + mockResponse = { + items: [ + { + cluster: { + name: 'cluster-a', + }, + resources: [{ type: 'pods', resources: [] }], + podMetrics: [ + { + pod: {}, + cpu: { currentUsage: 8, requestTotal: 2, limitTotal: 1 }, + memory: { currentUsage: 8, requestTotal: 2, limitTotal: 1 }, + containers: [ + { + container: 'test', + cpuUsage: { currentUsage: 8, requestTotal: 2, limitTotal: 1 }, + memoryUsage: { + currentUsage: 8, + requestTotal: 2, + limitTotal: 1, + }, + }, + ], + }, + ], + errors: [], + }, + ], + }; + }); + + it('hits the /clusters API', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); + worker.use( + rest.get('http://localhost:1234/api/kubernetes/clusters', (_, res, ctx) => + res(ctx.json({ items: [{ name: 'cluster-a', authProvider: 'aws' }] })), + ), + ); + + const clusters = await backendClient.getClusters(); + + expect(clusters).toStrictEqual([ + { name: 'cluster-a', authProvider: 'aws' }, + ]); + }); + + it('hits the /resources/custom/query API', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); + worker.use( + rest.post( + 'http://localhost:1234/api/kubernetes/resources/custom/query', + (_, res, ctx) => res(ctx.json(mockResponse)), + ), + ); + + const request: CustomObjectsByEntityRequest = { + auth: {}, + customResources: [ + { + group: 'test-group', + apiVersion: 'v1', + plural: 'none', + }, + ], + entity: { + apiVersion: 'v1', + kind: 'pod', + metadata: { + name: 'test-name', + }, + }, + }; + + const customObject: ObjectsByEntityResponse = + await backendClient.getCustomObjectsByEntity(request); + + expect(customObject).toStrictEqual(mockResponse); + }); + + it('hits the /services/{entityName} api', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); + worker.use( + rest.post( + 'http://localhost:1234/api/kubernetes/services/test-name', + (_, res, ctx) => res(ctx.json(mockResponse)), + ), + ); + + const request: KubernetesRequestBody = { + entity: { + apiVersion: 'v1', + kind: 'pod', + metadata: { + name: 'test-name', + }, + }, + }; + + const entityObject: ObjectsByEntityResponse = + await backendClient.getObjectsByEntity(request); + + expect(entityObject).toStrictEqual(mockResponse); + }); + + it('hits the /resources/workloads/query API', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); + worker.use( + rest.post( + 'http://localhost:1234/api/kubernetes/resources/workloads/query', + (_, res, ctx) => res(ctx.json(mockResponse)), + ), + ); + + const request: WorkloadsByEntityRequest = { + auth: {}, + entity: { + apiVersion: 'v1', + kind: 'pod', + metadata: { + name: 'test-name', + }, + }, + }; + + const response: ObjectsByEntityResponse = + await backendClient.getWorkloadsByEntity(request); + + expect(response).toStrictEqual(mockResponse); + }); + + it('hits the /proxy API', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); + kubernetesAuthProvidersApi.getBearerToken.mockResolvedValue( + 'Bearer k8-token', + ); + const nsResponse = { + kind: 'Namespace', + apiVersion: 'v1', + metadata: { + name: 'new-ns', + }, + }; + worker.use( + rest.get( + 'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces', + (_, res, ctx) => res(ctx.json(nsResponse)), + ), + rest.get('http://localhost:1234/api/kubernetes/clusters', (_, res, ctx) => + res(ctx.json({ items: [{ name: 'cluster-a', authProvider: 'aws' }] })), + ), + ); + + const request = { + clusterName: 'cluster-a', + path: '/api/v1/namespaces', + }; + + const response = await backendClient.proxy(request); + + expect(response).toStrictEqual(nsResponse); + }); + + it('/proxy API throws a ERROR_NOT_FOUND if the cluster in the request is not found', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); + worker.use( + rest.get('http://localhost:1234/api/kubernetes/clusters', (_, res, ctx) => + res(ctx.json({ items: [{ name: 'cluster-b', authProvider: 'aws' }] })), + ), + ); + + const request = { + clusterName: 'cluster-a', + path: '/api/v1/namespaces', + }; + + await expect(backendClient.proxy(request)).rejects.toThrow(NotFoundError); + }); + + it('hits /proxy api when signed in as a guest', async () => { + // when a user is signed in as a guest the result of the getCredentials() method resolves to the {} value. + identityApi.getCredentials.mockResolvedValue({}); + kubernetesAuthProvidersApi.getBearerToken.mockResolvedValue( + 'Bearer k8-token', + ); + const nsResponse = { + kind: 'Namespace', + apiVersion: 'v1', + metadata: { + name: 'new-ns', + }, + }; + worker.use( + rest.get( + 'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces', + (_, res, ctx) => res(ctx.status(200), ctx.json(nsResponse)), + ), + rest.get('http://localhost:1234/api/kubernetes/clusters', (_, res, ctx) => + res(ctx.json({ items: [{ name: 'cluster-a', authProvider: 'aws' }] })), + ), + ); + + const request = { + clusterName: 'cluster-a', + path: '/api/v1/namespaces', + }; + + const response = await backendClient.proxy(request); + expect(response).toStrictEqual(nsResponse); + }); +}); diff --git a/plugins/kubernetes/src/api/KubernetesBackendClient.ts b/plugins/kubernetes/src/api/KubernetesBackendClient.ts index b54046fbcd..dbeb1c3035 100644 --- a/plugins/kubernetes/src/api/KubernetesBackendClient.ts +++ b/plugins/kubernetes/src/api/KubernetesBackendClient.ts @@ -74,6 +74,23 @@ export class KubernetesBackendClient implements KubernetesApi { return this.handleResponse(response); } + private async getCluster( + clusterName: string, + ): Promise<{ name: string; authProvider: string }> { + const cluster = await this.getClusters().then(clusters => + clusters.find(c => c.name === clusterName), + ); + if (!cluster) { + throw new NotFoundError(`Cluster ${clusterName} not found`); + } + + return cluster; + } + + private async getBearerToken(authProvider: string): Promise { + return await this.kubernetesAuthProvidersApi.getBearerToken(authProvider); + } + async getObjectsByEntity( requestBody: KubernetesRequestBody, ): Promise { @@ -105,7 +122,6 @@ export class KubernetesBackendClient implements KubernetesApi { async getClusters(): Promise<{ name: string; authProvider: string }[]> { const { token: idToken } = await this.identityApi.getCredentials(); const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}/clusters`; - const response = await fetch(url, { method: 'GET', headers: { @@ -116,67 +132,31 @@ export class KubernetesBackendClient implements KubernetesApi { return (await this.handleResponse(response)).items; } - async proxy( - clusterName: string, - path: string, - init?: RequestInit, - ): Promise { - const { authProvider } = await this.getCluster(clusterName); - const token = await this.getBearerToken(authProvider); + async proxy(options: { + clusterName: string; + path: string; + init?: RequestInit; + }): Promise { + const { authProvider } = await this.getCluster(options.clusterName); + const k8sToken = await this.getBearerToken(authProvider); + const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}/proxy${ + options.path + }`; + const identityResponse = await this.identityApi.getCredentials(); + const headers = identityResponse.token + ? { + ...options.init?.headers, + [`Backstage-Kubernetes-Cluster`]: options.clusterName, + [`Backstage-Kubernetes-Authorization`]: `Bearer ${k8sToken}`, + Authorization: `Bearer ${identityResponse.token}`, + } + : { + ...options.init?.headers, + [`Backstage-Kubernetes-Cluster`]: options.clusterName, + [`Backstage-Kubernetes-Authorization`]: `Bearer ${k8sToken}`, + }; + const response = await fetch(url, { ...options.init, headers }); - const url = `${await this.discoveryApi.getBaseUrl( - 'kubernetes', - )}/proxy${path}`; - const headers = { - Authorization: `Bearer ${token}`, - ...init?.headers, - [`X-Kubernetes-Cluster`]: clusterName, - }; - const response = await fetch(url, { ...init, headers }); return this.handleResponse(response); } - - private async getCluster( - clusterName: string, - ): Promise<{ name: string; authProvider: string }> { - const cluster = await this.getClusters().then(clusters => - clusters.find(c => c.name === clusterName), - ); - if (!cluster) { - throw new NotFoundError(`Cluster ${clusterName} not found`); - } - return cluster; - } - - private async getBearerToken(authProvider: string): Promise { - // const { auth } = - // await this.kubernetesAuthProvidersApi.decorateRequestBodyForAuth( - // authProvider, - // { - // entity: { - // apiVersion: 'v1', - // kind: 'Pods', - // metadata: { - // name: 'podName', - // annotations: { - // 'backstage.io/kubernetes-label-selector': 'k8s-app=kube-dns', - // }, - // }, - // }, - // }, - // ); - return await this.kubernetesAuthProvidersApi.getBearerToken(authProvider); - - // if (auth) { - // if (authProvider === 'google' && auth.google) { - // return auth.google; - // } else if (authProvider.startsWith('oidc.') && auth.oidc) { - // const oidcTokenProvider = authProvider.replace(/^oidc\./, ''); - // return auth.oidc[oidcTokenProvider]; - // } else { - // throw new Error(`invalid auth provider '${authProvider}'`); - // } - // } - // return ''; - } } diff --git a/plugins/kubernetes/src/api/types.ts b/plugins/kubernetes/src/api/types.ts index 22e0644461..02f2fd3521 100644 --- a/plugins/kubernetes/src/api/types.ts +++ b/plugins/kubernetes/src/api/types.ts @@ -43,9 +43,9 @@ export interface KubernetesApi { getCustomObjectsByEntity( request: CustomObjectsByEntityRequest, ): Promise; - proxy( - clusterName: string, - path: string, - init?: RequestInit, - ): Promise; + proxy(options: { + clusterName: string; + path: string; + init?: RequestInit; + }): Promise; } diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts index 171ebc9960..4b46884b39 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts @@ -28,9 +28,7 @@ export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider { async decorateRequestBodyForAuth( requestBody: KubernetesRequestBody, ): Promise { - const googleAuthToken: string = await this.authProvider.getAccessToken( - 'https://www.googleapis.com/auth/cloud-platform', - ); + const googleAuthToken: string = await this.getBearerToken(); if ('auth' in requestBody) { requestBody.auth!.google = googleAuthToken; } else { diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts index 65dc45e131..45ba243d23 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -import { - KubernetesRequestAuth, - KubernetesRequestBody, -} from '@backstage/plugin-kubernetes-common'; +import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; import { KubernetesAuthProvider, KubernetesAuthProvidersApi } from './types'; import { GoogleKubernetesAuthProvider } from './GoogleKubernetesAuthProvider'; import { ServerSideKubernetesAuthProvider } from './ServerSideAuthProvider'; diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/OidcKubernetesAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/OidcKubernetesAuthProvider.ts index b992c71391..9d9d34fe1d 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/OidcKubernetesAuthProvider.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/OidcKubernetesAuthProvider.ts @@ -30,7 +30,7 @@ export class OidcKubernetesAuthProvider implements KubernetesAuthProvider { async decorateRequestBodyForAuth( requestBody: KubernetesRequestBody, ): Promise { - const authToken: string = await this.authProvider.getIdToken(); + const authToken: string = await this.getBearerToken(); const auth = { ...requestBody.auth }; if (auth.oidc) { auth.oidc[this.providerName] = authToken; diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/types.ts b/plugins/kubernetes/src/kubernetes-auth-provider/types.ts index de9f91c44e..395db3c1bd 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/types.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/types.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -import { - KubernetesRequestAuth, - KubernetesRequestBody, -} from '@backstage/plugin-kubernetes-common'; +import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; import { createApiRef } from '@backstage/core-plugin-api'; export interface KubernetesAuthProvider { diff --git a/plugins/kubernetes/src/setupTests.ts b/plugins/kubernetes/src/setupTests.ts index a373310a1a..f7752030ed 100644 --- a/plugins/kubernetes/src/setupTests.ts +++ b/plugins/kubernetes/src/setupTests.ts @@ -16,6 +16,7 @@ import '@testing-library/jest-dom'; // eslint-disable-next-line no-restricted-imports import { TextDecoder, TextEncoder } from 'util'; +import 'cross-fetch/polyfill'; // These are missing from jest-node, so not available on global. Object.defineProperty(global, 'TextEncoder', { From 98d06e02aafe88d66407ce3bacc894260097b573 Mon Sep 17 00:00:00 2001 From: Phred Date: Tue, 4 Apr 2023 09:16:18 -0500 Subject: [PATCH 103/247] renamed utility to entityRefToName based on code review Signed-off-by: Phred --- .../actions/builtin/github/githubRepoCreate.test.ts | 4 ++-- .../src/scaffolder/actions/builtin/github/helpers.ts | 6 +++--- .../src/scaffolder/actions/builtin/helpers.test.ts | 10 +++------- .../src/scaffolder/actions/builtin/helpers.ts | 2 +- .../scaffolder/actions/builtin/publish/github.test.ts | 10 +++++----- 5 files changed, 14 insertions(+), 18 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts index 58747d57ef..4f23839fca 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts @@ -28,7 +28,7 @@ import { import { when } from 'jest-when'; import { PassThrough } from 'stream'; import { createGithubRepoCreateAction } from './githubRepoCreate'; -import { familiarizeEntityName } from '../helpers'; +import { entityRefToName } from '../helpers'; const mockOctokit = { rest: { @@ -90,7 +90,7 @@ describe('github:repo:create', () => { integrations, githubCredentialsProvider, }); - (familiarizeEntityName as jest.Mock).mockImplementation((s: string) => s); + (entityRefToName as jest.Mock).mockImplementation((s: string) => s); }); afterEach(jest.resetAllMocks); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts index 23f6b804b7..57be72bc73 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts @@ -29,7 +29,7 @@ import { initRepoAndPush, } from '../helpers'; import { getRepoSourceDirectory, parseRepoUrl } from '../publish/util'; -import { familiarizeEntityName } from '../../builtin/helpers'; +import { entityRefToName } from '../../builtin/helpers'; const DEFAULT_TIMEOUT_MS = 60_000; @@ -219,13 +219,13 @@ export async function createGithubRepoWithCollaboratorsAndTopics( await client.rest.repos.addCollaborator({ owner, repo, - username: familiarizeEntityName(collaborator.user), + username: entityRefToName(collaborator.user), permission: collaborator.access, }); } else if ('team' in collaborator) { await client.rest.teams.addOrUpdateRepoPermissionsInOrg({ org: owner, - team_slug: familiarizeEntityName(collaborator.team), + team_slug: entityRefToName(collaborator.team), owner, repo, permission: collaborator.access, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.test.ts index 8cde257d64..239f7c7351 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.test.ts @@ -15,11 +15,7 @@ */ import { Git, getVoidLogger } from '@backstage/backend-common'; -import { - commitAndPushRepo, - familiarizeEntityName, - initRepoAndPush, -} from './helpers'; +import { commitAndPushRepo, entityRefToName, initRepoAndPush } from './helpers'; jest.mock('@backstage/backend-common', () => ({ Git: { @@ -306,7 +302,7 @@ describe('commitAndPushRepo', () => { }); }); -describe('familiarizeEntityName', () => { +describe('entityRefToName', () => { it.each([ 'user:default/catpants', 'group:default/catpants', @@ -316,6 +312,6 @@ describe('familiarizeEntityName', () => { 'group:custom/catpants', 'catpants', ])('should parse: "%s"', (entityName: string) => { - expect(familiarizeEntityName(entityName)).toEqual('catpants'); + expect(entityRefToName(entityName)).toEqual('catpants'); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts index 30b75d9e6f..c1630d0bf7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts @@ -297,6 +297,6 @@ export function getGitCommitMessage( : config.getOptionalString('scaffolder.defaultCommitMessage'); } -export function familiarizeEntityName(name: string): string { +export function entityRefToName(name: string): string { return name.replace(/^.*[:/]/g, ''); } diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts index 3347ca2e05..8c6b02c1c3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts @@ -29,7 +29,7 @@ import { when } from 'jest-when'; import { PassThrough } from 'stream'; import { enableBranchProtectionOnDefaultRepoBranch, - familiarizeEntityName, + entityRefToName, initRepoAndPush, } from '../helpers'; import { createPublishGithubAction } from './github'; @@ -69,7 +69,7 @@ describe('publish:github', () => { }, }); - const { familiarizeEntityName: realFamiliarizeEntityName } = + const { entityRefToName: realFamiliarizeEntityName } = jest.requireActual('../helpers'); const integrations = ScmIntegrations.fromConfig(config); let githubCredentialsProvider: GithubCredentialsProvider; @@ -99,7 +99,7 @@ describe('publish:github', () => { }); // restore real implmentation - (familiarizeEntityName as jest.Mock).mockImplementation( + (entityRefToName as jest.Mock).mockImplementation( realFamiliarizeEntityName, ); }); @@ -709,8 +709,8 @@ describe('publish:github', () => { permission: 'push', }); - expect(familiarizeEntityName).toHaveBeenCalledWith('user:robot-1'); - expect(familiarizeEntityName).toHaveBeenCalledWith('group:default/robot-2'); + expect(entityRefToName).toHaveBeenCalledWith('user:robot-1'); + expect(entityRefToName).toHaveBeenCalledWith('group:default/robot-2'); }); it('should ignore failures when adding multiple collaborators', async () => { From 4edf8d99a14ac4ef4ad017549ad73a5246666e99 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Apr 2023 15:37:50 +0000 Subject: [PATCH 104/247] fix(deps): update dependency @types/node-fetch to v2.6.3 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 75f4a41905..d591be8d68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16036,12 +16036,12 @@ __metadata: linkType: hard "@types/node-fetch@npm:^2.5.0, @types/node-fetch@npm:^2.5.12, @types/node-fetch@npm:^2.5.7, @types/node-fetch@npm:^2.6.1": - version: 2.6.2 - resolution: "@types/node-fetch@npm:2.6.2" + version: 2.6.3 + resolution: "@types/node-fetch@npm:2.6.3" dependencies: "@types/node": "*" form-data: ^3.0.0 - checksum: 6f73b1470000d303d25a6fb92875ea837a216656cb7474f66cdd67bb014aa81a5a11e7ac9c21fe19bee9ecb2ef87c1962bceeaec31386119d1ac86e4c30ad7a6 + checksum: b68cda58e91535a42dd5337932443c37f8e198ca1e8deeb95bd92a64a9a84d92071867b91c5eb84ee8e13f33d45a70549fe2bc11dd070a894dd561909f4d39f5 languageName: node linkType: hard From a580e4af62760d30a511ef4842271b4cbad073e0 Mon Sep 17 00:00:00 2001 From: Sam Blausten Date: Tue, 4 Apr 2023 17:48:11 +0200 Subject: [PATCH 105/247] Update .changeset/itchy-kiwis-unite.md Co-authored-by: Patrik Oldsberg Signed-off-by: Sam Blausten --- .changeset/itchy-kiwis-unite.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/itchy-kiwis-unite.md b/.changeset/itchy-kiwis-unite.md index c7bcad6759..f4d263589b 100644 --- a/.changeset/itchy-kiwis-unite.md +++ b/.changeset/itchy-kiwis-unite.md @@ -2,4 +2,4 @@ '@backstage/plugin-explore': patch --- -Extracted generic EntityExplorerContent component so that it is easy to show any component kinds in their own tab in the Explore page. +Extracted generic `EntityExplorerContent` component so that it is easy to show any component kinds in their own tab in the explore page. From f40e571360bf14dfb395757c0546ad97148fea47 Mon Sep 17 00:00:00 2001 From: Sam Blausten Date: Tue, 4 Apr 2023 17:49:35 +0200 Subject: [PATCH 106/247] Update plugins/explore/README.md Co-authored-by: Patrik Oldsberg Signed-off-by: Sam Blausten --- plugins/explore/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/explore/README.md b/plugins/explore/README.md index 44942ab2a3..a1cb273aab 100644 --- a/plugins/explore/README.md +++ b/plugins/explore/README.md @@ -117,10 +117,10 @@ export const ExplorePage = () => { subtitle="Browse our ecosystem" > - + - + From fab17a4f6dadd752f7150b93a9e7e829fa253a94 Mon Sep 17 00:00:00 2001 From: sblausten Date: Tue, 4 Apr 2023 17:51:53 +0200 Subject: [PATCH 107/247] Remove extra licencing comments Signed-off-by: sblausten --- .../src/components/DomainCard/DomainCard.test.tsx | 15 --------------- .../src/components/DomainCard/DomainCard.tsx | 15 --------------- .../explore/src/components/DomainCard/index.ts | 15 --------------- 3 files changed, 45 deletions(-) diff --git a/plugins/explore/src/components/DomainCard/DomainCard.test.tsx b/plugins/explore/src/components/DomainCard/DomainCard.test.tsx index 1e4b9b30f1..df9b84820a 100644 --- a/plugins/explore/src/components/DomainCard/DomainCard.test.tsx +++ b/plugins/explore/src/components/DomainCard/DomainCard.test.tsx @@ -52,18 +52,3 @@ describe('', () => { ); }); }); -/* - * 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. - */ diff --git a/plugins/explore/src/components/DomainCard/DomainCard.tsx b/plugins/explore/src/components/DomainCard/DomainCard.tsx index 867109f03c..0d37e3e273 100644 --- a/plugins/explore/src/components/DomainCard/DomainCard.tsx +++ b/plugins/explore/src/components/DomainCard/DomainCard.tsx @@ -73,18 +73,3 @@ export const DomainCard = (props: { entity: DomainEntity }) => { ); }; -/* - * 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. - */ diff --git a/plugins/explore/src/components/DomainCard/index.ts b/plugins/explore/src/components/DomainCard/index.ts index 47870e5645..dbae755460 100644 --- a/plugins/explore/src/components/DomainCard/index.ts +++ b/plugins/explore/src/components/DomainCard/index.ts @@ -15,18 +15,3 @@ */ export { DomainCard } from './DomainCard'; -/* - * 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. - */ From 6b2a5fe7482b27c47dca052cb71b80ccecd30f7c Mon Sep 17 00:00:00 2001 From: sblausten Date: Tue, 4 Apr 2023 17:53:23 +0200 Subject: [PATCH 108/247] Component name update Signed-off-by: sblausten --- .changeset/itchy-kiwis-unite.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/itchy-kiwis-unite.md b/.changeset/itchy-kiwis-unite.md index f4d263589b..0e3bcae282 100644 --- a/.changeset/itchy-kiwis-unite.md +++ b/.changeset/itchy-kiwis-unite.md @@ -2,4 +2,4 @@ '@backstage/plugin-explore': patch --- -Extracted generic `EntityExplorerContent` component so that it is easy to show any component kinds in their own tab in the explore page. +Extracted generic `CatalogKindExploreContent` component so that it is easy to show any component kinds in their own tab in the explore page. From c159ab64a609fed99d56eb4b52dcb871e9dc6892 Mon Sep 17 00:00:00 2001 From: Ruben Vallejo Date: Mon, 27 Mar 2023 11:30:13 -0400 Subject: [PATCH 109/247] add changeset and docs Signed-off-by: Ruben Vallejo --- .changeset/sweet-eels-hunt.md | 5 +++++ docs/features/kubernetes/proxy.md | 36 +++++-------------------------- 2 files changed, 10 insertions(+), 31 deletions(-) create mode 100644 .changeset/sweet-eels-hunt.md diff --git a/.changeset/sweet-eels-hunt.md b/.changeset/sweet-eels-hunt.md new file mode 100644 index 0000000000..39f124a860 --- /dev/null +++ b/.changeset/sweet-eels-hunt.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes': patch +--- + +`KubernetesBackendClient` now requires a `kubernetesAuthProvidersApi` value to be provided. `KubernetesApi` interface now has a proxy method requirement. diff --git a/docs/features/kubernetes/proxy.md b/docs/features/kubernetes/proxy.md index 7b6837dd7d..5811ff8ec4 100644 --- a/docs/features/kubernetes/proxy.md +++ b/docs/features/kubernetes/proxy.md @@ -14,42 +14,16 @@ Kubernetes backend plugin's proxy endpoint to allow them to make arbitrary requests to the [REST API](https://kubernetes.io/docs/reference/using-api/api-concepts/). -Here is a snippet fetching namespaces from a cluster configured with the -`google` [auth provider](https://backstage.io/docs/features/kubernetes/configuration#clustersauthprovider): +Here is a snippet fetching namespaces using the `KubernetesBackendClient` library ```typescript -import { - discoveryApiRef, - googleAuthApiRef, - useApi, - identityApiRef, -} from '@backstage/core-plugin-api'; +import { useApi } from '@backstage/core-plugin-api'; +import { kubernetesApiRef } from '@backstage/plugin-kubernetes'; const CLUSTER_NAME = ''; // use a known cluster name -// get a bearer token from Google -const googleAuthApi = useApi(googleAuthApiRef); -const token = await googleAuthApi.getAccessToken( - 'https://www.googleapis.com/auth/cloud-platform', -); - -// get a backstage ID token -const identityApi = useApi(identityApiRef); -const { token: userToken } = await identityApi.getCredentials(); - -const discoveryApi = useApi(discoveryApiRef); -const kubernetesBaseUrl = await discoveryApi.getBaseUrl('kubernetes'); -const kubernetesProxyEndpoint = `${kubernetesBaseUrl}/proxy`; - -// fetch namespaces -await fetch(`${kubernetesProxyEndpoint}/api/v1/namespaces`, { - method: 'GET', - headers: { - 'Backstage-Kubernetes-Cluster': CLUSTER_NAME, - 'Backstage-Kubernetes-Authorization': `Bearer ${token}`, - Authorization: `Bearer ${userToken}`, - }, -}); +const kubernetesApi = useApi(kubernetesApiRef); +await kubernetesApi.proxy(CLUSTER_NAME, '/api/v1/namespaces'); ``` ## How it works From 6bea150df58a9262724e9d743ac2fe3250652752 Mon Sep 17 00:00:00 2001 From: Ruben Vallejo Date: Wed, 29 Mar 2023 17:08:41 -0400 Subject: [PATCH 110/247] fix:addressing PR comments Signed-off-by: Ruben Vallejo --- plugins/kubernetes/api-report.md | 16 +- .../src/api/KubernetesBackendClient.test.ts | 407 +++++++++++++++--- .../src/api/KubernetesBackendClient.ts | 31 +- .../GoogleKubernetesAuthProvider.ts | 12 +- .../KubernetesAuthProviders.ts | 4 +- .../OidcKubernetesAuthProvider.ts | 8 +- .../ServerSideAuthProvider.ts | 4 +- .../src/kubernetes-auth-provider/types.ts | 4 +- 8 files changed, 384 insertions(+), 102 deletions(-) diff --git a/plugins/kubernetes/api-report.md b/plugins/kubernetes/api-report.md index 7c150689b4..a5f72861da 100644 --- a/plugins/kubernetes/api-report.md +++ b/plugins/kubernetes/api-report.md @@ -185,7 +185,9 @@ export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider { requestBody: KubernetesRequestBody, ): Promise; // (undocumented) - getBearerToken(): Promise; + getCredentials(): Promise<{ + token: string; + }>; } // Warning: (ae-missing-release-tag) "GroupedResponses" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -290,7 +292,9 @@ export class KubernetesAuthProviders implements KubernetesAuthProvidersApi { requestBody: KubernetesRequestBody, ): Promise; // (undocumented) - getBearerToken(authProvider: string): Promise; + getCredentials(authProvider: string): Promise<{ + token: string; + }>; } // Warning: (ae-missing-release-tag) "KubernetesAuthProvidersApi" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -303,7 +307,9 @@ export interface KubernetesAuthProvidersApi { requestBody: KubernetesRequestBody, ): Promise; // (undocumented) - getBearerToken(authProvider: string): Promise; + getCredentials(authProvider: string): Promise<{ + token: string; + }>; } // Warning: (ae-missing-release-tag) "kubernetesAuthProvidersApiRef" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -436,7 +442,9 @@ export class ServerSideKubernetesAuthProvider requestBody: KubernetesRequestBody, ): Promise; // (undocumented) - getBearerToken(): Promise; + getCredentials(): Promise<{ + token: string; + }>; } // Warning: (ae-forgotten-export) The symbol "ServicesAccordionsProps" needs to be exported by the entry point index.d.ts diff --git a/plugins/kubernetes/src/api/KubernetesBackendClient.test.ts b/plugins/kubernetes/src/api/KubernetesBackendClient.test.ts index ec18dd50bb..2c01f15b18 100644 --- a/plugins/kubernetes/src/api/KubernetesBackendClient.test.ts +++ b/plugins/kubernetes/src/api/KubernetesBackendClient.test.ts @@ -32,7 +32,7 @@ describe('KubernetesBackendClient', () => { let backendClient: KubernetesBackendClient; const kubernetesAuthProvidersApi: jest.Mocked = { decorateRequestBodyForAuth: jest.fn(), - getBearerToken: jest.fn(), + getCredentials: jest.fn(), }; let mockResponse: ObjectsByEntityResponse; const worker = setupServer(); @@ -100,6 +100,32 @@ describe('KubernetesBackendClient', () => { ]); }); + it('/clusters API throws a 404 Error', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); + worker.use( + rest.get('http://localhost:1234/api/kubernetes/clusters', (_, res, ctx) => + res(ctx.status(404)), + ), + ); + + await expect(backendClient.getClusters()).rejects.toThrow( + 'Could not find the Kubernetes Backend (HTTP 404). Make sure the plugin has been fully installed.', + ); + }); + + it('/clusters API throws a 500 Error', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); + worker.use( + rest.get('http://localhost:1234/api/kubernetes/clusters', (_, res, ctx) => + res(ctx.status(500)), + ), + ); + + await expect(backendClient.getClusters()).rejects.toThrow( + 'Request failed with 500 Internal Server Error, ', + ); + }); + it('hits the /resources/custom/query API', async () => { identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); worker.use( @@ -133,7 +159,75 @@ describe('KubernetesBackendClient', () => { expect(customObject).toStrictEqual(mockResponse); }); - it('hits the /services/{entityName} api', async () => { + it('/resources/custom/query API throws a 404 error', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); + worker.use( + rest.post( + 'http://localhost:1234/api/kubernetes/resources/custom/query', + (_, res, ctx) => res(ctx.status(404)), + ), + ); + + const request: CustomObjectsByEntityRequest = { + auth: {}, + customResources: [ + { + group: 'test-group', + apiVersion: 'v1', + plural: 'none', + }, + ], + entity: { + apiVersion: 'v1', + kind: 'pod', + metadata: { + name: 'test-name', + }, + }, + }; + + const response = backendClient.getCustomObjectsByEntity(request); + + await expect(response).rejects.toThrow( + 'Could not find the Kubernetes Backend (HTTP 404). Make sure the plugin has been fully installed.', + ); + }); + + it('/resources/custom/query API throws a 500 error', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); + worker.use( + rest.post( + 'http://localhost:1234/api/kubernetes/resources/custom/query', + (_, res, ctx) => res(ctx.status(500)), + ), + ); + + const request: CustomObjectsByEntityRequest = { + auth: {}, + customResources: [ + { + group: 'test-group', + apiVersion: 'v1', + plural: 'none', + }, + ], + entity: { + apiVersion: 'v1', + kind: 'pod', + metadata: { + name: 'test-name', + }, + }, + }; + + const response = backendClient.getCustomObjectsByEntity(request); + + await expect(response).rejects.toThrow( + 'Request failed with 500 Internal Server Error, ', + ); + }); + + it('hits the /services/{entityName} API', async () => { identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); worker.use( rest.post( @@ -158,6 +252,58 @@ describe('KubernetesBackendClient', () => { expect(entityObject).toStrictEqual(mockResponse); }); + it('services/{entityName} API throws a 404 error', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); + worker.use( + rest.post( + 'http://localhost:1234/api/kubernetes/services/test-name', + (_, res, ctx) => res(ctx.status(404)), + ), + ); + + const request: KubernetesRequestBody = { + entity: { + apiVersion: 'v1', + kind: 'pod', + metadata: { + name: 'test-name', + }, + }, + }; + + const response = backendClient.getObjectsByEntity(request); + + await expect(response).rejects.toThrow( + 'Could not find the Kubernetes Backend (HTTP 404). Make sure the plugin has been fully installed.', + ); + }); + + it('services/{entityName} API throws a 500 error', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); + worker.use( + rest.post( + 'http://localhost:1234/api/kubernetes/services/test-name', + (_, res, ctx) => res(ctx.status(500)), + ), + ); + + const request: KubernetesRequestBody = { + entity: { + apiVersion: 'v1', + kind: 'pod', + metadata: { + name: 'test-name', + }, + }, + }; + + const response = backendClient.getObjectsByEntity(request); + + await expect(response).rejects.toThrow( + 'Request failed with 500 Internal Server Error, ', + ); + }); + it('hits the /resources/workloads/query API', async () => { identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); worker.use( @@ -184,83 +330,210 @@ describe('KubernetesBackendClient', () => { expect(response).toStrictEqual(mockResponse); }); - it('hits the /proxy API', async () => { - identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); - kubernetesAuthProvidersApi.getBearerToken.mockResolvedValue( - 'Bearer k8-token', - ); - const nsResponse = { - kind: 'Namespace', - apiVersion: 'v1', - metadata: { - name: 'new-ns', - }, - }; - worker.use( - rest.get( - 'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces', - (_, res, ctx) => res(ctx.json(nsResponse)), - ), - rest.get('http://localhost:1234/api/kubernetes/clusters', (_, res, ctx) => - res(ctx.json({ items: [{ name: 'cluster-a', authProvider: 'aws' }] })), - ), - ); - - const request = { - clusterName: 'cluster-a', - path: '/api/v1/namespaces', - }; - - const response = await backendClient.proxy(request); - - expect(response).toStrictEqual(nsResponse); - }); - - it('/proxy API throws a ERROR_NOT_FOUND if the cluster in the request is not found', async () => { + it('/resources/workloads/query API throws a 404 error', async () => { identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); worker.use( - rest.get('http://localhost:1234/api/kubernetes/clusters', (_, res, ctx) => - res(ctx.json({ items: [{ name: 'cluster-b', authProvider: 'aws' }] })), + rest.post( + 'http://localhost:1234/api/kubernetes/resources/workloads/query', + (_, res, ctx) => res(ctx.status(404)), ), ); - const request = { - clusterName: 'cluster-a', - path: '/api/v1/namespaces', - }; - - await expect(backendClient.proxy(request)).rejects.toThrow(NotFoundError); - }); - - it('hits /proxy api when signed in as a guest', async () => { - // when a user is signed in as a guest the result of the getCredentials() method resolves to the {} value. - identityApi.getCredentials.mockResolvedValue({}); - kubernetesAuthProvidersApi.getBearerToken.mockResolvedValue( - 'Bearer k8-token', - ); - const nsResponse = { - kind: 'Namespace', - apiVersion: 'v1', - metadata: { - name: 'new-ns', + const request: WorkloadsByEntityRequest = { + auth: {}, + entity: { + apiVersion: 'v1', + kind: 'pod', + metadata: { + name: 'test-name', + }, }, }; + + const response = backendClient.getWorkloadsByEntity(request); + + await expect(response).rejects.toThrow( + 'Could not find the Kubernetes Backend (HTTP 404). Make sure the plugin has been fully installed.', + ); + }); + + it('/resources/workloads/query API throws a 500 error', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); worker.use( - rest.get( - 'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces', - (_, res, ctx) => res(ctx.status(200), ctx.json(nsResponse)), - ), - rest.get('http://localhost:1234/api/kubernetes/clusters', (_, res, ctx) => - res(ctx.json({ items: [{ name: 'cluster-a', authProvider: 'aws' }] })), + rest.post( + 'http://localhost:1234/api/kubernetes/resources/workloads/query', + (_, res, ctx) => res(ctx.status(500)), ), ); - const request = { - clusterName: 'cluster-a', - path: '/api/v1/namespaces', + const request: WorkloadsByEntityRequest = { + auth: {}, + entity: { + apiVersion: 'v1', + kind: 'pod', + metadata: { + name: 'test-name', + }, + }, }; - const response = await backendClient.proxy(request); - expect(response).toStrictEqual(nsResponse); + const response = backendClient.getWorkloadsByEntity(request); + + await expect(response).rejects.toThrow( + 'Request failed with 500 Internal Server Error, ', + ); + }); + + describe('proxy', () => { + beforeEach(() => { + worker.use( + rest.get( + 'http://localhost:1234/api/kubernetes/clusters', + (_, res, ctx) => + res( + ctx.json({ items: [{ name: 'cluster-a', authProvider: 'aws' }] }), + ), + ), + ); + }); + + it('hits the /proxy API', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); + kubernetesAuthProvidersApi.getCredentials.mockResolvedValue({ + token: 'k8-token', + }); + const nsResponse = { + kind: 'Namespace', + apiVersion: 'v1', + metadata: { + name: 'new-ns', + }, + }; + worker.use( + rest.get( + 'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces', + (req, res, ctx) => + res( + req.headers.get('Backstage-Kubernetes-Authorization') === + 'Bearer k8-token' + ? ctx.json(nsResponse) + : ctx.status(403), + ), + ), + ); + + const request = { + clusterName: 'cluster-a', + path: '/api/v1/namespaces', + }; + + const response = await backendClient.proxy(request); + + await expect(response.json()).resolves.toStrictEqual(nsResponse); + }); + + it('hits /proxy api when signed in as a guest', async () => { + // when a user is signed in as a guest the result of the getCredentials() method resolves to the {} value. + identityApi.getCredentials.mockResolvedValue({}); + kubernetesAuthProvidersApi.getCredentials.mockResolvedValue({ + token: 'k8-token', + }); + const nsResponse = { + kind: 'Namespace', + apiVersion: 'v1', + metadata: { + name: 'new-ns', + }, + }; + worker.use( + rest.get( + 'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces', + (req, res, ctx) => + res( + req.headers.get('Backstage-Kubernetes-Authorization') === + 'Bearer k8-token' + ? ctx.json(nsResponse) + : ctx.status(403), + ), + ), + ); + + const request = { + clusterName: 'cluster-a', + path: '/api/v1/namespaces', + }; + + const response = await backendClient.proxy(request); + await expect(response.json()).resolves.toStrictEqual(nsResponse); + }); + + it('/proxy API throws a 404 error', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); + kubernetesAuthProvidersApi.getCredentials.mockResolvedValue({ + token: 'k8-token', + }); + worker.use( + rest.get( + 'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces', + (_, res, ctx) => res(ctx.status(404)), + ), + ); + + const request = { + clusterName: 'cluster-a', + path: '/api/v1/namespaces', + }; + + const response = await backendClient.proxy(request); + + expect(response.status).toEqual(404); + }); + + it('throws a ERROR_NOT_FOUND if the cluster in the request is not found', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); + + const request = { + clusterName: 'cluster-b', + path: '/api/v1/namespaces', + }; + + await expect(backendClient.proxy(request)).rejects.toThrow(NotFoundError); + }); + + it('responds with an 403 error when invalid k8 token is provided', async () => { + // when a user is signed in as a guest the result of the getCredentials() method resolves to the {} value. + identityApi.getCredentials.mockResolvedValue({}); + kubernetesAuthProvidersApi.getCredentials.mockResolvedValue({ + token: 'wrong-token', + }); + + const nsResponse = { + kind: 'Namespace', + apiVersion: 'v1', + metadata: { + name: 'new-ns', + }, + }; + worker.use( + rest.get( + 'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces', + (req, res, ctx) => + res( + req.headers.get('Backstage-Kubernetes-Authorization') === + 'Bearer k8-token' + ? ctx.json(nsResponse) + : ctx.status(403), + ), + ), + ); + + const request = { + clusterName: 'cluster-a', + path: '/api/v1/namespaces', + }; + + const response = await backendClient.proxy(request); + expect(response.status).toEqual(403); + }); }); }); diff --git a/plugins/kubernetes/src/api/KubernetesBackendClient.ts b/plugins/kubernetes/src/api/KubernetesBackendClient.ts index dbeb1c3035..bc510589c8 100644 --- a/plugins/kubernetes/src/api/KubernetesBackendClient.ts +++ b/plugins/kubernetes/src/api/KubernetesBackendClient.ts @@ -87,8 +87,10 @@ export class KubernetesBackendClient implements KubernetesApi { return cluster; } - private async getBearerToken(authProvider: string): Promise { - return await this.kubernetesAuthProvidersApi.getBearerToken(authProvider); + private async getCredentials( + authProvider: string, + ): Promise<{ token: string }> { + return await this.kubernetesAuthProvidersApi.getCredentials(authProvider); } async getObjectsByEntity( @@ -138,25 +140,20 @@ export class KubernetesBackendClient implements KubernetesApi { init?: RequestInit; }): Promise { const { authProvider } = await this.getCluster(options.clusterName); - const k8sToken = await this.getBearerToken(authProvider); + const { token: k8sToken } = await this.getCredentials(authProvider); const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}/proxy${ options.path }`; const identityResponse = await this.identityApi.getCredentials(); - const headers = identityResponse.token - ? { - ...options.init?.headers, - [`Backstage-Kubernetes-Cluster`]: options.clusterName, - [`Backstage-Kubernetes-Authorization`]: `Bearer ${k8sToken}`, - Authorization: `Bearer ${identityResponse.token}`, - } - : { - ...options.init?.headers, - [`Backstage-Kubernetes-Cluster`]: options.clusterName, - [`Backstage-Kubernetes-Authorization`]: `Bearer ${k8sToken}`, - }; - const response = await fetch(url, { ...options.init, headers }); + const headers = { + ...options.init?.headers, + [`Backstage-Kubernetes-Cluster`]: options.clusterName, + [`Backstage-Kubernetes-Authorization`]: `Bearer ${k8sToken}`, + ...(identityResponse.token && { + Authorization: `Bearer ${identityResponse.token}`, + }), + }; - return this.handleResponse(response); + return await fetch(url, { ...options.init, headers }); } } diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts index 4b46884b39..5652549b8a 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts @@ -28,7 +28,7 @@ export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider { async decorateRequestBodyForAuth( requestBody: KubernetesRequestBody, ): Promise { - const googleAuthToken: string = await this.getBearerToken(); + const googleAuthToken: string = (await this.getCredentials()).token; if ('auth' in requestBody) { requestBody.auth!.google = googleAuthToken; } else { @@ -36,9 +36,11 @@ export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider { } return requestBody; } - async getBearerToken(): Promise { - return await this.authProvider.getAccessToken( - 'https://www.googleapis.com/auth/cloud-platform', - ); + async getCredentials(): Promise<{ token: string }> { + return { + token: await this.authProvider.getAccessToken( + 'https://www.googleapis.com/auth/cloud-platform', + ), + }; } } diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts index 45ba243d23..1873376487 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts @@ -92,12 +92,12 @@ export class KubernetesAuthProviders implements KubernetesAuthProvidersApi { ); } - async getBearerToken(authProvider: string): Promise { + async getCredentials(authProvider: string): Promise<{ token: string }> { const kubernetesAuthProvider: KubernetesAuthProvider | undefined = this.kubernetesAuthProviderMap.get(authProvider); if (kubernetesAuthProvider) { - return await kubernetesAuthProvider.getBearerToken(); + return await kubernetesAuthProvider.getCredentials(); } if (authProvider.startsWith('oidc.')) { diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/OidcKubernetesAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/OidcKubernetesAuthProvider.ts index 9d9d34fe1d..bd5760bde2 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/OidcKubernetesAuthProvider.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/OidcKubernetesAuthProvider.ts @@ -30,7 +30,7 @@ export class OidcKubernetesAuthProvider implements KubernetesAuthProvider { async decorateRequestBodyForAuth( requestBody: KubernetesRequestBody, ): Promise { - const authToken: string = await this.getBearerToken(); + const authToken: string = (await this.getCredentials()).token; const auth = { ...requestBody.auth }; if (auth.oidc) { auth.oidc[this.providerName] = authToken; @@ -41,7 +41,9 @@ export class OidcKubernetesAuthProvider implements KubernetesAuthProvider { return requestBody; } - async getBearerToken(): Promise { - return await this.authProvider.getIdToken(); + async getCredentials(): Promise<{ token: string }> { + return { + token: await this.authProvider.getIdToken(), + }; } } diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/ServerSideAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/ServerSideAuthProvider.ts index 9290a2eda3..8194f1e213 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/ServerSideAuthProvider.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/ServerSideAuthProvider.ts @@ -32,7 +32,7 @@ export class ServerSideKubernetesAuthProvider return requestBody; } - async getBearerToken(): Promise { - return ''; + async getCredentials(): Promise<{ token: string }> { + return { token: '' }; } } diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/types.ts b/plugins/kubernetes/src/kubernetes-auth-provider/types.ts index 395db3c1bd..117c966d36 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/types.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/types.ts @@ -21,7 +21,7 @@ export interface KubernetesAuthProvider { decorateRequestBodyForAuth( requestBody: KubernetesRequestBody, ): Promise; - getBearerToken(): Promise; + getCredentials(): Promise<{ token: string }>; } export const kubernetesAuthProvidersApiRef = @@ -34,5 +34,5 @@ export interface KubernetesAuthProvidersApi { authProvider: string, requestBody: KubernetesRequestBody, ): Promise; - getBearerToken(authProvider: string): Promise; + getCredentials(authProvider: string): Promise<{ token: string }>; } From 7917cfccfc7fe5e70aeb464e5e490a578290ecf7 Mon Sep 17 00:00:00 2001 From: Axel Hecht Date: Tue, 4 Apr 2023 19:58:50 +0200 Subject: [PATCH 111/247] Use page theme fontColor in scaffolder, episode 2 I missed some white fonts in #16713 Signed-off-by: Axel Hecht --- .changeset/famous-olives-try.md | 5 +++++ .../src/components/TemplateCard/TemplateCard.tsx | 2 +- .../scaffolder/src/next/OngoingTask/ContextMenu.tsx | 10 ++++++---- 3 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 .changeset/famous-olives-try.md diff --git a/.changeset/famous-olives-try.md b/.changeset/famous-olives-try.md new file mode 100644 index 0000000000..3150197203 --- /dev/null +++ b/.changeset/famous-olives-try.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Fix some hard-coded white font colors in scaffolder diff --git a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx index 825317ef73..d35636f507 100644 --- a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx +++ b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx @@ -107,7 +107,7 @@ const useStyles = makeStyles< top: theme.spacing(0.5), right: theme.spacing(0.5), padding: '0.25rem', - color: theme.palette.common.white, + color: ({ fontColor }) => fontColor, }, })); diff --git a/plugins/scaffolder/src/next/OngoingTask/ContextMenu.tsx b/plugins/scaffolder/src/next/OngoingTask/ContextMenu.tsx index 985dd98903..21db9f5b08 100644 --- a/plugins/scaffolder/src/next/OngoingTask/ContextMenu.tsx +++ b/plugins/scaffolder/src/next/OngoingTask/ContextMenu.tsx @@ -22,6 +22,7 @@ import { MenuItem, MenuList, Popover, + useTheme, } from '@material-ui/core'; import { useAsync } from '@react-hookz/web'; import Cancel from '@material-ui/icons/Cancel'; @@ -40,16 +41,18 @@ type ContextMenuProps = { taskId?: string; }; -const useStyles = makeStyles((theme: BackstageTheme) => ({ +const useStyles = makeStyles(() => ({ button: { - color: theme.palette.common.white, + color: ({ fontColor }) => fontColor, }, })); export const ContextMenu = (props: ContextMenuProps) => { const { cancelEnabled, logsVisible, onStartOver, onToggleLogs, taskId } = props; - const classes = useStyles(); + const { getPageTheme } = useTheme(); + const pageTheme = getPageTheme({ themeId: 'website' }); + const classes = useStyles({ fontColor: pageTheme.fontColor }); const scaffolderApi = useApi(scaffolderApiRef); const [anchorEl, setAnchorEl] = useState(); @@ -69,7 +72,6 @@ export const ContextMenu = (props: ContextMenuProps) => { setAnchorEl(event.currentTarget); }} data-testid="menu-button" - color="inherit" className={classes.button} > From 76453d497e56a091c455a31d93e6e189d5f4f2ac Mon Sep 17 00:00:00 2001 From: Alex Crome Date: Tue, 4 Apr 2023 20:31:27 +0100 Subject: [PATCH 112/247] Fixed sidebar issues Updated easy auth document preamble to better match other auth providers. Signed-off-by: Alex Crome --- docs/auth/microsoft/azure-easyauth.md | 6 ++++-- microsite/sidebars.json | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/auth/microsoft/azure-easyauth.md b/docs/auth/microsoft/azure-easyauth.md index 4daf0851f0..8ea626288c 100644 --- a/docs/auth/microsoft/azure-easyauth.md +++ b/docs/auth/microsoft/azure-easyauth.md @@ -1,10 +1,12 @@ --- -id: azure-easy-auth +id: easy-auth title: Azure EasyAuth Provider -sidebar_label: Azure EasyAuth +sidebar_label: Azure Easy Auth description: Adding Azure's EasyAuth Proxy as an authentication provider in Backstage --- +The Backstage `core-plugin-api` package comes with a Microsoft authentication provider that can authenticate users using Azure Active Directory for PaaS service hosted in Azure that support Easy Auth, such as Azure App Services. + ## Backstage Changes Add the following into your `app-config.yaml` or `app-config.production.yaml` file diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 412813263c..b3e6f07b0e 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -278,7 +278,7 @@ "auth/auth0/provider", "auth/atlassian/provider", "auth/microsoft/provider", - "auth/microsoft/easyauth", + "auth/microsoft/easy-auth", "auth/bitbucket/provider", "auth/github/provider", "auth/gitlab/provider", From 9e4198d88d360057746ee08d11315a503c652c5c Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Tue, 4 Apr 2023 15:40:00 -0400 Subject: [PATCH 113/247] Move core-app-api to dev. Signed-off-by: Aramis Sennyey --- plugins/techdocs/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 201dc3e71a..8f15b11152 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -35,7 +35,6 @@ "dependencies": { "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", - "@backstage/core-app-api": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/errors": "workspace:^", @@ -66,6 +65,7 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", "@backstage/dev-utils": "workspace:^", "@backstage/plugin-techdocs-module-addons-contrib": "workspace:^", "@backstage/test-utils": "workspace:^", From a34f7ba207f04fa4594e8be3f7c1db32f2e0f706 Mon Sep 17 00:00:00 2001 From: Viet Nguyen <19592926+v-ngu@users.noreply.github.com> Date: Tue, 4 Apr 2023 17:40:16 -0400 Subject: [PATCH 114/247] Delete bulletin-board.png Signed-off-by: Viet Nguyen <19592926+v-ngu@users.noreply.github.com> --- microsite/static/img/bulletin-board.png | Bin 1272 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 microsite/static/img/bulletin-board.png diff --git a/microsite/static/img/bulletin-board.png b/microsite/static/img/bulletin-board.png deleted file mode 100644 index 19deedd256c0af575f0e676ebd923529aed0609f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1272 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSoCO|{#S9G08$g&*{RsbBprB-l zYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&3=Ax-o-U3d6?5L+b zy+|W)iq6fvM+$xqSn`Cn6quh~bXWAnveokxUbI#<$}LiVu_$9=il~Z17$cWN9`nt> zoRuE2ky|t-K1xa7-qF7B%|)K0-|MxxohP2Z@_weG=42P3Q&2#F=DE-E^0leEIc9(R zcHDYf_6EW_S6dBGSqs~3YzCBYb&C8p$>S$N~tomDei3`7# zv444{5wz_&+X6YWm0LG1+`GbPr@QcNRh^LkFIqIz86=oyI5VK2)3!WUR5{-VT-vob zhyS;~&y+x`y=&#CpR@maTHJYeEsw8{{5T2UVOXcV)Ud)qwriai`U;fZ*A|^o%20+OTyGSyYGud_165Y z-uO25!70WSrQ1BWeA1Ut^>mx!m0Df!_@VoD;nI3#OWk^gCtqc|4mXKkU{g*FerBF2 zQx}(EdQg<9L9xM-rNL6Uy|MB6q=Ku?4cp%4)U3?$UhM3(csuv{M7g(99d173KJLNm z#`Ivfi=FO2@lUs3P4+eF^%ie%OGx?t{E)x_872gp^EY#an^Eu8+~PTIWiB2e`75uB zeto`b^%*VEhKsM`x2$om`xX1{<(&sl{iC)8vtb6@>MMy0^v*pHHMM$`|61qYp>38N z2Uwr%j$Kr~d)sLPCTE6X4kTp3b>Ym+K982u#mreO4#q#PulL>>Y{+8J&S0?JXYW+8 z$y?i}T)TEYj=$;V&GU9gOBVm$+`DPb&LW0m?)oQQ>hDvYICEZSs2bCxMTOt0rp=xI z`Fl)RJ-<{H!_0|OX0`<#U^~M&W3|it=N)W6*G9Ka>-~TEZ~cMKCwG3Y-PkL)Pr=ek zXIoBZs1n1v`%8Xp)$h`^KHHJDO8b{yFZZ2wZ5=BLH~EJcFkCM5d3-T*VQP2Ax^)vi z)at%o<-%|(&!%g|{kNIX_tz#wM>80(C-lYgUl8L#o~)Po2Wt#_&vl;aLw4W8PlpIN#ds7oIH1Uc|GbVdm}@)BK>^zs9N6SD7#T zOX-bT{p;PdcU-lf#mXi-NXuMTJ+J#bBl_H`0}{a4h9hNn#>pFTXta zxb&X%JU{)fc9k)I_P7 Date: Tue, 4 Apr 2023 17:41:53 -0400 Subject: [PATCH 115/247] Add bulletin board logo Signed-off-by: Viet Nguyen <19592926+v-ngu@users.noreply.github.com> --- microsite/static/img/bulletin-board.png | Bin 0 -> 9389 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 microsite/static/img/bulletin-board.png diff --git a/microsite/static/img/bulletin-board.png b/microsite/static/img/bulletin-board.png new file mode 100644 index 0000000000000000000000000000000000000000..918d81827a5e0af80eab7518be1a313008074e0f GIT binary patch literal 9389 zcmds7^;cBUx4$zC-2+I&P=b;I(lInhBQ2?bpdcxN)Sv=NBhu0--5rCd4BZXVEiGN| z`d#a-_x^;p-cR?;Ix}}NdYdyzFVQYNyDAEXpN4?^me5pZT{UpL z7s}fcPukUgFi={gcg|+;EtEU1&+x||*_1*}?>9xYo}!<4W+ZrKVt7M8m%OihdejPh zUcg>L;E|n*Q`@aMT_?k^qhafB_xk92PmO}09KG8&$N5cdBXeb!6Lj|MNT|3vvS;dZ z)dD!o0Lsaukt7ruz>3Ri)do8fVnvS#WVnljU>3Wd52>M`Xc6!S8YpOVN8CVGEP#!2 zLc!b;0c=DFce)ZMgaDWU@K*#N1V|DinJ_IVB~S~2W2~W+D27tsJE<@(z!8f*x&wTQ zo1ODW;OhXBII4&c@aa~6XyO3}4IutMX@HA1gyxJnis~ZY>~&1}vu_>Ws2kmA$iP>e zdW4K}Xx#lF%uvxvo8I)MFTG9D^|{F!K04_3$~bpo=haZA^ZVdCjK;YRTtTI-IngC0 zDF1Zhsc8=igmnp>ymjkF(^yoBsbs!E6QE4xj$1pEjUjWi!lX4xgsDA z?%o|L2yx=@!)nOIO+nuEQ-cN3=QV8#8u-Z59?MC}k-u%g-!g#fi9W}3YY;_iaGG(C zX4(;k+m^+$dFP47o5dkP^V6Ged5dKpjBz$fQ1GO{y*L%Pp zlfOUhAZjR>9lkF^#G-TcuAk5CGomveVAVze${tM*^RPlgcG?_LFVn6d83FvFp?!Lv z0sLP0;OpkB*Sj;9>VP%{A$~NnpIj`%+|PSJPIJU_-E7Fj3XXAwHs<-sLof%*=%sZ& z5GT9i(&}2!%Ju{{pcV^`F#w@rR^lC>0s|s}Ndi?w0=^3@x~FEKa$O4icE2bDim`>} z;US4gK6r?JZzMc)rUO@c3u72u9JgB-vRnATa3K1Nh#FO4%)+O<+3JC zZvB5q6C-=CLYO`LAo5`;6VGcBdif3=$i^qb<+@B>v4sHhDL=>nYWAfklGU2E1n}$6@@uLBJ9*qng!Ubz>?zBTDJ+^lqy#f$1 zT8pp=ET7TAbY&>J6(1xCDz$t~8TGb5CoYBsDv;f@Z*4F6n@B42&dtB@TZ2a*qwx})Kedc-N<6vv|5x`sp1ngTkcb*5B#gd&MXuHoqbf}LsNvB+Ow zxCvTdd!S%~DqtjwpY$fd1hv4?!Tk&{I>kX3^j19YXODITN8v^gMnqsQc?Mt;DeZnY zFdQTYn`VLy0N*AO414!KNkaHGU%}C`0TNdg;SPp&(zAiDRbNmz=x-_+n-Z)f)E=1I zF0E|9=6TD|HW4}3*ChG5(TDojAq#`gXb!$)=DUb1OWV>OaSi3_gZiiKTSg< zu@i22I-h8<=!=Z>uS%EZ*dfnKLiBsd|&XIEcfLCf-|cvY9UM)RT)M8FCn(7Z$u21)a zOvk6@iM-Ph3qF#LJzMxo)`$xYUQvPwz_O(&9Eus*6Q1y8*qX?)>f)A0Q7>L7l3#EV zJP=4qFGpYo>s;*Ubo(G}lFZgBX}knq*WDHl;t86o^GU`y)-EZT7=EA!{Da%;=9PJ9 z^ofM)=yRi_-c3_met5I`Xrv)rTV?-b(ZBiJff<@TvzA(V`MfV+J!LF%Kz!zMD1x4irlvqVZjT7btri}FVRySsTvIP9Id`JgjC z>LY4R_|3(Gx>*p7B29lV&8?0M1J2SRb45+Dp2je1!40|F2CjE|$B4adbhGfZ`otpg zntCw3jeWB^!WY2?wX%WeX@o(eS?kT-I=b%wtu5J9+b@Il_&hT@qA~-T0>9OEc{_wp zOl0}>@Ojj4J8RQlS-<| zA)|a{5surOi1${2yjQ{Mm$7aJpaTrQm<>B#oc^pP)wU{?X6!Q-&HC%z14ZTM0OtZ< z&-=B=+dii7dI8}$Qtu`KcDDDMzQ^+|%Re`mc!Tkj8~>SP^g-h|WX6Q8$3hYga0C!1+FzSQvG`#l*Kx-lhD%ekp88=L)}@fxq= z$+haatbgXbQapWxrpC{(x6^a-O>(1VUHQxYHEM=dLC%6k>9;_r6w69OmZ;@7P0B)N zWL4jC=u#Lx|Kbx#Q;s88Aa&r;cm3TfAo)Ho z;rob|s8LGI<%am9GgA7}oP=mK?H;@A^t5TzyqUeKByJ=i232h$@6$w6g;^b`(w|sd zl2Nr=ALoB+e)_d|CNQkXbb)u!K*cC^j$Zj2e}RAZ;p)0wKDAu*gp0do>-lN=llP+9 zuN1kS?nYL;9_u*!bt2+u{@a<0J{vAMV-&>9I8St*TFSu<4B1l#B7ycNmuKdD)Q3k$ zF$oEi?`*^vg6H=-dwVq+O9y;@FCU=SkJ@*%96w;lQJH6%7IEHlg2OkB)P}^GpQC9H4X>V9nSejdyKEo5;}Nagq0+nEOc8O-&p>JO%D0{MFNXiZ=A|IR-%L|U$=y68gAlF9o6Y_V(!ET*9`v=zjm@U&LmuzUL`lf9}w*^qkA!Z)@eo0Y7iqh87B!>gxk&%TYQX(QEWgCLG zmK9HfE=({_D`?*O_>3J1#tC!~5D<*2+H`$B!!70l2uD#KXPo06i{FS7M*MM^j#S1z zqD?m5tvw2;zdvIgFw{umoH}i#e5vd3sPneJRH%TPUX(Y=1Aa3%ma>fyy&GYItELRd zXOdl9T)13cyvDTI8uN#t;^N{8ii#9;b%*@IL>@fIREAVWT8*-1oGrMBjk4zS-mcDA z=CL1yL(758i%NW{wqwUfDpt#K+NXOliCBASFMqT#Ikanx#=LTSBh8dv&+&lxG{&8}nB#EuD(Y1g+j_ zhmE`g>FMr9WTO=SEy!VVezO&SH2G|h=}hrHThc%Q)qcm5QF7C4&2gdDSzk!N=4Z%0keo4@Uk-(Y{&$g@s5 zp89>jdi8fj+1IOUa-*X1$YrmQ^ix!d=c-Sy^RP|BsU&Xrz=e^W_3U@eL&z~q#Kq~(uba3U* zG+u6ZT5YF@*=CYi9J(HzNmj!Bb7+SWU7exqz{W&vTF1H`9`Hi7eTjYefqw9Ww(z1( zLd9a!J%AF`l>GZ}82?!FY%-aTd)jl@+t=3{`({3~bJ~IJ%9^%^ROVLtYdm?29TVt*fl-oYkryYgX~mTlMmSZ!7S_bNE7>y zUFGfK#7>cZQbZ(!21K>3m3rcVb<{(x1Z&otXM!ZPV`Hb>t$veK)LdT$wPRno z%kq+`^2EA2mldcyw>wesT;ym^dzRa~og1ocLg!hA$#sI;|tJtNPd>9Lbib_MGn zQt(jou1QaG>BQYLfcK>RC(c#+yz@gFU5%$e_uUL!!;?a`0l}S$9zNCA(8J!h&9A2? z{Ya~*Qo{iOCUi^FSk@ERm#2kjn!X*cx3SV#&oH&d;&-^) zf^KFqd)PfQ9b@`GypD)ph=|oZ`}-*M-V`SrAo!;3?QtQ&_QMGp@(nJFZrNzV>&aIz z#KCo%L*qy19mfcthn}V!+6UHbSkL-j%UR&xS?en>#@otwp;HgdM`VAP0`ge#4)cBx z#ByQax0e0LMC&d*6w#dR4c|84z6`;*TH$|ZQE}UQUik`P-EKDlgoopEy#y-nu=umX zX}*M^Tuozpi_^{K17*GUb9e`S^)R6(v_<5gLCH?%yAAIajNT!Z5F^;+*!a(OU1^zR zk$gDU8^e#TwqxvG%VyBVhXblNhZkBBhmJZrn_h=Fh!`!-0d)kKH-(jrj*j;wf-pc3 z9m?!sVc%jKp0W)^WWP&c``~cAH?{A;(VD@MbuY}{ zxvsP$C12!w2GID1>VXMF^-Ij0tO`UmBUpqtZsM6!v?$cGY9XAk?aPcSQ9Tp)TxHje z-0->W4)TMQ=La4C?NTgSjsmg`HM>zE(_)D>-Al&ZIB9Ie^)+6Y8@Uc$SSpr{YR($!;s!{k5zsoNr!uMjOz#;M9eJPri-;P$a~-+)2$U{JE$ldZI4%VbOJN+-}kn?iDw4&)OsyW63?Z z+?ivyPST#y8+p17@qU%li^ zaj74f6go^Iqi6_HJ+Z)W$t?+X7v7G==yAa8{=uzJhk)R-FSNeG-x7qGo?jH}=IjMd4 zN?n`UH)&uplo*T-r+1S+^WY+VyeOp+G8-*)IQNVrI?jz8YWf29nF^R{;Y{(e6FtQA zA>9;Qvgln_RLlRcelZVH-h+M7R6B@dAJAKpLBVFidezz*R~b%9o01nOzWF)UW^4Ct zu8b^7QJuW}v1j1ERIyMp4~e?iLx;N#xjsIUxJ-Yf4A@X-nC)@YVGYdTbAgLQG!H}e zp0mx(|$=L{y@0^UxDJ##RX){lIl~~-G$7Gk^ZEe)~ugsWhwM1xK zj*2#2h2u(ZohqWA+0*=ig{2BHgGFS{$Qw`3!YaU>?Lw6SXoznrEPNsN>d=WlAAi+t zP|}%N1X>HjjuFCN3NfQ#0xPxyyt#{}=9HWF5eWyW=J_m=zu^y)vemB|XBe-?2hAlK ziyfn2zBG^L|Ab|ZgJPvf%%!9zm6eZ^6?b~!kQguM^73+U7%E)ehJe+d^3I)KL0XWHVH;JXm_f^@SfzzDK@7Sr zQ(exUXWzRqRyhF{=vw zhKGtde}12r0GWpovU~!T_b!|nC3Nhm74e{`3$=0zd-kf96$v7HvQTcA_9ZA8aRKAC zVrzEXpr}iLDxtHH=%@5nFn%olE|=r+mZRLMD*h{tJNrECei{E#{N`Wr87Xe+x7DL0 z3n?>CDoX8)e-24b7zLdK5~!|ZfHlr*H0EOy%;~W`7DFa@b|kV_PSTctwSB(yOa1=G z%~^E`8+EsJ)lZ?tKHlYK`0We&kSO?kAy}*-J&(E^S{p>My2fX`>Y!?#m9DC_6g@|u zBd_Wlm((Wg%5bS=*+t}7fB6BQbh@(!isR56Gc)BarF&@>1B#6Qq=>joK{L89x^nkG zU1qG-PCV;t(Le6ak240`?pW;_E^KYRcnxr-;bDXhi*s1y5zuIRAS!6DT(m)pQM5@`fDQRE`m^(w(a)qt*yby$$G1AHn#z9-qcBuIL33Z zDpCSf8_xyiN_s92mimQ^&Bc=|so@sIdvzm;7v7??Hv`J#*XQc{?UkKFrWN;At9P!f zK)ka7n`pOU(J68i1L89|O3HZ+54i-vd|^*>g^@++#9x{7@q52mwgvSGvuSF#NPn^L z8|LGF?uPj%A3M02zH^R6@pT7vfblpEth{yJ#0Z8^$GU^+lXliVhPVAxNc_?4%uG=p znKB>rQJUah!ZI36)@*@Z%8-vLTNMde=;>SxP^$ z^?Tdx_Lx?DV+E; zC4B#%=2N`IAG{WuE}G59cYQHl<-mWFyPfz$?F%`|QLxb^8=8RAGbE~;^Yretw7~s; z%FV0G0n8CRL%)$OaC1^CTpDdHWG@@0&;WB=M1c#$41^R zFQ<&EqW@>s=%|FdXJYD4_n{=C%xD9{ty?{|AH@NGAx59$ADJY*@VU3T?XEJqlPClhkBJ@~CA0O@2664MVzH;&{3QUv>j5?{ zvi`f9k)J$lUyOJ8o^-iop;SM%{Mfy(rt+wa%G2^na;S>3jKRs!6{oV@P=9?{U9zac z*5HAZ_f%b~IPEVXa>j(W`3L<&^!D4y8f!z?vg)8}ynIb@(D;h)Ms1W?lK-abWZ}&~ zNxOEWC=0`DQ{=Bio)?d#jq94puz&wK5RdyDV)t}o<23t7`68s#?kFF~(W6k4smk&t zvYdHK5>>jTcBuAJ7L1Itbo=jnlGG$owk0aPeU2b;$Kp#0Ge(u}{!*9RX*r{ga5&qE zqdHq0>Z3{4`s3T&wUOA24J!C|75I8cmwLcj-t!1M*8LH`w>a6~qmk{g%K+0|GEm1f zo0zIRn|RwoCo54)yYAF{I=@ob-L-NLxNGPG=3{I&zN<#Rxalh)#v2~g$4~Z&)tVq% zY7~ETG`WgpLp!I^W2H~c?A-K!{i zp?VhVtYTbRsJ*{OPXt96WJfD?TnxKX@gwC!QXJs70k;jx&E7BAZ0;j_G(oSWj5fiD zzVUojezw|I^KL9oP$^M9i61YY&8|MdjRo_PGufrOX?(vr$?61j&SZJ%%1m3UquHlG z4N~?m#+pNvD?sI}Pp;i(`8K!c%Hgh=5iIW%L>UF4bXR9ktnqCb z(_DDidn#IfYDNqXxD9sa`CoiK@qc(2$7cLQIy^X3cQj5bqf!8<6QUE9Zn)XLJ~8lG zwxJ-s`H@?5CJS!Hl7y$^Gh;pBpw|yO?28$6Ut$X?MPK6(nsk;F$X4Ehtu&BXhjDD~ zBQ6QpH#Q2pAB@ID7Pa71Qn-$@ubk$C+UG$<-`0H>IiD6;ixw-T z^LjYeCyh}_i<~>RGL7}0aRcs{wB(yI5FiUzDi<13$|yL?OW)!>R2r!lF$`xjuL(a- zz<&KaE{qZ=QWau`m$N_!P{iCpd@(2ka3c(3>}1%j6hOz&8xokaLW)3W7qj;Hs-lCz zS&?GOMn$M0SWy`(C7(y5h(UuKjqiDUf)#@ZEe{nwVu8?ujqb^Ey$v}2Ri>9C0KlI7 zy9;2f1UiJuVNC%l0^+0GI(Lu*ULFb}_Wp2h2+UCxEdmvw?diN(Q1IeBD<^(1!~r+< z1sk6SIj4B7aku`aK_d@$D0a4bd`Mm-*Gi6kv0M1A((dFXv=5V)*elSEV@qcXRK)kH z5dfQ&-;I26Dg0K_(uEWO_SMHiFZY9Q@y)^v^H<53??T`owKK-WKgj;|3k^9kT@E;t zNf*!naGvF!87z?dQclOmEBGeDR2ObwzNUDjt2K)b^Lsj*H0p#2)=-2+waV6kwF~as ztT8es`!_|Fjc%nMGL`kPm^Jz;HeU!JBRm8UGG>V7{IIt5o{6HA<)+}PQFkoP# zxruMb^eI$a8=d=8XDRW!;5uTw@$INHTdxZw@%0PkH`b3%T`8>;c^^c4B%?g1FB%*?&9ufy+o6pqcvt~I2Ko|Hk-;+xdt@W;`qCH{4 zL>mvT%2X$>qmsoYjxCv;hoAND$#u9>LS_u88CfCAcpEgs!aKa06aG54*QYaIfDLD< zz2@-;jSuKLCNiG!3m0RdA3~PPg*f0C;Xqe=v9h1lQBTqI#Pheu_gq!+(Z#9rb@|_m zUP=nvM&w|9=^{e+%g; Xs5X4@^n(F-{|Hc1(o!syw|e(qFb6UZ literal 0 HcmV?d00001 From 56a28b559e51ecc4093fc258c92ef6bdaab41342 Mon Sep 17 00:00:00 2001 From: Riley Martine Date: Tue, 4 Apr 2023 17:44:33 -0600 Subject: [PATCH 116/247] Update kubernetes config schema to match available options (#17253) Signed-off-by: Riley Martine --- .changeset/afraid-horses-argue.md | 5 +++++ plugins/kubernetes-backend/config.d.ts | 10 +++++++++- plugins/kubernetes-backend/src/types/types.ts | 2 ++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .changeset/afraid-horses-argue.md diff --git a/.changeset/afraid-horses-argue.md b/.changeset/afraid-horses-argue.md new file mode 100644 index 0000000000..fa4b46df5d --- /dev/null +++ b/.changeset/afraid-horses-argue.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Updated kubernetes config schema to match available options diff --git a/plugins/kubernetes-backend/config.d.ts b/plugins/kubernetes-backend/config.d.ts index d3f197c21e..e9b5878920 100644 --- a/plugins/kubernetes-backend/config.d.ts +++ b/plugins/kubernetes-backend/config.d.ts @@ -21,11 +21,15 @@ export interface Config { | 'services' | 'configmaps' | 'deployments' + | 'limitranges' | 'replicasets' | 'horizontalpodautoscalers' | 'jobs' | 'cronjobs' | 'ingresses' + | 'customresources' + | 'statefulsets' + | 'daemonsets' >; serviceLocatorMethod: { type: 'multiTenant'; @@ -99,11 +103,15 @@ export interface Config { services?: string; configmaps?: string; deployments?: string; + limitranges?: string; replicasets?: string; horizontalpodautoscalers?: string; - cronjobs?: string; jobs?: string; + cronjobs?: string; ingresses?: string; + customresources?: string; + statefulsets?: string; + daemonsets?: string; } & { [pluralKind: string]: string }; }; } diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index 25c7abfd1f..87011b3bb9 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -104,6 +104,8 @@ export type KubernetesObjectTypes = | 'customresources' | 'statefulsets' | 'daemonsets'; +// If updating this list, also make sure to update +// `objectTypes` and `apiVersionOverrides` in config.d.ts! /** * Used to load cluster details from different sources From a2ec11a973dcfff8b3178acc8641753c4bdf1203 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 00:24:37 +0000 Subject: [PATCH 117/247] Update dependency @types/lodash to v4.14.192 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index c72790430e..0f28305422 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15900,9 +15900,9 @@ __metadata: linkType: hard "@types/lodash@npm:^4.14.151, @types/lodash@npm:^4.14.173, @types/lodash@npm:^4.14.175": - version: 4.14.185 - resolution: "@types/lodash@npm:4.14.185" - checksum: f81d13da5ecab110ca9c5c7cc2bedc3c9802a6acf668576aecd1b8f4b134ed81d06c15f1e600fb08f05975098280a0d97d30cddfc2cb39ec1c6b56e971ca53b3 + version: 4.14.192 + resolution: "@types/lodash@npm:4.14.192" + checksum: 31e1f0543a04158d2c429c45efd7c77882736630d0652f82eb337d6159ec0c249c5d175c0af731537b53271e665ff8d76f43221d75d03646d31cb4bd6f0056b1 languageName: node linkType: hard From 91aed4050002dfcbc98abf637569a217a320e00f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 01:00:32 +0000 Subject: [PATCH 118/247] Update dependency @types/node to v16.18.23 Signed-off-by: Renovate Bot --- yarn.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/yarn.lock b/yarn.lock index b01173db3b..c4ba084a34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16057,9 +16057,9 @@ __metadata: linkType: hard "@types/node@npm:*, @types/node@npm:>= 8, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0, @types/node@npm:^18.11.17": - version: 18.11.19 - resolution: "@types/node@npm:18.11.19" - checksum: d7cd19fcfc59cbdd3f9ba0b4072cb7adc21bd575bd8eb7d7e698975e63564aaa83f03434f32b12331f84f73d0b369d9cbe2371e359d9d7f5c3361f4987f4f7da + version: 18.15.11 + resolution: "@types/node@npm:18.15.11" + checksum: 977b4ad04708897ff0eb049ecf82246d210939c82461922d20f7d2dcfd81bbc661582ba3af28869210f7e8b1934529dcd46bff7d448551400f9d48b9d3bddec3 languageName: node linkType: hard @@ -16078,9 +16078,9 @@ __metadata: linkType: hard "@types/node@npm:^14.14.31": - version: 14.18.33 - resolution: "@types/node@npm:14.18.33" - checksum: 4e23f95186d8ae1d38c999bc6b46fe94e790da88744b0a3bfeedcbd0d9ffe2cb0ff39e85f43014f6739e5270292c1a1f6f97a1fc606fd573a0c17fda9a1d42de + version: 14.18.42 + resolution: "@types/node@npm:14.18.42" + checksum: 1c92f04a482ab54a21342b3911fc6f0093f04d3314197bc0e2f20012e9efc929c44e2ea41990b9b3cde420d7859c9ed716733f3e65c0cd6c2910a55799465f6b languageName: node linkType: hard @@ -16092,9 +16092,9 @@ __metadata: linkType: hard "@types/node@npm:^16.0.0, @types/node@npm:^16.11.26, @types/node@npm:^16.9.2": - version: 16.18.12 - resolution: "@types/node@npm:16.18.12" - checksum: fc3271182414f8593018ef8f00b4718116a92f463f619081bd399d9460e7861e1dd7eebc7cf94c23567e418ff397babed077011711aae8d47171b5a81c5bd71d + version: 16.18.23 + resolution: "@types/node@npm:16.18.23" + checksum: 00e51db28fc7a182747f37215b3f25400b1c7a8525e09fa14e55be5798891a118ebf636a49d3197335a3580fcb8222fd4ecc20c2ccff69f1c0d233fc5697465d languageName: node linkType: hard From 957a2da40a205c530c2f18903cc2b49da512e1c0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 01:09:31 +0000 Subject: [PATCH 119/247] fix(deps): update dependency swagger-ui-react to v4.18.2 Signed-off-by: Renovate Bot --- yarn.lock | 320 +++++++++++++++++++++++++++--------------------------- 1 file changed, 160 insertions(+), 160 deletions(-) diff --git a/yarn.lock b/yarn.lock index fcf6e67def..07a0d330d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14265,9 +14265,9 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-core@npm:=0.69.0, @swagger-api/apidom-core@npm:^0.69.0": - version: 0.69.0 - resolution: "@swagger-api/apidom-core@npm:0.69.0" +"@swagger-api/apidom-core@npm:>=0.69.2 <1.0.0, @swagger-api/apidom-core@npm:^0.69.2": + version: 0.69.2 + resolution: "@swagger-api/apidom-core@npm:0.69.2" dependencies: "@babel/runtime-corejs3": ^7.20.7 "@swagger-api/apidom-ast": ^0.69.0 @@ -14277,194 +14277,194 @@ __metadata: ramda-adjunct: =3.4.0 short-unique-id: =4.4.4 stampit: =4.3.2 - checksum: 036dc1198014fff9d7e879529186c4b0db423ae4c933eecec90650985e198cb3745638feff8681f85940f124703484d1b071dfca4eaf7b9457a4f99186c98972 + checksum: c7820c1fb7e2e790796f5b124054f48f2153e0480f949a8baa3bfd7a507ae63da0303c252573f76a30844c9d8acc50bd6a29a7139f36ba57d7d582c7ca8eb29f languageName: node linkType: hard -"@swagger-api/apidom-json-pointer@npm:=0.69.0, @swagger-api/apidom-json-pointer@npm:^0.69.0": - version: 0.69.0 - resolution: "@swagger-api/apidom-json-pointer@npm:0.69.0" +"@swagger-api/apidom-json-pointer@npm:>=0.69.2 <1.0.0, @swagger-api/apidom-json-pointer@npm:^0.69.2": + version: 0.69.2 + resolution: "@swagger-api/apidom-json-pointer@npm:0.69.2" dependencies: "@babel/runtime-corejs3": ^7.20.7 - "@swagger-api/apidom-core": ^0.69.0 + "@swagger-api/apidom-core": ^0.69.2 "@types/ramda": =0.28.23 ramda: =0.28.0 ramda-adjunct: =3.4.0 - checksum: 7930d938011c608d5c227c3b1cbfcb120eeebd32aedd447220219b079d030af6387a9a61e74461fe53578c06ba29aa78e5e5fdfcd561d5611d5a07ef5534ee49 + checksum: 3485f1ed477a4c5b2982b996ab94695612d922da1c57432576936eb1ef22023f399f26fbc7b498d95f269ba37445393ea6f9548e82006483fd33e86c25dcf953 languageName: node linkType: hard -"@swagger-api/apidom-ns-api-design-systems@npm:^0.69.0": - version: 0.69.0 - resolution: "@swagger-api/apidom-ns-api-design-systems@npm:0.69.0" +"@swagger-api/apidom-ns-api-design-systems@npm:^0.69.2": + version: 0.69.2 + resolution: "@swagger-api/apidom-ns-api-design-systems@npm:0.69.2" dependencies: "@babel/runtime-corejs3": ^7.20.7 - "@swagger-api/apidom-core": ^0.69.0 - "@swagger-api/apidom-ns-openapi-3-1": ^0.69.0 + "@swagger-api/apidom-core": ^0.69.2 + "@swagger-api/apidom-ns-openapi-3-1": ^0.69.2 "@types/ramda": =0.28.23 ramda: =0.28.0 ramda-adjunct: =3.4.0 stampit: =4.3.2 - checksum: 1c3047d4081cd07bbbdce6fa19c62407d1c57f7bf05b85947fd1e9b3cc2c2792fb85a926a54d3376c761a123ce3b130b06e38830ca68743a20c684aae89275f1 + checksum: 63dc8eb922bf41896645d09cb012126fd25c59e9d4c82c688b2677080c850ee5917163c84eb27ceffbdb4302eef4c5d9aee8a9dae9c4e28a63956d1fbd0dbf7b languageName: node linkType: hard -"@swagger-api/apidom-ns-asyncapi-2@npm:^0.69.0": - version: 0.69.0 - resolution: "@swagger-api/apidom-ns-asyncapi-2@npm:0.69.0" +"@swagger-api/apidom-ns-asyncapi-2@npm:^0.69.2": + version: 0.69.2 + resolution: "@swagger-api/apidom-ns-asyncapi-2@npm:0.69.2" dependencies: "@babel/runtime-corejs3": ^7.20.7 - "@swagger-api/apidom-core": ^0.69.0 - "@swagger-api/apidom-ns-json-schema-draft-7": ^0.69.0 + "@swagger-api/apidom-core": ^0.69.2 + "@swagger-api/apidom-ns-json-schema-draft-7": ^0.69.2 "@types/ramda": =0.28.23 ramda: =0.28.0 ramda-adjunct: =3.4.0 stampit: =4.3.2 - checksum: c5b9161166ddce0be047a474e7257520a756ed3fe426b806c19122334535d07b48529be60638dd560ab6ff23899641cee1110932cad16b4bf93f8fbe8f52ca05 + checksum: 067437e2f020a64824d6726b833e42c48776f1ebbe007552a2bf9659305a11cc1e852aa4860c529e730c792b698e77564cf8688131f6c48c63b0a230c049e030 languageName: node linkType: hard -"@swagger-api/apidom-ns-json-schema-draft-4@npm:^0.69.0": - version: 0.69.0 - resolution: "@swagger-api/apidom-ns-json-schema-draft-4@npm:0.69.0" +"@swagger-api/apidom-ns-json-schema-draft-4@npm:^0.69.2": + version: 0.69.2 + resolution: "@swagger-api/apidom-ns-json-schema-draft-4@npm:0.69.2" dependencies: "@babel/runtime-corejs3": ^7.20.7 - "@swagger-api/apidom-core": ^0.69.0 + "@swagger-api/apidom-core": ^0.69.2 "@types/ramda": =0.28.23 ramda: =0.28.0 ramda-adjunct: =3.4.0 stampit: =4.3.2 - checksum: d308227f5849071be3481bd8d4a8dc16d0e9e99edb61250927da88ed34595d176d59792003a2a666a49585643ff659a16774826790aab718a316beda9ce617c3 + checksum: 61d4ed9c69e7f4eeaa57cb56e789453deebc73209ecfaa9630c4ef9b90d54adb52424c877e039da266a80741521d41f3c02fd9792e244b4fec70803439b49070 languageName: node linkType: hard -"@swagger-api/apidom-ns-json-schema-draft-6@npm:^0.69.0": - version: 0.69.0 - resolution: "@swagger-api/apidom-ns-json-schema-draft-6@npm:0.69.0" +"@swagger-api/apidom-ns-json-schema-draft-6@npm:^0.69.2": + version: 0.69.2 + resolution: "@swagger-api/apidom-ns-json-schema-draft-6@npm:0.69.2" dependencies: "@babel/runtime-corejs3": ^7.20.7 - "@swagger-api/apidom-core": ^0.69.0 - "@swagger-api/apidom-ns-json-schema-draft-4": ^0.69.0 + "@swagger-api/apidom-core": ^0.69.2 + "@swagger-api/apidom-ns-json-schema-draft-4": ^0.69.2 "@types/ramda": =0.28.23 ramda: =0.28.0 ramda-adjunct: =3.4.0 stampit: =4.3.2 - checksum: df237753d0c9aa7d22029c22b5355310a14aa7a77aa484cc86d46850bd994db3f0901804510a6c62196e181c150e15fc7a031b97f0f45983b6f819fb7ed71ccb + checksum: fa37e493d6057739b1e18ba6bd21954e559185db1ea4318eed9d30cbe05e4b87c371712fdcd21bd81fccd9b94bdfda99c11f4260386764aaa95e0d8177921a7e languageName: node linkType: hard -"@swagger-api/apidom-ns-json-schema-draft-7@npm:^0.69.0": - version: 0.69.0 - resolution: "@swagger-api/apidom-ns-json-schema-draft-7@npm:0.69.0" +"@swagger-api/apidom-ns-json-schema-draft-7@npm:^0.69.2": + version: 0.69.2 + resolution: "@swagger-api/apidom-ns-json-schema-draft-7@npm:0.69.2" dependencies: "@babel/runtime-corejs3": ^7.20.7 - "@swagger-api/apidom-core": ^0.69.0 - "@swagger-api/apidom-ns-json-schema-draft-6": ^0.69.0 + "@swagger-api/apidom-core": ^0.69.2 + "@swagger-api/apidom-ns-json-schema-draft-6": ^0.69.2 "@types/ramda": =0.28.23 ramda: =0.28.0 ramda-adjunct: =3.4.0 stampit: =4.3.2 - checksum: a2059092cfb1797c90b15384270df65fadd6d1e8b469350082ab46d25a1840fd05ff0bea72a8005068f77ab277a049110962a15f1fbeeac63b9c0a34d21d78c2 + checksum: f6ae001675eb918aed498c1cc71da735463047c2f5cfadcf33746b663535ed029164a2fc8cafeabb118ab1b00a53bc93a08296f6593688c4225bf2c12b1fa2b5 languageName: node linkType: hard -"@swagger-api/apidom-ns-openapi-3-0@npm:^0.69.0": - version: 0.69.0 - resolution: "@swagger-api/apidom-ns-openapi-3-0@npm:0.69.0" +"@swagger-api/apidom-ns-openapi-3-0@npm:^0.69.2": + version: 0.69.2 + resolution: "@swagger-api/apidom-ns-openapi-3-0@npm:0.69.2" dependencies: "@babel/runtime-corejs3": ^7.20.7 - "@swagger-api/apidom-core": ^0.69.0 - "@swagger-api/apidom-ns-json-schema-draft-4": ^0.69.0 + "@swagger-api/apidom-core": ^0.69.2 + "@swagger-api/apidom-ns-json-schema-draft-4": ^0.69.2 "@types/ramda": =0.28.23 ramda: =0.28.0 ramda-adjunct: =3.4.0 stampit: =4.3.2 - checksum: e53bda9fada416e23a5220dd8987b3e4082b3020e3db40c5532fdd454aaa02953d4d72223fd0372897b0a6a73b321f89e83504d60bb8b52fa4fbcdfb5f097e59 + checksum: d876bde22b46d511ed74d0431d86f6d00e51567647e0d540288ab2965dfd7610189b1fe58f4a7accd9c83daa914ead87af572090daf2e07ef9df65c6c95e7bdd languageName: node linkType: hard -"@swagger-api/apidom-ns-openapi-3-1@npm:=0.69.0, @swagger-api/apidom-ns-openapi-3-1@npm:^0.69.0": - version: 0.69.0 - resolution: "@swagger-api/apidom-ns-openapi-3-1@npm:0.69.0" +"@swagger-api/apidom-ns-openapi-3-1@npm:>=0.69.2 <1.0.0, @swagger-api/apidom-ns-openapi-3-1@npm:^0.69.2": + version: 0.69.2 + resolution: "@swagger-api/apidom-ns-openapi-3-1@npm:0.69.2" dependencies: "@babel/runtime-corejs3": ^7.20.7 - "@swagger-api/apidom-core": ^0.69.0 - "@swagger-api/apidom-ns-openapi-3-0": ^0.69.0 + "@swagger-api/apidom-core": ^0.69.2 + "@swagger-api/apidom-ns-openapi-3-0": ^0.69.2 "@types/ramda": =0.28.23 ramda: =0.28.0 ramda-adjunct: =3.4.0 stampit: =4.3.2 - checksum: 7f194ebb36891655fdcd8dfe88e664e70f6efcd713ac6defc75dadefb0087d3fd0358b656dec4920d876342d23c44ff1cee034fa32cfa1f8b3d49ceba9509dbb + checksum: 2862f9e7855687a997658d17681cbb89531d2713d49fad9eca972c1fb904769990c019a7082aab1f5750ea9e2e4f108fb62181d1e02e2519b027742a1cd9d02b languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-api-design-systems-json@npm:^0.69.0": - version: 0.69.0 - resolution: "@swagger-api/apidom-parser-adapter-api-design-systems-json@npm:0.69.0" +"@swagger-api/apidom-parser-adapter-api-design-systems-json@npm:^0.69.2": + version: 0.69.2 + resolution: "@swagger-api/apidom-parser-adapter-api-design-systems-json@npm:0.69.2" dependencies: "@babel/runtime-corejs3": ^7.20.7 - "@swagger-api/apidom-core": ^0.69.0 - "@swagger-api/apidom-ns-api-design-systems": ^0.69.0 - "@swagger-api/apidom-parser-adapter-json": ^0.69.0 + "@swagger-api/apidom-core": ^0.69.2 + "@swagger-api/apidom-ns-api-design-systems": ^0.69.2 + "@swagger-api/apidom-parser-adapter-json": ^0.69.2 "@types/ramda": =0.28.23 ramda: =0.28.0 ramda-adjunct: =3.4.0 - checksum: 68b43e19effd7b9c26e52381903a39fff7ef0990ad3cbcbf4aa7de01cf3454d915dc10d8f688aa0fa4405b11f1a6d8f49bc160e6b72c482fadb484bd787bdebd + checksum: 062ad18d3fd402008558854415c725e94d9e1941b2049e40c54cbbdd190a9435c6f105808b7b8c36e0a3f0d8232bc17b2145d32f88957ff01e649aaa03fe2d8e languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-api-design-systems-yaml@npm:^0.69.0": - version: 0.69.0 - resolution: "@swagger-api/apidom-parser-adapter-api-design-systems-yaml@npm:0.69.0" +"@swagger-api/apidom-parser-adapter-api-design-systems-yaml@npm:^0.69.2": + version: 0.69.2 + resolution: "@swagger-api/apidom-parser-adapter-api-design-systems-yaml@npm:0.69.2" dependencies: "@babel/runtime-corejs3": ^7.20.7 - "@swagger-api/apidom-core": ^0.69.0 - "@swagger-api/apidom-ns-api-design-systems": ^0.69.0 - "@swagger-api/apidom-parser-adapter-yaml-1-2": ^0.69.0 + "@swagger-api/apidom-core": ^0.69.2 + "@swagger-api/apidom-ns-api-design-systems": ^0.69.2 + "@swagger-api/apidom-parser-adapter-yaml-1-2": ^0.69.2 "@types/ramda": =0.28.23 ramda: =0.28.0 ramda-adjunct: =3.4.0 - checksum: 6ded590b93087ad4b848ba4f742c6baeb35a572195d763a1ae0b479b8de4ae7d331175d5042116dab26684413ca6a58680080fb4b28ca5c66c7d72d6e624e174 + checksum: 09deb1fe9234780c93d5cae4c25bfbe2b31e0f13726fafeeed3f79120ad3bfcf1a4ca85790b8e2ddd5d0120df4c7cd667b7ff7ba9270f07e66c56316008b90a3 languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-asyncapi-json-2@npm:^0.69.0": - version: 0.69.0 - resolution: "@swagger-api/apidom-parser-adapter-asyncapi-json-2@npm:0.69.0" +"@swagger-api/apidom-parser-adapter-asyncapi-json-2@npm:^0.69.2": + version: 0.69.2 + resolution: "@swagger-api/apidom-parser-adapter-asyncapi-json-2@npm:0.69.2" dependencies: "@babel/runtime-corejs3": ^7.20.7 - "@swagger-api/apidom-core": ^0.69.0 - "@swagger-api/apidom-ns-asyncapi-2": ^0.69.0 - "@swagger-api/apidom-parser-adapter-json": ^0.69.0 + "@swagger-api/apidom-core": ^0.69.2 + "@swagger-api/apidom-ns-asyncapi-2": ^0.69.2 + "@swagger-api/apidom-parser-adapter-json": ^0.69.2 "@types/ramda": =0.28.23 ramda: =0.28.0 ramda-adjunct: =3.4.0 - checksum: c13b4ea5a603d7422ebf4dd87851a72a999c0036ff451b080cd37b96aa1b455f6483c447e4962303af52b44ac0fca3517573dfeeddbdb7f8d04cb7ff8796089f + checksum: 7730bf99d5c57be4b5af56701f376b4b50009e9d6ddb854fd643ea331720e12855834c869a95fca06959971879ebc7227db3f76074b56ea1ee07bb7b14bcb18a languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-asyncapi-yaml-2@npm:^0.69.0": - version: 0.69.0 - resolution: "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2@npm:0.69.0" +"@swagger-api/apidom-parser-adapter-asyncapi-yaml-2@npm:^0.69.2": + version: 0.69.2 + resolution: "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2@npm:0.69.2" dependencies: "@babel/runtime-corejs3": ^7.20.7 - "@swagger-api/apidom-core": ^0.69.0 - "@swagger-api/apidom-ns-asyncapi-2": ^0.69.0 - "@swagger-api/apidom-parser-adapter-yaml-1-2": ^0.69.0 + "@swagger-api/apidom-core": ^0.69.2 + "@swagger-api/apidom-ns-asyncapi-2": ^0.69.2 + "@swagger-api/apidom-parser-adapter-yaml-1-2": ^0.69.2 "@types/ramda": =0.28.23 ramda: =0.28.0 ramda-adjunct: =3.4.0 - checksum: f3415e86371f98e3677d22f432214f4b4e2267b80da2f505ed28d89c711fa32a627cea94040c6071e83d2ea3cf6aabc454331951d2fd3bcc138b091e78286423 + checksum: d3de29891320bed19f0a7ab161c73a2bef34cca634119f2c71dd782f41cd7086322f5f8e80a337e07585cd723d6cf1be8b1a7e7fe98154afb79fd88027b5b44e languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-json@npm:^0.69.0": - version: 0.69.0 - resolution: "@swagger-api/apidom-parser-adapter-json@npm:0.69.0" +"@swagger-api/apidom-parser-adapter-json@npm:^0.69.2": + version: 0.69.2 + resolution: "@swagger-api/apidom-parser-adapter-json@npm:0.69.2" dependencies: "@babel/runtime-corejs3": ^7.20.7 "@swagger-api/apidom-ast": ^0.69.0 - "@swagger-api/apidom-core": ^0.69.0 + "@swagger-api/apidom-core": ^0.69.2 "@types/ramda": =0.28.23 node-gyp: latest ramda: =0.28.0 @@ -14473,77 +14473,77 @@ __metadata: tree-sitter: =0.20.1 tree-sitter-json: =0.20.0 web-tree-sitter: =0.20.7 - checksum: e640ffbf749285f70ea4d125cbb030ff9920b011e8694e3cd5c379b3a6f3f387805c9535af7786c2bed6f0f67bfd9f6daad6eb65a64ca76b48b45f283933f4d5 + checksum: 30fbb4fd5f2c63440b4d64577997c33c5cc9d881a9eb07a08704fcfc5171a75e7ec9e9364c7de8aac829d08bf2e48be5e7803f22f3fdca72fa58e56bd90aaed6 languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-openapi-json-3-0@npm:^0.69.0": - version: 0.69.0 - resolution: "@swagger-api/apidom-parser-adapter-openapi-json-3-0@npm:0.69.0" +"@swagger-api/apidom-parser-adapter-openapi-json-3-0@npm:^0.69.2": + version: 0.69.2 + resolution: "@swagger-api/apidom-parser-adapter-openapi-json-3-0@npm:0.69.2" dependencies: "@babel/runtime-corejs3": ^7.20.7 - "@swagger-api/apidom-core": ^0.69.0 - "@swagger-api/apidom-ns-openapi-3-0": ^0.69.0 - "@swagger-api/apidom-parser-adapter-json": ^0.69.0 + "@swagger-api/apidom-core": ^0.69.2 + "@swagger-api/apidom-ns-openapi-3-0": ^0.69.2 + "@swagger-api/apidom-parser-adapter-json": ^0.69.2 "@types/ramda": =0.28.23 ramda: =0.28.0 ramda-adjunct: =3.4.0 - checksum: 50c61b46d16ed2a24816665ee810c295f6e82cad8a031ebc91d91aa3650bae92ddcede99ceea4346dcb69ca6edd811213018ebe8b23929012c08b425103a807d + checksum: 7c030340a7f9a24091dd0542377e5ea95b6c10967f9791ea065456301ac3766eb373ee402ed10766c4c215801308c7274f0dce925d4cab4b38b02bd3e2d58ed3 languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-openapi-json-3-1@npm:^0.69.0": - version: 0.69.0 - resolution: "@swagger-api/apidom-parser-adapter-openapi-json-3-1@npm:0.69.0" +"@swagger-api/apidom-parser-adapter-openapi-json-3-1@npm:^0.69.2": + version: 0.69.2 + resolution: "@swagger-api/apidom-parser-adapter-openapi-json-3-1@npm:0.69.2" dependencies: "@babel/runtime-corejs3": ^7.20.7 - "@swagger-api/apidom-core": ^0.69.0 - "@swagger-api/apidom-ns-openapi-3-1": ^0.69.0 - "@swagger-api/apidom-parser-adapter-json": ^0.69.0 + "@swagger-api/apidom-core": ^0.69.2 + "@swagger-api/apidom-ns-openapi-3-1": ^0.69.2 + "@swagger-api/apidom-parser-adapter-json": ^0.69.2 "@types/ramda": =0.28.23 ramda: =0.28.0 ramda-adjunct: =3.4.0 - checksum: 9c657b1eb82814958ed2b2c4e710a51378cbb11ca6957d0cc57ec544e0cba4a3bac412475f923e47c1fff8130f5a42cf8edcd99080703316150de8f1b30891b8 + checksum: b7535677eae57c5e4b6eadb834b17606e3bd4702e3753bdf4daaa675fbd1609691f3a76c355f6d8ad8e58a82d7f2e6089d0cff59edc3de7e5a505abf81f568f4 languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-openapi-yaml-3-0@npm:^0.69.0": - version: 0.69.0 - resolution: "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0@npm:0.69.0" +"@swagger-api/apidom-parser-adapter-openapi-yaml-3-0@npm:^0.69.2": + version: 0.69.2 + resolution: "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0@npm:0.69.2" dependencies: "@babel/runtime-corejs3": ^7.20.7 - "@swagger-api/apidom-core": ^0.69.0 - "@swagger-api/apidom-ns-openapi-3-0": ^0.69.0 - "@swagger-api/apidom-parser-adapter-yaml-1-2": ^0.69.0 + "@swagger-api/apidom-core": ^0.69.2 + "@swagger-api/apidom-ns-openapi-3-0": ^0.69.2 + "@swagger-api/apidom-parser-adapter-yaml-1-2": ^0.69.2 "@types/ramda": =0.28.23 ramda: =0.28.0 ramda-adjunct: =3.4.0 - checksum: ee32fc65e053ccda7eae265ad1a43e5acd0688460620912363156ada3babf3115fbec0dd35423ca633aaf78e9e4ed71ee4be030385cfdcc9ed69e8438319cc19 + checksum: f8129919930c6f79e7ff16a7eef0f58d1f585d4df3c69c52cebd2fb21fdde58be0192ac4a1a16ba4e0efc6e5aee4adcd34c63c726dc71b88f154b8ce785fcd2b languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-openapi-yaml-3-1@npm:^0.69.0": - version: 0.69.0 - resolution: "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1@npm:0.69.0" +"@swagger-api/apidom-parser-adapter-openapi-yaml-3-1@npm:^0.69.2": + version: 0.69.2 + resolution: "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1@npm:0.69.2" dependencies: "@babel/runtime-corejs3": ^7.20.7 - "@swagger-api/apidom-core": ^0.69.0 - "@swagger-api/apidom-ns-openapi-3-1": ^0.69.0 - "@swagger-api/apidom-parser-adapter-yaml-1-2": ^0.69.0 + "@swagger-api/apidom-core": ^0.69.2 + "@swagger-api/apidom-ns-openapi-3-1": ^0.69.2 + "@swagger-api/apidom-parser-adapter-yaml-1-2": ^0.69.2 "@types/ramda": =0.28.23 ramda: =0.28.0 ramda-adjunct: =3.4.0 - checksum: c21f6ad4b149f64bb5af24d7d7ba5bd8342c2ab2e0e37d85617ca0d4d3333ee45a4603ce7cdca460d4dcb17c6d41b47f733390d3ef6754664ae11705a628fe9c + checksum: fc9772e9e6028a67c089806207a289ac585b5c6ab4645377467fbf3d5c6f6f01d2ccddeb9324e5faca7495a2efb09f8d04570bf22acde58cecca682edc75d420 languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-yaml-1-2@npm:^0.69.0": - version: 0.69.0 - resolution: "@swagger-api/apidom-parser-adapter-yaml-1-2@npm:0.69.0" +"@swagger-api/apidom-parser-adapter-yaml-1-2@npm:^0.69.2": + version: 0.69.2 + resolution: "@swagger-api/apidom-parser-adapter-yaml-1-2@npm:0.69.2" dependencies: "@babel/runtime-corejs3": ^7.20.7 "@swagger-api/apidom-ast": ^0.69.0 - "@swagger-api/apidom-core": ^0.69.0 + "@swagger-api/apidom-core": ^0.69.2 "@types/ramda": =0.28.23 node-gyp: latest ramda: =0.28.0 @@ -14552,33 +14552,33 @@ __metadata: tree-sitter: =0.20.1 tree-sitter-yaml: =0.5.0 web-tree-sitter: =0.20.7 - checksum: 6067347f6ab9d2cfbbe55f13345609c86b1660bebb7cea215c90739eab011498695d785bc7bb75fc95055ea98bafc765bbc0d304ad19f72647b72ee4308d249e + checksum: 9d4fb2638e84e756ce01b9115956885c6f00ecdfe745af105ba4328b6092b4073cf1dcbc7cf33e420a5c4191ce7a4173fc8b0abc7dcb22d8af4a10ac62818ec6 languageName: node linkType: hard -"@swagger-api/apidom-reference@npm:=0.69.0": - version: 0.69.0 - resolution: "@swagger-api/apidom-reference@npm:0.69.0" +"@swagger-api/apidom-reference@npm:>=0.69.2 <1.0.0": + version: 0.69.2 + resolution: "@swagger-api/apidom-reference@npm:0.69.2" dependencies: "@babel/runtime-corejs3": ^7.20.7 - "@swagger-api/apidom-core": ^0.69.0 - "@swagger-api/apidom-json-pointer": ^0.69.0 - "@swagger-api/apidom-ns-asyncapi-2": ^0.69.0 - "@swagger-api/apidom-ns-openapi-3-0": ^0.69.0 - "@swagger-api/apidom-ns-openapi-3-1": ^0.69.0 - "@swagger-api/apidom-parser-adapter-api-design-systems-json": ^0.69.0 - "@swagger-api/apidom-parser-adapter-api-design-systems-yaml": ^0.69.0 - "@swagger-api/apidom-parser-adapter-asyncapi-json-2": ^0.69.0 - "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": ^0.69.0 - "@swagger-api/apidom-parser-adapter-json": ^0.69.0 - "@swagger-api/apidom-parser-adapter-openapi-json-3-0": ^0.69.0 - "@swagger-api/apidom-parser-adapter-openapi-json-3-1": ^0.69.0 - "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": ^0.69.0 - "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": ^0.69.0 - "@swagger-api/apidom-parser-adapter-yaml-1-2": ^0.69.0 + "@swagger-api/apidom-core": ^0.69.2 + "@swagger-api/apidom-json-pointer": ^0.69.2 + "@swagger-api/apidom-ns-asyncapi-2": ^0.69.2 + "@swagger-api/apidom-ns-openapi-3-0": ^0.69.2 + "@swagger-api/apidom-ns-openapi-3-1": ^0.69.2 + "@swagger-api/apidom-parser-adapter-api-design-systems-json": ^0.69.2 + "@swagger-api/apidom-parser-adapter-api-design-systems-yaml": ^0.69.2 + "@swagger-api/apidom-parser-adapter-asyncapi-json-2": ^0.69.2 + "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": ^0.69.2 + "@swagger-api/apidom-parser-adapter-json": ^0.69.2 + "@swagger-api/apidom-parser-adapter-openapi-json-3-0": ^0.69.2 + "@swagger-api/apidom-parser-adapter-openapi-json-3-1": ^0.69.2 + "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": ^0.69.2 + "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": ^0.69.2 + "@swagger-api/apidom-parser-adapter-yaml-1-2": ^0.69.2 "@types/ramda": =0.28.23 axios: =1.3.4 - minimatch: =7.3.0 + minimatch: =7.4.3 process: =0.11.10 ramda: =0.28.0 ramda-adjunct: =3.4.0 @@ -14612,7 +14612,7 @@ __metadata: optional: true "@swagger-api/apidom-parser-adapter-yaml-1-2": optional: true - checksum: f176e51e64377c09449e9f5bf50438be7a05fa63d4ff2f9ec5c45cf0722439537aafbe4804e2197661a652cec013c1aaa78f6a2a7ac4f6d63083aa07875c8671 + checksum: 92bda2e2d905a184f005a9be51cbe02cbaa3791a349d9fc956bf24b3d42b9215a0d6ff036929a5104cb0402713e7b12760a4ceeda23b89e3936fbd799c8515f0 languageName: node linkType: hard @@ -16664,11 +16664,11 @@ __metadata: linkType: hard "@types/swagger-ui-react@npm:^4.1.1": - version: 4.11.0 - resolution: "@types/swagger-ui-react@npm:4.11.0" + version: 4.18.0 + resolution: "@types/swagger-ui-react@npm:4.18.0" dependencies: "@types/react": "*" - checksum: cec88ffebaba7ce576d4945c901157a4aac360a669741abb6deacdf31435af5a679154d4f9281cc178214441ed1ac2cc800bd7a8f8588d1221984bef0c16c8aa + checksum: fa946a76ece76cdbaf8d8dc2945d690787f67b29a9ed14d95b706b8a7fca2d4161a62eeda74ce81c09b872c23b788f86dab36607e891744c91f425b92e302cef languageName: node linkType: hard @@ -21935,10 +21935,10 @@ __metadata: languageName: node linkType: hard -"dompurify@npm:=2.3.10": - version: 2.3.10 - resolution: "dompurify@npm:2.3.10" - checksum: ee343876b4c065e82d194818c66af76a6d2290264c7db583ad71761c11781fd626f0245f9f4670175d5707c4b8fcfb89adae80bed0418a9426a47ee7f36b0ffc +"dompurify@npm:=3.0.1": + version: 3.0.1 + resolution: "dompurify@npm:3.0.1" + checksum: 19447e6d96415a8ad3a67bc9c76ffe671eb31cf7c1e0a538cbe5037c8b8b03afe144d64cf96c6e0f7fe63b83721a33b5b913394d20a85e96550e42e31f7c086b languageName: node linkType: hard @@ -30578,12 +30578,12 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:=7.3.0": - version: 7.3.0 - resolution: "minimatch@npm:7.3.0" +"minimatch@npm:=7.4.3": + version: 7.4.3 + resolution: "minimatch@npm:7.4.3" dependencies: brace-expansion: ^2.0.1 - checksum: c4d9af992958339d5fc593c9ce496f4b9615597e57cdd663e57bd3a745ebd208c2bba19c3290c25ed8dcf259586fa1f68e4e1acf304f8694927b84c5381167d0 + checksum: daa954231b6859e3ba0e5fbd2486986d3cae283bb69acb7ed3833c84a293f8d7edb8514360ea62c01426ba791446b2a1e1cc0d718bed15c0212cef35c59a6b95 languageName: node linkType: hard @@ -37658,15 +37658,15 @@ __metadata: languageName: node linkType: hard -"swagger-client@npm:^3.19.1": - version: 3.19.2 - resolution: "swagger-client@npm:3.19.2" +"swagger-client@npm:^3.19.5": + version: 3.19.6 + resolution: "swagger-client@npm:3.19.6" dependencies: "@babel/runtime-corejs3": ^7.20.13 - "@swagger-api/apidom-core": =0.69.0 - "@swagger-api/apidom-json-pointer": =0.69.0 - "@swagger-api/apidom-ns-openapi-3-1": =0.69.0 - "@swagger-api/apidom-reference": =0.69.0 + "@swagger-api/apidom-core": ">=0.69.2 <1.0.0" + "@swagger-api/apidom-json-pointer": ">=0.69.2 <1.0.0" + "@swagger-api/apidom-ns-openapi-3-1": ">=0.69.2 <1.0.0" + "@swagger-api/apidom-reference": ">=0.69.2 <1.0.0" cookie: ~0.5.0 cross-fetch: ^3.1.5 deepmerge: ~4.3.0 @@ -37679,13 +37679,13 @@ __metadata: qs: ^6.10.2 traverse: ~0.6.6 url: ~0.11.0 - checksum: 9613387191b6e72afbe1e551d70646b7c74a136160d2a69ac3220cf23007175c500d3035b0c9ebf1ef0b49769a65d54cf343eb3b0c0d260c5797338776c872ba + checksum: de41a3cafd343f56478782cac463882305e73bd5c4298820bda2f6f35ad4bc65701e4599dbdd135f56592beebb2beca0fa5412051b52b1af8017cf981e692aa4 languageName: node linkType: hard "swagger-ui-react@npm:^4.11.1": - version: 4.18.1 - resolution: "swagger-ui-react@npm:4.18.1" + version: 4.18.2 + resolution: "swagger-ui-react@npm:4.18.2" dependencies: "@babel/runtime-corejs3": ^7.18.9 "@braintree/sanitize-url": =6.0.2 @@ -37693,7 +37693,7 @@ __metadata: classnames: ^2.3.1 css.escape: 1.5.1 deep-extend: 0.6.0 - dompurify: =2.3.10 + dompurify: =3.0.1 ieee754: ^1.2.1 immutable: ^3.x.x js-file-download: ^0.4.12 @@ -37716,7 +37716,7 @@ __metadata: reselect: ^4.1.5 serialize-error: ^8.1.0 sha.js: ^2.4.11 - swagger-client: ^3.19.1 + swagger-client: ^3.19.5 url-parse: ^1.5.8 xml: =1.0.1 xml-but-prettier: ^1.0.1 @@ -37724,7 +37724,7 @@ __metadata: peerDependencies: react: ">=17.0.0" react-dom: ">=17.0.0" - checksum: 183e15d7ccab2ef9197b3695029b66fe1e0556c48bab10f63150feabc304492ae87529a1f1b3e0ee689d19f8b3645a168dff0f3edf421737ac7e8a2d320d26c4 + checksum: 400a0fef6f00b82a8933a2698c12e8f9c9563debaf24901089d6bfa69408d9e1da66b2cb93abed659e4922d672c64609c67561363ab06c0d6d2577cdc670b755 languageName: node linkType: hard From e7bd6af572cd6b2ac7741210b80c545314f669d8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 02:06:42 +0000 Subject: [PATCH 120/247] chore(deps): update dependency @storybook/testing-library to ^0.1.0 Signed-off-by: Renovate Bot --- storybook/package.json | 2 +- storybook/yarn.lock | 422 +++++++++++++++++++++++++++-------------- 2 files changed, 276 insertions(+), 148 deletions(-) diff --git a/storybook/package.json b/storybook/package.json index b256fcaf33..6e1243c6f2 100644 --- a/storybook/package.json +++ b/storybook/package.json @@ -25,7 +25,7 @@ "@storybook/manager-webpack5": "^6.5.9", "@storybook/node-logger": "^6.5.9", "@storybook/react": "^6.5.9", - "@storybook/testing-library": "^0.0.13", + "@storybook/testing-library": "^0.1.0", "storybook-dark-mode": "^1.1.0", "typescript": "~4.7.0" }, diff --git a/storybook/yarn.lock b/storybook/yarn.lock index 4bb4a0a098..d3c336de8c 100644 --- a/storybook/yarn.lock +++ b/storybook/yarn.lock @@ -331,17 +331,17 @@ __metadata: languageName: node linkType: hard -"@babel/helper-string-parser@npm:^7.18.10": - version: 7.18.10 - resolution: "@babel/helper-string-parser@npm:7.18.10" - checksum: d554a4393365b624916b5c00a4cc21c990c6617e7f3fe30be7d9731f107f12c33229a7a3db9d829bfa110d2eb9f04790745d421640e3bd245bb412dc0ea123c1 +"@babel/helper-string-parser@npm:^7.19.4": + version: 7.19.4 + resolution: "@babel/helper-string-parser@npm:7.19.4" + checksum: b2f8a3920b30dfac81ec282ac4ad9598ea170648f8254b10f475abe6d944808fb006aab325d3eb5a8ad3bea8dfa888cfa6ef471050dae5748497c110ec060943 languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/helper-validator-identifier@npm:7.18.6" - checksum: e295254d616bbe26e48c196a198476ab4d42a73b90478c9842536cf910ead887f5af6b5c4df544d3052a25ccb3614866fa808dc1e3a5a4291acd444e243c0648 +"@babel/helper-validator-identifier@npm:^7.18.6, @babel/helper-validator-identifier@npm:^7.19.1": + version: 7.19.1 + resolution: "@babel/helper-validator-identifier@npm:7.19.1" + checksum: 0eca5e86a729162af569b46c6c41a63e18b43dbe09fda1d2a3c8924f7d617116af39cac5e4cd5d431bb760b4dca3c0970e0c444789b1db42bcf1fa41fbad0a3a languageName: node linkType: hard @@ -386,12 +386,12 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.12.11, @babel/parser@npm:^7.12.7, @babel/parser@npm:^7.18.10, @babel/parser@npm:^7.18.11": - version: 7.18.11 - resolution: "@babel/parser@npm:7.18.11" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.11, @babel/parser@npm:^7.12.7, @babel/parser@npm:^7.18.10, @babel/parser@npm:^7.18.11, @babel/parser@npm:^7.20.7": + version: 7.21.4 + resolution: "@babel/parser@npm:7.21.4" bin: parser: ./bin/babel-parser.js - checksum: 5ecc75b83e62ec53a947b1635a6ca75d6210d4a4f962f9f16f4239a6783f98e57f9662b598fa2fb1b8e12c0ad5c2bd86846ed0b97b85eb73dd7498b3a6d71a4b + checksum: de610ecd1bff331766d0c058023ca11a4f242bfafefc42caf926becccfb6756637d167c001987ca830dd4b34b93c629a4cef63f8c8c864a8564cdfde1989ac77 languageName: node linkType: hard @@ -1528,14 +1528,14 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.12.11, @babel/types@npm:^7.12.7, @babel/types@npm:^7.18.10, @babel/types@npm:^7.18.6, @babel/types@npm:^7.18.9, @babel/types@npm:^7.2.0, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": - version: 7.18.13 - resolution: "@babel/types@npm:7.18.13" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.7, @babel/types@npm:^7.18.10, @babel/types@npm:^7.18.6, @babel/types@npm:^7.18.9, @babel/types@npm:^7.2.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.3.0, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": + version: 7.21.4 + resolution: "@babel/types@npm:7.21.4" dependencies: - "@babel/helper-string-parser": ^7.18.10 - "@babel/helper-validator-identifier": ^7.18.6 + "@babel/helper-string-parser": ^7.19.4 + "@babel/helper-validator-identifier": ^7.19.1 to-fast-properties: ^2.0.0 - checksum: abc3ad1f3b6864df0ea0e778bcdf7d2c5ee2293811192962d50e8a8c05c1aeec90a48275f53b2a45aad882ed8bef9477ae1f8e70ac1d44d039e14930d1388dcc + checksum: 587bc55a91ce003b0f8aa10d70070f8006560d7dc0360dc0406d306a2cb2a10154e2f9080b9c37abec76907a90b330a536406cb75e6bdc905484f37b75c73219 languageName: node linkType: hard @@ -1952,28 +1952,6 @@ __metadata: languageName: node linkType: hard -"@storybook/addons@npm:6.5.10": - version: 6.5.10 - resolution: "@storybook/addons@npm:6.5.10" - dependencies: - "@storybook/api": 6.5.10 - "@storybook/channels": 6.5.10 - "@storybook/client-logger": 6.5.10 - "@storybook/core-events": 6.5.10 - "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/router": 6.5.10 - "@storybook/theming": 6.5.10 - "@types/webpack-env": ^1.16.0 - core-js: ^3.8.2 - global: ^4.4.0 - regenerator-runtime: ^0.13.7 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 9143908c77ab77064a5da3de1fcfb218e5f0e561f4b8a083e59b4104e442567c87fb571a752bb11c469317fc3bbcb9c2e42ebd9a5a41f825b3fd67a920d90621 - languageName: node - linkType: hard - "@storybook/addons@npm:6.5.16, @storybook/addons@npm:^6.0.0, @storybook/addons@npm:^6.5.9": version: 6.5.16 resolution: "@storybook/addons@npm:6.5.16" @@ -1996,34 +1974,6 @@ __metadata: languageName: node linkType: hard -"@storybook/api@npm:6.5.10": - version: 6.5.10 - resolution: "@storybook/api@npm:6.5.10" - dependencies: - "@storybook/channels": 6.5.10 - "@storybook/client-logger": 6.5.10 - "@storybook/core-events": 6.5.10 - "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/router": 6.5.10 - "@storybook/semver": ^7.3.2 - "@storybook/theming": 6.5.10 - core-js: ^3.8.2 - fast-deep-equal: ^3.1.3 - global: ^4.4.0 - lodash: ^4.17.21 - memoizerific: ^1.11.3 - regenerator-runtime: ^0.13.7 - store2: ^2.12.0 - telejson: ^6.0.8 - ts-dedent: ^2.0.0 - util-deprecate: ^1.0.2 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 49e01f35fa6de776329407533c0449aac84bbc9404bf717b1cebff5dc8961618956d7ba0003361c4e6cdc24e898619f778fea15db5a30eb320fc73a4b53adb40 - languageName: node - linkType: hard - "@storybook/api@npm:6.5.16, @storybook/api@npm:^6.0.0": version: 6.5.16 resolution: "@storybook/api@npm:6.5.16" @@ -2180,6 +2130,20 @@ __metadata: languageName: node linkType: hard +"@storybook/channel-postmessage@npm:7.0.2": + version: 7.0.2 + resolution: "@storybook/channel-postmessage@npm:7.0.2" + dependencies: + "@storybook/channels": 7.0.2 + "@storybook/client-logger": 7.0.2 + "@storybook/core-events": 7.0.2 + "@storybook/global": ^5.0.0 + qs: ^6.10.0 + telejson: ^7.0.3 + checksum: ef175027b4b5794af263c6ec40f6caab02deb3c189598493f5612cd5745d9cada1427914dbf39aef01dbf3e71f4299d35cfcaaaad727c06da47850b56044032c + languageName: node + linkType: hard + "@storybook/channel-websocket@npm:6.5.16": version: 6.5.16 resolution: "@storybook/channel-websocket@npm:6.5.16" @@ -2193,17 +2157,6 @@ __metadata: languageName: node linkType: hard -"@storybook/channels@npm:6.5.10": - version: 6.5.10 - resolution: "@storybook/channels@npm:6.5.10" - dependencies: - core-js: ^3.8.2 - ts-dedent: ^2.0.0 - util-deprecate: ^1.0.2 - checksum: 3837d2aff1575aa8d5af77162781b2824b909f18a7e7d3b961e6a14854b58011a56bd4f6c92bf065b8856fbcf7925a5849ffc56e42badac240701a560a26c627 - languageName: node - linkType: hard - "@storybook/channels@npm:6.5.16": version: 6.5.16 resolution: "@storybook/channels@npm:6.5.16" @@ -2215,6 +2168,13 @@ __metadata: languageName: node linkType: hard +"@storybook/channels@npm:7.0.2": + version: 7.0.2 + resolution: "@storybook/channels@npm:7.0.2" + checksum: 34ff0481a7f8bf613a9b1c9ffc3db4e48e8c80fea29d66b68a9c3b74d52040d87a5ed002d0e3e287813f33ad607440d46078e5e96367bf98713c72efb587d4a3 + languageName: node + linkType: hard + "@storybook/client-api@npm:6.5.16": version: 6.5.16 resolution: "@storybook/client-api@npm:6.5.16" @@ -2246,17 +2206,7 @@ __metadata: languageName: node linkType: hard -"@storybook/client-logger@npm:6.5.10": - version: 6.5.10 - resolution: "@storybook/client-logger@npm:6.5.10" - dependencies: - core-js: ^3.8.2 - global: ^4.4.0 - checksum: 6aa15e27e1f805b34332f647545eb53277c87492044073daf31ac6151b274cb7da6d2c8b3831484bb0c4c410f8adc1bb13322c3b80ee2f88e30856721c7d9ab1 - languageName: node - linkType: hard - -"@storybook/client-logger@npm:6.5.16, @storybook/client-logger@npm:^6.4.0": +"@storybook/client-logger@npm:6.5.16": version: 6.5.16 resolution: "@storybook/client-logger@npm:6.5.16" dependencies: @@ -2266,6 +2216,15 @@ __metadata: languageName: node linkType: hard +"@storybook/client-logger@npm:7.0.2, @storybook/client-logger@npm:^7.0.0-beta.0 || ^7.0.0-rc.0 || ^7.0.0": + version: 7.0.2 + resolution: "@storybook/client-logger@npm:7.0.2" + dependencies: + "@storybook/global": ^5.0.0 + checksum: 3deb50e8cde7777025753938e21318ee34c10237e29664cfb2dfd08f70d84b073961e1b546629e536ef53f26a91a9aed2c83223d7cfe1680787ecee7e39ce8a8 + languageName: node + linkType: hard + "@storybook/components@npm:6.5.16, @storybook/components@npm:^6.0.0": version: 6.5.16 resolution: "@storybook/components@npm:6.5.16" @@ -2384,15 +2343,6 @@ __metadata: languageName: node linkType: hard -"@storybook/core-events@npm:6.5.10": - version: 6.5.10 - resolution: "@storybook/core-events@npm:6.5.10" - dependencies: - core-js: ^3.8.2 - checksum: 89139f3f34a4ea0f2bbc02ebaa2968664cdc17abd88cc2e0467a0dfb1c11577e85fa402e5804fe4d6a99edd696d365abf93d30c396fc177563478cdbb68bcb85 - languageName: node - linkType: hard - "@storybook/core-events@npm:6.5.16, @storybook/core-events@npm:^6.0.0": version: 6.5.16 resolution: "@storybook/core-events@npm:6.5.16" @@ -2402,6 +2352,13 @@ __metadata: languageName: node linkType: hard +"@storybook/core-events@npm:7.0.2": + version: 7.0.2 + resolution: "@storybook/core-events@npm:7.0.2" + checksum: 5bca4db82f582b939ba1aaba26aee3e8f9aa0a2d36728934b4d26dbe858b6f455a46ddcf4e8e6c7b3f5fa5b517716c503626febca08e53712d516f2ca105bf11 + languageName: node + linkType: hard + "@storybook/core-server@npm:6.5.16": version: 6.5.16 resolution: "@storybook/core-server@npm:6.5.16" @@ -2522,6 +2479,15 @@ __metadata: languageName: node linkType: hard +"@storybook/csf@npm:^0.1.0": + version: 0.1.0 + resolution: "@storybook/csf@npm:0.1.0" + dependencies: + type-fest: ^2.19.0 + checksum: f1784f2aff27d5c27ab897878b08e3b04a64e7f62da1ea95fd11bfe9f558300e55f0d483d58282e8254a4b4e8935201178e70c264ccc96104c67403215d651f0 + languageName: node + linkType: hard + "@storybook/docs-tools@npm:6.5.16": version: 6.5.16 resolution: "@storybook/docs-tools@npm:6.5.16" @@ -2537,16 +2503,23 @@ __metadata: languageName: node linkType: hard -"@storybook/instrumenter@npm:^6.4.0": - version: 6.5.10 - resolution: "@storybook/instrumenter@npm:6.5.10" +"@storybook/global@npm:^5.0.0": + version: 5.0.0 + resolution: "@storybook/global@npm:5.0.0" + checksum: ede0ad35ec411fe31c61150dbd118fef344d1d0e72bf5d3502368e35cf68126f6b7ae4a0ab5e2ffe2f0baa3b4286f03ad069ba3e098e1725449ef08b7e154ba8 + languageName: node + linkType: hard + +"@storybook/instrumenter@npm:^7.0.0-beta.0 || ^7.0.0-rc.0 || ^7.0.0": + version: 7.0.2 + resolution: "@storybook/instrumenter@npm:7.0.2" dependencies: - "@storybook/addons": 6.5.10 - "@storybook/client-logger": 6.5.10 - "@storybook/core-events": 6.5.10 - core-js: ^3.8.2 - global: ^4.4.0 - checksum: cd393a8df6561561d416c6b36b46130040d91103a30e471a2161d6cb12a42c7b80d3134c2e8219a1fa1c08ad768a78194528467f5d0a15f4df0074dcc8bed057 + "@storybook/channels": 7.0.2 + "@storybook/client-logger": 7.0.2 + "@storybook/core-events": 7.0.2 + "@storybook/global": ^5.0.0 + "@storybook/preview-api": 7.0.2 + checksum: 1f0fcd2fd6561030897558ee6fb363da23bbbc67f08d12204f885da9ffc90f180703b3fbd85f01758e0f7a066437ebd6af415d4eefcb9ddb1e8ad16fd37e3eae languageName: node linkType: hard @@ -2677,6 +2650,29 @@ __metadata: languageName: node linkType: hard +"@storybook/preview-api@npm:7.0.2": + version: 7.0.2 + resolution: "@storybook/preview-api@npm:7.0.2" + dependencies: + "@storybook/channel-postmessage": 7.0.2 + "@storybook/channels": 7.0.2 + "@storybook/client-logger": 7.0.2 + "@storybook/core-events": 7.0.2 + "@storybook/csf": ^0.1.0 + "@storybook/global": ^5.0.0 + "@storybook/types": 7.0.2 + "@types/qs": ^6.9.5 + dequal: ^2.0.2 + lodash: ^4.17.21 + memoizerific: ^1.11.3 + qs: ^6.10.0 + synchronous-promise: ^2.0.15 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + checksum: 350811420c996c9616b59615fe2fe7785ec6590301ed3a6a54b07ca4b2bb45614f47b4cacf037c72ed1d7dd04a8647d0e99a4e2e0f23f91a0dc5d1e8f3486ee2 + languageName: node + linkType: hard + "@storybook/preview-web@npm:6.5.16": version: 6.5.16 resolution: "@storybook/preview-web@npm:6.5.16" @@ -2787,22 +2783,6 @@ __metadata: languageName: node linkType: hard -"@storybook/router@npm:6.5.10": - version: 6.5.10 - resolution: "@storybook/router@npm:6.5.10" - dependencies: - "@storybook/client-logger": 6.5.10 - core-js: ^3.8.2 - memoizerific: ^1.11.3 - qs: ^6.10.0 - regenerator-runtime: ^0.13.7 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 118598867067344607cff7ef6fdef7b7a18a3e08a53f75fc4beaa65013f435ae18d800d25eea52376662bc1d98a2822a143531e701d8cea7130d42dc48e2cce7 - languageName: node - linkType: hard - "@storybook/router@npm:6.5.16": version: 6.5.16 resolution: "@storybook/router@npm:6.5.16" @@ -2898,31 +2878,16 @@ __metadata: languageName: node linkType: hard -"@storybook/testing-library@npm:^0.0.13": - version: 0.0.13 - resolution: "@storybook/testing-library@npm:0.0.13" +"@storybook/testing-library@npm:^0.1.0": + version: 0.1.0 + resolution: "@storybook/testing-library@npm:0.1.0" dependencies: - "@storybook/client-logger": ^6.4.0 - "@storybook/instrumenter": ^6.4.0 + "@storybook/client-logger": ^7.0.0-beta.0 || ^7.0.0-rc.0 || ^7.0.0 + "@storybook/instrumenter": ^7.0.0-beta.0 || ^7.0.0-rc.0 || ^7.0.0 "@testing-library/dom": ^8.3.0 "@testing-library/user-event": ^13.2.1 ts-dedent: ^2.2.0 - checksum: 759361ad3fbc89bdfddfa6d5a15eef06ed6fa9110bfa40c08fcf2497e7acd85e8d5c73c26ea4a46934168b21db294256befb55755fee4292d3d277c576284a1c - languageName: node - linkType: hard - -"@storybook/theming@npm:6.5.10": - version: 6.5.10 - resolution: "@storybook/theming@npm:6.5.10" - dependencies: - "@storybook/client-logger": 6.5.10 - core-js: ^3.8.2 - memoizerific: ^1.11.3 - regenerator-runtime: ^0.13.7 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 2082d7847785a307a18eb605282468d844af01f57752916766a60047b5543cf6f0c6664b9c7a693809b4fdc121415989c2170833d3de7ca8b07fa056741787d0 + checksum: a413110dafe80f8fe64da912fddb653bc3c695c94bd023b8cb918e519e9f94119bae559909bfb8ad229041a4f17a12b33425c61572270702c116d0b727b4d4ba languageName: node linkType: hard @@ -2941,6 +2906,18 @@ __metadata: languageName: node linkType: hard +"@storybook/types@npm:7.0.2": + version: 7.0.2 + resolution: "@storybook/types@npm:7.0.2" + dependencies: + "@storybook/channels": 7.0.2 + "@types/babel__core": ^7.0.0 + "@types/express": ^4.7.0 + file-system-cache: ^2.0.0 + checksum: 329e271af5621619a8d17fff222e4170b62ca4b451bcb48b5a899d63bb8df211c5d3d8dafa2266ac3c9801bf64c4ac1aed73950a2f21da82333b19c448ea32e0 + languageName: node + linkType: hard + "@storybook/ui@npm:6.5.16": version: 6.5.16 resolution: "@storybook/ui@npm:6.5.16" @@ -3116,6 +3093,66 @@ __metadata: languageName: node linkType: hard +"@types/babel__core@npm:^7.0.0": + version: 7.20.0 + resolution: "@types/babel__core@npm:7.20.0" + dependencies: + "@babel/parser": ^7.20.7 + "@babel/types": ^7.20.7 + "@types/babel__generator": "*" + "@types/babel__template": "*" + "@types/babel__traverse": "*" + checksum: 49b601a0a7637f1f387442c8156bd086cfd10ff4b82b0e1994e73a6396643b5435366fb33d6b604eade8467cca594ef97adcbc412aede90bb112ebe88d0ad6df + languageName: node + linkType: hard + +"@types/babel__generator@npm:*": + version: 7.6.4 + resolution: "@types/babel__generator@npm:7.6.4" + dependencies: + "@babel/types": ^7.0.0 + checksum: 20effbbb5f8a3a0211e95959d06ae70c097fb6191011b73b38fe86deebefad8e09ee014605e0fd3cdaedc73d158be555866810e9166e1f09e4cfd880b874dcb0 + languageName: node + linkType: hard + +"@types/babel__template@npm:*": + version: 7.4.1 + resolution: "@types/babel__template@npm:7.4.1" + dependencies: + "@babel/parser": ^7.1.0 + "@babel/types": ^7.0.0 + checksum: 649fe8b42c2876be1fd28c6ed9b276f78152d5904ec290b6c861d9ef324206e0a5c242e8305c421ac52ecf6358fa7e32ab7a692f55370484825c1df29b1596ee + languageName: node + linkType: hard + +"@types/babel__traverse@npm:*": + version: 7.18.3 + resolution: "@types/babel__traverse@npm:7.18.3" + dependencies: + "@babel/types": ^7.3.0 + checksum: d20953338b2f012ab7750932ece0a78e7d1645b0a6ff42d49be90f55e9998085da1374a9786a7da252df89555c6586695ba4d1d4b4e88ab2b9f306bcd35e00d3 + languageName: node + linkType: hard + +"@types/body-parser@npm:*": + version: 1.19.2 + resolution: "@types/body-parser@npm:1.19.2" + dependencies: + "@types/connect": "*" + "@types/node": "*" + checksum: e17840c7d747a549f00aebe72c89313d09fbc4b632b949b2470c5cb3b1cb73863901ae84d9335b567a79ec5efcfb8a28ff8e3f36bc8748a9686756b6d5681f40 + languageName: node + linkType: hard + +"@types/connect@npm:*": + version: 3.4.35 + resolution: "@types/connect@npm:3.4.35" + dependencies: + "@types/node": "*" + checksum: fe81351470f2d3165e8b12ce33542eef89ea893e36dd62e8f7d72566dfb7e448376ae962f9f3ea888547ce8b55a40020ca0e01d637fab5d99567673084542641 + languageName: node + linkType: hard + "@types/eslint-scope@npm:^3.7.3": version: 3.7.4 resolution: "@types/eslint-scope@npm:3.7.4" @@ -3150,6 +3187,29 @@ __metadata: languageName: node linkType: hard +"@types/express-serve-static-core@npm:^4.17.33": + version: 4.17.33 + resolution: "@types/express-serve-static-core@npm:4.17.33" + dependencies: + "@types/node": "*" + "@types/qs": "*" + "@types/range-parser": "*" + checksum: dce580d16b85f207445af9d4053d66942b27d0c72e86153089fa00feee3e96ae336b7bedb31ed4eea9e553c99d6dd356ed6e0928f135375d9f862a1a8015adf2 + languageName: node + linkType: hard + +"@types/express@npm:^4.7.0": + version: 4.17.17 + resolution: "@types/express@npm:4.17.17" + dependencies: + "@types/body-parser": "*" + "@types/express-serve-static-core": ^4.17.33 + "@types/qs": "*" + "@types/serve-static": "*" + checksum: 0196dacc275ac3ce89d7364885cb08e7fb61f53ca101f65886dbf1daf9b7eb05c0943e2e4bbd01b0cc5e50f37e0eea7e4cbe97d0304094411ac73e1b7998f4da + languageName: node + linkType: hard + "@types/glob@npm:*, @types/glob@npm:^7.1.1": version: 7.2.0 resolution: "@types/glob@npm:7.2.0" @@ -3220,6 +3280,13 @@ __metadata: languageName: node linkType: hard +"@types/mime@npm:*": + version: 3.0.1 + resolution: "@types/mime@npm:3.0.1" + checksum: 4040fac73fd0cea2460e29b348c1a6173da747f3a87da0dbce80dd7a9355a3d0e51d6d9a401654f3e5550620e3718b5a899b2ec1debf18424e298a2c605346e7 + languageName: node + linkType: hard + "@types/minimatch@npm:*": version: 3.0.5 resolution: "@types/minimatch@npm:3.0.5" @@ -3286,13 +3353,30 @@ __metadata: languageName: node linkType: hard -"@types/qs@npm:^6.9.5": +"@types/qs@npm:*, @types/qs@npm:^6.9.5": version: 6.9.7 resolution: "@types/qs@npm:6.9.7" checksum: 7fd6f9c25053e9b5bb6bc9f9f76c1d89e6c04f7707a7ba0e44cc01f17ef5284adb82f230f542c2d5557d69407c9a40f0f3515e8319afd14e1e16b5543ac6cdba languageName: node linkType: hard +"@types/range-parser@npm:*": + version: 1.2.4 + resolution: "@types/range-parser@npm:1.2.4" + checksum: b7c0dfd5080a989d6c8bb0b6750fc0933d9acabeb476da6fe71d8bdf1ab65e37c136169d84148034802f48378ab94e3c37bb4ef7656b2bec2cb9c0f8d4146a95 + languageName: node + linkType: hard + +"@types/serve-static@npm:*": + version: 1.15.1 + resolution: "@types/serve-static@npm:1.15.1" + dependencies: + "@types/mime": "*" + "@types/node": "*" + checksum: 2e078bdc1e458c7dfe69e9faa83cc69194b8896cce57cb745016580543c7ab5af07fdaa8ac1765eb79524208c81017546f66056f44d1204f812d72810613de36 + languageName: node + linkType: hard + "@types/source-list-map@npm:*": version: 0.1.2 resolution: "@types/source-list-map@npm:0.1.2" @@ -5203,6 +5287,13 @@ __metadata: languageName: node linkType: hard +"dequal@npm:^2.0.2": + version: 2.0.3 + resolution: "dequal@npm:2.0.3" + checksum: 8679b850e1a3d0ebbc46ee780d5df7b478c23f335887464023a631d1b9af051ad4a6595a44220f9ff8ff95a8ddccf019b5ad778a976fd7bbf77383d36f412f90 + languageName: node + linkType: hard + "destroy@npm:1.2.0": version: 1.2.0 resolution: "destroy@npm:1.2.0" @@ -5902,6 +5993,16 @@ __metadata: languageName: node linkType: hard +"file-system-cache@npm:^2.0.0": + version: 2.0.2 + resolution: "file-system-cache@npm:2.0.2" + dependencies: + fs-extra: ^11.1.0 + ramda: ^0.28.0 + checksum: ac4f9065132ac4593dbfb7c8fc4683cccc0f58823279763690fb3fca859cc5e6b4446c846af718354059695fa90db316be4ce19e16578bbb0feab4a9159e9fbc + languageName: node + linkType: hard + "fill-range@npm:^4.0.0": version: 4.0.0 resolution: "fill-range@npm:4.0.0" @@ -6131,6 +6232,17 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:^11.1.0": + version: 11.1.1 + resolution: "fs-extra@npm:11.1.1" + dependencies: + graceful-fs: ^4.2.0 + jsonfile: ^6.0.1 + universalify: ^2.0.0 + checksum: fb883c68245b2d777fbc1f2082c9efb084eaa2bbf9fddaa366130d196c03608eebef7fb490541276429ee1ca99f317e2d73e96f5ca0999eefedf5a624ae1edfd + languageName: node + linkType: hard + "fs-extra@npm:^9.0.0, fs-extra@npm:^9.0.1": version: 9.1.0 resolution: "fs-extra@npm:9.1.0" @@ -10655,7 +10767,7 @@ __metadata: "@storybook/manager-webpack5": ^6.5.9 "@storybook/node-logger": ^6.5.9 "@storybook/react": ^6.5.9 - "@storybook/testing-library": ^0.0.13 + "@storybook/testing-library": ^0.1.0 "@swc/core": ^1.3.9 react: ^17.0.2 react-dom: ^17.0.2 @@ -10954,6 +11066,15 @@ __metadata: languageName: node linkType: hard +"telejson@npm:^7.0.3": + version: 7.1.0 + resolution: "telejson@npm:7.1.0" + dependencies: + memoizerific: ^1.11.3 + checksum: 8000e43dc862a87ab1ca342a2635641923d55c2585f85ea8c7c60293681d6f920e8b9570cc12d90ecef286f065c176da5f769f42f4828ba18a626627bed1ac07 + languageName: node + linkType: hard + "terser-webpack-plugin@npm:^4.2.3": version: 4.2.3 resolution: "terser-webpack-plugin@npm:4.2.3" @@ -11183,6 +11304,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^2.19.0": + version: 2.19.0 + resolution: "type-fest@npm:2.19.0" + checksum: a4ef07ece297c9fba78fc1bd6d85dff4472fe043ede98bd4710d2615d15776902b595abf62bd78339ed6278f021235fb28a96361f8be86ed754f778973a0d278 + languageName: node + linkType: hard + "type-is@npm:~1.6.18": version: 1.6.18 resolution: "type-is@npm:1.6.18" From 698c23ee289e4a1b6ef04ba4c3302e924dbb8d1c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 5 Apr 2023 10:20:19 +0200 Subject: [PATCH 121/247] Update docs/plugins/backend-plugin.md Signed-off-by: Patrik Oldsberg --- docs/plugins/backend-plugin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/backend-plugin.md b/docs/plugins/backend-plugin.md index 897a66ae79..73b8f9027a 100644 --- a/docs/plugins/backend-plugin.md +++ b/docs/plugins/backend-plugin.md @@ -187,7 +187,7 @@ export async function createRouter( const { identity } = options; router.post('/example', async (req, res) => { - const identity = await identity.getIdentity({ request: req }); + const userIdentity = await identity.getIdentity({ request: req }); ... }); ``` From 754be7c5106f2b5b28055b14ca5cbc1e44b66612 Mon Sep 17 00:00:00 2001 From: Matthew Clarke Date: Wed, 5 Apr 2023 04:27:00 -0400 Subject: [PATCH 122/247] refactor: kubernetes error detection (#17135) * feat: refactor kubenetes error detection to make way for proposed solutions Signed-off-by: Matthew Clarke * docs: api-docs Signed-off-by: Matthew Clarke * chore: changeset Signed-off-by: Matthew Clarke * refactor: use luxon Signed-off-by: Matthew Clarke --------- Signed-off-by: Matthew Clarke --- .changeset/calm-geese-obey.md | 7 + plugins/kubernetes/api-report.md | 24 +- plugins/kubernetes/package.json | 1 + .../ErrorReporting/ErrorReporting.tsx | 64 ++-- .../src/components/KubernetesContent.tsx | 5 +- .../__fixtures__/pod-crashing.json | 2 +- .../kubernetes/src/error-detection/common.ts | 54 +--- .../src/error-detection/deployments.ts | 38 ++- .../error-detection/error-detection.test.ts | 283 ++++++++++++++---- .../src/error-detection/error-detection.ts | 13 +- .../kubernetes/src/error-detection/hpas.ts | 52 ++-- .../kubernetes/src/error-detection/index.ts | 1 - .../kubernetes/src/error-detection/pods.ts | 163 ++++++---- .../kubernetes/src/error-detection/types.ts | 67 +++-- yarn.lock | 48 ++- 15 files changed, 504 insertions(+), 318 deletions(-) create mode 100644 .changeset/calm-geese-obey.md diff --git a/.changeset/calm-geese-obey.md b/.changeset/calm-geese-obey.md new file mode 100644 index 0000000000..b45295f8ff --- /dev/null +++ b/.changeset/calm-geese-obey.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-kubernetes': minor +--- + +refactor kubernetes error detection to make way for proposed solutions + +**BREAKING**: `DetectedError` now appears once per Kubernetes resource per error instead of for all resources which have that error, `namespace` and `name` fields are now in `sourceRef` object `message` is now a `string` instead of a `string[]`. `ErrorDetectableKind` has been removed. diff --git a/plugins/kubernetes/api-report.md b/plugins/kubernetes/api-report.md index 2cc2674a9e..ad1217e257 100644 --- a/plugins/kubernetes/api-report.md +++ b/plugins/kubernetes/api-report.md @@ -104,17 +104,21 @@ export interface DeploymentResources { // @public export interface DetectedError { // (undocumented) - cluster: string; + message: string; // (undocumented) - kind: ErrorDetectableKind; + occuranceCount: number; + // Warning: (ae-forgotten-export) The symbol "ProposedFix" needs to be exported by the entry point index.d.ts + // // (undocumented) - message: string[]; - // (undocumented) - names: string[]; - // (undocumented) - namespace: string; + proposedFix: ProposedFix[]; // (undocumented) severity: ErrorSeverity; + // Warning: (ae-forgotten-export) The symbol "ResourceRef" needs to be exported by the entry point index.d.ts + // + // (undocumented) + sourceRef: ResourceRef; + // (undocumented) + type: string; } // @public @@ -137,12 +141,6 @@ export type EntityKubernetesContentProps = { refreshIntervalMs?: number; }; -// @public -export type ErrorDetectableKind = - | 'Pod' - | 'Deployment' - | 'HorizontalPodAutoscaler'; - // Warning: (ae-forgotten-export) The symbol "ErrorPanelProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ErrorPanel" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 7b3b3d7b25..edff7348a4 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -47,6 +47,7 @@ "@types/react": "^16.13.1 || ^17.0.0", "cronstrue": "^2.2.0", "js-yaml": "^4.0.0", + "kubernetes-models": "^4.1.0", "lodash": "^4.17.21", "luxon": "^3.0.0", "react-use": "^17.2.4" diff --git a/plugins/kubernetes/src/components/ErrorReporting/ErrorReporting.tsx b/plugins/kubernetes/src/components/ErrorReporting/ErrorReporting.tsx index a99fc21902..367e791067 100644 --- a/plugins/kubernetes/src/components/ErrorReporting/ErrorReporting.tsx +++ b/plugins/kubernetes/src/components/ErrorReporting/ErrorReporting.tsx @@ -15,84 +15,64 @@ */ import * as React from 'react'; import { DetectedError, DetectedErrorsByCluster } from '../../error-detection'; -import { Chip } from '@material-ui/core'; import { Table, TableColumn } from '@backstage/core-components'; type ErrorReportingProps = { detectedErrors: DetectedErrorsByCluster; }; -const columns: TableColumn[] = [ +const columns: TableColumn[] = [ { title: 'cluster', width: '10%', - render: (detectedError: DetectedError) => detectedError.cluster, + render: (row: Row) => row.clusterName, }, { title: 'namespace', width: '10%', - render: (detectedError: DetectedError) => detectedError.namespace, + render: (row: Row) => row.error.sourceRef.namespace, }, { title: 'kind', width: '10%', - render: (detectedError: DetectedError) => detectedError.kind, + render: (row: Row) => row.error.sourceRef.kind, }, { title: 'name', width: '30%', - render: (detectedError: DetectedError) => { - const errorCount = detectedError.names.length; - - if (errorCount === 0) { - // This shouldn't happen - return null; - } - - const displayName = detectedError.names[0]; - - const otherErrorCount = errorCount - 1; - - return ( - <> - {displayName}{' '} - {otherErrorCount > 0 && ( - 1 ? 's' : '' - }`} - size="small" - /> - )} - - ); + render: (row: Row) => { + return <>{row.error.sourceRef.name} ; }, }, { title: 'messages', width: '40%', - render: (detectedError: DetectedError) => ( - <> - {detectedError.message.map((m, i) => ( -
{m}
- ))} - - ), + render: (row: Row) => row.error.message, }, ]; -const sortBySeverity = (a: DetectedError, b: DetectedError) => { - if (a.severity < b.severity) { +interface Row { + clusterName: string; + error: DetectedError; +} + +const sortBySeverity = (a: Row, b: Row) => { + if (a.error.severity < b.error.severity) { return 1; - } else if (b.severity < a.severity) { + } else if (b.error.severity < a.error.severity) { return -1; } return 0; }; export const ErrorReporting = ({ detectedErrors }: ErrorReportingProps) => { - const errors = Array.from(detectedErrors.values()) - .flat() + const errors = Array.from(detectedErrors.entries()) + .flatMap(([clusterName, resourceErrors]) => { + return resourceErrors.map(e => ({ + clusterName, + error: e, + })); + }) .sort(sortBySeverity); return ( diff --git a/plugins/kubernetes/src/components/KubernetesContent.tsx b/plugins/kubernetes/src/components/KubernetesContent.tsx index a42e9fbb0b..63aa52f1ba 100644 --- a/plugins/kubernetes/src/components/KubernetesContent.tsx +++ b/plugins/kubernetes/src/components/KubernetesContent.tsx @@ -114,9 +114,8 @@ export const KubernetesContent = ({ const podsWithErrors = new Set( detectedErrors .get(item.cluster.name) - ?.filter(de => de.kind === 'Pod') - .map(de => de.names) - .flat() ?? [], + ?.filter(de => de.sourceRef.kind === 'Pod') + .map(de => de.sourceRef.name), ); return ( diff --git a/plugins/kubernetes/src/error-detection/__fixtures__/pod-crashing.json b/plugins/kubernetes/src/error-detection/__fixtures__/pod-crashing.json index 79acd2900d..bff3347813 100644 --- a/plugins/kubernetes/src/error-detection/__fixtures__/pod-crashing.json +++ b/plugins/kubernetes/src/error-detection/__fixtures__/pod-crashing.json @@ -185,7 +185,7 @@ }, "name": "other-side-car", "ready": false, - "restartCount": 38, + "restartCount": 123, "started": false, "state": { "waiting": { diff --git a/plugins/kubernetes/src/error-detection/common.ts b/plugins/kubernetes/src/error-detection/common.ts index bc5fd2fc40..381f57b71a 100644 --- a/plugins/kubernetes/src/error-detection/common.ts +++ b/plugins/kubernetes/src/error-detection/common.ts @@ -14,59 +14,15 @@ * limitations under the License. */ -import { - DetectedError, - ErrorDetectable, - ErrorDetectableKind, - ErrorMapper, -} from './types'; +import { DetectedError, ErrorMapper } from './types'; // Run through the each error mapper for each object // returning a deduplicated (mostly) result -export const detectErrorsInObjects = ( +export const detectErrorsInObjects = ( objects: T[], - kind: ErrorDetectableKind, - clusterName: string, errorMappers: ErrorMapper[], ): DetectedError[] => { - // Build up a map of errors - // key: the joined message produced by an error - // value: the error - const errors = new Map(); - - for (const object of objects) { - for (const errorMapper of errorMappers) { - if (errorMapper.errorExists(object)) { - const message = errorMapper.messageAccessor(object); - - // TODO This is not perfect as errors with uuid/hashes/date/times will not be caught by this - const dedupKey = message.join(''); - - const value = errors.get(dedupKey); - - const name = object.metadata?.name ?? 'unknown'; - const namespace = object.metadata?.namespace ?? 'unknown'; - - if (value !== undefined) { - // This gets translated into the Chip "+5 others" - // in the ErrorReporting component - // but we need to keep the names so we can easily - // find which objects owns the error later - value.names.push(name); - errors.set(dedupKey, value); - } else { - errors.set(dedupKey, { - cluster: clusterName, - kind: kind, - names: [name], - message: message, - severity: errorMapper.severity, - namespace, - }); - } - } - } - } - - return Array.from(errors.values()); + return objects.flatMap(o => { + return errorMappers.flatMap(em => em.detectErrors(o)); + }); }; diff --git a/plugins/kubernetes/src/error-detection/deployments.ts b/plugins/kubernetes/src/error-detection/deployments.ts index 896cd231ff..7807110224 100644 --- a/plugins/kubernetes/src/error-detection/deployments.ts +++ b/plugins/kubernetes/src/error-detection/deployments.ts @@ -15,35 +15,33 @@ */ import { DetectedError, ErrorMapper } from './types'; -import { V1Deployment } from '@kubernetes/client-node'; +import { Deployment } from 'kubernetes-models/apps/v1'; import { detectErrorsInObjects } from './common'; -const deploymentErrorMappers: ErrorMapper[] = [ +const deploymentErrorMappers: ErrorMapper[] = [ { - // this is probably important - severity: 6, - errorExplanation: 'condition-message-present', - errorExists: deployment => { - return (deployment.status?.conditions ?? []) - .filter(c => c.status === 'False') - .some(c => c.message !== undefined); - }, - messageAccessor: deployment => { + detectErrors: deployment => { return (deployment.status?.conditions ?? []) .filter(c => c.status === 'False') .filter(c => c.message !== undefined) - .map(c => c.message ?? ''); + .map(c => ({ + type: 'condition-message-present', + message: c.message ?? '', + severity: 6, + proposedFix: [], // TODO next PR + sourceRef: { + name: deployment.metadata?.name ?? 'unknown hpa', + namespace: deployment.metadata?.namespace ?? 'unknown namespace', + kind: 'Deployment', + apiGroup: 'apps/v1', + }, + occuranceCount: 1, + })); }, }, ]; export const detectErrorsInDeployments = ( - deployments: V1Deployment[], - clusterName: string, + deployments: Deployment[], ): DetectedError[] => - detectErrorsInObjects( - deployments, - 'Deployment', - clusterName, - deploymentErrorMappers, - ); + detectErrorsInObjects(deployments, deploymentErrorMappers); diff --git a/plugins/kubernetes/src/error-detection/error-detection.test.ts b/plugins/kubernetes/src/error-detection/error-detection.test.ts index 2c6373f32d..e3b9e91f6b 100644 --- a/plugins/kubernetes/src/error-detection/error-detection.test.ts +++ b/plugins/kubernetes/src/error-detection/error-detection.test.ts @@ -147,51 +147,61 @@ describe('detectErrors', () => { const [err1, err2, err3, err4] = errors ?? []; expect(err1).toStrictEqual({ - cluster: 'cluster-a', - kind: 'Pod', - message: [ - 'container=other-side-car restarted 38 times', - 'container=side-car restarted 38 times', - ], - names: ['dice-roller-canary-7d64cd756c-55rfq'], - namespace: 'default', + sourceRef: { + apiGroup: 'v1', + kind: 'Pod', + name: 'dice-roller-canary-7d64cd756c-55rfq', + namespace: 'default', + }, + message: + 'back-off 5m0s restarting failed container=other-side-car pod=dice-roller-canary-7d64cd756c-55rfq_default(65ad28e3-5d51-4b4b-9bf8-4cb069803034)', severity: 4, + occuranceCount: 1, + proposedFix: [], + type: 'container-waiting', }); expect(err2).toStrictEqual({ - cluster: 'cluster-a', - kind: 'Pod', - message: [ - 'containers with unready status: [side-car other-side-car]', - 'containers with unready status: [side-car other-side-car]', - ], - names: ['dice-roller-canary-7d64cd756c-55rfq'], - namespace: 'default', - severity: 5, + sourceRef: { + apiGroup: 'v1', + kind: 'Pod', + name: 'dice-roller-canary-7d64cd756c-55rfq', + namespace: 'default', + }, + message: + 'back-off 5m0s restarting failed container=side-car pod=dice-roller-canary-7d64cd756c-55rfq_default(65ad28e3-5d51-4b4b-9bf8-4cb069803034)', + severity: 4, + occuranceCount: 1, + proposedFix: [], + type: 'container-waiting', }); expect(err3).toStrictEqual({ - cluster: 'cluster-a', - kind: 'Pod', - message: [ - 'back-off 5m0s restarting failed container=other-side-car pod=dice-roller-canary-7d64cd756c-55rfq_default(65ad28e3-5d51-4b4b-9bf8-4cb069803034)', - 'back-off 5m0s restarting failed container=side-car pod=dice-roller-canary-7d64cd756c-55rfq_default(65ad28e3-5d51-4b4b-9bf8-4cb069803034)', - ], - names: ['dice-roller-canary-7d64cd756c-55rfq'], - namespace: 'default', - severity: 6, + sourceRef: { + apiGroup: 'v1', + kind: 'Pod', + name: 'dice-roller-canary-7d64cd756c-55rfq', + namespace: 'default', + }, + message: 'container=other-side-car restarted 123 times', + severity: 4, + occuranceCount: 123, + proposedFix: [], + type: 'containers-restarting', }); expect(err4).toStrictEqual({ - cluster: 'cluster-a', - kind: 'Pod', - message: [ - 'container=other-side-car exited with error code (1)', - 'container=side-car exited with error code (1)', - ], - names: ['dice-roller-canary-7d64cd756c-55rfq'], - namespace: 'default', + sourceRef: { + apiGroup: 'v1', + kind: 'Pod', + name: 'dice-roller-canary-7d64cd756c-55rfq', + namespace: 'default', + }, + message: 'container=side-car restarted 38 times', severity: 4, + occuranceCount: 38, + proposedFix: [], + type: 'containers-restarting', }); }); it('should detect errors in pod with missing Config Map', () => { @@ -202,29 +212,22 @@ describe('detectErrors', () => { const errors = result.get(CLUSTER_NAME); expect(errors).toBeDefined(); - expect(errors).toHaveLength(2); + expect(errors).toHaveLength(1); - const [err1, err2] = errors ?? []; + const [err1] = errors ?? []; expect(err1).toStrictEqual({ - cluster: 'cluster-a', - kind: 'Pod', - message: [ - 'containers with unready status: [nginx]', - 'containers with unready status: [nginx]', - ], - names: ['dice-roller-bad-cm-855bf85464-mg6xb'], - namespace: 'default', - severity: 5, - }); - - expect(err2).toStrictEqual({ - cluster: 'cluster-a', - kind: 'Pod', - message: ['configmap "some-cm" not found'], - names: ['dice-roller-bad-cm-855bf85464-mg6xb'], - namespace: 'default', - severity: 6, + message: 'configmap "some-cm" not found', + occuranceCount: 1, + proposedFix: [], + severity: 4, + sourceRef: { + apiGroup: 'v1', + kind: 'Pod', + name: 'dice-roller-bad-cm-855bf85464-mg6xb', + namespace: 'default', + }, + type: 'container-waiting', }); }); it('should detect no errors in healthy deployment', () => { @@ -250,12 +253,17 @@ describe('detectErrors', () => { const [err1] = errors ?? []; expect(err1).toStrictEqual({ - cluster: 'cluster-a', - kind: 'Deployment', - message: ['Deployment does not have minimum availability.'], - names: ['dice-roller-canary'], - namespace: 'default', + sourceRef: { + apiGroup: 'apps/v1', + kind: 'Deployment', + name: 'dice-roller-canary', + namespace: 'default', + }, + message: 'Deployment does not have minimum availability.', severity: 6, + occuranceCount: 1, + proposedFix: [], + type: 'condition-message-present', }); }); it('should detect no errors in healthy hpa', () => { @@ -281,14 +289,159 @@ describe('detectErrors', () => { const [err1] = errors ?? []; expect(err1).toStrictEqual({ - cluster: 'cluster-a', - kind: 'HorizontalPodAutoscaler', - message: [ + sourceRef: { + apiGroup: 'autoscaling/v1', + kind: 'HorizontalPodAutoscaler', + name: 'dice-roller', + namespace: 'default', + }, + message: 'Current number of replicas (10) is equal to the configured max number of replicas (10)', - ], - names: ['dice-roller'], - namespace: 'default', severity: 8, + occuranceCount: 1, + proposedFix: [], + type: 'hpa-max-current-replicas', + }); + }); + it('pending pod is not an error', async () => { + const expiredReadiness = new Date(); + expiredReadiness.setFullYear(expiredReadiness.getFullYear() - 1); + const result = await detectErrors( + onePod({ + spec: { + containers: [ + { + name: 'some-container', + readinessProbe: { + initialDelaySeconds: 20000, + failureThreshold: 5, + periodSeconds: 5, + }, + }, + ], + }, + status: { + containerStatuses: [ + { + name: 'some-container', + image: 'some-image', + imageID: 'some-image-id', + restartCount: 0, + containerID: 'running-container', + ready: false, + state: { + running: { + startedAt: new Date().toISOString() as any, + }, + }, + }, + ], + message: 'Container running', + }, + }), + ); + + const errors = result.get(CLUSTER_NAME); + + expect(errors).toBeDefined(); + expect(errors).toHaveLength(0); + }); + it('no probe pod has no errors', async () => { + const expiredReadiness = new Date(); + expiredReadiness.setFullYear(expiredReadiness.getFullYear() - 1); + const result = await detectErrors( + onePod({ + spec: { + containers: [ + { + name: 'some-container', + }, + ], + }, + status: { + containerStatuses: [ + { + name: 'some-container', + image: 'some-image', + imageID: 'some-image-id', + restartCount: 0, + containerID: 'running-container', + ready: false, + state: { + running: { + startedAt: new Date().toISOString() as any, + }, + }, + }, + ], + message: 'Container running', + }, + }), + ); + + const errors = result.get(CLUSTER_NAME); + + expect(errors).toBeDefined(); + expect(errors).toHaveLength(0); + }); + it('readiness probe failure results in error', async () => { + const expiredReadiness = new Date(); + expiredReadiness.setFullYear(expiredReadiness.getFullYear() - 1); + const result = await detectErrors( + onePod({ + spec: { + containers: [ + { + name: 'some-container', + readinessProbe: { + initialDelaySeconds: 20, + failureThreshold: 5, + periodSeconds: 5, + }, + }, + ], + }, + status: { + containerStatuses: [ + { + name: 'some-container', + image: 'some-image', + imageID: 'some-image-id', + restartCount: 0, + containerID: 'running-container', + ready: false, + state: { + running: { + startedAt: expiredReadiness.toISOString() as any, + }, + }, + }, + ], + message: 'Container running', + }, + }), + ); + + const errors = result.get(CLUSTER_NAME); + + expect(errors).toBeDefined(); + expect(errors).toHaveLength(1); + + const [err1] = errors ?? []; + + expect(err1).toStrictEqual({ + message: + 'The container some-container failed to start properly, but is not crashing', + occuranceCount: 1, + proposedFix: [], + severity: 4, + sourceRef: { + apiGroup: 'v1', + kind: 'Pod', + name: 'unknown pod', + namespace: 'unknown namespace', + }, + type: 'readiness-probe-taking-too-long', }); }); }); diff --git a/plugins/kubernetes/src/error-detection/error-detection.ts b/plugins/kubernetes/src/error-detection/error-detection.ts index fddd4d838a..c33de18b80 100644 --- a/plugins/kubernetes/src/error-detection/error-detection.ts +++ b/plugins/kubernetes/src/error-detection/error-detection.ts @@ -20,6 +20,9 @@ import { groupResponses } from '../utils/response'; import { detectErrorsInPods } from './pods'; import { detectErrorsInDeployments } from './deployments'; import { detectErrorsInHpa } from './hpas'; +import { Deployment } from 'kubernetes-models/apps/v1'; +import { HorizontalPodAutoscaler } from 'kubernetes-models/autoscaling/v1'; +import { Pod } from 'kubernetes-models/v1'; /** * For each cluster try to find errors in each of the object types provided @@ -38,20 +41,16 @@ export const detectErrors = ( const groupedResponses = groupResponses(clusterResponse.resources); clusterErrors = clusterErrors.concat( - detectErrorsInPods(groupedResponses.pods, clusterResponse.cluster.name), + detectErrorsInPods(groupedResponses.pods as Pod[]), ); clusterErrors = clusterErrors.concat( - detectErrorsInDeployments( - groupedResponses.deployments, - clusterResponse.cluster.name, - ), + detectErrorsInDeployments(groupedResponses.deployments as Deployment[]), ); clusterErrors = clusterErrors.concat( detectErrorsInHpa( - groupedResponses.horizontalPodAutoscalers, - clusterResponse.cluster.name, + groupedResponses.horizontalPodAutoscalers as HorizontalPodAutoscaler[], ), ); diff --git a/plugins/kubernetes/src/error-detection/hpas.ts b/plugins/kubernetes/src/error-detection/hpas.ts index dd4674a7d0..2dc5e30ce6 100644 --- a/plugins/kubernetes/src/error-detection/hpas.ts +++ b/plugins/kubernetes/src/error-detection/hpas.ts @@ -14,37 +14,39 @@ * limitations under the License. */ -import { V1HorizontalPodAutoscaler } from '@kubernetes/client-node'; +import { HorizontalPodAutoscaler } from 'kubernetes-models/autoscaling/v1'; import { DetectedError, ErrorMapper } from './types'; import { detectErrorsInObjects } from './common'; -const hpaErrorMappers: ErrorMapper[] = [ +const hpaErrorMappers: ErrorMapper[] = [ { - // this is probably important - severity: 8, - errorExplanation: 'hpa-max-current-replicas', - errorExists: hpa => { - return (hpa.spec?.maxReplicas ?? -1) === hpa.status?.currentReplicas; - }, - messageAccessor: hpa => { - return [ - `Current number of replicas (${ - hpa.status?.currentReplicas - }) is equal to the configured max number of replicas (${ - hpa.spec?.maxReplicas ?? -1 - })`, - ]; + detectErrors: hpa => { + if ((hpa.spec?.maxReplicas ?? -1) === hpa.status?.currentReplicas) { + return [ + { + type: 'hpa-max-current-replicas', + message: `Current number of replicas (${ + hpa.status?.currentReplicas + }) is equal to the configured max number of replicas (${ + hpa.spec?.maxReplicas ?? -1 + })`, + severity: 8, + proposedFix: [], // TODO next PR + sourceRef: { + name: hpa.metadata?.name ?? 'unknown hpa', + namespace: hpa.metadata?.namespace ?? 'unknown namespace', + kind: 'HorizontalPodAutoscaler', + apiGroup: 'autoscaling/v1', + }, + occuranceCount: 1, + }, + ]; + } + return []; }, }, ]; export const detectErrorsInHpa = ( - hpas: V1HorizontalPodAutoscaler[], - clusterName: string, -): DetectedError[] => - detectErrorsInObjects( - hpas, - 'HorizontalPodAutoscaler', - clusterName, - hpaErrorMappers, - ); + hpas: HorizontalPodAutoscaler[], +): DetectedError[] => detectErrorsInObjects(hpas, hpaErrorMappers); diff --git a/plugins/kubernetes/src/error-detection/index.ts b/plugins/kubernetes/src/error-detection/index.ts index ced03066a7..67bfb70f3b 100644 --- a/plugins/kubernetes/src/error-detection/index.ts +++ b/plugins/kubernetes/src/error-detection/index.ts @@ -15,7 +15,6 @@ */ export type { - ErrorDetectableKind, DetectedError, DetectedErrorsByCluster, ErrorSeverity, diff --git a/plugins/kubernetes/src/error-detection/pods.ts b/plugins/kubernetes/src/error-detection/pods.ts index 1ad6e3485f..a85276eaa2 100644 --- a/plugins/kubernetes/src/error-detection/pods.ts +++ b/plugins/kubernetes/src/error-detection/pods.ts @@ -14,82 +14,121 @@ * limitations under the License. */ -import { V1Pod } from '@kubernetes/client-node'; -import { totalRestarts } from '../utils/pod'; +import { Pod, IContainerStatus, IContainer } from 'kubernetes-models/v1'; import { DetectedError, ErrorMapper } from './types'; import { detectErrorsInObjects } from './common'; +import lodash from 'lodash'; +import { DateTime } from 'luxon'; -const podErrorMappers: ErrorMapper[] = [ +function isPodReadinessProbeUnready({ + container, + containerStatus, +}: ContainerSpecAndStatus): boolean { + if ( + containerStatus.ready || + containerStatus.state?.running?.startedAt === undefined || + !container.readinessProbe + ) { + return false; + } + const startDateTime = DateTime.fromISO( + containerStatus.state?.running?.startedAt, + ) + // Add initial delay + .plus({ + seconds: container.readinessProbe?.initialDelaySeconds ?? 0, + }) + // Add failure threshold + .plus({ + seconds: + (container.readinessProbe?.periodSeconds ?? 0) * + (container.readinessProbe?.failureThreshold ?? 0), + }); + return startDateTime < DateTime.now(); +} + +interface ContainerSpecAndStatus { + container: IContainer; + containerStatus: IContainerStatus; +} + +const podToContainerSpecsAndStatuses = (pod: Pod): ContainerSpecAndStatus[] => { + const specs = lodash.groupBy(pod.spec?.containers ?? [], value => value.name); + + const result: ContainerSpecAndStatus[] = []; + + for (const cs of pod.status?.containerStatuses ?? []) { + const spec = specs[cs.name]; + if (spec.length > 0) { + result.push({ + container: spec[0], + containerStatus: cs, + }); + } + } + + return result; +}; + +const podErrorMappers: ErrorMapper[] = [ { - severity: 5, - errorExplanation: 'status-message', - errorExists: pod => { - return pod.status?.message !== undefined; - }, - messageAccessor: pod => { - return [pod.status?.message ?? '']; + detectErrors: pod => { + return podToContainerSpecsAndStatuses(pod) + .filter(isPodReadinessProbeUnready) + .map(cs => ({ + type: 'readiness-probe-taking-too-long', + message: `The container ${cs.container.name} failed to start properly, but is not crashing`, + severity: 4, + proposedFix: [], // TODO next PR + sourceRef: { + name: pod.metadata?.name ?? 'unknown pod', + namespace: pod.metadata?.namespace ?? 'unknown namespace', + kind: 'Pod', + apiGroup: 'v1', + }, + occuranceCount: 1, + })); }, }, { - severity: 4, - errorExplanation: 'containers-restarting', - errorExists: pod => { - // TODO magic number - return totalRestarts(pod) > 3; - }, - messageAccessor: pod => { - return (pod.status?.containerStatuses ?? []) - .filter(cs => cs.restartCount > 0) - .map(cs => `container=${cs.name} restarted ${cs.restartCount} times`); - }, - }, - { - severity: 5, - errorExplanation: 'condition-message-present', - errorExists: pod => { - return (pod.status?.conditions ?? []).some(c => c.message !== undefined); - }, - messageAccessor: pod => { - return (pod.status?.conditions ?? []) - .filter(c => c.message !== undefined) - .map(c => c.message ?? ''); - }, - }, - { - severity: 6, - errorExplanation: 'container-waiting', - errorExists: pod => { - return (pod.status?.containerStatuses ?? []).some( - cs => cs.state?.waiting?.message !== undefined, - ); - }, - messageAccessor: pod => { + detectErrors: pod => { return (pod.status?.containerStatuses ?? []) .filter(cs => cs.state?.waiting?.message !== undefined) - .map(cs => cs.state?.waiting?.message ?? ''); + .map(cs => ({ + type: 'container-waiting', + message: cs.state?.waiting?.message ?? 'container waiting', + severity: 4, + proposedFix: [], // TODO next PR + sourceRef: { + name: pod.metadata?.name ?? 'unknown pod', + namespace: pod.metadata?.namespace ?? 'unknown namespace', + kind: 'Pod', + apiGroup: 'v1', + }, + occuranceCount: 1, + })); }, }, { - severity: 4, - errorExplanation: 'container-last-state-error', - errorExists: pod => { - return (pod.status?.containerStatuses ?? []).some( - cs => (cs.lastState?.terminated?.reason ?? '') === 'Error', - ); - }, - messageAccessor: pod => { + detectErrors: pod => { return (pod.status?.containerStatuses ?? []) - .filter(cs => (cs.lastState?.terminated?.reason ?? '') === 'Error') - .map( - cs => - `container=${cs.name} exited with error code (${cs.lastState?.terminated?.exitCode})`, - ); + .filter(cs => cs.restartCount > 0) + .map(cs => ({ + type: 'containers-restarting', + message: `container=${cs.name} restarted ${cs.restartCount} times`, + severity: 4, + proposedFix: [], // TODO next PR + sourceRef: { + name: pod.metadata?.name ?? 'unknown pod', + namespace: pod.metadata?.namespace ?? 'unknown namespace', + kind: 'Pod', + apiGroup: 'v1', + }, + occuranceCount: cs.restartCount, + })); }, }, ]; -export const detectErrorsInPods = ( - pods: V1Pod[], - clusterName: string, -): DetectedError[] => - detectErrorsInObjects(pods, 'Pod', clusterName, podErrorMappers); +export const detectErrorsInPods = (pods: Pod[]): DetectedError[] => + detectErrorsInObjects(pods, podErrorMappers); diff --git a/plugins/kubernetes/src/error-detection/types.ts b/plugins/kubernetes/src/error-detection/types.ts index 4bc7ae164d..3698d9bc68 100644 --- a/plugins/kubernetes/src/error-detection/types.ts +++ b/plugins/kubernetes/src/error-detection/types.ts @@ -14,13 +14,6 @@ * limitations under the License. */ -// Higher is more sever, but it's relative -import { - V1Deployment, - V1HorizontalPodAutoscaler, - V1Pod, -} from '@kubernetes/client-node'; - /** * Severity of the error, where 10 is critical and 0 is very low. * @@ -28,18 +21,6 @@ import { */ export type ErrorSeverity = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; -export type ErrorDetectable = V1Pod | V1Deployment | V1HorizontalPodAutoscaler; - -/** - * Kubernetes kinds that errors might be reported by the plugin - * - * @public - */ -export type ErrorDetectableKind = - | 'Pod' - | 'Deployment' - | 'HorizontalPodAutoscaler'; - /** * A list of errors keyed by Cluster name * @@ -47,23 +28,51 @@ export type ErrorDetectableKind = */ export type DetectedErrorsByCluster = Map; +export interface ResourceRef { + name: string; + namespace: string; + kind: string; + apiGroup: string; +} + /** * Represents an error found on a Kubernetes object * * @public */ export interface DetectedError { + type: string; severity: ErrorSeverity; - cluster: string; - namespace: string; - kind: ErrorDetectableKind; - names: string[]; - message: string[]; + message: string; + proposedFix: ProposedFix[]; + sourceRef: ResourceRef; + occuranceCount: number; } -export interface ErrorMapper { - severity: ErrorSeverity; - errorExplanation: string; - errorExists: (object: T) => boolean; - messageAccessor: (object: T) => string[]; +type ProposedFix = LogSolution | DocsSolution | EventsSolution; + +interface ProposedFixBase { + errorType: string; + rootCauseExplanation: string; + possibleFixes: string[]; +} + +export interface LogSolution extends ProposedFixBase { + type: 'logs'; + container: string; +} + +export interface DocsSolution extends ProposedFixBase { + type: 'docs'; + docsLink: string; +} + +export interface EventsSolution extends ProposedFixBase { + type: 'events'; + docsLink: string; + podName: string; +} + +export interface ErrorMapper { + detectErrors: (resource: T) => DetectedError[]; } diff --git a/yarn.lock b/yarn.lock index b893d378de..ed252fd849 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7173,6 +7173,7 @@ __metadata: cronstrue: ^2.2.0 cross-fetch: ^3.1.5 js-yaml: ^4.0.0 + kubernetes-models: ^4.1.0 lodash: ^4.17.21 luxon: ^3.0.0 msw: ^1.0.0 @@ -11926,6 +11927,39 @@ __metadata: languageName: node linkType: hard +"@kubernetes-models/apimachinery@npm:^1.1.0": + version: 1.1.0 + resolution: "@kubernetes-models/apimachinery@npm:1.1.0" + dependencies: + "@kubernetes-models/base": ^4.0.0 + "@kubernetes-models/validate": ^3.0.0 + tslib: ^2.4.0 + checksum: 71fc127a8e50c3686e32d9f9df94a307ae4d69995d6e82ea50a7eb9a5ce6a18e9950700ad0eb445e1451c997aabb22e2594ab1037c11faa9c0051b51bab569c6 + languageName: node + linkType: hard + +"@kubernetes-models/base@npm:^4.0.0": + version: 4.0.0 + resolution: "@kubernetes-models/base@npm:4.0.0" + dependencies: + "@kubernetes-models/validate": ^3.0.0 + is-plain-object: ^5.0.0 + tslib: ^2.4.0 + checksum: 3c44f2220e119c24b874d6d674f38f9796f0ab682ba0cff1516d39549a6536055348bcee6c3c5257ebb8d4d6c8d1865d84e519d8f8948e6dd29eb1d72f3830cb + languageName: node + linkType: hard + +"@kubernetes-models/validate@npm:^3.0.0": + version: 3.0.0 + resolution: "@kubernetes-models/validate@npm:3.0.0" + dependencies: + ajv: ^8.11.0 + ajv-formats: ^2.1.1 + tslib: ^2.4.0 + checksum: b53b7181ddaad7e8faf6ceac228cb23df239ef5da3784887f3378dd3a7f89e1936b7b1884ab123255a905627311bab4302a06668fba35d26152e69fbf208a855 + languageName: node + linkType: hard + "@kubernetes/client-node@npm:0.18.1": version: 0.18.1 resolution: "@kubernetes/client-node@npm:0.18.1" @@ -17602,7 +17636,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^8.0.0, ajv@npm:^8.10.0, ajv@npm:^8.12.0, ajv@npm:^8.8.0": +"ajv@npm:^8.0.0, ajv@npm:^8.10.0, ajv@npm:^8.11.0, ajv@npm:^8.12.0, ajv@npm:^8.8.0": version: 8.12.0 resolution: "ajv@npm:8.12.0" dependencies: @@ -28790,6 +28824,18 @@ __metadata: languageName: node linkType: hard +"kubernetes-models@npm:^4.1.0": + version: 4.1.0 + resolution: "kubernetes-models@npm:4.1.0" + dependencies: + "@kubernetes-models/apimachinery": ^1.1.0 + "@kubernetes-models/base": ^4.0.0 + "@kubernetes-models/validate": ^3.0.0 + tslib: ^2.4.0 + checksum: 1c3195a78ca422a6436f9c4143288a4d29f53b75ff597d72f44cc7f274714daab40a5bb99dd13e02edf3ce11a41f1df34d3d0bb1930b6401cc21c44666ad2b6b + languageName: node + linkType: hard + "kuler@npm:^2.0.0": version: 2.0.0 resolution: "kuler@npm:2.0.0" From f03104f0d9b9c6f41df3acb63bd2af088ce1302d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 08:35:04 +0000 Subject: [PATCH 123/247] chore(deps): update dependency eslint-plugin-cypress to v2.13.2 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index ed252fd849..30157a92f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22941,13 +22941,13 @@ __metadata: linkType: hard "eslint-plugin-cypress@npm:^2.10.3": - version: 2.12.1 - resolution: "eslint-plugin-cypress@npm:2.12.1" + version: 2.13.2 + resolution: "eslint-plugin-cypress@npm:2.13.2" dependencies: globals: ^11.12.0 peerDependencies: eslint: ">= 3.2.1" - checksum: 1f1c36e149304e9a6f58e2292a761abad58274da33b3a48b24ad55ad20cbce3ac7467321f2b6fcb052f9563c89f67004de4766eba2e2bdbcb010a6a0666989cf + checksum: 81cc425cfced563cf50ad2553f887e20a39fe179275e95408c53989bcf8f65f0fac648b429d006130c84221edf99eada663a2c6419eb837bf81ece3d18998cf5 languageName: node linkType: hard From 8969784eb80d9a929164bd8622eda584933d7964 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Wed, 5 Apr 2023 09:44:05 +0100 Subject: [PATCH 124/247] fix example backend failing startup Signed-off-by: Brian Fletcher --- packages/backend/src/plugins/DemoEventBasedEntityProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts b/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts index b793816095..7a032198a3 100644 --- a/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts +++ b/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts @@ -37,9 +37,9 @@ export class DemoEventBasedEntityProvider topics: string[]; }) { const { eventBroker, logger, topics } = opts; - eventBroker.subscribe(this); this.logger = logger; this.topics = topics; + eventBroker.subscribe(this); } async onEvent(params: EventParams): Promise { From 4d64bb36070a95aa153d916b521630520e69b782 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 09:55:44 +0000 Subject: [PATCH 125/247] chore(deps): update graphqlcodegenerator monorepo to v3.3.0 Signed-off-by: Renovate Bot --- yarn.lock | 60 +++++++++++++++++++++++++++---------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/yarn.lock b/yarn.lock index 30157a92f5..3652a87b61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10603,14 +10603,14 @@ __metadata: linkType: hard "@graphql-codegen/cli@npm:^3.0.0": - version: 3.2.2 - resolution: "@graphql-codegen/cli@npm:3.2.2" + version: 3.3.0 + resolution: "@graphql-codegen/cli@npm:3.3.0" dependencies: "@babel/generator": ^7.18.13 "@babel/template": ^7.18.10 "@babel/types": ^7.18.13 "@graphql-codegen/core": ^3.1.0 - "@graphql-codegen/plugin-helpers": ^4.1.0 + "@graphql-codegen/plugin-helpers": ^4.2.0 "@graphql-tools/apollo-engine-loader": ^7.3.6 "@graphql-tools/code-file-loader": ^7.3.17 "@graphql-tools/git-loader": ^7.2.13 @@ -10648,7 +10648,7 @@ __metadata: graphql-code-generator: cjs/bin.js graphql-codegen: cjs/bin.js graphql-codegen-esm: esm/bin.js - checksum: b94284ac538a3504f96c7d507c140b7cfee30042209c4230ffc3068f05b6b27c75e1922b31c696363ab43e34e472ed194cc72996bbb10f59991cd2a4a35ff36e + checksum: 7c3ae7ba62cfb5576e6c1fc78f81a2c59d399415b9ef2ba55818af00aff2337a047a61b1cedb6e89e73dac30d9a00e57045ff6052245934bc4c5cf35c9762250 languageName: node linkType: hard @@ -10667,24 +10667,24 @@ __metadata: linkType: hard "@graphql-codegen/graphql-modules-preset@npm:^3.0.0": - version: 3.1.1 - resolution: "@graphql-codegen/graphql-modules-preset@npm:3.1.1" + version: 3.1.2 + resolution: "@graphql-codegen/graphql-modules-preset@npm:3.1.2" dependencies: - "@graphql-codegen/plugin-helpers": ^4.1.0 - "@graphql-codegen/visitor-plugin-common": 3.0.2 + "@graphql-codegen/plugin-helpers": ^4.2.0 + "@graphql-codegen/visitor-plugin-common": 3.1.0 "@graphql-tools/utils": ^9.0.0 change-case-all: 1.0.15 parse-filepath: ^1.0.2 tslib: ~2.5.0 peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - checksum: f437590afa33ac308c075d214b501404ae2f99888cac8071d05cc2b8c7ceaea5aef8f86bb60e7a9a45902126dc99bd20d77e951c0beef665aaa31ac3c2217e26 + checksum: d3b4e9fe169742ce7973466045793ff0823c690f0736ba22aef3d4578be498746df108726ccad0c190d1c45a33816ab4e010b9c6e9931ced06ef7489d85a3691 languageName: node linkType: hard -"@graphql-codegen/plugin-helpers@npm:^4.1.0": - version: 4.1.0 - resolution: "@graphql-codegen/plugin-helpers@npm:4.1.0" +"@graphql-codegen/plugin-helpers@npm:^4.1.0, @graphql-codegen/plugin-helpers@npm:^4.2.0": + version: 4.2.0 + resolution: "@graphql-codegen/plugin-helpers@npm:4.2.0" dependencies: "@graphql-tools/utils": ^9.0.0 change-case-all: 1.0.15 @@ -10694,7 +10694,7 @@ __metadata: tslib: ~2.5.0 peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - checksum: a4d89909d7a62bd7a93041b6073b1d46e912b42f7af1d15c44f7ed595ab1185b6c257cccf8272e0ae36ba22724578449d0e21524e8dbc21fb375da0620687868 + checksum: 5d26adc132026916db061d23b06fc2c329f372f19ecf56e39bd2b30082bff642f2030cd6dc0bad8d2d3ab781c1e145384e040310a479b83d85ec804b2983646d languageName: node linkType: hard @@ -10712,41 +10712,41 @@ __metadata: linkType: hard "@graphql-codegen/typescript-resolvers@npm:^3.0.0": - version: 3.1.1 - resolution: "@graphql-codegen/typescript-resolvers@npm:3.1.1" + version: 3.2.0 + resolution: "@graphql-codegen/typescript-resolvers@npm:3.2.0" dependencies: - "@graphql-codegen/plugin-helpers": ^4.1.0 - "@graphql-codegen/typescript": ^3.0.2 - "@graphql-codegen/visitor-plugin-common": 3.0.2 + "@graphql-codegen/plugin-helpers": ^4.2.0 + "@graphql-codegen/typescript": ^3.0.3 + "@graphql-codegen/visitor-plugin-common": 3.1.0 "@graphql-tools/utils": ^9.0.0 auto-bind: ~4.0.0 tslib: ~2.5.0 peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - checksum: 88faa1ca2335096aa9939101f3591007d9fa477836d969a3c622dbf67704505600387a2e9fbb416d61b5ef42dbd79a0d351003037c0756d8d0c7f32c7961e783 + checksum: d866b95775c95d1685e832a1d24df2a1e98feb67f35ef20a5d590c42b7314307b48399531b0cdc004e0be1062769ffe1e1a4757863ed1ca1f575dcd5e08b014d languageName: node linkType: hard -"@graphql-codegen/typescript@npm:^3.0.0, @graphql-codegen/typescript@npm:^3.0.2": - version: 3.0.2 - resolution: "@graphql-codegen/typescript@npm:3.0.2" +"@graphql-codegen/typescript@npm:^3.0.0, @graphql-codegen/typescript@npm:^3.0.3": + version: 3.0.3 + resolution: "@graphql-codegen/typescript@npm:3.0.3" dependencies: - "@graphql-codegen/plugin-helpers": ^4.1.0 + "@graphql-codegen/plugin-helpers": ^4.2.0 "@graphql-codegen/schema-ast": ^3.0.1 - "@graphql-codegen/visitor-plugin-common": 3.0.2 + "@graphql-codegen/visitor-plugin-common": 3.1.0 auto-bind: ~4.0.0 tslib: ~2.5.0 peerDependencies: graphql: ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - checksum: e92dc54804b6ad45fb2499b91cb89743e455a984684e2b1df1b1a8f479a0bbffa5c625be97ccb55874ac8b7316a820b546e9e312b22eda78012acd5c27594a28 + checksum: 5d292379b5f25f1df0dfdec62211f7e304a85910b6e30dcb28e89478dd15b565f05993913d9b8cf7f08b1e9166242f08e9cb68bd9688f486bd5eada7c6f82442 languageName: node linkType: hard -"@graphql-codegen/visitor-plugin-common@npm:3.0.2": - version: 3.0.2 - resolution: "@graphql-codegen/visitor-plugin-common@npm:3.0.2" +"@graphql-codegen/visitor-plugin-common@npm:3.1.0": + version: 3.1.0 + resolution: "@graphql-codegen/visitor-plugin-common@npm:3.1.0" dependencies: - "@graphql-codegen/plugin-helpers": ^4.1.0 + "@graphql-codegen/plugin-helpers": ^4.2.0 "@graphql-tools/optimize": ^1.3.0 "@graphql-tools/relay-operation-optimizer": ^6.5.0 "@graphql-tools/utils": ^9.0.0 @@ -10758,7 +10758,7 @@ __metadata: tslib: ~2.5.0 peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - checksum: c8f941df7f8304b722b492ceaf15dddcf33e3a69bc29b54970908ffa12b14d92276958005bd307648e0cdc55f9e243d0fb390862f73a17a26bd50f6484ac42d6 + checksum: 7532666c578680cda538d7b859ee7d5541113f3972f0f2396affa4dd6823a2e2d6f8b715c685912652b0906722dfafb4375c462aa3b4f991a12ec0a28f1b7ba7 languageName: node linkType: hard From 6a9009513367655dbc4123189b24fc76a9201e6b Mon Sep 17 00:00:00 2001 From: mingfu Date: Wed, 5 Apr 2023 17:55:14 +0800 Subject: [PATCH 126/247] chore: add common identify resolvers for `oidc` auth provider Signed-off-by: mingfu --- .changeset/silent-nails-bake.md | 5 +++++ plugins/auth-backend/api-report.md | 5 ++++- .../auth-backend/src/providers/oidc/provider.ts | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 .changeset/silent-nails-bake.md diff --git a/.changeset/silent-nails-bake.md b/.changeset/silent-nails-bake.md new file mode 100644 index 0000000000..1161686b34 --- /dev/null +++ b/.changeset/silent-nails-bake.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Add common identify resolvers for `oidc` auth provider. diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 7515083c09..8883c3ed97 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -641,7 +641,10 @@ export const providers: Readonly<{ } | undefined, ) => AuthProviderFactory; - resolvers: never; + resolvers: Readonly<{ + emailLocalPartMatchingUserEntityName: () => SignInResolver; + emailMatchingUserEntityProfileEmail: () => SignInResolver; + }>; }>; okta: Readonly<{ create: ( diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index bce8ba843b..a5196f0930 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -44,6 +44,10 @@ import { SignInResolver, } from '../types'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; +import { + commonByEmailLocalPartResolver, + commonByEmailResolver, +} from '../resolvers'; type PrivateInfo = { refreshToken?: string; @@ -255,4 +259,14 @@ export const oidc = createAuthProviderIntegration({ }); }); }, + resolvers: { + /** + * Looks up the user by matching their email local part to the entity name. + */ + emailLocalPartMatchingUserEntityName: () => commonByEmailLocalPartResolver, + /** + * Looks up the user by matching their email to the entity email. + */ + emailMatchingUserEntityProfileEmail: () => commonByEmailResolver, + }, }); From 046e04a7cf4076ff30873dac6202bde98f3db35b Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Tue, 10 Jan 2023 13:07:12 -0500 Subject: [PATCH 127/247] Translated all endpoints to the new format, seeking feedback. Signed-off-by: Aramis Sennyey --- plugins/catalog-backend/package.json | 2 + .../src/service/createRouter.ts | 301 +++++++++--------- yarn.lock | 187 ++++++++++- 3 files changed, 331 insertions(+), 159 deletions(-) diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index db9e1845fe..65a3232e69 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -65,11 +65,13 @@ "codeowners-utils": "^1.0.2", "core-js": "^3.6.5", "express": "^4.17.1", + "express-openapi": "^12.1.0", "express-promise-router": "^4.1.0", "fast-json-stable-stringify": "^2.1.0", "fs-extra": "10.1.0", "git-url-parse": "^13.0.0", "glob": "^7.1.6", + "js-yaml": "^4.1.0", "knex": "^2.0.0", "lodash": "^4.17.21", "luxon": "^3.0.0", diff --git a/plugins/catalog-backend/src/service/createRouter.ts b/plugins/catalog-backend/src/service/createRouter.ts index 849096c601..6b733a0a9a 100644 --- a/plugins/catalog-backend/src/service/createRouter.ts +++ b/plugins/catalog-backend/src/service/createRouter.ts @@ -23,7 +23,7 @@ import { stringifyEntityRef, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { NotFoundError, serializeError } from '@backstage/errors'; +import { InputError, NotFoundError, serializeError } from '@backstage/errors'; import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; @@ -50,6 +50,16 @@ import { locationInput, validateRequestBody, } from './util'; +import { initialize } from 'express-openapi'; +import yaml from 'js-yaml'; +import fs from 'fs'; +import path from 'path'; + +class ParsingError extends Error { + toString() { + return `ParsingError: ${this.message}`; + } +} /** * Options used by {@link createRouter}. @@ -94,26 +104,41 @@ export async function createRouter( logger.info('Catalog is running in readonly mode'); } - if (refreshService) { - router.post('/refresh', async (req, res) => { - const refreshOptions: RefreshOptions = req.body; - refreshOptions.authorizationToken = getBearerToken( - req.header('authorization'), - ); + const validateDependency = ( + dependency: any, + next: (req: express.Request, res: express.Response) => any, + ) => { + return (req: express.Request, res: express.Response) => { + if (!dependency) { + console.log('no dependency'); + throw new NotFoundError( + 'Dependency Error', + 'Dependency not set up for this endpoint.', + ); + } + return next(req, res); + }; + }; - await refreshService.refresh(refreshOptions); - res.status(200).end(); - }); - } + initialize({ + app: router as any, + // NOTE: If using yaml you can provide a path relative to process.cwd() e.g. + // apiDoc: './api-v1/api-doc.yml', + apiDoc: yaml.load( + fs.readFileSync(path.resolve(__dirname, '../../openapi.yaml'), 'utf-8'), + ) as any, + operations: { + RefreshEntity: validateDependency(refreshService, async (req, res) => { + const refreshOptions: RefreshOptions = req.body; + refreshOptions.authorizationToken = getBearerToken( + req.header('authorization'), + ); - if (permissionIntegrationRouter) { - router.use(permissionIntegrationRouter); - } - - if (entitiesCatalog) { - router - .get('/entities', async (req, res) => { - const { entities, pageInfo } = await entitiesCatalog.entities({ + await refreshService!.refresh(refreshOptions); + res.status(200).end(); + }), + GetEntities: validateDependency(entitiesCatalog, async (req, res) => { + const { entities, pageInfo } = await entitiesCatalog!.entities({ filter: parseEntityFilterParams(req.query), fields: parseEntityTransformParams(req.query), order: parseEntityOrderParams(req.query), @@ -131,30 +156,10 @@ export async function createRouter( // TODO(freben): encode the pageInfo in the response res.json(entities); - }) - .get('/entities/by-query', async (req, res) => { - const { items, pageInfo, totalItems } = - await entitiesCatalog.queryEntities({ - ...parseQueryEntitiesParams(req.query), - authorizationToken: getBearerToken(req.header('authorization')), - }); - - res.json({ - items, - totalItems, - pageInfo: { - ...(pageInfo.nextCursor && { - nextCursor: encodeCursor(pageInfo.nextCursor), - }), - ...(pageInfo.prevCursor && { - prevCursor: encodeCursor(pageInfo.prevCursor), - }), - }, - }); - }) - .get('/entities/by-uid/:uid', async (req, res) => { + }), + GetEntityByUid: validateDependency(entitiesCatalog, async (req, res) => { const { uid } = req.params; - const { entities } = await entitiesCatalog.entities({ + const { entities } = await entitiesCatalog!.entities({ filter: basicEntityFilter({ 'metadata.uid': uid }), authorizationToken: getBearerToken(req.header('authorization')), }); @@ -162,17 +167,10 @@ export async function createRouter( throw new NotFoundError(`No entity with uid ${uid}`); } res.status(200).json(entities[0]); - }) - .delete('/entities/by-uid/:uid', async (req, res) => { - const { uid } = req.params; - await entitiesCatalog.removeEntityByUid(uid, { - authorizationToken: getBearerToken(req.header('authorization')), - }); - res.status(204).end(); - }) - .get('/entities/by-name/:kind/:namespace/:name', async (req, res) => { + }), + GetEntityByName: validateDependency(entitiesCatalog, async (req, res) => { const { kind, namespace, name } = req.params; - const { entities } = await entitiesCatalog.entities({ + const { entities } = await entitiesCatalog!.entities({ filter: basicEntityFilter({ kind: kind, 'metadata.namespace': namespace, @@ -186,41 +184,27 @@ export async function createRouter( ); } res.status(200).json(entities[0]); - }) - .get( - '/entities/by-name/:kind/:namespace/:name/ancestry', + }), + GetEntityAncestryByName: validateDependency( + entitiesCatalog, async (req, res) => { const { kind, namespace, name } = req.params; const entityRef = stringifyEntityRef({ kind, namespace, name }); - const response = await entitiesCatalog.entityAncestry(entityRef, { + const response = await entitiesCatalog!.entityAncestry(entityRef, { authorizationToken: getBearerToken(req.header('authorization')), }); res.status(200).json(response); }, - ) - .post('/entities/by-refs', async (req, res) => { - const request = entitiesBatchRequest(req); - const token = getBearerToken(req.header('authorization')); - const response = await entitiesCatalog.entitiesBatch({ - entityRefs: request.entityRefs, - fields: parseEntityTransformParams(req.query, request.fields), - authorizationToken: token, - }); - res.status(200).json(response); - }) - .get('/entity-facets', async (req, res) => { - const response = await entitiesCatalog.facets({ + ), + GetEntityFacets: validateDependency(entitiesCatalog, async (req, res) => { + const response = await entitiesCatalog!.facets({ filter: parseEntityFilterParams(req.query), facets: parseEntityFacetParams(req.query), authorizationToken: getBearerToken(req.header('authorization')), }); res.status(200).json(response); - }); - } - - if (locationService) { - router - .post('/locations', async (req, res) => { + }), + CreateLocation: validateDependency(locationService, async (req, res) => { const location = await validateRequestBody(req, locationInput); const dryRun = yn(req.query.dryRun, { default: false }); @@ -234,22 +218,21 @@ export async function createRouter( authorizationToken: getBearerToken(req.header('authorization')), }); res.status(201).json(output); - }) - .get('/locations', async (req, res) => { + }), + GetLocations: validateDependency(locationService, async (req, res) => { const locations = await locationService.listLocations({ authorizationToken: getBearerToken(req.header('authorization')), }); res.status(200).json(locations.map(l => ({ data: l }))); - }) - - .get('/locations/:id', async (req, res) => { + }), + GetLocation: validateDependency(locationService, async (req, res) => { const { id } = req.params; const output = await locationService.getLocation(id, { authorizationToken: getBearerToken(req.header('authorization')), }); res.status(200).json(output); - }) - .delete('/locations/:id', async (req, res) => { + }), + DeleteLocation: validateDependency(locationService, async (req, res) => { disallowReadonlyMode(readonlyEnabled); const { id } = req.params; @@ -257,74 +240,108 @@ export async function createRouter( authorizationToken: getBearerToken(req.header('authorization')), }); res.status(204).end(); - }); - } - - if (locationAnalyzer) { - router.post('/analyze-location', async (req, res) => { - const body = await validateRequestBody( - req, - z.object({ + }), + AnalyzeLocation: validateDependency(locationService, async (req, res) => { + const body = await validateRequestBody( + req, + z.object({ + location: locationInput, + catalogFilename: z.string().optional(), + }), + ); + const schema = z.object({ location: locationInput, catalogFilename: z.string().optional(), - }), - ); - const schema = z.object({ - location: locationInput, - catalogFilename: z.string().optional(), - }); - const output = await locationAnalyzer.analyzeLocation(schema.parse(body)); - res.status(200).json(output); - }); - } - - if (orchestrator) { - router.post('/validate-entity', async (req, res) => { - const bodySchema = z.object({ - entity: z.unknown(), - location: z.string(), - }); - - let body: z.infer; - let entity: Entity; - let location: { type: string; target: string }; - try { - body = await validateRequestBody(req, bodySchema); - entity = validateEntityEnvelope(body.entity); - location = parseLocationRef(body.location); - if (location.type !== 'url') - throw new TypeError( - `Invalid location ref ${body.location}, only 'url:' is supported, e.g. url:https://host/path`, - ); - } catch (err) { - return res.status(400).json({ - errors: [serializeError(err)], }); - } + const output = await locationAnalyzer!.analyzeLocation( + schema.parse(body), + ); + res.status(200).json(output); + }), + ValidateEntity: validateDependency(orchestrator, async (req, res) => { + const bodySchema = z.object({ + entity: z.unknown(), + location: z.string(), + }); - const processingResult = await orchestrator.process({ - entity: { - ...entity, - metadata: { - ...entity.metadata, - annotations: { - [ANNOTATION_LOCATION]: body.location, - [ANNOTATION_ORIGIN_LOCATION]: body.location, - ...entity.metadata.annotations, + let body: z.infer; + let entity: Entity; + let location: { type: string; target: string }; + try { + body = await validateRequestBody(req, bodySchema); + entity = validateEntityEnvelope(body.entity); + location = parseLocationRef(body.location); + if (location.type !== 'url') + throw new TypeError( + `Invalid location ref ${body.location}, only 'url:' is supported, e.g. url:https://host/path`, + ); + } catch (err) { + return res.status(400).json({ + errors: [serializeError(err)], + }); + } + + const processingResult = await orchestrator!.process({ + entity: { + ...entity, + metadata: { + ...entity.metadata, + annotations: { + [ANNOTATION_LOCATION]: body.location, + [ANNOTATION_ORIGIN_LOCATION]: body.location, + ...entity.metadata.annotations, + }, }, }, - }, - }); - - if (!processingResult.ok) - res.status(400).json({ - errors: processingResult.errors.map(e => serializeError(e)), }); - return res.status(200).end(); - }); - } - router.use(errorHandler()); + if (!processingResult.ok) + res.status(400).json({ + errors: processingResult.errors.map(e => serializeError(e)), + }); + return res.status(200).end(); + }), + DeleteEntityByUid: validateDependency( + entitiesCatalog, + async (req, res) => { + const { uid } = req.params; + await entitiesCatalog!.removeEntityByUid(uid, { + authorizationToken: getBearerToken(req.header('authorization')), + }); + res.status(204).end(); + }, + ), + GetEntitiesByRefs: validateDependency( + entitiesCatalog, + async (req, res) => { + const request = entitiesBatchRequest(req); + const token = getBearerToken(req.header('authorization')); + const response = await entitiesCatalog!.entitiesBatch({ + entityRefs: request.entityRefs, + fields: parseEntityTransformParams(req.query, request.fields), + authorizationToken: token, + }); + res.status(200).json(response); + }, + ), + }, + enableObjectCoercion: true, + errorMiddleware: errorHandler(), + errorTransformer: (openapiError, ajvError) => { + switch (openapiError.errorCode) { + case 'type.openapi.requestValidation': + throw new InputError( + `Invalid field ${openapiError.path}`, + new ParsingError(openapiError.message), + ); + } + return {}; + }, + }); + + if (permissionIntegrationRouter) { + router.use(permissionIntegrationRouter); + } return router; } diff --git a/yarn.lock b/yarn.lock index 3652a87b61..859a44fda4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5371,11 +5371,13 @@ __metadata: codeowners-utils: ^1.0.2 core-js: ^3.6.5 express: ^4.17.1 + express-openapi: ^12.1.0 express-promise-router: ^4.1.0 fast-json-stable-stringify: ^2.1.0 fs-extra: 10.1.0 git-url-parse: ^13.0.0 glob: ^7.1.6 + js-yaml: ^4.1.0 knex: ^2.0.0 lodash: ^4.17.21 luxon: ^3.0.0 @@ -17590,7 +17592,7 @@ __metadata: languageName: node linkType: hard -"ajv-formats@npm:^2.1.1": +"ajv-formats@npm:^2.0.2, ajv-formats@npm:^2.1.0, ajv-formats@npm:^2.1.1": version: 2.1.1 resolution: "ajv-formats@npm:2.1.1" dependencies: @@ -17636,7 +17638,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^8.0.0, ajv@npm:^8.10.0, ajv@npm:^8.11.0, ajv@npm:^8.12.0, ajv@npm:^8.8.0": +"ajv@npm:^8.0.0, ajv@npm:^8.1.0, ajv@npm:^8.10.0, ajv@npm:^8.11.0, ajv@npm:^8.12.0, ajv@npm:^8.3.0, ajv@npm:^8.4.0, ajv@npm:^8.8.0": version: 8.12.0 resolution: "ajv@npm:8.12.0" dependencies: @@ -20392,6 +20394,13 @@ __metadata: languageName: node linkType: hard +"content-type@npm:^1.0.4": + version: 1.0.4 + resolution: "content-type@npm:1.0.4" + checksum: 3d93585fda985d1554eca5ebd251994327608d2e200978fdbfba21c0c679914d5faf266d17027de44b34a72c7b0745b18584ecccaa7e1fdfb6a68ac7114f12e0 + languageName: node + linkType: hard + "content-type@npm:~1.0.4, content-type@npm:~1.0.5": version: 1.0.5 resolution: "content-type@npm:1.0.5" @@ -21787,6 +21796,15 @@ __metadata: languageName: node linkType: hard +"difunc@npm:0.0.4": + version: 0.0.4 + resolution: "difunc@npm:0.0.4" + dependencies: + esprima: ^4.0.0 + checksum: 19b850dae20cba3bdf238b85fa90f79526974e7634348a3157eef3da6eb161e7f43142aa531f6be61eed1235d1f4b6a16e07912f313b5afcceee56d998c2e300 + languageName: node + linkType: hard + "dir-glob@npm:^3.0.1": version: 3.0.1 resolution: "dir-glob@npm:3.0.1" @@ -23687,6 +23705,24 @@ __metadata: languageName: node linkType: hard +"express-normalize-query-params-middleware@npm:^0.5.0": + version: 0.5.1 + resolution: "express-normalize-query-params-middleware@npm:0.5.1" + checksum: 20fef3a200c452cbfef25f738e8ed44704ad30f04899cce21d3984b74e4fcbeefdc5dcec3aed1a0e4e54f231c55f3cba2c3d0cc3faebfea0cab437a852bca09b + languageName: node + linkType: hard + +"express-openapi@npm:^12.1.0": + version: 12.1.0 + resolution: "express-openapi@npm:12.1.0" + dependencies: + express-normalize-query-params-middleware: ^0.5.0 + openapi-framework: ^12.1.0 + openapi-types: ^12.1.0 + checksum: 6b87b7990891b1f4d4bd3e071eddd72d7433ae770d5ad0cfbbee234a0d4e712150640ec15d8fe9d1af99b49209700c58f4694045f00bd380a569a86ba5008755 + languageName: node + linkType: hard + "express-prom-bundle@npm:^6.3.6": version: 6.6.0 resolution: "express-prom-bundle@npm:6.6.0" @@ -24584,6 +24620,15 @@ __metadata: languageName: node linkType: hard +"fs-routes@npm:^12.0.0": + version: 12.0.0 + resolution: "fs-routes@npm:12.0.0" + peerDependencies: + glob: ">=7.1.6" + checksum: 4e97b08584c4ee04cf00215ac5175ba6f3aeaa4fb254cd108149d675edffcf90e802ead9965a5252271f26e1a47b31c341b41e209b9d210b2a584ae2a642c466 + languageName: node + linkType: hard + "fs.realpath@npm:^1.0.0": version: 1.0.0 resolution: "fs.realpath@npm:1.0.0" @@ -24905,6 +24950,19 @@ __metadata: languageName: node linkType: hard +"glob@npm:*, glob@npm:8.0.3": + version: 8.0.3 + resolution: "glob@npm:8.0.3" + dependencies: + fs.realpath: ^1.0.0 + inflight: ^1.0.4 + inherits: 2 + minimatch: ^5.0.1 + once: ^1.3.0 + checksum: 50bcdea19d8e79d8de5f460b1939ffc2b3299eac28deb502093fdca22a78efebc03e66bf54f0abc3d3d07d8134d19a32850288b7440d77e072aa55f9d33b18c5 + languageName: node + linkType: hard + "glob@npm:7.1.6": version: 7.1.6 resolution: "glob@npm:7.1.6" @@ -24919,19 +24977,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:8.0.3": - version: 8.0.3 - resolution: "glob@npm:8.0.3" - dependencies: - fs.realpath: ^1.0.0 - inflight: ^1.0.4 - inherits: 2 - minimatch: ^5.0.1 - once: ^1.3.0 - checksum: 50bcdea19d8e79d8de5f460b1939ffc2b3299eac28deb502093fdca22a78efebc03e66bf54f0abc3d3d07d8134d19a32850288b7440d77e072aa55f9d33b18c5 - languageName: node - linkType: hard - "glob@npm:^7.0.0, glob@npm:^7.1.1, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6, glob@npm:^7.1.7, glob@npm:^7.2.0": version: 7.2.3 resolution: "glob@npm:7.2.3" @@ -26591,6 +26636,13 @@ __metadata: languageName: node linkType: hard +"is-dir@npm:^1.0.0": + version: 1.0.0 + resolution: "is-dir@npm:1.0.0" + checksum: b81430f22d03318b144391ec9633abdd6fdcd8fd02509bee1ab5a20b79311505ba4344282d2cab565114779ac36626fd034168b9bd47c3be12d956ae2ca76a5c + languageName: node + linkType: hard + "is-docker@npm:^2.0.0, is-docker@npm:^2.1.1": version: 2.2.1 resolution: "is-docker@npm:2.2.1" @@ -29360,7 +29412,7 @@ __metadata: languageName: node linkType: hard -"lodash.merge@npm:^4.6.2": +"lodash.merge@npm:^4.6.1, lodash.merge@npm:^4.6.2": version: 4.6.2 resolution: "lodash.merge@npm:4.6.2" checksum: ad580b4bdbb7ca1f7abf7e1bce63a9a0b98e370cf40194b03380a46b4ed799c9573029599caebc1b14e3f24b111aef72b96674a56cfa105e0f5ac70546cdc005 @@ -31924,6 +31976,79 @@ __metadata: languageName: node linkType: hard +"openapi-default-setter@npm:^12.1.0": + version: 12.1.0 + resolution: "openapi-default-setter@npm:12.1.0" + dependencies: + openapi-types: ^12.1.0 + checksum: 3b4543d7c091134690fcfc986258481b6211f17b4322552764b3a4a9725784202bc17419a524302dd1187ecea556dea4f2cb525f4a0258c2ab5ddec884bf00e2 + languageName: node + linkType: hard + +"openapi-framework@npm:^12.1.0": + version: 12.1.0 + resolution: "openapi-framework@npm:12.1.0" + dependencies: + difunc: 0.0.4 + fs-routes: ^12.0.0 + glob: "*" + is-dir: ^1.0.0 + js-yaml: ^3.10.0 + openapi-default-setter: ^12.1.0 + openapi-request-coercer: ^12.1.0 + openapi-request-validator: ^12.1.0 + openapi-response-validator: ^12.1.0 + openapi-schema-validator: ^12.1.0 + openapi-security-handler: ^12.1.0 + openapi-types: ^12.1.0 + ts-log: ^2.1.4 + checksum: 4cbb35e2870604c91c01f397aa2ea8fb072f648acb9a47b830d0d67c1e68bb75eadc3e97bcbe2ea994b7374ba38866b6d91734df6e2481ce42e6625d66ccc5bd + languageName: node + linkType: hard + +"openapi-jsonschema-parameters@npm:^12.1.0": + version: 12.1.0 + resolution: "openapi-jsonschema-parameters@npm:12.1.0" + dependencies: + openapi-types: ^12.1.0 + checksum: 9465549e132d02ad4be67326e22c9a2e3f0cf19ffa0e3c1fe69a542a675707f11c33080836b0eebd75c02501dbcb5f4130fa446d470ede3ba67b7c52e1bc341f + languageName: node + linkType: hard + +"openapi-request-coercer@npm:^12.1.0": + version: 12.1.0 + resolution: "openapi-request-coercer@npm:12.1.0" + dependencies: + openapi-types: ^12.1.0 + ts-log: ^2.1.4 + checksum: 4359d4fd58b032146838aabb9762fc2ac01771e7bea4679af5a241964f3df15ddced7968e6a8d419954d166e93ad32e08f530da0d9a8483d3c4d97be7c7c848c + languageName: node + linkType: hard + +"openapi-request-validator@npm:^12.1.0": + version: 12.1.0 + resolution: "openapi-request-validator@npm:12.1.0" + dependencies: + ajv: ^8.3.0 + ajv-formats: ^2.1.0 + content-type: ^1.0.4 + openapi-jsonschema-parameters: ^12.1.0 + openapi-types: ^12.1.0 + ts-log: ^2.1.4 + checksum: 794b1c15dbf04107ede7b8304e5b7c2fa183ad0c4bd65b3e0ee4d415cf508324932959f661120f1bfb58a23d88b5458dc0db868bc36f9ec44ea03b99ea9d42df + languageName: node + linkType: hard + +"openapi-response-validator@npm:^12.1.0": + version: 12.1.0 + resolution: "openapi-response-validator@npm:12.1.0" + dependencies: + ajv: ^8.4.0 + openapi-types: ^12.1.0 + checksum: f07036661ac846c5d508b423d01039cd5bd33d564e053fdc039046f964fad71b03a7c9254195b1d5a8a1912fcc45dce13616af97c445c6d7b040c825e1f91de1 + languageName: node + linkType: hard + "openapi-sampler@npm:^1.2.1": version: 1.2.1 resolution: "openapi-sampler@npm:1.2.1" @@ -31934,7 +32059,28 @@ __metadata: languageName: node linkType: hard -"openapi-types@npm:^12.0.0": +"openapi-schema-validator@npm:^12.1.0": + version: 12.1.0 + resolution: "openapi-schema-validator@npm:12.1.0" + dependencies: + ajv: ^8.1.0 + ajv-formats: ^2.0.2 + lodash.merge: ^4.6.1 + openapi-types: ^12.1.0 + checksum: 1008ef82910cc30e85132c041b269a4cc2991dad17e2d7c4cddbfb5641a15e934016bc4d1bab32f2dc79f7f3673b82b655c3634838c9ce3a8f497af3be762aa5 + languageName: node + linkType: hard + +"openapi-security-handler@npm:^12.1.0": + version: 12.1.0 + resolution: "openapi-security-handler@npm:12.1.0" + dependencies: + openapi-types: ^12.1.0 + checksum: 07f645300fdc0ca5d01790fb77e01d8e537f6b0439f55ec9af5097d3dd044e3e0fc55ce0acb9551f04256819d7738f8c3d468645e581f7b0487648eed428d519 + languageName: node + linkType: hard + +"openapi-types@npm:^12.0.0, openapi-types@npm:^12.1.0": version: 12.1.0 resolution: "openapi-types@npm:12.1.0" checksum: d8f3e2bae519aa6bcf2012f4a5592b7283fe928c06f3bdc182776ce637c7ed8d5f9f342724f68c95c9572aefe21ed5e89b18a0295a703da61af5f97bd2aea9a7 @@ -38457,6 +38603,13 @@ __metadata: languageName: node linkType: hard +"ts-log@npm:^2.1.4": + version: 2.2.5 + resolution: "ts-log@npm:2.2.5" + checksum: 28f78ab15b8555d56c089dbc243327d8ce4331219956242a29fc4cb3bad6bb0cb8234dd17a292381a1b1dba99a7e4849a2181b2e1a303e8247e9f4ca4e284f2d + languageName: node + linkType: hard + "ts-log@npm:^2.2.3": version: 2.2.3 resolution: "ts-log@npm:2.2.3" From d7585d4003dc93f83f38fc0d266fe79399cd774f Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Thu, 19 Jan 2023 17:17:28 -0500 Subject: [PATCH 128/247] Starting router. Signed-off-by: Aramis Sennyey --- .../src/service/createRouter.ts | 18 + plugins/openapi-router-common/.eslintrc.js | 1 + plugins/openapi-router-common/README.md | 5 + plugins/openapi-router-common/package.json | 37 + plugins/openapi-router-common/src/index.ts | 23 + plugins/openapi-router-common/src/router.ts | 357 ++++++++++ .../openapi-router-common/src/setupTests.ts | 16 + .../openapi-router-common/src/types/fields.ts | 66 ++ .../openapi-router-common/src/types/path.ts | 666 ++++++++++++++++++ .../openapi-router-common/src/types/utils.ts | 144 ++++ yarn.lock | 43 +- 11 files changed, 1374 insertions(+), 2 deletions(-) create mode 100644 plugins/openapi-router-common/.eslintrc.js create mode 100644 plugins/openapi-router-common/README.md create mode 100644 plugins/openapi-router-common/package.json create mode 100644 plugins/openapi-router-common/src/index.ts create mode 100644 plugins/openapi-router-common/src/router.ts create mode 100644 plugins/openapi-router-common/src/setupTests.ts create mode 100644 plugins/openapi-router-common/src/types/fields.ts create mode 100644 plugins/openapi-router-common/src/types/path.ts create mode 100644 plugins/openapi-router-common/src/types/utils.ts diff --git a/plugins/catalog-backend/src/service/createRouter.ts b/plugins/catalog-backend/src/service/createRouter.ts index 6b733a0a9a..6685332002 100644 --- a/plugins/catalog-backend/src/service/createRouter.ts +++ b/plugins/catalog-backend/src/service/createRouter.ts @@ -54,6 +54,7 @@ import { initialize } from 'express-openapi'; import yaml from 'js-yaml'; import fs from 'fs'; import path from 'path'; +import OpenAPIBackend from 'openapi-backend'; class ParsingError extends Error { toString() { @@ -104,6 +105,23 @@ export async function createRouter( logger.info('Catalog is running in readonly mode'); } + // create api with your definition file or object + const api = new OpenAPIBackend({ definition: './petstore.yml' }); + + // register your framework specific request handlers here + api.register({ + getPets: (c, req, res) => { + return res.status(200).json({ result: 'ok' }); + }, + getPetById: (c, req, res) => res.status(200).json({ result: 'ok' }), + validationFail: (c, req, res) => + res.status(400).json({ err: c.validation.errors }), + notFound: (c, req, res) => res.status(404).json({ err: 'not found' }), + }); + + // initalize the backend + api.init(); + const validateDependency = ( dependency: any, next: (req: express.Request, res: express.Response) => any, diff --git a/plugins/openapi-router-common/.eslintrc.js b/plugins/openapi-router-common/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/openapi-router-common/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/openapi-router-common/README.md b/plugins/openapi-router-common/README.md new file mode 100644 index 0000000000..cda5d7fc7f --- /dev/null +++ b/plugins/openapi-router-common/README.md @@ -0,0 +1,5 @@ +# @backstage/plugin-openapi-router-common + +Welcome to the common package for the openapi-router plugin! + +_This plugin was created through the Backstage CLI_ diff --git a/plugins/openapi-router-common/package.json b/plugins/openapi-router-common/package.json new file mode 100644 index 0000000000..a852edfcec --- /dev/null +++ b/plugins/openapi-router-common/package.json @@ -0,0 +1,37 @@ +{ + "name": "@backstage/plugin-openapi-router", + "description": "OpenAPI router with type completions.", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "files": [ + "dist" + ], + "dependencies": { + "express": "^4.18.2", + "json-schema-to-ts": "^2.6.2", + "openapi-types": "^12.1.0" + } +} diff --git a/plugins/openapi-router-common/src/index.ts b/plugins/openapi-router-common/src/index.ts new file mode 100644 index 0000000000..7edca76f93 --- /dev/null +++ b/plugins/openapi-router-common/src/index.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * Common functionalities for the openapi-router plugin. + * + * @packageDocumentation + */ + +export * from './router'; diff --git a/plugins/openapi-router-common/src/router.ts b/plugins/openapi-router-common/src/router.ts new file mode 100644 index 0000000000..c7940cdbd0 --- /dev/null +++ b/plugins/openapi-router-common/src/router.ts @@ -0,0 +1,357 @@ +/* + * 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 { OpenAPIV3_1 } from 'openapi-types'; +import { FieldValues } from './types/fields'; +import { + AppendNonBlankKey, + FieldPath, + FieldPathValue, + Path, +} from './types/path'; +import { IRouter, Router, IRouterMatcher } from 'express'; +import core, { + ParamsDictionary, + RequestHandler, +} from 'express-serve-static-core'; +import { FromSchema } from 'json-schema-to-ts'; + +type RouterFn = < + TFieldName extends FieldPath = FieldPath, +>( + name: TFieldName, +) => null; + +const doc = { + openapi: '3.1.0', + info: { + version: '1.0.0', + title: 'Swagger Petstore', + license: { + name: 'MIT', + url: 'https://opensource.org/licenses/MIT', + }, + }, + servers: [ + { + url: 'http://petstore.swagger.io/v1', + }, + ], + paths: { + '/pets': { + get: { + summary: 'List all pets', + operationId: 'listPets', + tags: ['pets'], + parameters: [ + { + name: 'limit', + in: 'query', + description: 'How many items to return at one time (max 100)', + required: false, + schema: { + type: 'integer', + format: 'int32', + }, + }, + ], + responses: { + '200': { + description: 'A paged array of pets', + headers: { + 'x-next': { + description: 'A link to the next page of responses', + schema: { + type: 'string', + }, + }, + }, + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Pets', + }, + }, + }, + }, + default: { + description: 'unexpected error', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error', + }, + }, + }, + }, + }, + }, + post: { + summary: 'Create a pet', + operationId: 'createPets', + tags: ['pets'], + responses: { + '201': { + description: 'Null response', + }, + default: { + description: 'unexpected error', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error', + }, + }, + }, + }, + }, + }, + }, + '/pets/{petId}': { + get: { + summary: 'Info for a specific pet', + operationId: 'showPetById', + tags: ['pets'], + parameters: [ + { + name: 'petId', + in: 'path', + required: true, + description: 'The id of the pet to retrieve', + schema: { + type: 'string', + }, + }, + ], + responses: { + '200': { + description: 'Expected response to a valid request', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Pet', + }, + }, + }, + }, + default: { + description: 'unexpected error', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error', + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Pet: { + type: 'object', + required: ['id', 'name'], + properties: { + id: { + type: 'integer', + format: 'int64', + }, + name: { + type: 'string', + }, + tag: { + type: 'string', + }, + }, + }, + Pets: { + type: 'array', + items: { + $ref: '#/components/schemas/Pet', + }, + }, + Error: { + type: 'object', + required: ['code', 'message'], + properties: { + code: { + type: 'integer', + format: 'int32', + }, + message: { + type: 'string', + }, + }, + }, + }, + }, +} as const; + +const isUndefined = (a: any): a is undefined => a === undefined; +const isNull = (a: any): a is null => a === null; + +const isNullOrUndefined = (a: any): a is undefined | null => + isUndefined(a) || isNull(a); + +const isDateObject = (value: unknown): value is Date => value instanceof Date; + +const isObjectType = (value: unknown) => typeof value === 'object'; + +const isObject = (value: unknown): value is T => + !isNullOrUndefined(value) && + !Array.isArray(value) && + isObjectType(value) && + !isDateObject(value); + +const compact = (value: TValue[]) => + Array.isArray(value) ? value.filter(Boolean) : []; + +export const resolve = ( + obj: T, + path: string, + defaultValue?: unknown, +): any => { + if (!path || !isObject(obj)) { + return defaultValue; + } + + const result = compact(path.split(/[,[\].]+?/)).reduce( + (result, key) => + isNullOrUndefined(result) ? result : result[key as keyof {}], + obj, + ); + if (result === undefined || result === obj) { + return obj[path as keyof T] === undefined + ? defaultValue + : obj[path as keyof T]; + } + return result; +}; + +/** + * We want this to input path and have + * @param name + * @returns + */ +const x: RouterFn = name => { + console.log(resolve(doc, name)); + return null; +}; + +x('/pets/{petId}'); + +type RemoveTail< + S extends string, + Tail extends string, +> = S extends `${infer P}${Tail}` ? P : S; +type GetRouteParameter = RemoveTail< + RemoveTail, `-${string}`>, + `.${string}` +>; +export type RouteParameters = string extends Route + ? ParamsDictionary + : Route extends `${string}(${string}` + ? ParamsDictionary + : Route extends `${string}:${infer Rest}` + ? (GetRouteParameter extends never + ? ParamsDictionary + : GetRouteParameter extends `${infer ParamName}?` + ? { [P in ParamName]?: string } + : { [P in GetRouteParameter]: string }) & + (Rest extends `${GetRouteParameter}${infer Next}` + ? RouteParameters + : unknown) + : {}; +interface ParsedQs { + [key: string]: undefined | string | string[] | ParsedQs | ParsedQs[]; +} +interface ApiRouterMatcher< + TFieldValues extends FieldValues, + T, + Method extends + | 'all' + | 'get' + | 'post' + | 'put' + | 'delete' + | 'patch' + | 'options' + | 'head', +> { + < + TFieldName extends Path = Path, + P = RouteParameters, + ResBody = any, + ReqBody = any, + ReqQuery = ParsedQs, + Locals extends Record = Record, + >( + // (it's used as the default type parameter for P) + path: TFieldName, + // (This generic is meant to be passed explicitly.) + ...handlers: Array> + ): T; +} + +type path = ApiRouterMatcher; +const test: path = a => { + console.log(a); +}; +test('/pets/{petId}'); + +export interface IApiRouter extends IRouter { + all: ApiRouterMatcher; + get: ApiRouterMatcher; +} + +export default class ApiRouter< + ApiSpec, + PathSpec extends OpenAPIV3_1.Document, + T, +> implements IApiRouter +{ + private _router = Router(); + + constructor(private spec: OpenAPIV3_1.Document) {} + + static fromSpec(spec: OpenAPIV3_1.Document) { + return new ApiRouter(spec); + } + + get< + TFieldValues extends FieldValues = PathSpec, + TFieldName extends FieldPath = FieldPath, + >( + path: TFieldName, + ...handlers: core.RequestHandler< + core.ParamsDictionary, + any, + FieldPathValue, 'get'>, + ParsedQs, + Record + >[] + ) { + console.log(path); + return this._router.get(path, ...handlers); + } +} + +const router = ApiRouter.fromSpec(doc); + +router.get('/pets', (req, res) => { + req.body.tags; +}); diff --git a/plugins/openapi-router-common/src/setupTests.ts b/plugins/openapi-router-common/src/setupTests.ts new file mode 100644 index 0000000000..4b9026cde5 --- /dev/null +++ b/plugins/openapi-router-common/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * 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 {}; diff --git a/plugins/openapi-router-common/src/types/fields.ts b/plugins/openapi-router-common/src/types/fields.ts new file mode 100644 index 0000000000..e97eeace9d --- /dev/null +++ b/plugins/openapi-router-common/src/types/fields.ts @@ -0,0 +1,66 @@ +/* + * 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 { IsFlatObject, Noop } from './utils'; + +export type InternalFieldName = string; + +export type FieldName = + IsFlatObject extends true + ? Extract + : string; + +export type CustomElement = { + name: FieldName; + type?: string; + value?: any; + disabled?: boolean; + checked?: boolean; + options?: HTMLOptionsCollection; + files?: FileList | null; + focus?: Noop; +}; + +export type FieldValue = + TFieldValues[InternalFieldName]; + +export type FieldValues = Record; + +export type NativeFieldValue = + | string + | number + | boolean + | null + | undefined + | unknown[]; + +export type FieldElement = + | HTMLInputElement + | HTMLSelectElement + | HTMLTextAreaElement + | CustomElement; + +export type Ref = FieldElement; + +export type Field = { + _f: { + ref: Ref; + name: InternalFieldName; + refs?: HTMLInputElement[]; + mount?: boolean; + }; +}; + +export type FieldRefs = Partial>; diff --git a/plugins/openapi-router-common/src/types/path.ts b/plugins/openapi-router-common/src/types/path.ts new file mode 100644 index 0000000000..c1ffbd6378 --- /dev/null +++ b/plugins/openapi-router-common/src/types/path.ts @@ -0,0 +1,666 @@ +/* + * 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 { FieldValues } from './fields'; +import { + BrowserNativeObject, + IsAny, + IsNever, + IsEqual, + Primitive, +} from './utils'; + +/** + * Type alias to `string` which describes a lodash-like path through an object. + * E.g. `'foo.bar.0.baz'` + */ +export type PathString = string; + +/** + * Type which can be traversed through with a {@link PathString}. + * I.e. objects, arrays, and tuples + */ +export type Traversable = object; + +/** + * Type to query whether an array type T is a tuple type. + * @typeParam T - type which may be an array or tuple + * @example + * ``` + * IsTuple<[number]> = true + * IsTuple = false + * ``` + */ +export type IsTuple> = number extends T['length'] + ? false + : true; + +/** + * Type which can be used to index an array or tuple type. + */ +export type ArrayKey = number; + +/** + * Type which can be used to index an object. + */ +export type Key = string; + +/** + * Type to assert that a type is a {@link Key}. + * @typeParam T - type which may be a {@link Key} + */ +export type AsKey = Extract; + +/** + * Type to convert a type to a {@link Key}. + * @typeParam T - type which may be converted to a {@link Key} + */ +export type ToKey = T extends ArrayKey ? `${T}` : AsKey; + +/** + * Type which describes a path through an object + * as a list of individual {@link Key}s. + */ +export type PathTuple = Key[]; + +/** + * Type to assert that a type is a {@link PathTuple}. + * @typeParam T - type which may be a {@link PathTuple} + */ +export type AsPathTuple = Extract; + +/** + * Type to intersect a union type. + * See https://fettblog.eu/typescript-union-to-intersection/ + * @typeParam U - union + * @example + * ``` + * UnionToIntersection<{ foo: string } | { bar: number }> + * = { foo: string; bar: number } + * ``` + */ +export type UnionToIntersection = ( + U extends any ? (_: U) => any : never +) extends (_: infer I) => any + ? I + : never; + +/** + * Type which appends a {@link Key} to the {@link PathTuple} only if it is not + * blank, i.e. not the empty string. + * @typeParam PT - path + * @typeParam K - key + * @example + * ``` + * AppendNonBlankKey<['foo'], 'bar'> = ['foo', 'bar'] + * AppendNonBlankKey<['foo'], ''> = ['foo'] + * ``` + */ +export type AppendNonBlankKey< + PT extends PathTuple, + K extends Key, +> = K extends '' ? PT : [...PT, K]; + +/** + * Type to implement {@link SplitPathString} tail recursively. + * @typeParam PS - remaining {@link PathString} which should be split into its + * individual {@link Key}s + * @typeParam PT - accumulator of the {@link Key}s which have been split from + * the original {@link PathString} already + */ +type SplitPathStringImpl< + PS extends PathString, + PT extends PathTuple, +> = PS extends `${infer K}.${infer R}` + ? SplitPathStringImpl> + : AppendNonBlankKey; + +/** + * Type to split a {@link PathString} into a {@link PathTuple}. + * The individual {@link Key}s may be empty strings. + * @typeParam PS - {@link PathString} which should be split into its + * individual {@link Key}s + * @example + * ``` + * SplitPathString<'foo'> = ['foo'] + * SplitPathString<'foo.bar.0.baz'> = ['foo', 'bar', '0', 'baz'] + * SplitPathString<'.'> = [] + * ``` + */ +export type SplitPathString = SplitPathStringImpl< + PS, + [] +>; + +/** + * Type to implement {@link JoinPathTuple} tail-recursively. + * @typeParam PT - remaining {@link Key}s which needs to be joined + * @typeParam PS - accumulator of the already joined {@link Key}s + */ +type JoinPathTupleImpl< + PT extends PathTuple, + PS extends PathString, +> = PT extends [infer K, ...infer R] + ? JoinPathTupleImpl, `${PS}.${AsKey}`> + : PS; + +/** + * Type to join a {@link PathTuple} to a {@link PathString}. + * @typeParam PT - {@link PathTuple} which should be joined. + * @example + * ``` + * JoinPathTuple<['foo']> = 'foo' + * JoinPathTuple<['foo', 'bar', '0', 'baz']> = 'foo.bar.0.baz' + * JoinPathTuple<[]> = never + * ``` + */ +export type JoinPathTuple = PT extends [ + infer K, + ...infer R, +] + ? JoinPathTupleImpl, AsKey> + : never; + +/** + * Type which converts all keys of an object to {@link Key}s. + * @typeParam T - object type + * @example + * ``` + * MapKeys<{0: string}> = {'0': string} + * ``` + */ +type MapKeys = { [K in keyof T as ToKey]: T[K] }; + +/** + * Type to access a type by a key. + * - Returns undefined if it can't be indexed by that key. + * - Returns null if the type is null. + * - Returns undefined if the type is not traversable. + * @typeParam T - type which is indexed by the key + * @typeParam K - key into the type + * ``` + * TryAccess<{foo: string}, 'foo'> = string + * TryAccess<{foo: string}, 'bar'> = undefined + * TryAccess = null + * TryAccess = undefined + * ``` + */ +type TryAccess = K extends keyof T + ? T[K] + : T extends null + ? null + : undefined; + +/** + * Type to access an array type by a key. + * Returns undefined if the key is non-numeric. + * @typeParam T - type which is indexed by the key + * @typeParam K - key into the type + * ``` + * TryAccessArray = string + * TryAccessArray = undefined + * ``` + */ +type TryAccessArray< + T extends ReadonlyArray, + K extends Key, +> = K extends `${ArrayKey}` ? T[number] : TryAccess; + +/** + * Type to evaluate the type which the given key points to. + * @typeParam T - type which is indexed by the key + * @typeParam K - key into the type + * @example + * ``` + * EvaluateKey<{foo: string}, 'foo'> = string + * EvaluateKey<[number, string], '1'> = string + * EvaluateKey = string + * ``` + */ +export type EvaluateKey = T extends ReadonlyArray + ? IsTuple extends true + ? TryAccess + : TryAccessArray + : TryAccess, K>; + +/** + * Type to evaluate the type which the given path points to. + * @typeParam T - deeply nested type which is indexed by the path + * @typeParam PT - path into the deeply nested type + * @example + * ``` + * EvaluatePath<{foo: {bar: string}}, ['foo', 'bar']> = string + * EvaluatePath<[number, string], ['1']> = string + * EvaluatePath = number + * EvaluatePath = undefined + * ``` + */ +export type EvaluatePath = PT extends [ + infer K, + ...infer R, +] + ? EvaluatePath>, AsPathTuple> + : T; + +/** + * Type which given a tuple type returns its own keys, i.e. only its indices. + * @typeParam T - tuple type + * @example + * ``` + * TupleKeys<[number, string]> = '0' | '1' + * ``` + */ +export type TupleKeys> = Exclude< + keyof T, + keyof any[] +>; + +/** + * Type which extracts all numeric keys from an object. + * @typeParam T - type + * @example + * ``` + * NumericObjectKeys<{0: string, '1': string, foo: string}> = '0' | '1' + * ``` + */ +type NumericObjectKeys = ToKey< + Extract +>; + +/** + * Type which extracts all numeric keys from an object, tuple, or array. + * If a union is passed, it evaluates to the overlapping numeric keys. + * @typeParam T - type + * @example + * ``` + * NumericKeys<{0: string, '1': string, foo: string}> = '0' | '1' + * NumericKeys = `${number}` + * NumericKeys<[string, number]> = '0' | '1' + * NumericKeys<{0: string, '1': string} | [number] | number[]> = '0' + * ``` + */ +export type NumericKeys = UnionToIntersection< + T extends ReadonlyArray + ? IsTuple extends true + ? [TupleKeys] + : [ToKey] + : [NumericObjectKeys] +>[never]; + +/** + * Type which extracts all keys from an object. + * If a union is passed, it evaluates to the overlapping keys. + * @typeParam T - object type + * @example + * ``` + * ObjectKeys<{foo: string, bar: string}, string> = 'foo' | 'bar' + * ObjectKeys<{foo: string, bar: number}, string> = 'foo' + * ``` + */ +export type ObjectKeys = Exclude< + ToKey, + `${string}.${string}` | '' +>; + +/** + * Type to check whether a type's property matches the constraint type + * and return its key. Converts the key to a {@link Key}. + * @typeParam T - type whose property should be checked + * @typeParam K - key of the property + * @typeParam U - constraint type + * @example + * ``` + * CheckKeyConstraint<{foo: string}, 'foo', string> = 'foo' + * CheckKeyConstraint<{foo: string}, 'foo', number> = never + * CheckKeyConstraint = `${number}` + * ``` + */ +export type CheckKeyConstraint = K extends any + ? EvaluateKey extends U + ? K + : never + : never; + +/** + * Type which evaluates to true when the type is an array or tuple or is a union + * which contains an array or tuple. + * @typeParam T - type + * @example + * ``` + * ContainsIndexable<{foo: string}> = false + * ContainsIndexable<{foo: string} | number[]> = true + * ``` + */ +export type ContainsIndexable = IsNever< + Extract> +> extends true + ? false + : true; + +/** + * Type to implement {@link Keys} for non-nullable values. + * @typeParam T - non-nullable type whose property should be checked + */ +type KeysImpl = [T] extends [Traversable] + ? ContainsIndexable extends true + ? NumericKeys + : ObjectKeys + : never; + +/** + * Type to find all properties of a type that match the constraint type + * and return their keys. + * If a union is passed, it evaluates to the overlapping keys. + * @typeParam T - type whose property should be checked + * @typeParam U - constraint type + * @example + * ``` + * Keys<{foo: string, bar: string}, string> = 'foo' | 'bar' + * Keys<{foo?: string, bar?: string}> = 'foo' | 'bar' + * Keys<{foo: string, bar: number}, string> = 'foo' + * Keys<[string, number], string> = '0' + * Keys = `${number}` + * Keys<{0: string, '1': string} | [number] | number[]> = '0' + * ``` + */ +export type Keys = IsAny extends true + ? Key + : IsNever extends true + ? Key + : IsNever> extends true + ? never + : CheckKeyConstraint>, U>; + +/** + * Type to check whether a {@link Key} is present in a type. + * If a union of {@link Key}s is passed, all {@link Key}s have to be present + * in the type. + * @typeParam T - type which is introspected + * @typeParam K - key + * @example + * ``` + * HasKey<{foo: string}, 'foo'> = true + * HasKey<{foo: string}, 'bar'> = false + * HasKey<{foo: string}, 'foo' | 'bar'> = false + * ``` + */ +export type HasKey = IsNever>>; + +/** + * Type to implement {@link ValidPathPrefix} tail recursively. + * @typeParam T - type which the path should be checked against + * @typeParam PT - path which should exist within the given type + * @typeParam VPT - accumulates the prefix of {@link Key}s which have been + * confirmed to exist already + */ +type ValidPathPrefixImpl< + T, + PT extends PathTuple, + VPT extends PathTuple, +> = PT extends [infer K, ...infer R] + ? HasKey> extends true + ? ValidPathPrefixImpl< + EvaluateKey>, + AsPathTuple, + AsPathTuple<[...VPT, K]> + > + : VPT + : VPT; + +/** + * Type to find the longest path prefix which is still valid, + * i.e. exists within the given type. + * @typeParam T - type which the path should be checked against + * @typeParam PT - path which should exist within the given type + * @example + * ``` + * ValidPathPrefix<{foo: {bar: string}}, ['foo', 'bar']> = ['foo', 'bar'] + * ValidPathPrefix<{foo: {bar: string}}, ['foo', 'ba']> = ['foo'] + * ``` + */ +export type ValidPathPrefix = ValidPathPrefixImpl< + T, + PT, + [] +>; + +/** + * Type to check whether a path through a type exists. + * @typeParam T - type which the path should be checked against + * @typeParam PT - path which should exist within the given type + * @example + * ``` + * HasPath<{foo: {bar: string}}, ['foo', 'bar']> = true + * HasPath<{foo: {bar: string}}, ['foo', 'ba']> = false + * ``` + */ +export type HasPath = ValidPathPrefix extends PT + ? true + : false; + +/** + * Helper function to break apart T1 and check if any are equal to T2 + * + * See {@link IsEqual} + */ +type AnyIsEqual = T1 extends T2 + ? IsEqual extends true + ? true + : never + : never; + +type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'all'; + +/** + * Helper type for recursively constructing paths through a type. + * This actually constructs the strings and recurses into nested + * object types. + * + * See {@link Path} + */ +type PathImpl = V extends + | Primitive + | BrowserNativeObject + ? `${K}` + : // Check so that we don't recurse into the same type + // by ensuring that the types are mutually assignable + // mutually required to avoid false positives of subtypes + true extends AnyIsEqual + ? `${K}` + : true extends AnyIsEqual + ? `` + : `${K}` | `${K}.${PathInternal}`; + +/** + * Helper type for recursively constructing paths through a type. + * This obsucres the internal type param TraversedTypes from exported contract. + * + * See {@link Path} + */ +type PathInternal = T extends ReadonlyArray + ? IsTuple extends true + ? { + [K in TupleKeys]-?: PathImpl; + }[TupleKeys] + : PathImpl + : { + [K in keyof T]-?: PathImpl; + }[keyof T]; + +/** + * Type which eagerly collects all paths through a type + * @typeParam T - type which should be introspected + * @example + * ``` + * Path<{foo: {bar: string}}> = 'foo' | 'foo.bar' + * ``` + */ +// We want to explode the union type and process each individually +// so assignable types don't leak onto the stack from the base. +export type Path = T extends any ? PathInternal : never; + +/** + * See {@link Path} + */ +export type FieldPath = Path; + +/** + * Helper type for recursively constructing paths through a type. + * This actually constructs the strings and recurses into nested + * object types. + * + * See {@link ArrayPath} + */ +type ArrayPathImpl = V extends + | Primitive + | BrowserNativeObject + ? IsAny extends true + ? string + : never + : V extends ReadonlyArray + ? U extends Primitive | BrowserNativeObject + ? IsAny extends true + ? string + : never + : // Check so that we don't recurse into the same type + // by ensuring that the types are mutually assignable + // mutually required to avoid false positives of subtypes + true extends AnyIsEqual + ? never + : `${K}` | `${K}.${ArrayPathInternal}` + : true extends AnyIsEqual + ? never + : `${K}.${ArrayPathInternal}`; + +/** + * Helper type for recursively constructing paths through a type. + * This obsucres the internal type param TraversedTypes from exported contract. + * + * See {@link ArrayPath} + */ +type ArrayPathInternal = T extends ReadonlyArray + ? IsTuple extends true + ? { + [K in TupleKeys]-?: ArrayPathImpl; + }[TupleKeys] + : ArrayPathImpl + : { + [K in keyof T]-?: ArrayPathImpl; + }[keyof T]; + +/** + * Type which eagerly collects all paths through a type which point to an array + * type. + * @typeParam T - type which should be introspected. + * @example + * ``` + * Path<{foo: {bar: string[], baz: number[]}}> = 'foo.bar' | 'foo.baz' + * ``` + */ +// We want to explode the union type and process each individually +// so assignable types don't leak onto the stack from the base. +export type ArrayPath = T extends any ? ArrayPathInternal : never; + +/** + * See {@link ArrayPath} + */ +export type FieldArrayPath = + ArrayPath; + +/** + * Type to evaluate the type which the given path points to. + * @typeParam T - deeply nested type which is indexed by the path + * @typeParam P - path into the deeply nested type + * @example + * ``` + * PathValue<{foo: {bar: string}}, 'foo.bar'> = string + * PathValue<[number, string], '1'> = string + * ``` + */ +export type PathValue | ArrayPath> = T extends any + ? P extends `${infer K}.${infer R}` + ? K extends keyof T + ? R extends Path + ? PathValue + : never + : K extends `${ArrayKey}` + ? T extends ReadonlyArray + ? PathValue> + : never + : never + : P extends keyof T + ? T[P] + : P extends `${ArrayKey}` + ? T extends ReadonlyArray + ? V + : never + : never + : never; + +/** + * See {@link PathValue} + */ +export type FieldPathValue< + TFieldValues extends FieldValues, + TFieldPath extends FieldPath, +> = PathValue; + +/** + * See {@link PathValue} + */ +export type FieldArrayPathValue< + TFieldValues extends FieldValues, + TFieldArrayPath extends FieldArrayPath, +> = PathValue; + +/** + * Type to evaluate the type which the given paths point to. + * @typeParam TFieldValues - field values which are indexed by the paths + * @typeParam TPath - paths into the deeply nested field values + * @example + * ``` + * FieldPathValues<{foo: {bar: string}}, ['foo', 'foo.bar']> + * = [{bar: string}, string] + * ``` + */ +export type FieldPathValues< + TFieldValues extends FieldValues, + TPath extends FieldPath[] | readonly FieldPath[], +> = {} & { + [K in keyof TPath]: FieldPathValue< + TFieldValues, + TPath[K] & FieldPath + >; +}; + +/** + * Type which eagerly collects all paths through a fieldType that matches a give type + * @typeParam TFieldValues - field values which are indexed by the paths + * @typeParam TValue - the value you want to match into each type + * @example + * ```typescript + * FieldPathByValue<{foo: {bar: number}, baz: number, bar: string}, number> + * = 'foo.bar' | 'baz' + * ``` + */ +export type FieldPathByValue = { + [FieldKey in FieldPath]: FieldPathValue< + TFieldValues, + FieldKey + > extends TValue + ? FieldKey + : never; +}[FieldPath]; diff --git a/plugins/openapi-router-common/src/types/utils.ts b/plugins/openapi-router-common/src/types/utils.ts new file mode 100644 index 0000000000..25282e1da2 --- /dev/null +++ b/plugins/openapi-router-common/src/types/utils.ts @@ -0,0 +1,144 @@ +/* + * 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. + */ +declare const $NestedValue: unique symbol; + +/** + * @deprecated to be removed in the next major version + */ +export type NestedValue = { + [$NestedValue]: never; +} & TValue; + +/* +Projects that React Hook Form installed don't include the DOM library need these interfaces to compile. +React Native applications is no DOM available. The JavaScript runtime is ES6/ES2015 only. +These definitions allow such projects to compile with only --lib ES6. + +Warning: all of these interfaces are empty. +If you want type definitions for various properties, you need to add `--lib DOM` (via command line or tsconfig.json). +*/ + +export type Noop = () => void; + +interface File extends Blob { + readonly lastModified: number; + readonly name: string; +} + +interface FileList { + readonly length: number; + item(index: number): File | null; + [index: number]: File; +} + +export type Primitive = + | null + | undefined + | string + | number + | boolean + | symbol + | bigint; + +export type BrowserNativeObject = Date | FileList | File; + +export type EmptyObject = { [K in string | number]: never }; + +export type NonUndefined = T extends undefined ? never : T; + +export type LiteralUnion = + | T + | (U & { _?: never }); + +export type DeepPartial = T extends BrowserNativeObject | NestedValue + ? T + : { [K in keyof T]?: DeepPartial }; + +export type DeepPartialSkipArrayKey = T extends + | BrowserNativeObject + | NestedValue + ? T + : T extends ReadonlyArray + ? { [K in keyof T]: DeepPartialSkipArrayKey } + : { [K in keyof T]?: DeepPartialSkipArrayKey }; + +/** + * Checks whether the type is any + * See {@link https://stackoverflow.com/a/49928360/3406963} + * @typeParam T - type which may be any + * ``` + * IsAny = true + * IsAny = false + * ``` + */ +export type IsAny = 0 extends 1 & T ? true : false; + +/** + * Checks whether the type is never + * @typeParam T - type which may be never + * ``` + * IsAny = true + * IsAny = false + * ``` + */ +export type IsNever = [T] extends [never] ? true : false; + +/** + * Checks whether T1 can be exactly (mutually) assigned to T2 + * @typeParam T1 - type to check + * @typeParam T2 - type to check against + * ``` + * IsEqual = true + * IsEqual<'foo', 'foo'> = true + * IsEqual = false + * IsEqual = false + * IsEqual = false + * IsEqual<'foo', string> = false + * IsEqual<'foo' | 'bar', 'foo'> = boolean // 'foo' is assignable, but 'bar' is not (true | false) -> boolean + * ``` + */ +export type IsEqual = T1 extends T2 + ? (() => G extends T1 ? 1 : 2) extends () => G extends T2 ? 1 : 2 + ? true + : false + : false; + +export type DeepMap = IsAny extends true + ? any + : T extends BrowserNativeObject | NestedValue + ? TValue + : T extends object + ? { [K in keyof T]: DeepMap, TValue> } + : TValue; + +export type IsFlatObject = Extract< + Exclude, + any[] | object +> extends never + ? true + : false; + +export type Merge = { + [K in keyof A | keyof B]?: K extends keyof A & keyof B + ? [A[K], B[K]] extends [object, object] + ? Merge + : A[K] | B[K] + : K extends keyof A + ? A[K] + : K extends keyof B + ? B[K] + : never; +}; diff --git a/yarn.lock b/yarn.lock index 859a44fda4..eb39765348 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3342,7 +3342,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.6, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.6, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": version: 7.20.13 resolution: "@babel/runtime@npm:7.20.13" dependencies: @@ -7446,6 +7446,17 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-openapi-router@workspace:plugins/openapi-router-common": + version: 0.0.0-use.local + resolution: "@backstage/plugin-openapi-router@workspace:plugins/openapi-router-common" + dependencies: + "@backstage/cli": "workspace:^" + express: ^4.18.2 + json-schema-to-ts: ^2.6.2 + openapi-types: ^12.1.0 + languageName: unknown + linkType: soft + "@backstage/plugin-org-react@workspace:plugins/org-react": version: 0.0.0-use.local resolution: "@backstage/plugin-org-react@workspace:plugins/org-react" @@ -23777,7 +23788,7 @@ __metadata: languageName: node linkType: hard -"express@npm:^4.17.1, express@npm:^4.17.3, express@npm:^4.18.1": +"express@npm:^4.17.1, express@npm:^4.17.3, express@npm:^4.18.1, express@npm:^4.18.2": version: 4.18.2 resolution: "express@npm:4.18.2" dependencies: @@ -28324,6 +28335,18 @@ __metadata: languageName: node linkType: hard +"json-schema-to-ts@npm:^2.6.2": + version: 2.6.2 + resolution: "json-schema-to-ts@npm:2.6.2" + dependencies: + "@babel/runtime": ^7.18.3 + "@types/json-schema": ^7.0.9 + ts-algebra: ^1.1.1 + ts-toolbelt: ^9.6.0 + checksum: e408f8d3d32dda1a001f194cf5514fea7ef0a4dc4ed4f5448b9ea02df3a071119a0517aaffb639de725f4a12102406dfdf7743455e0e30317e231bb551362039 + languageName: node + linkType: hard + "json-schema-traverse@npm:^0.4.1": version: 0.4.1 resolution: "json-schema-traverse@npm:0.4.1" @@ -38580,6 +38603,15 @@ __metadata: languageName: node linkType: hard +"ts-algebra@npm:^1.1.1": + version: 1.1.1 + resolution: "ts-algebra@npm:1.1.1" + dependencies: + ts-toolbelt: ^9.6.0 + checksum: 29fe27215863520377a94a14ccf1e78e921be6a5c514a0a8250dab09e325b006a9571ef1f366875d58e495b59ba880620718a1dd38b6af598cc6ea6de82d4f66 + languageName: node + linkType: hard + "ts-easing@npm:^0.2.0": version: 0.2.0 resolution: "ts-easing@npm:0.2.0" @@ -38672,6 +38704,13 @@ __metadata: languageName: node linkType: hard +"ts-toolbelt@npm:^9.6.0": + version: 9.6.0 + resolution: "ts-toolbelt@npm:9.6.0" + checksum: 9f35fd95d895a5d32ea9fd2e532a695b0bae6cbff6832b77292efa188a0ed1ed6e54f63f74a8920390f3d909a7a3adb20a144686372a8e78b420246a9bd3d58a + languageName: node + linkType: hard + "tsconfig-paths@npm:^3.14.1": version: 3.14.1 resolution: "tsconfig-paths@npm:3.14.1" From ef4d42ec301f586560b2a195d23a091acb8d06e9 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Thu, 19 Jan 2023 18:51:12 -0500 Subject: [PATCH 129/247] Testing Signed-off-by: Aramis Sennyey --- plugins/openapi-router-common/src/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/openapi-router-common/src/router.ts b/plugins/openapi-router-common/src/router.ts index c7940cdbd0..740dc8dc03 100644 --- a/plugins/openapi-router-common/src/router.ts +++ b/plugins/openapi-router-common/src/router.ts @@ -340,7 +340,7 @@ export default class ApiRouter< ...handlers: core.RequestHandler< core.ParamsDictionary, any, - FieldPathValue, 'get'>, + FieldPathValue, ParsedQs, Record >[] From e1962a4f0a0db599e9a5a66307f603d7ebf7753b Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Thu, 16 Feb 2023 16:13:26 -0500 Subject: [PATCH 130/247] Ading server testing and better handlers. Signed-off-by: Aramis Sennyey --- .../src/service/createRouter.ts | 33 +- plugins/openapi-router-common/package.json | 4 +- plugins/openapi-router-common/src/router.ts | 295 ++++---- plugins/openapi-router-common/src/run.ts | 33 + .../src/standaloneServer.ts | 48 ++ .../openapi-router-common/src/types/common.ts | 116 +++ .../openapi-router-common/src/types/fields.ts | 66 -- .../openapi-router-common/src/types/index.ts | 2 + .../openapi-router-common/src/types/path.ts | 666 ------------------ .../src/types/requests.ts | 36 + .../src/types/response.ts | 68 ++ .../openapi-router-common/src/types/utils.ts | 144 ---- yarn.lock | 11 + 13 files changed, 491 insertions(+), 1031 deletions(-) create mode 100644 plugins/openapi-router-common/src/run.ts create mode 100644 plugins/openapi-router-common/src/standaloneServer.ts create mode 100644 plugins/openapi-router-common/src/types/common.ts delete mode 100644 plugins/openapi-router-common/src/types/fields.ts create mode 100644 plugins/openapi-router-common/src/types/index.ts delete mode 100644 plugins/openapi-router-common/src/types/path.ts create mode 100644 plugins/openapi-router-common/src/types/requests.ts create mode 100644 plugins/openapi-router-common/src/types/response.ts delete mode 100644 plugins/openapi-router-common/src/types/utils.ts diff --git a/plugins/catalog-backend/src/service/createRouter.ts b/plugins/catalog-backend/src/service/createRouter.ts index 6685332002..f6c2e441e5 100644 --- a/plugins/catalog-backend/src/service/createRouter.ts +++ b/plugins/catalog-backend/src/service/createRouter.ts @@ -54,7 +54,6 @@ import { initialize } from 'express-openapi'; import yaml from 'js-yaml'; import fs from 'fs'; import path from 'path'; -import OpenAPIBackend from 'openapi-backend'; class ParsingError extends Error { toString() { @@ -105,23 +104,6 @@ export async function createRouter( logger.info('Catalog is running in readonly mode'); } - // create api with your definition file or object - const api = new OpenAPIBackend({ definition: './petstore.yml' }); - - // register your framework specific request handlers here - api.register({ - getPets: (c, req, res) => { - return res.status(200).json({ result: 'ok' }); - }, - getPetById: (c, req, res) => res.status(200).json({ result: 'ok' }), - validationFail: (c, req, res) => - res.status(400).json({ err: c.validation.errors }), - notFound: (c, req, res) => res.status(404).json({ err: 'not found' }), - }); - - // initalize the backend - api.init(); - const validateDependency = ( dependency: any, next: (req: express.Request, res: express.Response) => any, @@ -143,6 +125,7 @@ export async function createRouter( // NOTE: If using yaml you can provide a path relative to process.cwd() e.g. // apiDoc: './api-v1/api-doc.yml', apiDoc: yaml.load( + // eslint-disable-next-line no-restricted-syntax fs.readFileSync(path.resolve(__dirname, '../../openapi.yaml'), 'utf-8'), ) as any, operations: { @@ -345,12 +328,18 @@ export async function createRouter( }, enableObjectCoercion: true, errorMiddleware: errorHandler(), - errorTransformer: (openapiError, ajvError) => { - switch (openapiError.errorCode) { + errorTransformer: openapiError => { + const error = openapiError as { + errorCode: string; + path: string; + message: string; + }; + // eslint-disable-next-line default-case + switch (error.errorCode) { case 'type.openapi.requestValidation': throw new InputError( - `Invalid field ${openapiError.path}`, - new ParsingError(openapiError.message), + `Invalid field ${error.path}`, + new ParsingError(error.message), ); } return {}; diff --git a/plugins/openapi-router-common/package.json b/plugins/openapi-router-common/package.json index a852edfcec..3a40ddecd0 100644 --- a/plugins/openapi-router-common/package.json +++ b/plugins/openapi-router-common/package.json @@ -32,6 +32,8 @@ "dependencies": { "express": "^4.18.2", "json-schema-to-ts": "^2.6.2", - "openapi-types": "^12.1.0" + "openapi-types": "^12.1.0", + "openapi3-ts": "^3.1.2", + "ts-node": "^10.9.1" } } diff --git a/plugins/openapi-router-common/src/router.ts b/plugins/openapi-router-common/src/router.ts index 740dc8dc03..99a45b8e0c 100644 --- a/plugins/openapi-router-common/src/router.ts +++ b/plugins/openapi-router-common/src/router.ts @@ -13,26 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { OpenAPIV3_1 } from 'openapi-types'; -import { FieldValues } from './types/fields'; +import { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'; +import { IRouter, Router } from 'express'; +import core, { ParamsDictionary } from 'express-serve-static-core'; +import { FromSchema, JSONSchema7 } from 'json-schema-to-ts'; import { - AppendNonBlankKey, - FieldPath, - FieldPathValue, - Path, -} from './types/path'; -import { IRouter, Router, IRouterMatcher } from 'express'; -import core, { - ParamsDictionary, - RequestHandler, -} from 'express-serve-static-core'; -import { FromSchema } from 'json-schema-to-ts'; + DocPath, + DocPathMethod, + DocPathTemplate, + MethodAwareDocPath, + PathTemplate, + RequestBodySchema, + RequiredDoc, + ValueOf, +} from './types'; +import { ResponseSchemas } from './types/response'; -type RouterFn = < - TFieldName extends FieldPath = FieldPath, ->( - name: TFieldName, -) => null; +type DeepWriteable = { -readonly [P in keyof T]: DeepWriteable }; const doc = { openapi: '3.1.0', @@ -201,59 +198,6 @@ const doc = { }, } as const; -const isUndefined = (a: any): a is undefined => a === undefined; -const isNull = (a: any): a is null => a === null; - -const isNullOrUndefined = (a: any): a is undefined | null => - isUndefined(a) || isNull(a); - -const isDateObject = (value: unknown): value is Date => value instanceof Date; - -const isObjectType = (value: unknown) => typeof value === 'object'; - -const isObject = (value: unknown): value is T => - !isNullOrUndefined(value) && - !Array.isArray(value) && - isObjectType(value) && - !isDateObject(value); - -const compact = (value: TValue[]) => - Array.isArray(value) ? value.filter(Boolean) : []; - -export const resolve = ( - obj: T, - path: string, - defaultValue?: unknown, -): any => { - if (!path || !isObject(obj)) { - return defaultValue; - } - - const result = compact(path.split(/[,[\].]+?/)).reduce( - (result, key) => - isNullOrUndefined(result) ? result : result[key as keyof {}], - obj, - ); - if (result === undefined || result === obj) { - return obj[path as keyof T] === undefined - ? defaultValue - : obj[path as keyof T]; - } - return result; -}; - -/** - * We want this to input path and have - * @param name - * @returns - */ -const x: RouterFn = name => { - console.log(resolve(doc, name)); - return null; -}; - -x('/pets/{petId}'); - type RemoveTail< S extends string, Tail extends string, @@ -279,79 +223,166 @@ export type RouteParameters = string extends Route interface ParsedQs { [key: string]: undefined | string | string[] | ParsedQs | ParsedQs[]; } -interface ApiRouterMatcher< - TFieldValues extends FieldValues, + +// oh boy don't do this +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( + k: infer I, +) => void + ? I + : never; +type LastOf = UnionToIntersection< + T extends any ? () => T : never +> extends () => infer R + ? R + : never; + +// TS4.0+ +type Push = [...T, V]; + +// TS4.1+ +type TuplifyUnion< T, - Method extends - | 'all' - | 'get' - | 'post' - | 'put' - | 'delete' - | 'patch' - | 'options' - | 'head', -> { - < - TFieldName extends Path = Path, - P = RouteParameters, - ResBody = any, - ReqBody = any, - ReqQuery = ParsedQs, - Locals extends Record = Record, - >( - // (it's used as the default type parameter for P) - path: TFieldName, - // (This generic is meant to be passed explicitly.) - ...handlers: Array> - ): T; -} + L = LastOf, + N = [T] extends [never] ? true : false, +> = true extends N ? [] : Push>, L>; -type path = ApiRouterMatcher; -const test: path = a => { - console.log(a); -}; -test('/pets/{petId}'); +type ConvertAll = []> = T extends [ + infer First extends JSONSchema7, + ...infer Rest, +] + ? ConvertAll]> + : R; -export interface IApiRouter extends IRouter { - all: ApiRouterMatcher; - get: ApiRouterMatcher; -} +type ResponseToJsonSchema< + Doc extends RequiredDoc, + Path extends PathTemplate>, + Method extends DocPathMethod, +> = ConvertAll< + TuplifyUnion>> +>[number]; -export default class ApiRouter< - ApiSpec, - PathSpec extends OpenAPIV3_1.Document, - T, -> implements IApiRouter -{ +type DocRequestHandler< + Doc extends RequiredDoc, + Path extends DocPathTemplate, + Method extends keyof Doc['paths'][Path], +> = core.RequestHandler< + core.ParamsDictionary, + // From https://stackoverflow.com/questions/71393738/typescript-intersection-not-union-type-from-json-schema. + ResponseToJsonSchema, + RequestBodySchema, + ParsedQs, + Record +>; + +export default class ApiRouter { private _router = Router(); - constructor(private spec: OpenAPIV3_1.Document) {} + constructor(private spec: OpenAPIV3_1.Document | OpenAPIV3.Document) {} - static fromSpec(spec: OpenAPIV3_1.Document) { - return new ApiRouter(spec); + static fromSpec( + spec: OpenAPIV3_1.Document | OpenAPIV3.Document, + ) { + return new ApiRouter(spec); } - get< - TFieldValues extends FieldValues = PathSpec, - TFieldName extends FieldPath = FieldPath, - >( - path: TFieldName, - ...handlers: core.RequestHandler< - core.ParamsDictionary, - any, - FieldPathValue, - ParsedQs, - Record - >[] + get, 'get'>>( + path: Path, + ...handlers: DocRequestHandler[] ) { console.log(path); - return this._router.get(path, ...handlers); + this._router.get(path, ...handlers); + return this; + } + + post, 'post'>>( + path: Path, + ...handlers: DocRequestHandler[] + ) { + console.log(path); + this._router.post(path, ...handlers); + return this; + } + + all, 'all'>>( + path: Path, + ...handlers: DocRequestHandler[] + ) { + console.log(path); + this._router.all(path, ...handlers); + return this; + } + + put, 'put'>>( + path: Path, + ...handlers: DocRequestHandler[] + ) { + console.log(path); + this._router.put(path, ...handlers); + return this; + } + delete, 'delete'>>( + path: Path, + ...handlers: DocRequestHandler[] + ) { + console.log(path); + this._router.delete(path, ...handlers); + return this; + } + patch, 'patch'>>( + path: Path, + ...handlers: DocRequestHandler[] + ) { + console.log(path); + this._router.patch(path, ...handlers); + return this; + } + options< + Path extends MethodAwareDocPath, 'options'>, + >(path: Path, ...handlers: DocRequestHandler[]) { + console.log(path); + this._router.options(path, ...handlers); + return this; + } + head, 'head'>>( + path: Path, + ...handlers: DocRequestHandler[] + ) { + console.log(path); + this._router.head(path, ...handlers); + return this; + } + + use(...handlers: core.RequestHandler[]) { + return this._router.use(handlers); + } + + build() { + return this._router; } } -const router = ApiRouter.fromSpec(doc); +interface RouterOptions {} -router.get('/pets', (req, res) => { - req.body.tags; -}); +export async function createRouter(options: RouterOptions) { + const router = ApiRouter.fromSpec>( + // As const forces the doc to readonly which conflicts with imported types. + doc as DeepWriteable, + ); + + router.get('/pets/:uid', (req, res) => { + res.json({ + id: 1, + name: req.params['uid'], + }); + }); + + // router.get('/pet') will complain with a TS error + + router.post('/pets', (req, res) => { + res.json({ + message: req.path, + code: 1, + }); + }); + return router.build(); +} diff --git a/plugins/openapi-router-common/src/run.ts b/plugins/openapi-router-common/src/run.ts new file mode 100644 index 0000000000..9a14d0e57d --- /dev/null +++ b/plugins/openapi-router-common/src/run.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import yn from 'yn'; +import { getRootLogger } from '@backstage/backend-common'; +import { startStandaloneServer } from './standaloneServer'; + +const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 3004; +const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); +const logger = getRootLogger(); + +startStandaloneServer({ port, enableCors, logger }).catch(err => { + logger.error(err); + process.exit(1); +}); + +process.on('SIGINT', () => { + logger.info('CTRL+C pressed; exiting.'); + process.exit(0); +}); diff --git a/plugins/openapi-router-common/src/standaloneServer.ts b/plugins/openapi-router-common/src/standaloneServer.ts new file mode 100644 index 0000000000..aef8f006ff --- /dev/null +++ b/plugins/openapi-router-common/src/standaloneServer.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Server } from 'http'; +import { Logger } from 'winston'; +import { createServiceBuilder } from '@backstage/backend-common'; +import { createRouter } from './router'; + +export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; +} + +export async function startStandaloneServer( + options: ServerOptions, +): Promise { + const logger = options.logger.child({ service: 'app-backend' }); + logger.debug('Starting application server...'); + const router = await createRouter({ + logger, + appPackageName: 'example-app', + }); + + const service = createServiceBuilder(module) + .setPort(options.port) + .addRouter('', router); + + return await service.start().catch(err => { + logger.error(err); + process.exit(1); + }); +} + +module.hot?.accept(); diff --git a/plugins/openapi-router-common/src/types/common.ts b/plugins/openapi-router-common/src/types/common.ts new file mode 100644 index 0000000000..2f0d6735a4 --- /dev/null +++ b/plugins/openapi-router-common/src/types/common.ts @@ -0,0 +1,116 @@ +/** + * Pulled from https://github.com/varanauskas/oatx. + */ + +import type { + ContentObject, + OpenAPIObject, + ReferenceObject, +} from 'openapi3-ts'; + +export type RequiredDoc = Pick; +export type PathDoc = { paths: Record }; + +/** + * Get value types of `T` + */ +export type ValueOf = T[keyof T]; + +/** + * Validate a string against OpenAPI path template + * ``` + * const path = PathTemplate<"/posts/{postId}/comments/{commentId}"> = "/posts/1/comments/2"const pathWithParams: PathTemplate<"/posts/{postId}/comments/{commentId}"> = "/posts/1/comments/2"; + * const pathWithoutParams: PathTemplate<"/posts/comments"> = "/posts/comments";``` + * https://spec.openapis.org/oas/v3.1.0#path-templating-matching + */ +export type PathTemplate = + Path extends `${infer Prefix}{${string}}${infer Suffix}` + ? `${Prefix}${string}${PathTemplate}` + : Path; + +/** + * Extract path as specified in OpenAPI `Doc` based on request path + * ``` + * const spec = { + * paths: { + * "/posts/{postId}/comments/{commentId}": {}, + * "/posts/comments": {}, + * } + * }; + * const specPathWithParams: DocPath = "/posts/{postId}/comments/{commentId}"; + * const specPathWithoutParams: DocPath = "/posts/comments"; + * ``` + */ +export type DocPath< + Doc extends PathDoc, + Path extends PathTemplate>, +> = ValueOf<{ + [Template in Extract< + keyof Doc['paths'], + string + >]: Path extends PathTemplate