From a5d73da942a3f33584326068b453f737dae0dd12 Mon Sep 17 00:00:00 2001 From: Mengnan Gong Date: Fri, 3 Jun 2022 14:26:51 +0800 Subject: [PATCH 001/401] techdocs-cli: fix the legacyCopyReadmeMdToIndexMd flag in techdocs-cli generate Signed-off-by: Mengnan Gong --- .changeset/happy-boxes-melt.md | 6 ++++++ packages/techdocs-cli/src/commands/generate/generate.ts | 2 +- plugins/techdocs-node/src/stages/generate/techdocs.ts | 6 +++--- 3 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 .changeset/happy-boxes-melt.md diff --git a/.changeset/happy-boxes-melt.md b/.changeset/happy-boxes-melt.md new file mode 100644 index 0000000000..c478e783d3 --- /dev/null +++ b/.changeset/happy-boxes-melt.md @@ -0,0 +1,6 @@ +--- +'@techdocs/cli': patch +'@backstage/plugin-techdocs-node': patch +--- + +Fix the flag parsing for `legacyCopyReadmeMdToIndexMd` in `techdocs-cli generate` command, and decouple it's logic from the `techdocs-ref` flag. diff --git a/packages/techdocs-cli/src/commands/generate/generate.ts b/packages/techdocs-cli/src/commands/generate/generate.ts index 309b9c7969..1e4ed220d9 100644 --- a/packages/techdocs-cli/src/commands/generate/generate.ts +++ b/packages/techdocs-cli/src/commands/generate/generate.ts @@ -57,8 +57,8 @@ export default async function generate(opts: OptionValues) { runIn: opts.docker ? 'docker' : 'local', dockerImage, pullImage, - legacyCopyReadmeMdToIndexMd, mkdocs: { + legacyCopyReadmeMdToIndexMd, omitTechdocsCorePlugin, }, }, diff --git a/plugins/techdocs-node/src/stages/generate/techdocs.ts b/plugins/techdocs-node/src/stages/generate/techdocs.ts index 660b8e8cb5..e3debab526 100644 --- a/plugins/techdocs-node/src/stages/generate/techdocs.ts +++ b/plugins/techdocs-node/src/stages/generate/techdocs.ts @@ -111,10 +111,10 @@ export class TechdocsGenerator implements GeneratorBase { parsedLocationAnnotation, this.scmIntegrations, ); + } - if (this.options.legacyCopyReadmeMdToIndexMd) { - await patchIndexPreBuild({ inputDir, logger: childLogger, docsDir }); - } + if (this.options.legacyCopyReadmeMdToIndexMd) { + await patchIndexPreBuild({ inputDir, logger: childLogger, docsDir }); } if (!this.options.omitTechdocsCoreMkdocsPlugin) { From d996458580de4b63f9c07ebacac98626efab4125 Mon Sep 17 00:00:00 2001 From: sjeyaraman Date: Sun, 5 Jun 2022 12:40:43 -0700 Subject: [PATCH 002/401] customize unregister entity menuitem Signed-off-by: sjeyaraman --- .../app/src/components/catalog/EntityPage.tsx | 15 ++++++- .../EntityContextMenu.test.tsx | 25 ++++++++++- .../EntityContextMenu/EntityContextMenu.tsx | 43 +++++++++++++------ .../components/EntityLayout/EntityLayout.tsx | 4 +- 4 files changed, 70 insertions(+), 17 deletions(-) diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 799fe3e4d9..ab8c02c168 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -161,9 +161,22 @@ const EntityLayoutWrapper = (props: { children?: ReactNode }) => { ]; }, []); + type VisibleType = 'visible' | 'hidden' | 'disabled'; + + type contextMenuOptions = { + disableUnregister: boolean | VisibleType; + }; + + const options: contextMenuOptions = { + disableUnregister: false, + }; + return ( <> - + {props.children} { it('should call onUnregisterEntity on button click', async () => { const mockCallback = jest.fn(); - await render( { expect(mockCallback).toBeCalled(); }); + it('check Unregister entity button is disabled', async () => { + const mockCallback = jest.fn(); + + const { getByText } = await render( + {}} + />, + ); + + const button = await screen.findByTestId('menu-button'); + expect(button).toBeInTheDocument(); + fireEvent.click(button); + + const unregister = await screen.getByText('Unregister entity'); + expect(unregister).toBeInTheDocument(); + + const unregisterSpanItem = getByText(/Unregister entity/); + const unregisterMenuListItem = + unregisterSpanItem?.parentElement?.parentElement; + expect(unregisterMenuListItem).toHaveAttribute('aria-disabled'); + }); + it('should call onInspectEntity on button click', async () => { const mockCallback = jest.fn(); diff --git a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx index 5664186ab1..955c42b95a 100644 --- a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx @@ -47,9 +47,11 @@ interface ExtraContextMenuItem { onClick: () => void; } +type VisibleType = 'visible' | 'hidden' | 'disabled'; + // unstable context menu option, eg: disable the unregister entity menu interface contextMenuOptions { - disableUnregister: boolean; + disableUnregister: boolean | VisibleType; } interface EntityContextMenuProps { @@ -98,11 +100,35 @@ export function EntityContextMenu(props: EntityContextMenuProps) { , ]; + const isBoolean = + typeof UNSTABLE_contextMenuOptions?.disableUnregister === 'boolean'; + const disableUnregister = (!unregisterPermission.allowed || - UNSTABLE_contextMenuOptions?.disableUnregister) ?? + (isBoolean + ? UNSTABLE_contextMenuOptions?.disableUnregister + : UNSTABLE_contextMenuOptions?.disableUnregister === 'disabled')) ?? false; + let unregisterButton = <>; + + if (UNSTABLE_contextMenuOptions?.disableUnregister !== 'hidden') { + unregisterButton = ( + { + onClose(); + onUnregisterEntity(); + }} + disabled={disableUnregister} + > + + + + + + ); + } + return ( <> {extraItems} - { - onClose(); - onUnregisterEntity(); - }} - disabled={disableUnregister} - > - - - - - + {unregisterButton} { onClose(); diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx index 693137fe21..4cbb72db5c 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx @@ -142,9 +142,11 @@ interface ExtraContextMenuItem { onClick: () => void; } +type VisibleType = 'visible' | 'hidden' | 'disabled'; + // unstable context menu option, eg: disable the unregister entity menu interface contextMenuOptions { - disableUnregister: boolean; + disableUnregister: boolean | VisibleType; } /** @public */ From 9083fae0885053988fed87d64c124f1a6e70ba47 Mon Sep 17 00:00:00 2001 From: sjeyaraman Date: Sun, 5 Jun 2022 17:10:23 -0700 Subject: [PATCH 003/401] add-change-set Signed-off-by: sjeyaraman --- .changeset/cold-apples-film.md | 9 +++++++++ packages/app/src/components/catalog/EntityPage.tsx | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .changeset/cold-apples-film.md diff --git a/.changeset/cold-apples-film.md b/.changeset/cold-apples-film.md new file mode 100644 index 0000000000..d8c7ec283d --- /dev/null +++ b/.changeset/cold-apples-film.md @@ -0,0 +1,9 @@ +--- +'example-app': patch +'@backstage/plugin-catalog': patch +--- + +Adding ability to customize the "unregister entity" menu item in the entity context menu on the entity page with options 'visible','hidden','disabled'. +With this three new options, one can hide the "unregister entity" menu item from the list, disable or keep it enabled. + +The boolean input for "unregister entity" will be deprecated later in favour of the above three options. diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index ab8c02c168..fc17ccf909 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -168,7 +168,7 @@ const EntityLayoutWrapper = (props: { children?: ReactNode }) => { }; const options: contextMenuOptions = { - disableUnregister: false, + disableUnregister: 'visible', }; return ( From d09cee28ba428ea0a036929ca31ef36730988126 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Wed, 8 Jun 2022 13:11:30 +0200 Subject: [PATCH 004/401] docs: add sign-in resolvers in getting started auth Signed-off-by: Himanshu Mishra --- docs/auth/identity-resolver.md | 151 ++++++++++++++++++++------------- 1 file changed, 90 insertions(+), 61 deletions(-) diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index c7f90d0bbf..3acdf7ea20 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -79,6 +79,7 @@ Now let's look at the example, with the rest of the commentary being made with i the code comments: ```ts +// File: packages/backend/src/plugins/auth.ts import { createRouter, providers, @@ -139,11 +140,21 @@ property of each of the auth provider integrations. For example, the Google prov a built in resolver that works just like the one we defined above: ```ts -providers.google.create({ - signIn: { - resolver: providers.google.resolvers.emailLocalPartMatchingUserEntityName(), - }, -}); +// File: packages/backend/src/plugins/auth.ts +export default async function createPlugin( + // ... + return await createRouter({ + // ... + providerFactories: { + // ... + google: providers.google.create({ + signIn: { + resolver: providers.google.resolvers.emailLocalPartMatchingUserEntityName(), + }, + }); + } + }) +) ``` There are also other options, like the this one that looks up a user @@ -164,40 +175,49 @@ that happens during sign-in you can replace `ctx.signInWithCatalogUser` with a s of lower-level calls: ```ts +// File: packages/backend/src/plugins/auth.ts import { getDefaultOwnershipRefs } from '@backstage/plugin-auth-backend'; -// This example only shows the resolver function itself. -async ({ profile: { email } }, ctx) => { - if (!email) { - throw new Error('User profile contained no email'); - } +export default async function createPlugin( + // ... + return await createRouter({ + // ... + providerFactories: { + // ... + google: async ({ profile: { email } }, ctx) => { + if (!email) { + throw new Error('User profile contained no email'); + } - // This step calls the catalog to look up a user entity. You could for example - // replace it with a call to a different external system. - const { entity } = await ctx.findCatalogUser({ - annotations: { - 'acme.org/email': email, - }, - }); + // This step calls the catalog to look up a user entity. You could for example + // replace it with a call to a different external system. + const { entity } = await ctx.findCatalogUser({ + annotations: { + 'acme.org/email': email, + }, + }); - // In this step we extract the ownership references from the user entity using - // the standard logic. It uses a reference to the entity itself, as well as the - // target of each `memberOf` relation where the target is of the kind `Group`. - // - // If you replace the catalog lookup with something does not return - // an entity you will need to replace this step as well. - // - // You might also replace it if you for example want to filter out certain groups. - const ownershipRefs = getDefaultOwnershipRefs(entity); + // In this step we extract the ownership references from the user entity using + // the standard logic. It uses a reference to the entity itself, as well as the + // target of each `memberOf` relation where the target is of the kind `Group`. + // + // If you replace the catalog lookup with something does not return + // an entity you will need to replace this step as well. + // + // You might also replace it if you for example want to filter out certain groups. + const ownershipRefs = getDefaultOwnershipRefs(entity); - // The last step is to issue the token, where we might provide more options in the future. - return ctx.issueToken({ - claims: { - sub: stringifyEntityRef(entity), - ent: ownershipRefs, - }, - }); -}; + // The last step is to issue the token, where we might provide more options in the future. + return ctx.issueToken({ + claims: { + sub: stringifyEntityRef(entity), + ent: ownershipRefs, + }, + }); + }; + } + }) +) ``` ## Sign-In without Users in the Catalog @@ -221,38 +241,47 @@ check like in the example below, or you might for example look up the GitHub org that the user belongs to using the user access token in the provided result object. ```ts +// File: packages/backend/src/plugins/auth.ts import { DEFAULT_NAMESPACE, stringifyEntityRef, } from '@backstage/catalog-model'; -// This example only shows the resolver function itself. -async ({ profile }, ctx) => { - if (!profile.email) { - throw new Error( - 'Login failed, user profile does not contain an email', - ); - } - // Split the email into the local part and the domain. - const [localPart, domain] = profile.email.split('@'); +export default async function createPlugin( + // ... + return await createRouter({ + // ... + providerFactories: { + // ... + google: async ({ profile }, ctx) => { + if (!profile.email) { + throw new Error( + 'Login failed, user profile does not contain an email', + ); + } + // Split the email into the local part and the domain. + const [localPart, domain] = profile.email.split('@'); - // Next we verify the email domain. It is recommended to include this - // kind of check if you don't look up the user in an external service. - if (domain !== 'acme.org') { - throw new Error('Login failed, user email domain check failed'); - } + // Next we verify the email domain. It is recommended to include this + // kind of check if you don't look up the user in an external service. + if (domain !== 'acme.org') { + throw new Error('Login failed, user email domain check failed'); + } - // By using `stringifyEntityRef` we ensure that the reference is formatted correctly - const userEntityRef = stringifyEntityRef({ - kind: 'User', - name: localPart, - namespace: DEFAULT_NAMESPACE, - }); + // By using `stringifyEntityRef` we ensure that the reference is formatted correctly + const userEntityRef = stringifyEntityRef({ + kind: 'User', + name: localPart, + namespace: DEFAULT_NAMESPACE, + }); - return ctx.issueToken({ - claims: { - sub: userEntityRef, - ent: [userEntityRef], - }, - }); -}, + return ctx.issueToken({ + claims: { + sub: userEntityRef, + ent: [userEntityRef], + }, + }); + }, + } + }) +) ``` ## AuthHandler From 9cee3738fc0a4fc6f6d1f61f7a19076dfedf2d6b Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Wed, 8 Jun 2022 13:12:04 +0200 Subject: [PATCH 005/401] docs: nits to identity resolver page Signed-off-by: Himanshu Mishra --- docs/getting-started/configuration.md | 73 ++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/docs/getting-started/configuration.md b/docs/getting-started/configuration.md index 1dc1ffc1b1..c16f4145ed 100644 --- a/docs/getting-started/configuration.md +++ b/docs/getting-started/configuration.md @@ -155,6 +155,8 @@ auth: clientSecret: YOUR CLIENT SECRET ``` +### Add sign-in option to the frontend + Backstage will re-read the configuration. If there's no errors, that's great! We can continue with the last part of the configuration. The next step is needed to change the sign-in page, this you actually need to add in the source code. @@ -187,10 +189,77 @@ components: { }, ``` +### Add sign-in resolver in the backend + +The Auth backend plugin is responsible for assigning a [Backstage User Identity](../auth/identity-resolver.md#backstage-user-identity) when a user signs in. The identity is mainly used in determining the ownership of Backstage Catalog entities for the user. This is achieved by writing a [Sign In Resolver](../auth/identity-resolver.md#sign-in-resolvers) which takes care of assigning the right identity to the user and even reject SignIn attempts from unrecognized email domains. + > Since [v1.1.0](https://github.com/backstage/backstage/releases/tag/v1.1.0-next.3), you must provide an [explicit sign-in resolver](../auth/identity-resolver.md). -That should be it. You can stop your Backstage App. When you start it again and -go to your Backstage portal in your browser, you should have your login prompt! +For now, let's create a simple Sign In Resolver which uses the first part of the user's email address to assign a Backstage User Identity. + +Replace your `packages/backend/src/plugin/auth.ts` file with the following + +```tsx +import { + createRouter, + providers, + defaultAuthProviderFactories, +} from '@backstage/plugin-auth-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + return await createRouter({ + logger: env.logger, + config: env.config, + database: env.database, + discovery: env.discovery, + tokenManager: env.tokenManager, + providerFactories: { + ...defaultAuthProviderFactories, + github: providers.github.create({ + signIn: { + resolver: async ({ profile }, ctx) => { + if (!profile.email) { + throw new Error( + 'Login failed, user profile does not contain an email', + ); + } + // Split the email into the local part and the domain. + // You can use the email domain and verify that it belongs to your own org + // e.g. acme.org. It is recommended to include this kind of check for security. + const [localPart, _] = profile.email.split('@'); + + // By using `stringifyEntityRef` we ensure that the reference is formatted correctly + const userEntityRef = stringifyEntityRef({ + kind: 'User', + name: localPart, + namespace: DEFAULT_NAMESPACE, + }); + + return ctx.issueToken({ + claims: { + sub: userEntityRef, + ent: [userEntityRef], + }, + }); + }, + }, + }), + }, + }); +} +``` + +Restart Backstage from the terminal, by stopping it with `Control-C`, and starting it with `yarn dev` . You should be welcomed by a login prompt! + +> Note: Sometimes the frontend starts before the backend resulting in errors on the sign in page. Wait for the backend to start and then reload Backstage to proceed. To learn more about Authentication in Backstage, here are some docs you could read: From 04fa958718a74359fab77ddd1b13a19aee98a606 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Wed, 8 Jun 2022 17:46:56 +0200 Subject: [PATCH 006/401] correct explanation of user identity and include domain check in email addresses Signed-off-by: Himanshu Mishra --- docs/auth/identity-resolver.md | 2 +- docs/getting-started/configuration.md | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index 3acdf7ea20..3ae2f46360 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -201,7 +201,7 @@ export default async function createPlugin( // the standard logic. It uses a reference to the entity itself, as well as the // target of each `memberOf` relation where the target is of the kind `Group`. // - // If you replace the catalog lookup with something does not return + // If you replace the catalog lookup with something that does not return // an entity you will need to replace this step as well. // // You might also replace it if you for example want to filter out certain groups. diff --git a/docs/getting-started/configuration.md b/docs/getting-started/configuration.md index c16f4145ed..7c7fa4d98f 100644 --- a/docs/getting-started/configuration.md +++ b/docs/getting-started/configuration.md @@ -191,13 +191,15 @@ components: { ### Add sign-in resolver in the backend -The Auth backend plugin is responsible for assigning a [Backstage User Identity](../auth/identity-resolver.md#backstage-user-identity) when a user signs in. The identity is mainly used in determining the ownership of Backstage Catalog entities for the user. This is achieved by writing a [Sign In Resolver](../auth/identity-resolver.md#sign-in-resolvers) which takes care of assigning the right identity to the user and even reject SignIn attempts from unrecognized email domains. +The Auth backend plugin is responsible for assigning a [Backstage User Identity](../auth/identity-resolver.md#backstage-user-identity) when a user signs in. The Backstage User Identity is fundamentally a JSON Web Token which consists of two parts. The `sub` part is your identity and that identifies your user for any purpose within the Backstage ecosystem. It can for example be used as a primary key in a per-user settings backend, or any other similar purpose. The ent is what relates to ownership (the list of entity refs through which you claim ownership). This is achieved by writing a [Sign In Resolver](../auth/identity-resolver.md#sign-in-resolvers) which takes care of assigning the right identity to the user and even reject SignIn attempts from unrecognized email domains. > Since [v1.1.0](https://github.com/backstage/backstage/releases/tag/v1.1.0-next.3), you must provide an [explicit sign-in resolver](../auth/identity-resolver.md). For now, let's create a simple Sign In Resolver which uses the first part of the user's email address to assign a Backstage User Identity. -Replace your `packages/backend/src/plugin/auth.ts` file with the following +Replace your `packages/backend/src/plugin/auth.ts` file with the following. + +> NOTE: Please update `acme.org` below with your actual company domain to allow login requests from email address with that domain. ```tsx import { @@ -232,9 +234,13 @@ export default async function createPlugin( ); } // Split the email into the local part and the domain. - // You can use the email domain and verify that it belongs to your own org - // e.g. acme.org. It is recommended to include this kind of check for security. - const [localPart, _] = profile.email.split('@'); + const [localPart, domain] = profile.email.split('@'); + + // Next we verify the email domain. It is recommended to include this + // kind of check if you don't look up the user in an external service. + if (domain !== 'acme.org') { + throw new Error('Login failed, user email domain check failed'); + } // By using `stringifyEntityRef` we ensure that the reference is formatted correctly const userEntityRef = stringifyEntityRef({ From 88c697dc89309e3cca1853df228e308d9e7b94e7 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Wed, 8 Jun 2022 17:51:09 +0200 Subject: [PATCH 007/401] vale: add ent in keyword Signed-off-by: Himanshu Mishra --- .github/vale/Vocab/Backstage/accept.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/vale/Vocab/Backstage/accept.txt b/.github/vale/Vocab/Backstage/accept.txt index 7631640199..b87f5d3881 100644 --- a/.github/vale/Vocab/Backstage/accept.txt +++ b/.github/vale/Vocab/Backstage/accept.txt @@ -92,6 +92,7 @@ elasticsearch esbuild eslint etag +ent Expedia facto failover From 13767bd7b69d745cad8ec9bc54c8e0ac78ee2af1 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Thu, 9 Jun 2022 13:49:43 +0200 Subject: [PATCH 008/401] remove custom sign in resolver Signed-off-by: Himanshu Mishra --- docs/getting-started/configuration.md | 74 +-------------------------- 1 file changed, 1 insertion(+), 73 deletions(-) diff --git a/docs/getting-started/configuration.md b/docs/getting-started/configuration.md index 7c7fa4d98f..286c6cff1e 100644 --- a/docs/getting-started/configuration.md +++ b/docs/getting-started/configuration.md @@ -189,79 +189,7 @@ components: { }, ``` -### Add sign-in resolver in the backend - -The Auth backend plugin is responsible for assigning a [Backstage User Identity](../auth/identity-resolver.md#backstage-user-identity) when a user signs in. The Backstage User Identity is fundamentally a JSON Web Token which consists of two parts. The `sub` part is your identity and that identifies your user for any purpose within the Backstage ecosystem. It can for example be used as a primary key in a per-user settings backend, or any other similar purpose. The ent is what relates to ownership (the list of entity refs through which you claim ownership). This is achieved by writing a [Sign In Resolver](../auth/identity-resolver.md#sign-in-resolvers) which takes care of assigning the right identity to the user and even reject SignIn attempts from unrecognized email domains. - -> Since [v1.1.0](https://github.com/backstage/backstage/releases/tag/v1.1.0-next.3), you must provide an [explicit sign-in resolver](../auth/identity-resolver.md). - -For now, let's create a simple Sign In Resolver which uses the first part of the user's email address to assign a Backstage User Identity. - -Replace your `packages/backend/src/plugin/auth.ts` file with the following. - -> NOTE: Please update `acme.org` below with your actual company domain to allow login requests from email address with that domain. - -```tsx -import { - createRouter, - providers, - defaultAuthProviderFactories, -} from '@backstage/plugin-auth-backend'; -import { Router } from 'express'; -import { PluginEnvironment } from '../types'; -import { - DEFAULT_NAMESPACE, - stringifyEntityRef, -} from '@backstage/catalog-model'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - return await createRouter({ - logger: env.logger, - config: env.config, - database: env.database, - discovery: env.discovery, - tokenManager: env.tokenManager, - providerFactories: { - ...defaultAuthProviderFactories, - github: providers.github.create({ - signIn: { - resolver: async ({ profile }, ctx) => { - if (!profile.email) { - throw new Error( - 'Login failed, user profile does not contain an email', - ); - } - // Split the email into the local part and the domain. - const [localPart, domain] = profile.email.split('@'); - - // Next we verify the email domain. It is recommended to include this - // kind of check if you don't look up the user in an external service. - if (domain !== 'acme.org') { - throw new Error('Login failed, user email domain check failed'); - } - - // By using `stringifyEntityRef` we ensure that the reference is formatted correctly - const userEntityRef = stringifyEntityRef({ - kind: 'User', - name: localPart, - namespace: DEFAULT_NAMESPACE, - }); - - return ctx.issueToken({ - claims: { - sub: userEntityRef, - ent: [userEntityRef], - }, - }); - }, - }, - }), - }, - }); -} -``` +> Note: The default Backstage app comes with a guest Sign In Resolver. This resolver makes all users share a single "guest" identity and is only intended as a minimum requirement to quickly get up and running. You can read more about how [Sign In Resolvers](../auth/identity-resolver.md#sign-in-resolvers) play a role in creating a [Backstage User Identity](../auth/identity-resolver.md#backstage-user-identity) for logged in users. Restart Backstage from the terminal, by stopping it with `Control-C`, and starting it with `yarn dev` . You should be welcomed by a login prompt! From 389f6602bcb4e988366e105725f5d616bbdb2a69 Mon Sep 17 00:00:00 2001 From: "Daniele.Mazzotta" Date: Fri, 10 Jun 2022 17:38:19 +0400 Subject: [PATCH 009/401] added new getDags method to fetch specific dagIds Signed-off-by: Daniele.Mazzotta --- .../src/api/ApacheAirflowApi.ts | 1 + .../src/api/ApacheAirflowClient.test.ts | 27 +++++++++++++++++++ .../src/api/ApacheAirflowClient.ts | 19 +++++++++++++ .../DagTableComponent/DagTableComponent.tsx | 17 +++++++++++- 4 files changed, 63 insertions(+), 1 deletion(-) diff --git a/plugins/apache-airflow/src/api/ApacheAirflowApi.ts b/plugins/apache-airflow/src/api/ApacheAirflowApi.ts index dde8c57802..838292524a 100644 --- a/plugins/apache-airflow/src/api/ApacheAirflowApi.ts +++ b/plugins/apache-airflow/src/api/ApacheAirflowApi.ts @@ -25,6 +25,7 @@ export type ApacheAirflowApi = { discoveryApi: DiscoveryApi; baseUrl: string; listDags(options?: { objectsPerRequest: number }): Promise; + getDags(dagIds: string[]): Promise<{ dags: Dag[]; dagsNotFound: string[] }>; updateDag(dagId: string, isPaused: boolean): Promise; getInstanceStatus(): Promise; getInstanceVersion(): Promise; diff --git a/plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts b/plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts index 704b17eb34..e56c622ff2 100644 --- a/plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts +++ b/plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts @@ -160,4 +160,31 @@ describe('ApacheAirflowClient', () => { expect(response.dag_id).toEqual(dagId); expect(response.is_paused).toEqual(true); }); + + it('get only some dags', async () => { + setupHandlers(); + const client = new ApacheAirflowClient({ + discoveryApi: discoveryApi, + baseUrl: 'localhost:8080/', + }); + const dagIds = ['mock_dag_1', 'mock_dag_3']; + const response = await client.getDags(dagIds); + expect(response.dags.length).toEqual(dagIds.length); + response.dags.forEach((dag, index) => + expect(dag.dag_id).toEqual(dagIds[index]), + ); + expect(response.dagsNotFound.length).toEqual(0); + }); + + it('get dags but ignore NOT FOUND errors', async () => { + setupHandlers(); + const client = new ApacheAirflowClient({ + discoveryApi: discoveryApi, + baseUrl: 'localhost:8080/', + }); + const dagIds = ['mock_dag_1', 'a-random-DAG-id']; + const response = await client.getDags(dagIds); + expect(response.dags[0].dag_id).toEqual('mock_dag_1'); + expect(response.dagsNotFound[0]).toEqual('a-random-DAG-id'); + }); }); diff --git a/plugins/apache-airflow/src/api/ApacheAirflowClient.ts b/plugins/apache-airflow/src/api/ApacheAirflowClient.ts index 847a1b1ca6..bf095a96ba 100644 --- a/plugins/apache-airflow/src/api/ApacheAirflowClient.ts +++ b/plugins/apache-airflow/src/api/ApacheAirflowClient.ts @@ -75,6 +75,25 @@ export class ApacheAirflowClient implements ApacheAirflowApi { return dags; } + async getDags( + dagIds: string[], + ): Promise<{ dags: Dag[]; dagsNotFound: string[] }> { + const dagsNotFound: string[] = []; + const response = await Promise.all( + dagIds.map(id => { + return this.fetch(`/dags/${id}`).catch(e => { + if (e.message === 'NOT FOUND') { + dagsNotFound.push(id); + } else { + throw e; + } + }); + }), + ); + const dags = response.filter(Boolean) as Dag[]; + return { dags, dagsNotFound }; + } + async updateDag(dagId: string, isPaused: boolean): Promise { const init = { method: 'PATCH', diff --git a/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx b/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx index cf343a596e..18439e1902 100644 --- a/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx +++ b/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx @@ -129,9 +129,24 @@ export const DenseTable = ({ dags }: DenseTableProps) => { ); }; -export const DagTableComponent = () => { +type DagTableComponentProps = { + dagIds?: string[]; +}; + +export const DagTableComponent = ({ dagIds }: DagTableComponentProps) => { const apiClient = useApi(apacheAirflowApiRef); const { value, loading, error } = useAsync(async (): Promise => { + if (dagIds) { + const { dags, dagsNotFound } = await apiClient.getDags(dagIds); + if (dagsNotFound.length) { + throw new Error( + `${dagsNotFound.length} DAGs were not found:\n${dagsNotFound.join( + ';\n', + )}`, + ); + } + return dags; + } return await apiClient.listDags(); }, []); From b1c5b42849a400dbac414df2d8f20c813d2111a1 Mon Sep 17 00:00:00 2001 From: Alex Rybchenko Date: Wed, 15 Jun 2022 13:58:33 +0200 Subject: [PATCH 010/401] added type predicates for entities Signed-off-by: Alex Rybchenko --- .../src/components/EntitySwitch/conditions.ts | 61 ++++++++++++++++++- .../src/components/EntitySwitch/index.ts | 2 +- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/plugins/catalog/src/components/EntitySwitch/conditions.ts b/plugins/catalog/src/components/EntitySwitch/conditions.ts index 5f50fde5c9..b6837627c0 100644 --- a/plugins/catalog/src/components/EntitySwitch/conditions.ts +++ b/plugins/catalog/src/components/EntitySwitch/conditions.ts @@ -14,7 +14,17 @@ * limitations under the License. */ -import { Entity, ComponentEntity } from '@backstage/catalog-model'; +import { + ApiEntity, + ComponentEntity, + DomainEntity, + Entity, + GroupEntity, + LocationEntity, + ResourceEntity, + SystemEntity, + UserEntity, +} from '@backstage/catalog-model'; function strCmp(a: string | undefined, b: string | undefined): boolean { return Boolean( @@ -57,3 +67,52 @@ export function isComponentType(types: string | string[]) { export function isNamespace(namespaces: string | string[]) { return (entity: Entity) => strCmpAll(entity.metadata?.namespace, namespaces); } + +/** + * @public + */ +export function isApiEntity(entity: Entity): entity is ApiEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'API'; +} +/** + * @public + */ +export function isComponentEntity(entity: Entity): entity is ComponentEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'COMPONENT'; +} +/** + * @public + */ +export function isDomainEntity(entity: Entity): entity is DomainEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'DOMAIN'; +} +/** + * @public + */ +export function isGroupEntity(entity: Entity): entity is GroupEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'GROUP'; +} +/** + * @public + */ +export function isLocationEntity(entity: Entity): entity is LocationEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'LOCATION'; +} +/** + * @public + */ +export function isResourceEntity(entity: Entity): entity is ResourceEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'RESOURCE'; +} +/** + * @public + */ +export function isSystemEntity(entity: Entity): entity is SystemEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'SYSTEM'; +} +/** + * @public + */ +export function isUserEntity(entity: Entity): entity is UserEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'USER'; +} diff --git a/plugins/catalog/src/components/EntitySwitch/index.ts b/plugins/catalog/src/components/EntitySwitch/index.ts index 6cccf4be6e..5d0537bfa5 100644 --- a/plugins/catalog/src/components/EntitySwitch/index.ts +++ b/plugins/catalog/src/components/EntitySwitch/index.ts @@ -16,4 +16,4 @@ export { EntitySwitch } from './EntitySwitch'; export type { EntitySwitchProps, EntitySwitchCaseProps } from './EntitySwitch'; -export { isKind, isNamespace, isComponentType } from './conditions'; +export * from './conditions'; From 49ff472c0b527bea729ce23de65fb62f26dd20ad Mon Sep 17 00:00:00 2001 From: ivgo Date: Wed, 15 Jun 2022 15:27:30 +0200 Subject: [PATCH 011/401] Add possibility to scan all the groups in the project Signed-off-by: ivgo --- .changeset/two-crews-accept.md | 14 ++++ docs/integrations/gitlab/discovery.md | 12 +++ .../catalog-backend-module-gitlab/config.d.ts | 74 ++++++++++++------- .../src/providers/config.test.ts | 17 +++++ .../src/providers/config.ts | 17 ++++- 5 files changed, 105 insertions(+), 29 deletions(-) create mode 100644 .changeset/two-crews-accept.md diff --git a/.changeset/two-crews-accept.md b/.changeset/two-crews-accept.md new file mode 100644 index 0000000000..5885b1346d --- /dev/null +++ b/.changeset/two-crews-accept.md @@ -0,0 +1,14 @@ +--- +'@backstage/plugin-catalog-backend-module-gitlab': minor +--- + +Add the possibility in the `GitlabDiscoveryEntityProvider` to scan the whole project instead of concrete groups. For that, use a configuration like this one: + +```yaml +catalog: + providers: + gitlab: + host: gitlab-host # Identifies one of the hosts set up in the integrations + branch: main # Optional. Uses `master` as default + entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml` +``` diff --git a/docs/integrations/gitlab/discovery.md b/docs/integrations/gitlab/discovery.md index ea151e9415..a7dd0a03df 100644 --- a/docs/integrations/gitlab/discovery.md +++ b/docs/integrations/gitlab/discovery.md @@ -26,6 +26,18 @@ catalog: entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml` ``` +If you desire so, it's also possible to execute the gitlab discovery in your entire +project. In order to do that, use this catalog configuration instead: + +```yaml +catalog: + providers: + gitlab: + host: gitlab-host # Identifies one of the hosts set up in the integrations + branch: main # Optional. Uses `master` as default + entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml` +``` + As this provider is not one of the default providers, you will first need to install the gitlab catalog plugin: diff --git a/plugins/catalog-backend-module-gitlab/config.d.ts b/plugins/catalog-backend-module-gitlab/config.d.ts index 5b0d42c81d..0626cb9488 100644 --- a/plugins/catalog-backend-module-gitlab/config.d.ts +++ b/plugins/catalog-backend-module-gitlab/config.d.ts @@ -23,33 +23,53 @@ export interface Config { /** * GitlabDiscoveryEntityProvider configuration */ - gitlab?: Record< - string, - { - /** - * (Required) Gitlab's host name. - * @visibility backend - */ - host: string; - /** - * (Required) Gitlab's group[/subgroup] where the discovery is done. - * @visibility backend - */ - group: string; - /** - * (Optional) Default branch to read the catalog-info.yaml file. - * If not set, 'master' will be used. - * @visibility backend - */ - branch?: string; - /** - * (Optional) The name used for the catalog file. - * If not set, 'catalog-info.yaml' will be used. - * @visibility backend - */ - entityFilename?: string; - } - >; + gitlab?: + | { + /** + * (Required) Gitlab's host name. + * @visibility backend + */ + host: string; + /** + * (Optional) Default branch to read the catalog-info.yaml file. + * If not set, 'master' will be used. + * @visibility backend + */ + branch?: string; + /** + * (Optional) The name used for the catalog file. + * If not set, 'catalog-info.yaml' will be used. + * @visibility backend + */ + entityFilename?: string; + } + | Record< + string, + { + /** + * (Required) Gitlab's host name. + * @visibility backend + */ + host: string; + /** + * (Required) Gitlab's group[/subgroup] where the discovery is done. + * @visibility backend + */ + group: string; + /** + * (Optional) Default branch to read the catalog-info.yaml file. + * If not set, 'master' will be used. + * @visibility backend + */ + branch?: string; + /** + * (Optional) The name used for the catalog file. + * If not set, 'catalog-info.yaml' will be used. + * @visibility backend + */ + entityFilename?: string; + } + >; }; }; } diff --git a/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts index d28930b151..429743f4bd 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts @@ -103,4 +103,21 @@ describe('config', () => { "Missing required config value at 'catalog.providers.gitlab.test.group'", ); }); + + it('read full gitlab project', () => { + const config = new ConfigReader({ + catalog: { + providers: { + gitlab: { + host: 'host', + branch: 'main', + }, + }, + }, + }); + + const result = readGitlabConfigs(config); + expect(result).toHaveLength(1); + expect(result[0].group).toEqual(''); + }); }); diff --git a/plugins/catalog-backend-module-gitlab/src/providers/config.ts b/plugins/catalog-backend-module-gitlab/src/providers/config.ts index f7eb316997..56df8d62f1 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/config.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/config.ts @@ -48,6 +48,7 @@ function readGitlabConfig(id: string, config: Config): GitlabProviderConfig { * @param config - The config object to extract from */ export function readGitlabConfigs(config: Config): GitlabProviderConfig[] { + const reservedParams = ['host', 'group', 'branch', 'catalogFile']; const configs: GitlabProviderConfig[] = []; const providerConfigs = config.getOptionalConfig('catalog.providers.gitlab'); @@ -56,8 +57,20 @@ export function readGitlabConfigs(config: Config): GitlabProviderConfig[] { return configs; } - for (const id of providerConfigs.keys()) { - configs.push(readGitlabConfig(id, providerConfigs.getConfig(id))); + if (providerConfigs.keys().some(r => reservedParams.indexOf(r) >= 0)) { + configs.push({ + id: 'full-check', + group: '', + branch: providerConfigs.getOptionalString('branch') ?? 'master', + host: providerConfigs.getString('host'), + catalogFile: + providerConfigs.getOptionalString('entityFilename') ?? + 'catalog-info.yaml', + }); + } else { + for (const id of providerConfigs.keys()) { + configs.push(readGitlabConfig(id, providerConfigs.getConfig(id))); + } } return configs; From 8ebc1dea21c221e5395945026d8de46b619aa358 Mon Sep 17 00:00:00 2001 From: Maximilian Ressel Date: Tue, 31 May 2022 16:08:50 +0200 Subject: [PATCH 012/401] Add allowedRepos Option and move Repo Field to own Component Signed-off-by: Maximilian Ressel --- .../fields/RepoUrlPicker/AzureRepoPicker.tsx | 15 +--- .../RepoUrlPicker/BitbucketRepoPicker.tsx | 15 +--- .../fields/RepoUrlPicker/GerritRepoPicker.tsx | 15 +--- .../fields/RepoUrlPicker/GithubRepoPicker.tsx | 15 +--- .../fields/RepoUrlPicker/GitlabRepoPicker.tsx | 18 +---- .../fields/RepoUrlPicker/RepoUrlPicker.tsx | 19 +++++ .../RepoUrlPicker/RepoUrlPickerRepoName.tsx | 77 +++++++++++++++++++ 7 files changed, 103 insertions(+), 71 deletions(-) create mode 100644 plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerRepoName.tsx diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx index 0efadf4017..0246c67801 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx @@ -27,7 +27,7 @@ export const AzureRepoPicker = (props: { rawErrors: string[]; }) => { const { rawErrors, state, onChange } = props; - const { organization, repoName, owner } = state; + const { organization, owner } = state; return ( <> The Owner that this repo will belong to - 0 && !repoName} - > - Repository - onChange({ repoName: e.target.value })} - value={repoName} - /> - The name of the repository - ); }; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx index 64eb4b8dac..fc59398d94 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx @@ -26,7 +26,7 @@ export const BitbucketRepoPicker = (props: { rawErrors: string[]; }) => { const { onChange, rawErrors, state } = props; - const { host, workspace, project, repoName } = state; + const { host, workspace, project } = state; return ( <> {host === 'bitbucket.org' && ( @@ -61,19 +61,6 @@ export const BitbucketRepoPicker = (props: { The Project that this repo will belong to - 0 && !repoName} - > - Repository - onChange({ repoName: e.target.value })} - value={repoName} - /> - The name of the repository - ); }; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GerritRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GerritRepoPicker.tsx index 4cbc0859e8..f021948532 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GerritRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GerritRepoPicker.tsx @@ -26,7 +26,7 @@ export const GerritRepoPicker = (props: { rawErrors: string[]; }) => { const { onChange, rawErrors, state } = props; - const { workspace, repoName, owner } = state; + const { workspace, owner } = state; return ( <> - 0 && !repoName} - > - Repository - onChange({ repoName: e.target.value })} - value={repoName} - /> - The name of the repository - ); }; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx index 13b57cb449..ec59d89ef8 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx @@ -32,7 +32,7 @@ export const GithubRepoPicker = (props: { ? allowedOwners.map(i => ({ label: i, value: i })) : [{ label: 'Loading...', value: 'loading' }]; - const { owner, repoName } = state; + const { owner } = state; return ( <> @@ -66,19 +66,6 @@ export const GithubRepoPicker = (props: { The organization, user or project that this repo will belong to - 0 && !repoName} - > - Repository - onChange({ repoName: e.target.value })} - value={repoName} - /> - The name of the repository - ); }; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx index e2052b792f..c628b364b9 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx @@ -23,16 +23,17 @@ import { RepoUrlPickerState } from './types'; export const GitlabRepoPicker = (props: { allowedOwners?: string[]; + allowedRepos?: string[]; state: RepoUrlPickerState; onChange: (state: RepoUrlPickerState) => void; rawErrors: string[]; }) => { - const { allowedOwners = [], rawErrors, state, onChange } = props; + const { allowedOwners = [], state, onChange, rawErrors } = props; const ownerItems: SelectItem[] = allowedOwners ? allowedOwners.map(i => ({ label: i, value: i })) : [{ label: 'Loading...', value: 'loading' }]; - const { owner, repoName } = state; + const { owner } = state; return ( <> @@ -69,19 +70,6 @@ export const GitlabRepoPicker = (props: { namespaces in gitlab), that this repo will belong to - 0 && !repoName} - > - Repository - onChange({ repoName: e.target.value })} - value={repoName} - /> - The name of the repository - ); }; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index 6f762933b6..b36b332d9e 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -26,6 +26,7 @@ import { BitbucketRepoPicker } from './BitbucketRepoPicker'; import { GerritRepoPicker } from './GerritRepoPicker'; import { FieldExtensionComponentProps } from '../../../extensions'; import { RepoUrlPickerHost } from './RepoUrlPickerHost'; +import { RepoUrlPickerRepoName } from './RepoUrlPickerRepoName'; import { parseRepoPickerUrl, serializeRepoPickerUrl } from './utils'; import { RepoUrlPickerState } from './types'; import useDebounce from 'react-use/lib/useDebounce'; @@ -40,6 +41,7 @@ import { useTemplateSecrets } from '../../secrets'; export interface RepoUrlPickerUiOptions { allowedHosts?: string[]; allowedOwners?: string[]; + allowedRepos?: string[]; requestUserCredentials?: { secretsKey: string; additionalScopes?: { @@ -76,6 +78,10 @@ export const RepoUrlPicker = ( () => uiSchema?.['ui:options']?.allowedOwners ?? [], [uiSchema], ); + const allowedRepos = useMemo( + () => uiSchema?.['ui:options']?.allowedRepos ?? [], + [uiSchema], + ); useEffect(() => { onChange(serializeRepoPickerUrl(state)); @@ -87,6 +93,11 @@ export const RepoUrlPicker = ( setState(prevState => ({ ...prevState, owner: allowedOwners[0] })); } }, [setState, allowedOwners]); + useEffect(() => { + if (allowedRepos.length === 1) { + setState(prevState => ({ ...prevState, repoName: allowedRepos[0] })); + } + }, [setState, allowedRepos]); const updateLocalState = useCallback( (newState: RepoUrlPickerState) => { @@ -179,6 +190,14 @@ export const RepoUrlPicker = ( onChange={updateLocalState} /> )} + + setState(prevState => ({ ...prevState, repoName })) + } + rawErrors={rawErrors} + /> ); }; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerRepoName.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerRepoName.tsx new file mode 100644 index 0000000000..f2d68b08f3 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerRepoName.tsx @@ -0,0 +1,77 @@ +/* + * 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 React, { useEffect } from 'react'; +import { Select, SelectItem } from '@backstage/core-components'; +import FormControl from '@material-ui/core/FormControl'; +import FormHelperText from '@material-ui/core/FormHelperText'; +import Input from '@material-ui/core/Input'; +import InputLabel from '@material-ui/core/InputLabel'; + +export const RepoUrlPickerRepoName = (props: { + repoName?: string; + allowedRepos?: string[]; + onChange: (host: string) => void; + rawErrors: string[]; +}) => { + const { repoName, allowedRepos, onChange, rawErrors } = props; + + useEffect(() => { + // If there is no repoName chosen currently + if (!repoName) { + // Set the first of the allowedRepos option if that available + if (allowedRepos?.length) { + onChange(allowedRepos[0]); + } + } + }, [allowedRepos, repoName, onChange]); + + const repoItems: SelectItem[] = allowedRepos + ? allowedRepos.map(i => ({ label: i, value: i })) + : [{ label: 'Loading...', value: 'loading' }]; + + return ( + <> + 0 && !repoName} + > + {allowedRepos?.length ? ( + onChange(String(e.target.value))} + value={repoName} + /> + + )} + The name of the repository + + + ); +}; From d8eb82f4474f04f413896deee1fccf2db20287d9 Mon Sep 17 00:00:00 2001 From: Maixmilian Ressel Date: Wed, 15 Jun 2022 16:46:57 +0200 Subject: [PATCH 013/401] Add changeset Signed-off-by: Maixmilian Ressel --- .changeset/hot-rice-sin.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/hot-rice-sin.md diff --git a/.changeset/hot-rice-sin.md b/.changeset/hot-rice-sin.md new file mode 100644 index 0000000000..5a7363bdc8 --- /dev/null +++ b/.changeset/hot-rice-sin.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +RepoUrlPicker: Add allowedRepos option and move repoName field to own component From 2de3fc32316cf7256633a5cbb1b7306ce5cc33cc Mon Sep 17 00:00:00 2001 From: Maixmilian Ressel Date: Wed, 15 Jun 2022 17:18:44 +0200 Subject: [PATCH 014/401] Add documentation for allowedOwners and allowedRepos Signed-off-by: Maixmilian Ressel --- .../software-templates/writing-templates.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 27e6e76817..4b11cc4990 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -320,6 +320,30 @@ The `allowedHosts` part should be set to where you wish to enable this template to publish to. And it can be any host that is listed in your `integrations` config in `app-config.yaml`. +Besides specifying `allowedHosts` you can also restrict the template to publish to +repositories owned by specific users/groups/namespaces by setting the `allowedOwners` +option. With the `allowedRepos` option you are able to narrow it down further to a +specific set of repository names. A full example could look like this: + +```yaml +- title: Choose a location + required: + - repoUrl + properties: + repoUrl: + title: Repository Location + type: string + ui:field: RepoUrlPicker + ui:options: + allowedHosts: + - github.com + allowedOwners: + - backstage + - someGithubUser + allowedRepos: + - backstage +``` + The `RepoUrlPicker` is a custom field that we provide part of the `plugin-scaffolder`. You can provide your own custom fields by [writing your own Custom Field Extensions](./writing-custom-field-extensions.md) From 9d8ec897eb3a7f64153513983d5ed69c96ba32a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Tresarrieu?= Date: Thu, 16 Jun 2022 20:22:27 +0200 Subject: [PATCH 015/401] feat(MarkdownContent): Expose `transformLinkUri` and `transformImageUri` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Tresarrieu --- .../MarkdownContent/MarkdownContent.test.tsx | 38 +++++++++++++++++++ .../MarkdownContent/MarkdownContent.tsx | 12 +++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/packages/core-components/src/components/MarkdownContent/MarkdownContent.test.tsx b/packages/core-components/src/components/MarkdownContent/MarkdownContent.test.tsx index 2e1550078f..9c08b83c40 100644 --- a/packages/core-components/src/components/MarkdownContent/MarkdownContent.test.tsx +++ b/packages/core-components/src/components/MarkdownContent/MarkdownContent.test.tsx @@ -62,4 +62,42 @@ describe('', () => { expect(fp2).toBeInTheDocument(); expect(rendered.getByText(');', { selector: 'span' })).toBeInTheDocument(); }); + + it('render MarkdownContent component with transformed link', async () => { + const rendered = await renderWithEffects( + wrapInTestApp( + { + return `${href}-modified`; + }} + />, + ), + ); + const fp1 = rendered.getByText('Title', { + selector: 'a', + }); + expect(fp1).toBeInTheDocument(); + expect(fp1.getAttribute('href')).toEqual( + 'https://backstage.io/link-modified', + ); + }); + + it('render MarkdownContent component with transformed image', async () => { + const rendered = await renderWithEffects( + wrapInTestApp( + { + return `https://example.com/blog/assets/6/header.png`; + }} + />, + ), + ); + const fp1 = rendered.getByAltText('Image'); + expect(fp1).toBeInTheDocument(); + expect(fp1.getAttribute('src')).toEqual( + 'https://example.com/blog/assets/6/header.png', + ); + }); }); diff --git a/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx b/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx index df4ad56776..4059482b79 100644 --- a/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx +++ b/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx @@ -68,6 +68,8 @@ type Props = { content: string; dialect?: 'gfm' | 'common-mark'; linkTarget?: Options['linkTarget']; + transformLinkUri?: Options['transformLinkUri']; + transformImageUri?: Options['transformImageUri']; }; const components: Options['components'] = { @@ -91,7 +93,13 @@ const components: Options['components'] = { * If you just want to render to plain [CommonMark](https://commonmark.org/), set the dialect to `'common-mark'` */ export function MarkdownContent(props: Props) { - const { content, dialect = 'gfm', linkTarget } = props; + const { + content, + dialect = 'gfm', + linkTarget, + transformLinkUri, + transformImageUri, + } = props; const classes = useStyles(); return ( ); } From 32204fa79454a1ef5907b6cd5616e022a14b335b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Tresarrieu?= Date: Thu, 16 Jun 2022 20:23:14 +0200 Subject: [PATCH 016/401] chore: add changeset file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Tresarrieu --- .changeset/cool-toys-flow.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cool-toys-flow.md diff --git a/.changeset/cool-toys-flow.md b/.changeset/cool-toys-flow.md new file mode 100644 index 0000000000..799bf3ace4 --- /dev/null +++ b/.changeset/cool-toys-flow.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': minor +--- + +Add `transformLinkUri` and `transformImageUri` to `MarkdownContent` From 1e984b11fccefdffefb4544e448c6040bcd5a741 Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Thu, 16 Jun 2022 15:53:48 -0400 Subject: [PATCH 017/401] refactor(MyGroupsSidebarItem): Render namespaced teams within dropdown items Signed-off-by: Phil Kuang --- .changeset/shiny-turkeys-doubt.md | 5 +++ .../MyGroupsSidebarItem.tsx | 38 +++++++++++++++---- 2 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 .changeset/shiny-turkeys-doubt.md diff --git a/.changeset/shiny-turkeys-doubt.md b/.changeset/shiny-turkeys-doubt.md new file mode 100644 index 0000000000..0f663a4c74 --- /dev/null +++ b/.changeset/shiny-turkeys-doubt.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-org': patch +--- + +Render namespaced teams within dropdown items in `MyGroupsSidebarItem` diff --git a/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.tsx b/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.tsx index fa8f8405ff..445bf8e6fe 100644 --- a/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.tsx +++ b/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.tsx @@ -15,7 +15,11 @@ */ import React from 'react'; -import { stringifyEntityRef } from '@backstage/catalog-model'; +import { + DEFAULT_NAMESPACE, + Entity, + stringifyEntityRef, +} from '@backstage/catalog-model'; import { SidebarItem, SidebarSubmenu, @@ -32,7 +36,6 @@ import { catalogApiRef, CatalogApi, entityRouteRef, - humanizeEntityRef, } from '@backstage/plugin-catalog-react'; import { getCompoundEntityRef } from '@backstage/catalog-model'; @@ -87,23 +90,42 @@ export const MyGroupsSidebarItem = (props: { ); } + const namespacedGroupsMap: { [namespace: string]: Entity[] } = {}; + groups.forEach(group => { + namespacedGroupsMap[group.metadata.namespace!] = + namespacedGroupsMap[group.metadata.namespace!] ?? []; + namespacedGroupsMap[group.metadata.namespace!].push(group); + }); + // Member of more than one group return ( - {groups?.map(function groupsMap(group) { + {namespacedGroupsMap[DEFAULT_NAMESPACE]?.map(group => { return ( ); })} + {Object.entries(namespacedGroupsMap) + .filter(([n]) => n !== DEFAULT_NAMESPACE) + .map(([namespace, namespacedGroups]) => { + return ( + ({ + title: group.metadata.title || group.metadata.name, + to: catalogEntityRoute(getCompoundEntityRef(group)), + }))} + /> + ); + })} ); From 860765ff4524d68d18bf77b7b2895ea195b3c384 Mon Sep 17 00:00:00 2001 From: Niklas Aronsson Date: Mon, 25 Apr 2022 13:15:21 +0200 Subject: [PATCH 018/401] [TechDocs] Added configuration for local publishing target This patch adds a configuration option for setting the "local" techdocs target directory. The target directory can be set using the "techdocs.publisher.local.publishDirectory". This fixes two "TODOs" in the "plugins/techdocs-node/src/stages /publish/local.ts" file: * Use a more persistent storage than node_modules or /tmp directory. Make it configurable with techdocs.publisher.local.publishDirectory * Move the logic of setting staticDocsDir based on config over to fromConfig, and set the value as a class parameter. Signed-off-by: Niklas Aronsson --- .changeset/calm-experts-buy.md | 5 ++ .changeset/weak-llamas-repeat.md | 5 ++ docs/features/techdocs/configuration.md | 6 ++ plugins/techdocs-backend/config.d.ts | 7 +++ .../src/stages/publish/local.test.ts | 27 +++++++++ .../techdocs-node/src/stages/publish/local.ts | 57 +++++++++++-------- 6 files changed, 82 insertions(+), 25 deletions(-) create mode 100644 .changeset/calm-experts-buy.md create mode 100644 .changeset/weak-llamas-repeat.md diff --git a/.changeset/calm-experts-buy.md b/.changeset/calm-experts-buy.md new file mode 100644 index 0000000000..d95844e298 --- /dev/null +++ b/.changeset/calm-experts-buy.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-node': minor +--- + +Added local publishing target directory `config`: `techdocs.publisher.local.publishDirectory` diff --git a/.changeset/weak-llamas-repeat.md b/.changeset/weak-llamas-repeat.md new file mode 100644 index 0000000000..a86ce3a964 --- /dev/null +++ b/.changeset/weak-llamas-repeat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': minor +--- + +Added local publishing target directory `config`: `techdocs.publisher.local.publishDirectory` diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index d14850da77..9440d9a824 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -74,6 +74,12 @@ techdocs: type: 'local' + # Optional when techdocs.publisher.type is set to 'local'. + + local: + # (Optional). Set this to specify where the generated documentation is stored. + publishDirectory: '/path/to/local/directory' + # Required when techdocs.publisher.type is set to 'googleGcs'. Skip otherwise. googleGcs: diff --git a/plugins/techdocs-backend/config.d.ts b/plugins/techdocs-backend/config.d.ts index c64a001f5f..fdacc9cfbc 100644 --- a/plugins/techdocs-backend/config.d.ts +++ b/plugins/techdocs-backend/config.d.ts @@ -66,6 +66,13 @@ export interface Config { publisher?: | { type: 'local'; + + local?: { + /** + * Directory to store generated static files. + */ + publishDirectory?: string; + }; } | { type: 'awsS3'; diff --git a/plugins/techdocs-node/src/stages/publish/local.test.ts b/plugins/techdocs-node/src/stages/publish/local.test.ts index 0119def16d..ce2e0c3ccd 100644 --- a/plugins/techdocs-node/src/stages/publish/local.test.ts +++ b/plugins/techdocs-node/src/stages/publish/local.test.ts @@ -236,5 +236,32 @@ describe('local publisher', () => { ); expect(response.status).toBe(404); }); + + it('should work with a configured directory', async () => { + const customConfig = new ConfigReader({ + techdocs: { + publisher: { + local: { + publishDirectory: tmpDir, + }, + }, + }, + }); + mockFs({ + [tmpDir]: { + 'index.html': 'found it', + }, + }); + const legacyPublisher = LocalPublish.fromConfig( + customConfig, + logger, + testDiscovery, + ); + app = express().use(legacyPublisher.docsRouter()); + + const response = await request(app).get('/index.html'); + expect(response.status).toBe(200); + expect(response.text).toEqual('found it'); + }); }); }); diff --git a/plugins/techdocs-node/src/stages/publish/local.ts b/plugins/techdocs-node/src/stages/publish/local.ts index 11c6e6dd2b..1a02cb7eee 100644 --- a/plugins/techdocs-node/src/stages/publish/local.ts +++ b/plugins/techdocs-node/src/stages/publish/local.ts @@ -44,40 +44,27 @@ import { } from './helpers'; import { ForwardedError } from '@backstage/errors'; -// TODO: Use a more persistent storage than node_modules or /tmp directory. -// Make it configurable with techdocs.publisher.local.publishDirectory -let staticDocsDir = ''; -try { - staticDocsDir = resolvePackagePath( - '@backstage/plugin-techdocs-backend', - 'static/docs', - ); -} catch (err) { - // This will most probably never be used. - // The try/catch is introduced so that techdocs-cli can import @backstage/plugin-techdocs-node - // on CI/CD without installing techdocs backend plugin. - staticDocsDir = os.tmpdir(); -} - /** - * Local publisher which uses the local filesystem to store the generated static files. It uses a directory - * called "static" at the root of techdocs-backend plugin. + * Local publisher which uses the local filesystem to store the generated static files. It uses by default a + * directory called "static" at the root of techdocs-backend plugin unless a directory has been configured by + * "techdocs.publisher.local.publishDirectory". */ export class LocalPublish implements PublisherBase { private readonly legacyPathCasing: boolean; private readonly logger: Logger; private readonly discovery: PluginEndpointDiscovery; + private readonly staticDocsDir: string; - // TODO: Move the logic of setting staticDocsDir based on config over to - // fromConfig, and set the value as a class parameter. constructor(options: { logger: Logger; discovery: PluginEndpointDiscovery; legacyPathCasing: boolean; + staticDocsDir: string; }) { this.logger = options.logger; this.discovery = options.discovery; this.legacyPathCasing = options.legacyPathCasing; + this.staticDocsDir = options.staticDocsDir; } static fromConfig( @@ -90,10 +77,28 @@ export class LocalPublish implements PublisherBase { 'techdocs.legacyUseCaseSensitiveTripletPaths', ) || false; + let staticDocsDir = config.getOptionalString( + 'techdocs.publisher.local.publishDirectory', + ); + if (!staticDocsDir) { + try { + staticDocsDir = resolvePackagePath( + '@backstage/plugin-techdocs-backend', + 'static/docs', + ); + } catch (err) { + // This will most probably never be used. + // The try/catch is introduced so that techdocs-cli can import @backstage/plugin-techdocs-node + // on CI/CD without installing techdocs backend plugin. + staticDocsDir = os.tmpdir(); + } + } + return new LocalPublish({ logger, discovery, legacyPathCasing, + staticDocsDir, }); } @@ -144,7 +149,7 @@ export class LocalPublish implements PublisherBase { const techdocsApiUrl = await this.discovery.getBaseUrl('techdocs'); const publishedFilePaths = (await getFileTreeRecursively(publishDir)).map( abs => { - return abs.split(`${staticDocsDir}/`)[1]; + return abs.split(`${this.staticDocsDir}/`)[1]; }, ); @@ -222,9 +227,8 @@ export class LocalPublish implements PublisherBase { // Otherwise, redirect to the new path. return res.redirect(301, req.baseUrl + newPath); }); - router.use( - express.static(staticDocsDir, { + express.static(this.staticDocsDir, { // Handle content-type header the same as all other publishers. setHeaders: (res, filePath) => { const fileExtension = path.extname(filePath); @@ -275,13 +279,16 @@ export class LocalPublish implements PublisherBase { concurrency = 25, }): Promise { // Iterate through every file in the root of the publisher. - const files = await getFileTreeRecursively(staticDocsDir); + const files = await getFileTreeRecursively(this.staticDocsDir); const limit = createLimiter(concurrency); await Promise.all( files.map(f => limit(async file => { - const relativeFile = file.replace(`${staticDocsDir}${path.sep}`, ''); + const relativeFile = file.replace( + `${this.staticDocsDir}${path.sep}`, + '', + ); const newFile = lowerCaseEntityTripletInStoragePath(relativeFile); // If all parts are already lowercase, ignore. @@ -311,7 +318,7 @@ export class LocalPublish implements PublisherBase { * Utility wrapper around path.join(), used to control legacy case logic. */ protected staticEntityPathJoin(...allParts: string[]): string { - let staticEntityPath = staticDocsDir; + let staticEntityPath = this.staticDocsDir; allParts .map(part => part.split(path.sep)) From 874782422123e8b83f5ed428489e2fe10f24c859 Mon Sep 17 00:00:00 2001 From: planeiii Date: Thu, 16 Jun 2022 14:49:36 -0500 Subject: [PATCH 019/401] feat(jenkins-backend): add multiple branch support Signed-off-by: planeiii --- .changeset/many-vans-thank.md | 8 ++++++ .../src/service/jenkinsApi.test.ts | 28 +++++++++++++++++++ .../jenkins-backend/src/service/jenkinsApi.ts | 13 +++++---- plugins/jenkins/README.md | 7 +++-- 4 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 .changeset/many-vans-thank.md diff --git a/.changeset/many-vans-thank.md b/.changeset/many-vans-thank.md new file mode 100644 index 0000000000..942aa4cc7c --- /dev/null +++ b/.changeset/many-vans-thank.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-jenkins': patch +'@backstage/plugin-jenkins-backend': patch +--- + +--- + +feature: added support for multiple branches to the `JenkinsApi` diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.test.ts b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts index 29da414470..ad1a09a40f 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.test.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts @@ -134,6 +134,34 @@ describe('JenkinsApi', () => { }); expect(result).toHaveLength(1); }); + + it('supports multiple branches', async () => { + mockedJenkinsClient.job.get.mockResolvedValue(project); + + const result = await jenkinsApi.getProjects( + jenkinsInfo, + 'foo,bar,catpants', + ); + + expect(mockedJenkins).toHaveBeenCalledWith({ + baseUrl: jenkinsInfo.baseUrl, + headers: jenkinsInfo.headers, + promisify: true, + }); + expect(mockedJenkinsClient.job.get).toBeCalledWith({ + name: `${jenkinsInfo.jobFullName}/foo`, + tree: expect.anything(), + }); + expect(mockedJenkinsClient.job.get).toBeCalledWith({ + name: `${jenkinsInfo.jobFullName}/bar`, + tree: expect.anything(), + }); + expect(mockedJenkinsClient.job.get).toBeCalledWith({ + name: `${jenkinsInfo.jobFullName}/catpants`, + tree: expect.anything(), + }); + expect(result).toHaveLength(1); + }); }); describe('augmented', () => { const projectWithScmActions: JenkinsProject = { diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.ts b/plugins/jenkins-backend/src/service/jenkinsApi.ts index 8069f57692..07469dc83a 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.ts @@ -75,13 +75,16 @@ export class JenkinsApiImpl { const projects: BackstageProject[] = []; if (branch) { - // we have been asked to filter to a single branch. // Assume jenkinsInfo.jobFullName is a folder which contains one job per branch. // TODO: extract a strategy interface for this - const job = await client.job.get({ - name: `${jenkinsInfo.jobFullName}/${branch}`, - tree: JenkinsApiImpl.jobTreeSpec.replace(/\s/g, ''), - }); + const job = await Promise.any( + branch.split(/,/g).map(name => + client.job.get({ + name: `${jenkinsInfo.jobFullName}/${name}`, + tree: JenkinsApiImpl.jobTreeSpec.replace(/\s/g, ''), + }), + ), + ); projects.push(this.augmentProject(job)); } else { // We aren't filtering diff --git a/plugins/jenkins/README.md b/plugins/jenkins/README.md index 80e39d2095..2a0ec451c8 100644 --- a/plugins/jenkins/README.md +++ b/plugins/jenkins/README.md @@ -19,7 +19,7 @@ yarn add --cwd packages/app @backstage/plugin-jenkins 3. Add the `EntityJenkinsContent` extension to the `CI/CD` page and `EntityLatestJenkinsRunCard` to the `overview` page in the app (or wherever you'd prefer): -Note that if you configured a custom JenkinsInfoProvider in step 2, you may need a custom isJenkinsAvailable. +Note that if you configured a custom JenkinsInfoProvider in step 2, you may need a custom isJenkinsAvailable. Also if you're transitioning to a new default branch name, you can pass multiple branch names as a comma-separated list and it will check for each branch name. ```tsx // In packages/app/src/components/catalog/EntityPage.tsx @@ -38,7 +38,10 @@ const serviceEntityPage = ( - + {/* ... */} From c05f081e61b5f02b77fc0e98838d96c2ddc51f7e Mon Sep 17 00:00:00 2001 From: "Daniele.Mazzotta" Date: Fri, 17 Jun 2022 12:11:47 +0400 Subject: [PATCH 020/401] test for dags found and not found length Signed-off-by: Daniele.Mazzotta --- plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts b/plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts index e56c622ff2..329f89ee52 100644 --- a/plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts +++ b/plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts @@ -184,7 +184,9 @@ describe('ApacheAirflowClient', () => { }); const dagIds = ['mock_dag_1', 'a-random-DAG-id']; const response = await client.getDags(dagIds); + expect(response.dags.length).toEqual(1); expect(response.dags[0].dag_id).toEqual('mock_dag_1'); + expect(response.dagsNotFound.length).toEqual(1); expect(response.dagsNotFound[0]).toEqual('a-random-DAG-id'); }); }); From 95d2f8831376c6e8318e0e5a870622112a820583 Mon Sep 17 00:00:00 2001 From: "Daniele.Mazzotta" Date: Fri, 17 Jun 2022 12:42:30 +0400 Subject: [PATCH 021/401] add warning for missing dag ids Signed-off-by: Daniele.Mazzotta --- .../DagTableComponent/DagTableComponent.tsx | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx b/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx index 18439e1902..7619b06497 100644 --- a/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx +++ b/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx @@ -20,6 +20,7 @@ import { StatusOK, Table, TableColumn, + WarningPanel, } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; import Box from '@material-ui/core/Box'; @@ -30,7 +31,7 @@ import Tooltip from '@material-ui/core/Tooltip'; import Typography from '@material-ui/core/Typography'; import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser'; import Alert from '@material-ui/lab/Alert'; -import React from 'react'; +import React, { useState } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { apacheAirflowApiRef } from '../../api'; import { Dag } from '../../api/types'; @@ -135,15 +136,14 @@ type DagTableComponentProps = { export const DagTableComponent = ({ dagIds }: DagTableComponentProps) => { const apiClient = useApi(apacheAirflowApiRef); + const [dagsNotFound, setDagsNotFound] = useState(); + const { value, loading, error } = useAsync(async (): Promise => { if (dagIds) { + // eslint-disable-next-line @typescript-eslint/no-shadow const { dags, dagsNotFound } = await apiClient.getDags(dagIds); if (dagsNotFound.length) { - throw new Error( - `${dagsNotFound.length} DAGs were not found:\n${dagsNotFound.join( - ';\n', - )}`, - ); + setDagsNotFound(dagsNotFound); } return dags; } @@ -162,5 +162,18 @@ export const DagTableComponent = ({ dagIds }: DagTableComponentProps) => { dagUrl: `${apiClient.baseUrl}dag_details?dag_id=${el.dag_id}`, // construct path to DAG using `baseUrl` })); - return ; + return ( + <> + {dagsNotFound ? ( + + {dagsNotFound.map(dagId => ( + {dagId} + ))} + + ) : ( + '' + )} + + + ); }; From 6b8093497f3a2e9a677f1f476813ca730fa7993a Mon Sep 17 00:00:00 2001 From: "Daniele.Mazzotta" Date: Fri, 17 Jun 2022 12:43:28 +0400 Subject: [PATCH 022/401] written tests for all dags + selected dags Signed-off-by: Daniele.Mazzotta --- .../DagTableComponent.test.tsx | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.test.tsx diff --git a/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.test.tsx b/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.test.tsx new file mode 100644 index 0000000000..16ff3120be --- /dev/null +++ b/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.test.tsx @@ -0,0 +1,62 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ApacheAirflowApi, apacheAirflowApiRef } from '../../api'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import React from 'react'; +import { DagTableComponent } from './DagTableComponent'; + +describe('DagTableComponent', () => { + const mockApi: jest.Mocked = { + listDags: jest.fn().mockResolvedValue([ + { + dag_id: 'mock_dag_1', + }, + { + dag_id: 'mock_dag_2', + }, + { + dag_id: 'mock_dag_3', + }, + ]), + getDags: jest.fn().mockResolvedValue({ + dags: [{ dag_id: 'mock_dag_1' }], + dagsNotFound: ['a-random-id'], + }), + } as any; + + it('should render all DAGs', async () => { + const { getByText } = await renderInTestApp( + + + , + ); + + ['mock_dag_1', 'mock_dag_2', 'mock_dag_3'].forEach(dagId => { + expect(getByText(dagId)).toBeInTheDocument(); + }); + }); + + it('should render only selected DAGs', async () => { + const { getByText } = await renderInTestApp( + + + , + ); + expect(getByText('mock_dag_1')).toBeInTheDocument(); + expect(getByText('Warning: 1 DAGs were not found')).toBeInTheDocument(); + expect(getByText('a-random-id')).toBeInTheDocument(); + }); +}); From fdffb40c70b3cce481e16be2abc66fd064269b87 Mon Sep 17 00:00:00 2001 From: "Daniele.Mazzotta" Date: Fri, 17 Jun 2022 12:47:33 +0400 Subject: [PATCH 023/401] export DagTableComponent Signed-off-by: Daniele.Mazzotta --- plugins/apache-airflow/src/index.ts | 6 +++++- plugins/apache-airflow/src/plugin.ts | 13 ++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/plugins/apache-airflow/src/index.ts b/plugins/apache-airflow/src/index.ts index 7186839c0a..e8353c7760 100644 --- a/plugins/apache-airflow/src/index.ts +++ b/plugins/apache-airflow/src/index.ts @@ -20,4 +20,8 @@ * @packageDocumentation */ -export { apacheAirflowPlugin, ApacheAirflowPage } from './plugin'; +export { + apacheAirflowPlugin, + ApacheAirflowPage, + ApacheAirflowDagTable, +} from './plugin'; diff --git a/plugins/apache-airflow/src/plugin.ts b/plugins/apache-airflow/src/plugin.ts index 04ef1974d4..f7e25c7003 100644 --- a/plugins/apache-airflow/src/plugin.ts +++ b/plugins/apache-airflow/src/plugin.ts @@ -17,11 +17,12 @@ import { rootRouteRef } from './routes'; import { apacheAirflowApiRef, ApacheAirflowClient } from './api'; import { + configApiRef, createApiFactory, + createComponentExtension, createPlugin, createRoutableExtension, discoveryApiRef, - configApiRef, } from '@backstage/core-plugin-api'; export const apacheAirflowPlugin = createPlugin({ @@ -49,3 +50,13 @@ export const ApacheAirflowPage = apacheAirflowPlugin.provide( mountPoint: rootRouteRef, }), ); + +export const ApacheAirflowDagTable = apacheAirflowPlugin.provide( + createComponentExtension({ + name: 'ApacheAirflowDagTable', + component: { + lazy: () => + import('./components/DagTableComponent').then(m => m.DagTableComponent), + }, + }), +); From 7e35d62a13baef3aeafbe0d28cb9b02395234c40 Mon Sep 17 00:00:00 2001 From: Vladimir Masarik Date: Fri, 17 Jun 2022 14:24:03 +0200 Subject: [PATCH 024/401] docs: custom tempalte actions clearly express that the builtin actions are replaced Signed-off-by: Vladimir Masarik --- docs/features/software-templates/writing-custom-actions.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index 8e698e1a05..635a373eb3 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -9,8 +9,9 @@ by writing custom actions which can be used along side our [built-in actions](./builtin-actions.md). > Note: When adding custom actions, the actions array will **replace the -> built-in actions too**. To ensure you can continue to include the builtin -> actions, see below to include them during registration of your action. +> built-in actions too**. Meaning, you will no longer be able to use them. +> If you want to continue using the builtin actions, include them in the actions +> array when registering your custom actions, as seen below. ## Writing your Custom Action From 1ee1a02d676199a7425c774824661acade56db48 Mon Sep 17 00:00:00 2001 From: Vladimir Masarik Date: Fri, 17 Jun 2022 14:26:56 +0200 Subject: [PATCH 025/401] docs: custom template actions update code exmaple Users don't need the container runner, and variables have to be passed in differently Signed-off-by: Vladimir Masarik --- .../writing-custom-actions.md | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index 635a373eb3..abff0cf7c7 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -123,27 +123,32 @@ will set the available actions that the scaffolder has access to. ```ts import { createBuiltinActions } from '@backstage/plugin-scaffolder-backend'; import { ScmIntegrations } from '@backstage/integration'; +import { createNewFileAction } from './actions/custom'; -const integrations = ScmIntegrations.fromConfig(env.config); +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + const catalogClient = new CatalogClient({ discoveryApi: env.discovery }); + const integrations = ScmIntegrations.fromConfig(env.config); -const builtInActions = createBuiltinActions({ - containerRunner, - integrations, - catalogClient, - config: env.config, - reader: env.reader, -}); + const builtInActions = createBuiltinActions({ + integrations, + catalogClient, + config: env.config, + reader: env.reader, + }); + + const actions = [...builtInActions, createNewFileAction()]; -const actions = [...builtInActions, createNewFileAction()]; -return await createRouter({ - containerRunner, - catalogClient, - actions, - logger: env.logger, - config: env.config, - database: env.database, - reader: env.reader, -}); + return createRouter({ + actions, + catalogClient: catalogClient, + logger: env.logger, + config: env.config, + database: env.database, + reader: env.reader, + }); +} ``` ## List of custom action packages From 4ae8c4b0b76b20b04ca48dba6fbb33c1b20083fc Mon Sep 17 00:00:00 2001 From: Vladimir Masarik Date: Fri, 17 Jun 2022 14:49:33 +0200 Subject: [PATCH 026/401] docs: Adding templates, add template as a file example Signed-off-by: Vladimir Masarik --- docs/features/software-templates/adding-templates.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/features/software-templates/adding-templates.md b/docs/features/software-templates/adding-templates.md index db765fc7ee..821e44b0ab 100644 --- a/docs/features/software-templates/adding-templates.md +++ b/docs/features/software-templates/adding-templates.md @@ -97,6 +97,8 @@ catalog: target: https://github.com/backstage/software-templates/blob/main/scaffolder-templates/react-ssr-template/template.yaml rules: - allow: [Template] + - type: file + target: template.yaml # Backstage will expect the file to be in packages/backend/template.yaml ``` Or you can add the template using the `catalog-import` plugin, which unless From 01f976ea728a4fe40f9d5c7a387506e09120e32b Mon Sep 17 00:00:00 2001 From: "Daniele.Mazzotta" Date: Fri, 17 Jun 2022 17:06:56 +0400 Subject: [PATCH 027/401] added changeset Signed-off-by: Daniele.Mazzotta --- .changeset/lemon-goats-obey.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lemon-goats-obey.md diff --git a/.changeset/lemon-goats-obey.md b/.changeset/lemon-goats-obey.md new file mode 100644 index 0000000000..2c45c67282 --- /dev/null +++ b/.changeset/lemon-goats-obey.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-apache-airflow': patch +--- + +Exposed DagTableComponent as standalone component + added a prop to get only select DAGs instead of the full list From 221fbffe4464bb272b925a175538362beebd5a57 Mon Sep 17 00:00:00 2001 From: "Daniele.Mazzotta" Date: Fri, 17 Jun 2022 17:16:28 +0400 Subject: [PATCH 028/401] updated README Signed-off-by: Daniele.Mazzotta --- plugins/apache-airflow/README.md | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/plugins/apache-airflow/README.md b/plugins/apache-airflow/README.md index bdbb1b981f..11fce89e4c 100644 --- a/plugins/apache-airflow/README.md +++ b/plugins/apache-airflow/README.md @@ -3,14 +3,17 @@ Welcome to the apache-airflow plugin! This plugin serves as frontend to the REST API exposed by Apache Airflow. -Note only [Airflow v2 (and later)](https://airflow.apache.org/docs/apache-airflow/stable/deprecated-rest-api-ref.html) integrate with the plugin. +Note only [Airflow v2 (and later)](https://airflow.apache.org/docs/apache-airflow/stable/deprecated-rest-api-ref.html) +integrate with the plugin. ## Feature Requests & Ideas - [ ] Add support for running multiple instances of Airflow for monitoring - various deployment stages or business domains. ([Suggested by @JGoldman110](https://github.com/backstage/backstage/issues/735#issuecomment-985063468)) + various deployment stages or business + domains. ([Suggested by @JGoldman110](https://github.com/backstage/backstage/issues/735#issuecomment-985063468)) - [ ] Make owner chips in the DAG table clickable, resolving to a user or group - in the entity catalog. ([Suggested by @julioz](https://github.com/backstage/backstage/pull/8348#discussion_r764766295)) + in the entity + catalog. ([Suggested by @julioz](https://github.com/backstage/backstage/pull/8348#discussion_r764766295)) ## Installation @@ -42,6 +45,27 @@ yarn --cwd packages/app add @backstage/plugin-apache-airflow ); ``` +If you just want to embed the DAGs into an existing page, you can use the `ApacheAirflowDagTable` + +```typescript +import {ApacheAirflowDagTable} from '@backstage/plugin-apache-airflow'; + +export function SomeEntityPage(): JSX.Element { + return ( + + + < /Grid> +) + ; +} +``` + ## Configuration For links to the Airflow instance, the `baseUrl` must be defined in From 7fc1f50f57ff01c24ccdddf43871bd02afe83760 Mon Sep 17 00:00:00 2001 From: Vladimir Masarik Date: Fri, 17 Jun 2022 15:00:58 +0200 Subject: [PATCH 029/401] docs: Adding templates, added note for users that they need to refresh the location Othwise, backstage won't display the updated or newly added template, and it won't report any errors either. Signed-off-by: Vladimir Masarik --- docs/features/software-templates/adding-templates.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/features/software-templates/adding-templates.md b/docs/features/software-templates/adding-templates.md index 821e44b0ab..62604df1b7 100644 --- a/docs/features/software-templates/adding-templates.md +++ b/docs/features/software-templates/adding-templates.md @@ -86,6 +86,13 @@ contains more information about the required fields. Once we have a `template.yaml` ready, we can then add it to the software catalog for use by the scaffolder. +> Note: When you add or modify a template, you will need to refresh the location entity. +> Otherwise, Backstage won't display the template in the available templates, +> or it will keep showing the old template. You can refresh the location instance by +> going into `Catalog` web page, choosing `Locations` instead of `Components`, and selecting the correct location entity. +> From there, you can click on the refresh icon representing "Sheduled entity refresh" action. +> Afterwards, you should see your template updated. + You can add the template files to the catalog through [static location configuration](../software-catalog/configuration.md#static-location-configuration), for example: From a6321c9b9f6a9b78ee525c6af66f1ec5b98c6d6a Mon Sep 17 00:00:00 2001 From: Vladimir Masarik Date: Fri, 17 Jun 2022 15:25:14 +0200 Subject: [PATCH 030/401] docs: writing templates, add examples of how to use values Add information on how to actually use the parameters from the UI in the actual code so that users can fully utilize the templating power of the default templating action. Signed-off-by: Vladimir Masarik --- .../software-templates/writing-templates.md | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index ff9962d8d4..5597916bee 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -485,6 +485,66 @@ the value of `firstName` from the parameters). This is great for passing the values from the form into different steps and reusing these input variables. These template strings preserve the type of the parameter. +The `${{ parameters.firstName }}` pattern will work only in the template file. +If you want to start using values provided from the UI in your code, you will have to use +the `${{ values.firstName }}` pattern. Additionally, you have to pass +the parameters from the UI to the input of the `fetch:template` step. + +```yaml +apiVersion: scaffolder.backstage.io/v1beta3 +kind: Template +metadata: + name: v1beta3-demo + title: Test Action + description: scaffolder v1beta3 template demo +spec: + owner: backstage/techdocs-core + type: service + parameters: + - title: Fill in some steps + required: + - name + properties: + name: + title: Name + type: string + description: Unique name of your project + urlParameter: + title: URL endpoint + type: string + description: URL endpoint at which the component can be reached + default: "https://www.example.com" + enabledDB: + title: Enable Database + type: boolean + default: false + +... + + steps: + - id: fetch-base + name: Fetch Base + action: fetch:template + input: + url: ./template + values: + name: ${{ parameters.name }} + url: ${{ parameters.urlParameter }} + enabledDB: ${{ parameters.enabledDB }} +``` + +Afterwards, if you are using the builtin templating action, you can start using +the variables in your code. You can use also any other templating fuctions from +[Nunjucks](https://mozilla.github.io/nunjucks/templating.html#tags) as well. + +```bash +#!/bin/bash +echo "Hi my name is ${{ values.name }}, and you can fine me at ${{ values.url }}!" +{% if values.enabledDB %} +echo "You have enabled your database!" +{% endif %} +``` + As you can see above in the `Outputs` section, `actions` and `steps` can also output things. You can grab that output using `steps.$stepId.output.$property`. From d9ebda237e8020fe641361d635c348f8d43a4262 Mon Sep 17 00:00:00 2001 From: "Daniele.Mazzotta" Date: Fri, 17 Jun 2022 17:50:27 +0400 Subject: [PATCH 031/401] build api-reports Signed-off-by: Daniele.Mazzotta --- plugins/apache-airflow/api-report.md | 11 +++++++++-- plugins/apache-airflow/src/plugin.ts | 7 +++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/plugins/apache-airflow/api-report.md b/plugins/apache-airflow/api-report.md index ca151f0734..09123de613 100644 --- a/plugins/apache-airflow/api-report.md +++ b/plugins/apache-airflow/api-report.md @@ -5,8 +5,15 @@ ```ts /// -import { BackstagePlugin } from '@backstage/core-plugin-api'; -import { RouteRef } from '@backstage/core-plugin-api'; +import {BackstagePlugin} from '@backstage/core-plugin-api'; +import {RouteRef} from '@backstage/core-plugin-api'; + +// @public +export const ApacheAirflowDagTable: ({ + dagIds, + }: { + dagIds?: string[] | undefined; +}) => JSX.Element; // Warning: (ae-missing-release-tag) "ApacheAirflowPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/apache-airflow/src/plugin.ts b/plugins/apache-airflow/src/plugin.ts index f7e25c7003..adf5879196 100644 --- a/plugins/apache-airflow/src/plugin.ts +++ b/plugins/apache-airflow/src/plugin.ts @@ -51,6 +51,13 @@ export const ApacheAirflowPage = apacheAirflowPlugin.provide( }), ); +/** + * Render the DAGs in a table + * If the dagIds is specified, only those DAGs are loaded. + * Otherwise, it's going to list all the DAGs + * @public + * @param dagIds - string[] + */ export const ApacheAirflowDagTable = apacheAirflowPlugin.provide( createComponentExtension({ name: 'ApacheAirflowDagTable', From e81be86682cee2a94c71aeaa593726b1a9dd190e Mon Sep 17 00:00:00 2001 From: Vladimir Masarik Date: Fri, 17 Jun 2022 16:01:37 +0200 Subject: [PATCH 032/401] docs: fix template typos Signed-off-by: Vladimir Masarik --- docs/features/software-templates/adding-templates.md | 2 +- docs/features/software-templates/writing-templates.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/features/software-templates/adding-templates.md b/docs/features/software-templates/adding-templates.md index 62604df1b7..e8176a688b 100644 --- a/docs/features/software-templates/adding-templates.md +++ b/docs/features/software-templates/adding-templates.md @@ -90,7 +90,7 @@ for use by the scaffolder. > Otherwise, Backstage won't display the template in the available templates, > or it will keep showing the old template. You can refresh the location instance by > going into `Catalog` web page, choosing `Locations` instead of `Components`, and selecting the correct location entity. -> From there, you can click on the refresh icon representing "Sheduled entity refresh" action. +> From there, you can click on the refresh icon representing "Scheduled entity refresh" action. > Afterwards, you should see your template updated. You can add the template files to the catalog through diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 5597916bee..3173a0500a 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -534,7 +534,7 @@ spec: ``` Afterwards, if you are using the builtin templating action, you can start using -the variables in your code. You can use also any other templating fuctions from +the variables in your code. You can use also any other templating functions from [Nunjucks](https://mozilla.github.io/nunjucks/templating.html#tags) as well. ```bash From ea07eb0bc8932ab68d7f1cc3d6bf2cae30655d91 Mon Sep 17 00:00:00 2001 From: Marco Crivellaro Date: Sat, 18 Jun 2022 14:02:10 +0100 Subject: [PATCH 033/401] feat: introduce github:repo:create github:repo:push scaffolder actions Signed-off-by: Marco Crivellaro --- .../actions/builtin/createBuiltinActions.ts | 39 +- .../builtin/github/githubRepoCreate.test.ts | 416 ++++++++++++++++++ .../builtin/github/githubRepoCreate.ts | 337 ++++++++++++++ .../builtin/github/githubRepoPush.test.ts | 400 +++++++++++++++++ .../actions/builtin/github/githubRepoPush.ts | 224 ++++++++++ .../actions/builtin/github/index.ts | 4 +- 6 files changed, 1405 insertions(+), 15 deletions(-) create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index 2ffbc09ce5..91f1e180d0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -15,25 +15,34 @@ */ import { UrlReader } from '@backstage/backend-common'; -import { JsonObject } from '@backstage/types'; import { CatalogApi } from '@backstage/catalog-client'; -import { - GithubCredentialsProvider, - ScmIntegrations, - DefaultGithubCredentialsProvider, -} from '@backstage/integration'; import { Config } from '@backstage/config'; import { - createCatalogWriteAction, + DefaultGithubCredentialsProvider, + GithubCredentialsProvider, + ScmIntegrations, +} from '@backstage/integration'; +import { JsonObject } from '@backstage/types'; +import { createCatalogRegisterAction, + createCatalogWriteAction, } from './catalog'; +import { TemplateFilter } from '../../../lib'; +import { TemplateAction } from '../types'; import { createDebugLogAction } from './debug'; import { createFetchPlainAction, createFetchTemplateAction } from './fetch'; import { createFilesystemDeleteAction, createFilesystemRenameAction, } from './filesystem'; +import { + createGithubActionsDispatchAction, + createGithubIssuesLabelAction, + createGithubRepoCreateAction, + createGithubRepoPushAction, + createGithubWebhookAction, +} from './github'; import { createPublishAzureAction, createPublishBitbucketAction, @@ -45,13 +54,6 @@ import { createPublishGitlabAction, createPublishGitlabMergeRequestAction, } from './publish'; -import { - createGithubActionsDispatchAction, - createGithubWebhookAction, - createGithubIssuesLabelAction, -} from './github'; -import { TemplateFilter } from '../../../lib'; -import { TemplateAction } from '../types'; /** * The options passed to {@link createBuiltinActions} @@ -165,6 +167,15 @@ export const createBuiltinActions = ( integrations, githubCredentialsProvider, }), + createGithubRepoCreateAction({ + integrations, + githubCredentialsProvider, + }), + createGithubRepoPushAction({ + integrations, + config, + githubCredentialsProvider, + }), ]; return actions as TemplateAction[]; 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 new file mode 100644 index 0000000000..0e7e907917 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts @@ -0,0 +1,416 @@ +/* + * 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 { TemplateAction } from '../../types'; + +jest.mock('../helpers'); + +import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { + DefaultGithubCredentialsProvider, + GithubCredentialsProvider, + ScmIntegrations, +} from '@backstage/integration'; +import { when } from 'jest-when'; +import { PassThrough } from 'stream'; +import { createGithubRepoCreateAction } from './githubRepoCreate'; + +const mockOctokit = { + rest: { + users: { + getByUsername: jest.fn(), + }, + repos: { + addCollaborator: jest.fn(), + createInOrg: jest.fn(), + createForAuthenticatedUser: jest.fn(), + replaceAllTopics: jest.fn(), + }, + teams: { + addOrUpdateRepoPermissionsInOrg: jest.fn(), + }, + }, +}; +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + +describe('github:repo:create', () => { + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + let githubCredentialsProvider: GithubCredentialsProvider; + let action: TemplateAction; + + const mockContext = { + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + description: 'description', + repoVisibility: 'private' as const, + access: 'owner/blam', + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + githubCredentialsProvider = + DefaultGithubCredentialsProvider.fromIntegrations(integrations); + action = createGithubRepoCreateAction({ + integrations, + githubCredentialsProvider, + }); + }); + + it('should call the githubApis with the correct values for createInOrg', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + await action.handler(mockContext); + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + description: 'description', + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + allow_merge_commit: true, + allow_rebase_merge: true, + visibility: 'private', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoVisibility: 'public', + }, + }); + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + description: 'description', + name: 'repo', + org: 'owner', + private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + allow_merge_commit: true, + allow_rebase_merge: true, + visibility: 'public', + }); + }); + + it('should call the githubApis with the correct values for createForAuthenticatedUser', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + data: {}, + }); + + await action.handler(mockContext); + expect( + mockOctokit.rest.repos.createForAuthenticatedUser, + ).toHaveBeenCalledWith({ + description: 'description', + name: 'repo', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + allow_merge_commit: true, + allow_rebase_merge: true, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoVisibility: 'public', + }, + }); + expect( + mockOctokit.rest.repos.createForAuthenticatedUser, + ).toHaveBeenCalledWith({ + description: 'description', + name: 'repo', + private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + allow_merge_commit: true, + allow_rebase_merge: true, + }); + }); + + it('should add access for the team when it starts with the owner', 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); + + expect( + mockOctokit.rest.teams.addOrUpdateRepoPermissionsInOrg, + ).toHaveBeenCalledWith({ + org: 'owner', + team_slug: 'blam', + owner: 'owner', + repo: 'repo', + permission: 'admin', + }); + }); + + it('should add outside collaborators when provided', 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, + access: 'outsidecollaborator', + }, + }); + + expect(mockOctokit.rest.repos.addCollaborator).toHaveBeenCalledWith({ + username: 'outsidecollaborator', + owner: 'owner', + repo: 'repo', + permission: 'admin', + }); + }); + + it('should add multiple collaborators when provided', 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: 'robot-1', + }, + { + access: 'push', + team: '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', + }); + }); + + it('should ignore failures when adding multiple 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', + }, + }); + + when(mockOctokit.rest.teams.addOrUpdateRepoPermissionsInOrg) + .calledWith({ + org: 'owner', + owner: 'owner', + repo: 'repo', + team_slug: 'robot-1', + permission: 'pull', + }) + .mockRejectedValueOnce(new Error('Something bad happened') as never); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + collaborators: [ + { + access: 'pull', + team: 'robot-1', + }, + { + access: 'push', + team: 'robot-2', + }, + ], + }, + }); + + expect( + mockOctokit.rest.teams.addOrUpdateRepoPermissionsInOrg.mock.calls[2], + ).toEqual([ + { + org: 'owner', + owner: 'owner', + repo: 'repo', + team_slug: 'robot-2', + permission: 'push', + }, + ]); + }); + + it('should add topics when provided', 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', + }, + }); + + mockOctokit.rest.repos.replaceAllTopics.mockResolvedValue({ + data: { + names: ['node.js'], + }, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + topics: ['node.js'], + }, + }); + + expect(mockOctokit.rest.repos.replaceAllTopics).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + names: ['node.js'], + }); + }); + + it('should lowercase topics when provided', 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', + }, + }); + + mockOctokit.rest.repos.replaceAllTopics.mockResolvedValue({ + data: { + names: ['backstage'], + }, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + topics: ['BACKSTAGE'], + }, + }); + + expect(mockOctokit.rest.repos.replaceAllTopics).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + names: ['backstage'], + }); + }); + + it('should call output with the remoteUrl', 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); + + expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://github.com/clone/url.git', + ); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts new file mode 100644 index 0000000000..494a47516c --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts @@ -0,0 +1,337 @@ +/* + * 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 { assertError, InputError } from '@backstage/errors'; +import { + GithubCredentialsProvider, + ScmIntegrationRegistry, +} from '@backstage/integration'; +import { Octokit } from 'octokit'; +import { createTemplateAction } from '../../createTemplateAction'; +import { parseRepoUrl } from '../publish/util'; +import { getOctokitOptions } from './helpers'; + +/** + * Creates a new action that initializes a git repository + * + * @public + */ +export function createGithubRepoCreateAction(options: { + integrations: ScmIntegrationRegistry; + githubCredentialsProvider?: GithubCredentialsProvider; +}) { + const { integrations, githubCredentialsProvider } = options; + + return createTemplateAction<{ + repoUrl: string; + description?: string; + access?: string; + deleteBranchOnMerge?: boolean; + gitAuthorName?: string; + gitAuthorEmail?: string; + allowRebaseMerge?: boolean; + allowSquashMerge?: boolean; + allowMergeCommit?: boolean; + requireCodeOwnerReviews?: boolean; + requiredStatusCheckContexts?: string[]; + repoVisibility?: 'private' | 'internal' | 'public'; + collaborators?: Array< + | { + user: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + } + | { + team: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + } + >; + token?: string; + topics?: string[]; + }>({ + id: 'github:repo:create', + description: 'Creates a GitHub repository.', + schema: { + input: { + type: 'object', + required: ['repoUrl'], + properties: { + repoUrl: { + title: 'Repository Location', + description: `Accepts the format 'github.com?repo=reponame&owner=owner' where 'reponame' is the new repository name and 'owner' is an organization or username`, + type: 'string', + }, + description: { + title: 'Repository Description', + type: 'string', + }, + access: { + title: 'Repository Access', + description: `Sets an admin collaborator on the repository. Can either be a user reference different from 'owner' in 'repoUrl' or team reference, eg. 'org/team-name'`, + type: 'string', + }, + requireCodeOwnerReviews: { + title: 'Require CODEOWNER Reviews?', + description: + 'Require an approved review in PR including files with a designated Code Owner', + type: 'boolean', + }, + requiredStatusCheckContexts: { + title: 'Required Status Check Contexts', + description: + 'The list of status checks to require in order to merge into this branch', + type: 'array', + items: { + type: 'string', + }, + }, + repoVisibility: { + title: 'Repository Visibility', + type: 'string', + enum: ['private', 'public', 'internal'], + }, + deleteBranchOnMerge: { + title: 'Delete Branch On Merge', + type: 'boolean', + description: `Delete the branch after merging the PR. The default value is 'false'`, + }, + gitAuthorName: { + title: 'Default Author Name', + type: 'string', + description: `Sets the default author name for the commit. The default value is 'Scaffolder'`, + }, + gitAuthorEmail: { + title: 'Default Author Email', + type: 'string', + description: `Sets the default author email for the commit.`, + }, + allowMergeCommit: { + title: 'Allow Merge Commits', + type: 'boolean', + description: `Allow merge commits. The default value is 'true'`, + }, + allowSquashMerge: { + title: 'Allow Squash Merges', + type: 'boolean', + description: `Allow squash merges. The default value is 'true'`, + }, + allowRebaseMerge: { + title: 'Allow Rebase Merges', + type: 'boolean', + description: `Allow rebase merges. The default value is 'true'`, + }, + collaborators: { + title: 'Collaborators', + description: 'Provide additional users or teams with permissions', + type: 'array', + items: { + type: 'object', + additionalProperties: false, + required: ['access'], + properties: { + access: { + type: 'string', + description: 'The type of access for the user', + enum: ['push', 'pull', 'admin', 'maintain', 'triage'], + }, + user: { + type: 'string', + description: + 'The name of the user that will be added as a collaborator', + }, + team: { + type: 'string', + description: + 'The name of the team that will be added as a collaborator', + }, + }, + oneOf: [{ required: ['user'] }, { required: ['team'] }], + }, + }, + token: { + title: 'Authentication Token', + type: 'string', + description: 'The token to use for authorization to GitHub', + }, + topics: { + title: 'Topics', + type: 'array', + items: { + type: 'string', + }, + }, + }, + }, + output: { + type: 'object', + properties: { + remoteUrl: { + title: 'A URL to the repository with the provider', + type: 'string', + }, + repoContentsUrl: { + title: 'A URL to the root of the repository', + type: 'string', + }, + }, + }, + }, + async handler(ctx) { + const { + repoUrl, + description, + access, + repoVisibility = 'private', + deleteBranchOnMerge = false, + allowMergeCommit = true, + allowSquashMerge = true, + allowRebaseMerge = true, + collaborators, + topics, + token: providedToken, + } = ctx.input; + + const { owner, repo } = parseRepoUrl(repoUrl, integrations); + + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); + } + + const octokitOptions = await getOctokitOptions({ + integrations, + credentialsProvider: githubCredentialsProvider, + token: providedToken, + repoUrl, + }); + + const client = new Octokit(octokitOptions); + + const user = await client.rest.users.getByUsername({ + username: owner, + }); + + const repoCreationPromise = + user.data.type === 'Organization' + ? client.rest.repos.createInOrg({ + name: repo, + org: owner, + private: repoVisibility === 'private', + visibility: repoVisibility, + description: description, + delete_branch_on_merge: deleteBranchOnMerge, + allow_merge_commit: allowMergeCommit, + allow_squash_merge: allowSquashMerge, + allow_rebase_merge: allowRebaseMerge, + }) + : client.rest.repos.createForAuthenticatedUser({ + name: repo, + private: repoVisibility === 'private', + description: description, + delete_branch_on_merge: deleteBranchOnMerge, + allow_merge_commit: allowMergeCommit, + allow_squash_merge: allowSquashMerge, + allow_rebase_merge: allowRebaseMerge, + }); + + let newRepo; + + try { + newRepo = (await repoCreationPromise).data; + } catch (e) { + assertError(e); + if (e.message === 'Resource not accessible by integration') { + ctx.logger.warn( + `The GitHub app or token provided may not have the required permissions to create the ${user.data.type} repository ${owner}/${repo}.`, + ); + } + throw new Error( + `Failed to create the ${user.data.type} repository ${owner}/${repo}, ${e.message}`, + ); + } + + if (access?.startsWith(`${owner}/`)) { + const [, team] = access.split('/'); + await client.rest.teams.addOrUpdateRepoPermissionsInOrg({ + org: owner, + team_slug: team, + owner, + repo, + permission: 'admin', + }); + // No need to add access if it's the person who owns the personal account + } else if (access && access !== owner) { + await client.rest.repos.addCollaborator({ + owner, + repo, + username: access, + permission: 'admin', + }); + } + + if (collaborators) { + for (const collaborator of collaborators) { + try { + if ('user' in collaborator) { + await client.rest.repos.addCollaborator({ + owner, + repo, + username: collaborator.user, + permission: collaborator.access, + }); + } else if ('team' in collaborator) { + await client.rest.teams.addOrUpdateRepoPermissionsInOrg({ + org: owner, + team_slug: collaborator.team, + owner, + repo, + permission: collaborator.access, + }); + } + } catch (e) { + assertError(e); + const name = extractCollaboratorName(collaborator); + ctx.logger.warn( + `Skipping ${collaborator.access} access for ${name}, ${e.message}`, + ); + } + } + } + + if (topics) { + try { + await client.rest.repos.replaceAllTopics({ + owner, + repo, + names: topics.map(t => t.toLowerCase()), + }); + } catch (e) { + assertError(e); + ctx.logger.warn(`Skipping topics ${topics.join(' ')}, ${e.message}`); + } + } + + const remoteUrl = newRepo.clone_url; + + ctx.output('remoteUrl', remoteUrl); + }, + }); +} + +function extractCollaboratorName( + collaborator: { user: string } | { team: string } | { username: string }, +) { + if ('username' in collaborator) return collaborator.username; + if ('user' in collaborator) return collaborator.user; + return collaborator.team; +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.test.ts new file mode 100644 index 0000000000..f23e3dde9f --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.test.ts @@ -0,0 +1,400 @@ +/* + * 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 { TemplateAction } from '../../types'; + +jest.mock('../helpers'); + +import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { + DefaultGithubCredentialsProvider, + GithubCredentialsProvider, + ScmIntegrations, +} from '@backstage/integration'; +import { PassThrough } from 'stream'; +import { + enableBranchProtectionOnDefaultRepoBranch, + initRepoAndPush, +} from '../helpers'; +import { createGithubRepoPushAction } from './githubRepoPush'; + +const mockOctokit = { + rest: { + repos: { + get: jest.fn(), + }, + }, +}; +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + +describe('github:repo:push', () => { + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + let githubCredentialsProvider: GithubCredentialsProvider; + let action: TemplateAction; + + const mockContext = { + input: { + repoUrl: 'github.com?repo=repository&owner=owner', + description: 'description', + repoVisibility: 'private' as const, + access: 'owner/blam', + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + githubCredentialsProvider = + DefaultGithubCredentialsProvider.fromIntegrations(integrations); + action = createGithubRepoPushAction({ + integrations, + config, + githubCredentialsProvider, + }); + }); + + it('should call initRepoAndPush with the correct values', async () => { + mockOctokit.rest.repos.get.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await action.handler(mockContext); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://github.com/clone/url.git', + defaultBranch: 'master', + auth: { username: 'x-access-token', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: {}, + }); + }); + + it('should call initRepoAndPush with the correct defaultBranch main', async () => { + mockOctokit.rest.repos.get.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + defaultBranch: 'main', + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://github.com/clone/url.git', + defaultBranch: 'main', + auth: { username: 'x-access-token', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: {}, + }); + }); + + it('should call initRepoAndPush with the configured defaultAuthor', async () => { + const customAuthorConfig = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + scaffolder: { + defaultAuthor: { + name: 'Test', + email: 'example@example.com', + }, + }, + }); + + const customAuthorIntegrations = + ScmIntegrations.fromConfig(customAuthorConfig); + const customAuthorAction = createGithubRepoPushAction({ + integrations: customAuthorIntegrations, + config: customAuthorConfig, + githubCredentialsProvider, + }); + + mockOctokit.rest.repos.get.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await customAuthorAction.handler(mockContext); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://github.com/clone/url.git', + defaultBranch: 'master', + auth: { username: 'x-access-token', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: { name: 'Test', email: 'example@example.com' }, + }); + }); + + it('should call initRepoAndPush with the configured defaultCommitMessage', async () => { + const customAuthorConfig = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + scaffolder: { + defaultCommitMessage: 'Test commit message', + }, + }); + + const customAuthorIntegrations = + ScmIntegrations.fromConfig(customAuthorConfig); + const customAuthorAction = createGithubRepoPushAction({ + integrations: customAuthorIntegrations, + config: customAuthorConfig, + githubCredentialsProvider, + }); + + mockOctokit.rest.repos.get.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await customAuthorAction.handler(mockContext); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://github.com/clone/url.git', + defaultBranch: 'master', + auth: { username: 'x-access-token', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: { email: undefined, name: undefined }, + }); + }); + + it('should call output with the remoteUrl and the repoContentsUrl', async () => { + mockOctokit.rest.repos.get.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await action.handler(mockContext); + + expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://github.com/clone/url.git', + ); + expect(mockContext.output).toHaveBeenCalledWith( + 'repoContentsUrl', + 'https://github.com/html/url/blob/master', + ); + }); + + it('should use main as default branch', async () => { + mockOctokit.rest.repos.get.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + defaultBranch: 'main', + }, + }); + + expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://github.com/clone/url.git', + ); + expect(mockContext.output).toHaveBeenCalledWith( + 'repoContentsUrl', + 'https://github.com/html/url/blob/main', + ); + }); + + it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of requireCodeOwnerReviews', async () => { + mockOctokit.rest.repos.get.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await action.handler(mockContext); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repository', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + requiredStatusCheckContexts: [], + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + requireCodeOwnerReviews: true, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repository', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: true, + requiredStatusCheckContexts: [], + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + requireCodeOwnerReviews: false, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repository', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + requiredStatusCheckContexts: [], + }); + }); + + it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of requiredStatusCheckContexts', async () => { + mockOctokit.rest.repos.get.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await action.handler(mockContext); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repository', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + requiredStatusCheckContexts: [], + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + requiredStatusCheckContexts: ['statusCheck'], + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repository', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + requiredStatusCheckContexts: ['statusCheck'], + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + requiredStatusCheckContexts: [], + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repository', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + requiredStatusCheckContexts: [], + }); + }); + + it('should not call enableBranchProtectionOnDefaultRepoBranch with protectDefaultBranch disabled', async () => { + mockOctokit.rest.repos.get.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + protectDefaultBranch: false, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).not.toHaveBeenCalled(); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts new file mode 100644 index 0000000000..cdf9a9b4e6 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts @@ -0,0 +1,224 @@ +/* + * 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 { Config } from '@backstage/config'; +import { assertError, InputError } from '@backstage/errors'; +import { + GithubCredentialsProvider, + ScmIntegrationRegistry, +} from '@backstage/integration'; +import { Octokit } from 'octokit'; +import { createTemplateAction } from '../../createTemplateAction'; +import { + enableBranchProtectionOnDefaultRepoBranch, + initRepoAndPush, +} from '../helpers'; +import { getRepoSourceDirectory, parseRepoUrl } from '../publish/util'; +import { getOctokitOptions } from './helpers'; + +/** + * Creates a new action that initializes a git repository of the content in the workspace + * and publishes it to GitHub. + * + * @public + */ +export function createGithubRepoPushAction(options: { + integrations: ScmIntegrationRegistry; + config: Config; + githubCredentialsProvider?: GithubCredentialsProvider; +}) { + const { integrations, config, githubCredentialsProvider } = options; + + return createTemplateAction<{ + repoUrl: string; + description?: string; + defaultBranch?: string; + protectDefaultBranch?: boolean; + gitCommitMessage?: string; + gitAuthorName?: string; + gitAuthorEmail?: string; + requireCodeOwnerReviews?: boolean; + requiredStatusCheckContexts?: string[]; + sourcePath?: string; + token?: string; + }>({ + id: 'github:repo:push', + description: + 'Initializes a git repository of contents in workspace and publishes it to GitHub.', + schema: { + input: { + type: 'object', + required: ['repoUrl'], + properties: { + repoUrl: { + title: 'Repository Location', + description: `Accepts the format 'github.com?repo=reponame&owner=owner' where 'reponame' is the new repository name and 'owner' is an organization or username`, + type: 'string', + }, + requireCodeOwnerReviews: { + title: 'Require CODEOWNER Reviews?', + description: + 'Require an approved review in PR including files with a designated Code Owner', + type: 'boolean', + }, + requiredStatusCheckContexts: { + title: 'Required Status Check Contexts', + description: + 'The list of status checks to require in order to merge into this branch', + type: 'array', + items: { + type: 'string', + }, + }, + defaultBranch: { + title: 'Default Branch', + type: 'string', + description: `Sets the default branch on the repository. The default value is 'master'`, + }, + protectDefaultBranch: { + title: 'Protect Default Branch', + type: 'boolean', + description: `Protect the default branch after creating the repository. The default value is 'true'`, + }, + gitCommitMessage: { + title: 'Git Commit Message', + type: 'string', + description: `Sets the commit message on the repository. The default value is 'initial commit'`, + }, + gitAuthorName: { + title: 'Default Author Name', + type: 'string', + description: `Sets the default author name for the commit. The default value is 'Scaffolder'`, + }, + gitAuthorEmail: { + title: 'Default Author Email', + type: 'string', + description: `Sets the default author email for the commit.`, + }, + sourcePath: { + title: 'Source Path', + description: + 'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository.', + type: 'string', + }, + token: { + title: 'Authentication Token', + type: 'string', + description: 'The token to use for authorization to GitHub', + }, + topics: { + title: 'Topics', + type: 'array', + items: { + type: 'string', + }, + }, + }, + }, + output: { + type: 'object', + properties: { + remoteUrl: { + title: 'A URL to the repository with the provider', + type: 'string', + }, + repoContentsUrl: { + title: 'A URL to the root of the repository', + type: 'string', + }, + }, + }, + }, + async handler(ctx) { + const { + repoUrl, + defaultBranch = 'master', + protectDefaultBranch = true, + gitCommitMessage = 'initial commit', + gitAuthorName, + gitAuthorEmail, + requireCodeOwnerReviews = false, + requiredStatusCheckContexts = [], + token: providedToken, + } = ctx.input; + + const { owner, repo } = parseRepoUrl(repoUrl, integrations); + + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); + } + + const octokitOptions = await getOctokitOptions({ + integrations, + credentialsProvider: githubCredentialsProvider, + token: providedToken, + repoUrl, + }); + + const client = new Octokit(octokitOptions); + + const targetRepo = await client.rest.repos.get({ owner, repo }); + + const remoteUrl = targetRepo.data.clone_url; + const repoContentsUrl = `${targetRepo.data.html_url}/blob/${defaultBranch}`; + + const gitAuthorInfo = { + name: gitAuthorName + ? gitAuthorName + : config.getOptionalString('scaffolder.defaultAuthor.name'), + email: gitAuthorEmail + ? gitAuthorEmail + : config.getOptionalString('scaffolder.defaultAuthor.email'), + }; + + await initRepoAndPush({ + dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath), + remoteUrl, + defaultBranch, + auth: { + username: 'x-access-token', + password: octokitOptions.auth, + }, + logger: ctx.logger, + commitMessage: gitCommitMessage + ? gitCommitMessage + : config.getOptionalString('scaffolder.defaultCommitMessage'), + gitAuthorInfo, + }); + + if (protectDefaultBranch) { + try { + await enableBranchProtectionOnDefaultRepoBranch({ + owner, + client, + repoName: repo, + logger: ctx.logger, + defaultBranch, + requireCodeOwnerReviews, + requiredStatusCheckContexts, + }); + } catch (e) { + assertError(e); + ctx.logger.warn( + `Skipping: default branch protection on '${repo}', ${e.message}`, + ); + } + } + + ctx.output('remoteUrl', remoteUrl); + ctx.output('repoContentsUrl', repoContentsUrl); + }, + }); +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts index c5788afc3a..6746aef37e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts @@ -15,5 +15,7 @@ */ export { createGithubActionsDispatchAction } from './githubActionsDispatch'; -export { createGithubWebhookAction } from './githubWebhook'; export { createGithubIssuesLabelAction } from './githubIssuesLabel'; +export { createGithubRepoCreateAction } from './githubRepoCreate'; +export { createGithubRepoPushAction } from './githubRepoPush'; +export { createGithubWebhookAction } from './githubWebhook'; From 2db07887cb8f37f47afd0626626425c56e4fd40b Mon Sep 17 00:00:00 2001 From: Marco Crivellaro Date: Sat, 18 Jun 2022 14:10:41 +0100 Subject: [PATCH 034/401] chore: changeset Signed-off-by: Marco Crivellaro --- .changeset/thirty-rivers-watch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/thirty-rivers-watch.md diff --git a/.changeset/thirty-rivers-watch.md b/.changeset/thirty-rivers-watch.md new file mode 100644 index 0000000000..d92343f365 --- /dev/null +++ b/.changeset/thirty-rivers-watch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Added two new scaffolder actions: `github:repo:create` and `github:repo:push` From 4e8787ac5755d9bcd2a4c7323ad2ea56d0b2b102 Mon Sep 17 00:00:00 2001 From: Marco Crivellaro Date: Sat, 18 Jun 2022 15:44:53 +0100 Subject: [PATCH 035/401] chore: api-report Signed-off-by: Marco Crivellaro --- plugins/scaffolder-backend/api-report.md | 52 ++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 707b8e62ba..029000399b 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -168,6 +168,58 @@ export type CreateGithubPullRequestClientFactoryInput = { token?: string; }; +// @public +export function createGithubRepoCreateAction(options: { + integrations: ScmIntegrationRegistry; + githubCredentialsProvider?: GithubCredentialsProvider; +}): TemplateAction<{ + repoUrl: string; + description?: string | undefined; + access?: string | undefined; + deleteBranchOnMerge?: boolean | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; + allowRebaseMerge?: boolean | undefined; + allowSquashMerge?: boolean | undefined; + allowMergeCommit?: boolean | undefined; + requireCodeOwnerReviews?: boolean | undefined; + requiredStatusCheckContexts?: string[] | undefined; + repoVisibility?: 'internal' | 'private' | 'public' | undefined; + collaborators?: + | ( + | { + user: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + } + | { + team: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + } + )[] + | undefined; + token?: string | undefined; + topics?: string[] | undefined; +}>; + +// @public +export function createGithubRepoPushAction(options: { + integrations: ScmIntegrationRegistry; + config: Config; + githubCredentialsProvider?: GithubCredentialsProvider; +}): TemplateAction<{ + repoUrl: string; + description?: string | undefined; + defaultBranch?: string | undefined; + protectDefaultBranch?: boolean | undefined; + gitCommitMessage?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; + requireCodeOwnerReviews?: boolean | undefined; + requiredStatusCheckContexts?: string[] | undefined; + sourcePath?: string | undefined; + token?: string | undefined; +}>; + // @public export function createGithubWebhookAction(options: { integrations: ScmIntegrationRegistry; From 8b8c692be318d4b7468ca8f2b2867a28a15ade5c Mon Sep 17 00:00:00 2001 From: Daniele Mazzotta Date: Mon, 20 Jun 2022 12:58:38 +0400 Subject: [PATCH 036/401] updated api-reports Signed-off-by: Daniele Mazzotta --- plugins/apache-airflow/src/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/apache-airflow/src/plugin.ts b/plugins/apache-airflow/src/plugin.ts index adf5879196..664cde36cc 100644 --- a/plugins/apache-airflow/src/plugin.ts +++ b/plugins/apache-airflow/src/plugin.ts @@ -56,7 +56,7 @@ export const ApacheAirflowPage = apacheAirflowPlugin.provide( * If the dagIds is specified, only those DAGs are loaded. * Otherwise, it's going to list all the DAGs * @public - * @param dagIds - string[] + * @param dagIds - optional string[] of the DAGs to show in the table. If undefined, it will list all DAGs */ export const ApacheAirflowDagTable = apacheAirflowPlugin.provide( createComponentExtension({ From 1189f00d23d12c4728bfe4eaa39845af14b461cc Mon Sep 17 00:00:00 2001 From: ivgo Date: Mon, 20 Jun 2022 11:48:58 +0200 Subject: [PATCH 037/401] Set group as optional parameter. Scan everything if not present Signed-off-by: ivgo --- .changeset/two-crews-accept.md | 9 ++- docs/integrations/gitlab/discovery.md | 14 +--- .../catalog-backend-module-gitlab/config.d.ts | 75 +++++++------------ .../src/providers/config.test.ts | 8 +- .../src/providers/config.ts | 19 +---- 5 files changed, 42 insertions(+), 83 deletions(-) diff --git a/.changeset/two-crews-accept.md b/.changeset/two-crews-accept.md index 5885b1346d..deacf1559d 100644 --- a/.changeset/two-crews-accept.md +++ b/.changeset/two-crews-accept.md @@ -2,13 +2,14 @@ '@backstage/plugin-catalog-backend-module-gitlab': minor --- -Add the possibility in the `GitlabDiscoveryEntityProvider` to scan the whole project instead of concrete groups. For that, use a configuration like this one: +Add the possibility in the `GitlabDiscoveryEntityProvider` to scan the whole project instead of concrete groups. For that, use a configuration like this one, where the group parameter is omitted (not mandatory anymore): ```yaml catalog: providers: gitlab: - host: gitlab-host # Identifies one of the hosts set up in the integrations - branch: main # Optional. Uses `master` as default - entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml` + yourProviderId: + host: gitlab-host # Identifies one of the hosts set up in the integrations + branch: main # Optional. Uses `master` as default + entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml` ``` diff --git a/docs/integrations/gitlab/discovery.md b/docs/integrations/gitlab/discovery.md index a7dd0a03df..aad73c8209 100644 --- a/docs/integrations/gitlab/discovery.md +++ b/docs/integrations/gitlab/discovery.md @@ -22,22 +22,10 @@ catalog: yourProviderId: host: gitlab-host # Identifies one of the hosts set up in the integrations branch: main # Optional. Uses `master` as default - group: example-group # Group and subgroup (if needed) to look for repositories + group: example-group # Optional. Group and subgroup (if needed) to look for repositories. If not present the whole project will be scanned entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml` ``` -If you desire so, it's also possible to execute the gitlab discovery in your entire -project. In order to do that, use this catalog configuration instead: - -```yaml -catalog: - providers: - gitlab: - host: gitlab-host # Identifies one of the hosts set up in the integrations - branch: main # Optional. Uses `master` as default - entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml` -``` - As this provider is not one of the default providers, you will first need to install the gitlab catalog plugin: diff --git a/plugins/catalog-backend-module-gitlab/config.d.ts b/plugins/catalog-backend-module-gitlab/config.d.ts index 0626cb9488..82f1a5db7c 100644 --- a/plugins/catalog-backend-module-gitlab/config.d.ts +++ b/plugins/catalog-backend-module-gitlab/config.d.ts @@ -23,53 +23,34 @@ export interface Config { /** * GitlabDiscoveryEntityProvider configuration */ - gitlab?: - | { - /** - * (Required) Gitlab's host name. - * @visibility backend - */ - host: string; - /** - * (Optional) Default branch to read the catalog-info.yaml file. - * If not set, 'master' will be used. - * @visibility backend - */ - branch?: string; - /** - * (Optional) The name used for the catalog file. - * If not set, 'catalog-info.yaml' will be used. - * @visibility backend - */ - entityFilename?: string; - } - | Record< - string, - { - /** - * (Required) Gitlab's host name. - * @visibility backend - */ - host: string; - /** - * (Required) Gitlab's group[/subgroup] where the discovery is done. - * @visibility backend - */ - group: string; - /** - * (Optional) Default branch to read the catalog-info.yaml file. - * If not set, 'master' will be used. - * @visibility backend - */ - branch?: string; - /** - * (Optional) The name used for the catalog file. - * If not set, 'catalog-info.yaml' will be used. - * @visibility backend - */ - entityFilename?: string; - } - >; + gitlab?: Record< + string, + { + /** + * (Required) Gitlab's host name. + * @visibility backend + */ + host: string; + /** + * (Optional) Gitlab's group[/subgroup] where the discovery is done. + * If not defined the whole project will be scanned. + * @visibility backend + */ + group?: string; + /** + * (Optional) Default branch to read the catalog-info.yaml file. + * If not set, 'master' will be used. + * @visibility backend + */ + branch?: string; + /** + * (Optional) The name used for the catalog file. + * If not set, 'catalog-info.yaml' will be used. + * @visibility backend + */ + entityFilename?: string; + } + >; }; }; } diff --git a/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts index 429743f4bd..892ce5f25c 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts @@ -100,7 +100,7 @@ describe('config', () => { }); expect(() => readGitlabConfigs(config)).toThrow( - "Missing required config value at 'catalog.providers.gitlab.test.group'", + "Missing required config value at 'catalog.providers.gitlab.test.host'", ); }); @@ -109,8 +109,10 @@ describe('config', () => { catalog: { providers: { gitlab: { - host: 'host', - branch: 'main', + test: { + host: 'host', + branch: 'main', + }, }, }, }, diff --git a/plugins/catalog-backend-module-gitlab/src/providers/config.ts b/plugins/catalog-backend-module-gitlab/src/providers/config.ts index 56df8d62f1..d6ad28b2ca 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/config.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/config.ts @@ -25,7 +25,7 @@ import { GitlabProviderConfig } from '../lib/types'; * @param config - The config object to extract from */ function readGitlabConfig(id: string, config: Config): GitlabProviderConfig { - const group = config.getString('group'); + const group = config.getOptionalString('group') ?? ''; const host = config.getString('host'); const branch = config.getOptionalString('branch') ?? 'master'; const catalogFile = @@ -48,7 +48,6 @@ function readGitlabConfig(id: string, config: Config): GitlabProviderConfig { * @param config - The config object to extract from */ export function readGitlabConfigs(config: Config): GitlabProviderConfig[] { - const reservedParams = ['host', 'group', 'branch', 'catalogFile']; const configs: GitlabProviderConfig[] = []; const providerConfigs = config.getOptionalConfig('catalog.providers.gitlab'); @@ -57,20 +56,8 @@ export function readGitlabConfigs(config: Config): GitlabProviderConfig[] { return configs; } - if (providerConfigs.keys().some(r => reservedParams.indexOf(r) >= 0)) { - configs.push({ - id: 'full-check', - group: '', - branch: providerConfigs.getOptionalString('branch') ?? 'master', - host: providerConfigs.getString('host'), - catalogFile: - providerConfigs.getOptionalString('entityFilename') ?? - 'catalog-info.yaml', - }); - } else { - for (const id of providerConfigs.keys()) { - configs.push(readGitlabConfig(id, providerConfigs.getConfig(id))); - } + for (const id of providerConfigs.keys()) { + configs.push(readGitlabConfig(id, providerConfigs.getConfig(id))); } return configs; From 894e3412cfd802bf5408cb0762a0ce189ac5720d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 17 Jun 2022 12:12:01 +0200 Subject: [PATCH 038/401] auth-backend: remove deprecated provider exports Signed-off-by: Patrik Oldsberg --- .../src/providers/atlassian/index.ts | 7 +-- .../src/providers/atlassian/provider.ts | 28 ++-------- .../auth-backend/src/providers/auth0/index.ts | 3 +- .../src/providers/auth0/provider.ts | 28 ---------- .../src/providers/aws-alb/index.ts | 4 +- .../src/providers/aws-alb/provider.ts | 28 ---------- .../src/providers/bitbucket/index.ts | 7 +-- .../src/providers/bitbucket/provider.ts | 42 --------------- .../auth-backend/src/providers/factories.ts | 46 ---------------- .../src/providers/gcp-iap/index.ts | 8 +-- .../src/providers/gcp-iap/provider.ts | 6 --- .../src/providers/gcp-iap/types.ts | 26 +-------- .../src/providers/github/index.ts | 4 +- .../src/providers/github/provider.ts | 46 ---------------- .../src/providers/gitlab/index.ts | 3 +- .../src/providers/gitlab/provider.ts | 31 ----------- .../src/providers/google/index.ts | 3 +- .../src/providers/google/provider.ts | 35 ------------ plugins/auth-backend/src/providers/index.ts | 4 +- .../src/providers/microsoft/index.ts | 6 +-- .../src/providers/microsoft/provider.ts | 35 ------------ .../src/providers/oauth2-proxy/index.ts | 4 +- .../src/providers/oauth2-proxy/provider.ts | 27 ---------- .../src/providers/oauth2/index.ts | 3 +- .../src/providers/oauth2/provider.ts | 18 ------- .../auth-backend/src/providers/oidc/index.ts | 4 +- .../src/providers/oidc/provider.test.ts | 6 +-- .../src/providers/oidc/provider.ts | 18 ------- .../auth-backend/src/providers/okta/index.ts | 3 +- .../src/providers/okta/provider.ts | 35 ------------ .../src/providers/onelogin/index.ts | 3 +- .../src/providers/onelogin/provider.ts | 28 ---------- .../auth-backend/src/providers/providers.ts | 54 +++++++++++++------ .../auth-backend/src/providers/saml/index.ts | 7 +-- .../src/providers/saml/provider.ts | 35 ------------ 35 files changed, 69 insertions(+), 576 deletions(-) delete mode 100644 plugins/auth-backend/src/providers/factories.ts diff --git a/plugins/auth-backend/src/providers/atlassian/index.ts b/plugins/auth-backend/src/providers/atlassian/index.ts index cf3eb901d9..dc6ce77166 100644 --- a/plugins/auth-backend/src/providers/atlassian/index.ts +++ b/plugins/auth-backend/src/providers/atlassian/index.ts @@ -14,8 +14,5 @@ * limitations under the License. */ -export { createAtlassianProvider } from './provider'; -export type { - AtlassianAuthProvider, - AtlassianProviderOptions, -} from './provider'; +export { atlassian } from './provider'; +export type { AtlassianAuthProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/atlassian/provider.ts b/plugins/auth-backend/src/providers/atlassian/provider.ts index 2aa85dab96..82feef5369 100644 --- a/plugins/auth-backend/src/providers/atlassian/provider.ts +++ b/plugins/auth-backend/src/providers/atlassian/provider.ts @@ -58,6 +58,9 @@ export const atlassianDefaultAuthHandler: AuthHandler = async ({ profile: makeProfileInfo(fullProfile, params.id_token), }); +/** + * @deprecated This export is deprecated and will be removed in the future. + */ export class AtlassianAuthProvider implements OAuthHandlers { private readonly _strategy: AtlassianStrategy; private readonly signInResolver?: SignInResolver; @@ -161,25 +164,6 @@ export class AtlassianAuthProvider implements OAuthHandlers { } } -/** - * @public - * @deprecated This type has been inlined into the create method and will be removed. - */ -export type AtlassianProviderOptions = { - /** - * The profile transformation function used to verify and convert the auth response - * into the profile that will be presented to the user. - */ - authHandler?: AuthHandler; - - /** - * Configure sign-in for this provider, without it the provider can not be used to sign users in. - */ - signIn?: { - resolver: SignInResolver; - }; -}; - /** * Auth provider integration for atlassian auth * @@ -230,9 +214,3 @@ export const atlassian = createAuthProviderIntegration({ }); }, }); - -/** - * @public - * @deprecated Use `providers.atlassian.create` instead - */ -export const createAtlassianProvider = atlassian.create; diff --git a/plugins/auth-backend/src/providers/auth0/index.ts b/plugins/auth-backend/src/providers/auth0/index.ts index b201177d69..94a08a5809 100644 --- a/plugins/auth-backend/src/providers/auth0/index.ts +++ b/plugins/auth-backend/src/providers/auth0/index.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -export { createAuth0Provider } from './provider'; -export type { Auth0ProviderOptions } from './provider'; +export { auth0 } from './provider'; diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 9ddd587508..a53f504f50 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -169,28 +169,6 @@ export class Auth0AuthProvider implements OAuthHandlers { } } -/** - * @public - * @deprecated This type has been inlined into the create method and will be removed. - */ -export type Auth0ProviderOptions = { - /** - * The profile transformation function used to verify and convert the auth response - * into the profile that will be presented to the user. - */ - authHandler?: AuthHandler; - - /** - * Configure sign-in for this provider, without it the provider can not be used to sign users in. - */ - signIn?: { - /** - * Maps an auth result to a Backstage identity for the user. - */ - resolver: SignInResolver; - }; -}; - /** * Auth provider integration for auth0 auth * @@ -249,9 +227,3 @@ export const auth0 = createAuthProviderIntegration({ }); }, }); - -/** - * @public - * @deprecated Use `providers.auth0.create` instead. - */ -export const createAuth0Provider = auth0.create; diff --git a/plugins/auth-backend/src/providers/aws-alb/index.ts b/plugins/auth-backend/src/providers/aws-alb/index.ts index 710d86ca3d..9b3e8920ac 100644 --- a/plugins/auth-backend/src/providers/aws-alb/index.ts +++ b/plugins/auth-backend/src/providers/aws-alb/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export { createAwsAlbProvider } from './provider'; -export type { AwsAlbProviderOptions, AwsAlbResult } from './provider'; +export { awsAlb } from './provider'; +export type { AwsAlbResult } from './provider'; diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index b8d43a11e6..5af606539e 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -211,28 +211,6 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { }; } -/** - * @public - * @deprecated This type has been inlined into the create method and will be removed. - */ -export type AwsAlbProviderOptions = { - /** - * The profile transformation function used to verify and convert the auth response - * into the profile that will be presented to the user. - */ - authHandler?: AuthHandler; - - /** - * Configure sign-in for this provider, without it the provider can not be used to sign users in. - */ - signIn: { - /** - * Maps an auth result to a Backstage identity for the user. - */ - resolver: SignInResolver; - }; -}; - /** * Auth provider integration for AWS ALB auth * @@ -282,9 +260,3 @@ export const awsAlb = createAuthProviderIntegration({ }; }, }); - -/** - * @public - * @deprecated Use `providers.awsAlb.create` instead - */ -export const createAwsAlbProvider = awsAlb.create; diff --git a/plugins/auth-backend/src/providers/bitbucket/index.ts b/plugins/auth-backend/src/providers/bitbucket/index.ts index 55a5735655..9d82c1066c 100644 --- a/plugins/auth-backend/src/providers/bitbucket/index.ts +++ b/plugins/auth-backend/src/providers/bitbucket/index.ts @@ -14,13 +14,8 @@ * limitations under the License. */ -export { - createBitbucketProvider, - bitbucketUsernameSignInResolver, - bitbucketUserIdSignInResolver, -} from './provider'; +export { bitbucket } from './provider'; export type { - BitbucketProviderOptions, BitbucketPassportProfile, BitbucketOAuthResult, } from './provider'; diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.ts b/plugins/auth-backend/src/providers/bitbucket/provider.ts index 5b9919764b..e3bd044466 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.ts @@ -190,28 +190,6 @@ export class BitbucketAuthProvider implements OAuthHandlers { } } -/** - * @public - * @deprecated This type has been inlined into the create method and will be removed. - */ -export type BitbucketProviderOptions = { - /** - * The profile transformation function used to verify and convert the auth response - * into the profile that will be presented to the user. - */ - authHandler?: AuthHandler; - - /** - * Configure sign-in for this provider, without it the provider can not be used to sign users in. - */ - signIn?: { - /** - * Maps an auth result to a Backstage identity for the user. - */ - resolver: SignInResolver; - }; -}; - /** * Auth provider integration for BitBucket auth * @@ -305,23 +283,3 @@ export const bitbucket = createAuthProviderIntegration({ }, }, }); - -/** - * @public - * @deprecated Use `providers.bitbucket.create` instead - */ -export const createBitbucketProvider = bitbucket.create; - -/** - * @public - * @deprecated Use `providers.bitbucket.resolvers.usernameMatchingUserEntityAnnotation()` instead. - */ -export const bitbucketUsernameSignInResolver = - bitbucket.resolvers.usernameMatchingUserEntityAnnotation(); - -/** - * @public - * @deprecated Use `providers.bitbucket.resolvers.userIdMatchingUserEntityAnnotation()` instead. - */ -export const bitbucketUserIdSignInResolver = - bitbucket.resolvers.userIdMatchingUserEntityAnnotation(); diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts deleted file mode 100644 index 42d3569815..0000000000 --- a/plugins/auth-backend/src/providers/factories.ts +++ /dev/null @@ -1,46 +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 { createGithubProvider } from './github'; -import { createGitlabProvider } from './gitlab'; -import { createGoogleProvider } from './google'; -import { createOAuth2Provider } from './oauth2'; -import { createOidcProvider } from './oidc'; -import { createOktaProvider } from './okta'; -import { createSamlProvider } from './saml'; -import { createAuth0Provider } from './auth0'; -import { createMicrosoftProvider } from './microsoft'; -import { createOneLoginProvider } from './onelogin'; -import { AuthProviderFactory } from './types'; -import { createAwsAlbProvider } from './aws-alb'; -import { createBitbucketProvider } from './bitbucket'; -import { createAtlassianProvider } from './atlassian'; - -export const factories: { [providerId: string]: AuthProviderFactory } = { - google: createGoogleProvider(), - github: createGithubProvider(), - gitlab: createGitlabProvider(), - saml: createSamlProvider(), - okta: createOktaProvider(), - auth0: createAuth0Provider(), - microsoft: createMicrosoftProvider(), - oauth2: createOAuth2Provider(), - oidc: createOidcProvider(), - onelogin: createOneLoginProvider(), - awsalb: createAwsAlbProvider(), - bitbucket: createBitbucketProvider(), - atlassian: createAtlassianProvider(), -}; diff --git a/plugins/auth-backend/src/providers/gcp-iap/index.ts b/plugins/auth-backend/src/providers/gcp-iap/index.ts index 97e31b0051..12f76ec142 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/index.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/index.ts @@ -14,9 +14,5 @@ * limitations under the License. */ -export { createGcpIapProvider } from './provider'; -export type { - GcpIapProviderOptions, - GcpIapResult, - GcpIapTokenInfo, -} from './types'; +export { gcpIap } from './provider'; +export type { GcpIapResult, GcpIapTokenInfo } from './types'; diff --git a/plugins/auth-backend/src/providers/gcp-iap/provider.ts b/plugins/auth-backend/src/providers/gcp-iap/provider.ts index cb5d5979cd..500b062d5b 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/provider.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/provider.ts @@ -117,9 +117,3 @@ export const gcpIap = createAuthProviderIntegration({ }; }, }); - -/** - * @public - * @deprecated Use `providers.gcpIap.create` instead - */ -export const createGcpIapProvider = gcpIap.create; diff --git a/plugins/auth-backend/src/providers/gcp-iap/types.ts b/plugins/auth-backend/src/providers/gcp-iap/types.ts index 6e5a022a3d..25828b7d51 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/types.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/types.ts @@ -15,7 +15,7 @@ */ import { JsonValue } from '@backstage/types'; -import { AuthHandler, AuthResponse, SignInResolver } from '../types'; +import { AuthResponse } from '../types'; /** * The header name used by the IAP. @@ -69,27 +69,3 @@ export type GcpIapProviderInfo = { * The shape of the response to return to callers. */ export type GcpIapResponse = AuthResponse; - -/** - * @public - * @deprecated This type has been inlined into the create method and will be removed. - */ -export type GcpIapProviderOptions = { - /** - * The profile transformation function used to verify and convert the auth - * response into the profile that will be presented to the user. The default - * implementation just provides the authenticated email that the IAP - * presented. - */ - authHandler?: AuthHandler; - - /** - * Configures sign-in for this provider. - */ - signIn: { - /** - * Maps an auth result to a Backstage identity for the user. - */ - resolver: SignInResolver; - }; -}; diff --git a/plugins/auth-backend/src/providers/github/index.ts b/plugins/auth-backend/src/providers/github/index.ts index a855b13b83..2f4bb1f6cd 100644 --- a/plugins/auth-backend/src/providers/github/index.ts +++ b/plugins/auth-backend/src/providers/github/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export { createGithubProvider } from './provider'; -export type { GithubOAuthResult, GithubProviderOptions } from './provider'; +export { github } from './provider'; +export type { GithubOAuthResult } from './provider'; diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index e018a6cf99..9c115b5eb8 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -227,46 +227,6 @@ export class GithubAuthProvider implements OAuthHandlers { } } -/** - * @public - * @deprecated This type has been inlined into the create method and will be removed. - */ -export type GithubProviderOptions = { - /** - * The profile transformation function used to verify and convert the auth response - * into the profile that will be presented to the user. - */ - authHandler?: AuthHandler; - - /** - * Configure sign-in for this provider, without it the provider can not be used to sign users in. - */ - signIn?: { - /** - * Maps an auth result to a Backstage identity for the user. - */ - resolver: SignInResolver; - }; - - /** - * The state encoder used to encode the 'state' parameter on the OAuth request. - * - * It should return a string that takes the state params (from the request), url encodes the params - * and finally base64 encodes them. - * - * Providing your own stateEncoder will allow you to add addition parameters to the state field. - * - * It is typed as follows: - * `export type StateEncoder = (input: OAuthState) => Promise<{encodedState: string}>;` - * - * Note: the stateEncoder must encode a 'nonce' value and an 'env' value. Without this, the OAuth flow will fail - * (These two values will be set by the req.state by default) - * - * For more information, please see the helper module in ../../oauth/helpers #readState - */ - stateEncoder?: StateEncoder; -}; - /** * Auth provider integration for GitHub auth * @@ -381,9 +341,3 @@ export const github = createAuthProviderIntegration({ }, }, }); - -/** - * @public - * @deprecated Use `providers.github.create` instead - */ -export const createGithubProvider = github.create; diff --git a/plugins/auth-backend/src/providers/gitlab/index.ts b/plugins/auth-backend/src/providers/gitlab/index.ts index 52ede48756..9b60d1f18a 100644 --- a/plugins/auth-backend/src/providers/gitlab/index.ts +++ b/plugins/auth-backend/src/providers/gitlab/index.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -export { createGitlabProvider } from './provider'; -export type { GitlabProviderOptions } from './provider'; +export { gitlab } from './provider'; diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 868357bcda..c24c2e128d 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -178,31 +178,6 @@ export class GitlabAuthProvider implements OAuthHandlers { } } -/** - * @public - * @deprecated This type has been inlined into the create method and will be removed. - */ -export type GitlabProviderOptions = { - /** - * The profile transformation function used to verify and convert the auth response - * into the profile that will be presented to the user. - */ - authHandler?: AuthHandler; - - /** - * Configure sign-in for this provider, without it the provider can not be used to sign users in. - */ - /** - * Maps an auth result to a Backstage identity for the user. - * - * Set to `'email'` to use the default email-based sign in resolver, which will search - * the catalog for a single user entity that has a matching `microsoft.com/email` annotation. - */ - signIn?: { - resolver: SignInResolver; - }; -}; - /** * Auth provider integration for GitLab auth * @@ -254,9 +229,3 @@ export const gitlab = createAuthProviderIntegration({ }); }, }); - -/** - * @public - * @deprecated Use `providers.gitlab.create` instead - */ -export const createGitlabProvider = gitlab.create; diff --git a/plugins/auth-backend/src/providers/google/index.ts b/plugins/auth-backend/src/providers/google/index.ts index 209484ad04..5e8c3236e4 100644 --- a/plugins/auth-backend/src/providers/google/index.ts +++ b/plugins/auth-backend/src/providers/google/index.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -export { createGoogleProvider, googleEmailSignInResolver } from './provider'; -export type { GoogleProviderOptions } from './provider'; +export { google } from './provider'; diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 2d58bc2aa6..f60d4e9947 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -168,28 +168,6 @@ export class GoogleAuthProvider implements OAuthHandlers { } } -/** - * @public - * @deprecated This type has been inlined into the create method and will be removed. - */ -export type GoogleProviderOptions = { - /** - * The profile transformation function used to verify and convert the auth response - * into the profile that will be presented to the user. - */ - authHandler?: AuthHandler; - - /** - * Configure sign-in for this provider, without it the provider can not be used to sign users in. - */ - signIn?: { - /** - * Maps an auth result to a Backstage identity for the user. - */ - resolver: SignInResolver; - }; -}; - /** * Auth provider integration for Google auth * @@ -272,16 +250,3 @@ export const google = createAuthProviderIntegration({ }, }, }); - -/** - * @public - * @deprecated Use `providers.google.create` instead. - */ -export const createGoogleProvider = google.create; - -/** - * @public - * @deprecated Use `providers.google.resolvers.emailMatchingUserEntityAnnotation()` instead. - */ -export const googleEmailSignInResolver = - google.resolvers.emailMatchingUserEntityAnnotation(); diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 1f33a60e55..bac1143fd1 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -30,9 +30,7 @@ export * from './onelogin'; export * from './saml'; export * from './gcp-iap'; -export { providers } from './providers'; - -export { factories as defaultAuthProviderFactories } from './factories'; +export { providers, defaultAuthProviderFactories } from './providers'; export type { AuthProviderConfig, diff --git a/plugins/auth-backend/src/providers/microsoft/index.ts b/plugins/auth-backend/src/providers/microsoft/index.ts index 3167de5d0d..16dadc3bb0 100644 --- a/plugins/auth-backend/src/providers/microsoft/index.ts +++ b/plugins/auth-backend/src/providers/microsoft/index.ts @@ -14,8 +14,4 @@ * limitations under the License. */ -export { - createMicrosoftProvider, - microsoftEmailSignInResolver, -} from './provider'; -export type { MicrosoftProviderOptions } from './provider'; +export { microsoft } from './provider'; diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index 0c58049619..db79dba225 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -190,28 +190,6 @@ export class MicrosoftAuthProvider implements OAuthHandlers { } } -/** - * @public - * @deprecated This type has been inlined into the create method and will be removed. - */ -export type MicrosoftProviderOptions = { - /** - * The profile transformation function used to verify and convert the auth response - * into the profile that will be presented to the user. - */ - authHandler?: AuthHandler; - - /** - * Configure sign-in for this provider, without it the provider can not be used to sign users in. - */ - signIn?: { - /** - * Maps an auth result to a Backstage identity for the user. - */ - resolver: SignInResolver; - }; -}; - /** * Auth provider integration for Microsoft auth * @@ -301,16 +279,3 @@ export const microsoft = createAuthProviderIntegration({ }, }, }); - -/** - * @public - * @deprecated Use `providers.microsoft.create` instead - */ -export const createMicrosoftProvider = microsoft.create; - -/** - * @public - * @deprecated Use `providers.microsoft.resolvers.emailMatchingUserEntityAnnotation()` instead. - */ -export const microsoftEmailSignInResolver = - microsoft.resolvers.emailMatchingUserEntityAnnotation(); diff --git a/plugins/auth-backend/src/providers/oauth2-proxy/index.ts b/plugins/auth-backend/src/providers/oauth2-proxy/index.ts index 62304b1d1f..d3004f31e4 100644 --- a/plugins/auth-backend/src/providers/oauth2-proxy/index.ts +++ b/plugins/auth-backend/src/providers/oauth2-proxy/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export { createOauth2ProxyProvider } from './provider'; -export type { Oauth2ProxyProviderOptions, OAuth2ProxyResult } from './provider'; +export { oauth2Proxy } from './provider'; +export type { OAuth2ProxyResult } from './provider'; diff --git a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts index 625c9e3d62..3ac7d09000 100644 --- a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts @@ -80,27 +80,6 @@ export type OAuth2ProxyResult = { getHeader(name: string): string | undefined; }; -/** - * @public - * @deprecated This type has been inlined into the create method and will be removed. - */ -export type Oauth2ProxyProviderOptions = { - /** - * Configure an auth handler to generate a profile for the user. - */ - authHandler: AuthHandler>; - - /** - * Configure sign-in for this provider, without it the provider can not be used to sign users in. - */ - signIn: { - /** - * Maps an auth result to a Backstage identity for the user. - */ - resolver: SignInResolver>; - }; -}; - interface Options { resolverContext: AuthResolverContext; signInResolver: SignInResolver>; @@ -231,9 +210,3 @@ export const oauth2Proxy = createAuthProviderIntegration({ }; }, }); - -/** - * @public - * @deprecated Use `providers.oauth2Proxy.create` instead - */ -export const createOauth2ProxyProvider = oauth2Proxy.create; diff --git a/plugins/auth-backend/src/providers/oauth2/index.ts b/plugins/auth-backend/src/providers/oauth2/index.ts index d68208a148..14485e04ff 100644 --- a/plugins/auth-backend/src/providers/oauth2/index.ts +++ b/plugins/auth-backend/src/providers/oauth2/index.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -export { createOAuth2Provider } from './provider'; -export type { OAuth2ProviderOptions } from './provider'; +export { oauth2 } from './provider'; diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index 6b4787e985..6fa649c421 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -192,18 +192,6 @@ export class OAuth2AuthProvider implements OAuthHandlers { } } -/** - * @public - * @deprecated This type has been inlined into the create method and will be removed. - */ -export type OAuth2ProviderOptions = { - authHandler?: AuthHandler; - - signIn?: { - resolver: SignInResolver; - }; -}; - /** * Auth provider integration for generic OAuth2 auth * @@ -260,9 +248,3 @@ export const oauth2 = createAuthProviderIntegration({ }); }, }); - -/** - * @public - * @deprecated Use `providers.oauth2.create` instead - */ -export const createOAuth2Provider = oauth2.create; diff --git a/plugins/auth-backend/src/providers/oidc/index.ts b/plugins/auth-backend/src/providers/oidc/index.ts index e63c9920df..6b2282411c 100644 --- a/plugins/auth-backend/src/providers/oidc/index.ts +++ b/plugins/auth-backend/src/providers/oidc/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export type { OidcAuthResult, OidcProviderOptions } from './provider'; -export { createOidcProvider } from './provider'; +export { oidc } from './provider'; +export type { OidcAuthResult } from './provider'; diff --git a/plugins/auth-backend/src/providers/oidc/provider.test.ts b/plugins/auth-backend/src/providers/oidc/provider.test.ts index 59a717f132..ee8aa50b01 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.test.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.test.ts @@ -23,7 +23,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { ClientMetadata, IssuerMetadata } from 'openid-client'; import { OAuthAdapter } from '../../lib/oauth'; -import { createOidcProvider, OidcAuthProvider, Options } from './provider'; +import { oidc, OidcAuthProvider, Options } from './provider'; import { AuthResolverContext } from '../types'; const issuerMetadata = { @@ -155,7 +155,7 @@ describe('OidcAuthProvider', () => { expect(requestSequence).toEqual([0, 1, 2].map(i => requests[i].url)); }); - it('createOidcProvider', async () => { + it('oidc.create', async () => { const handler = jest.fn((_req, res, ctx) => { return res( ctx.status(200), @@ -172,7 +172,7 @@ describe('OidcAuthProvider', () => { metadataUrl: 'https://oidc.test/.well-known/openid-configuration', }, } as any); - const provider = createOidcProvider()({ + const provider = oidc.create()({ globalConfig: { appUrl: 'https://oidc.test', baseUrl: 'https://oidc.test', diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index 69a65d1cae..eb963a3711 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -198,18 +198,6 @@ export class OidcAuthProvider implements OAuthHandlers { } } -/** - * @public - * @deprecated This type has been inlined into the create method and will be removed. - */ -export type OidcProviderOptions = { - authHandler?: AuthHandler; - - signIn?: { - resolver: SignInResolver; - }; -}; - /** * Auth provider integration for generic OpenID Connect auth * @@ -268,9 +256,3 @@ export const oidc = createAuthProviderIntegration({ }); }, }); - -/** - * @public - * @deprecated Use `providers.oidc.create` instead - */ -export const createOidcProvider = oidc.create; diff --git a/plugins/auth-backend/src/providers/okta/index.ts b/plugins/auth-backend/src/providers/okta/index.ts index fd9ebf46da..3387fa3668 100644 --- a/plugins/auth-backend/src/providers/okta/index.ts +++ b/plugins/auth-backend/src/providers/okta/index.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -export { createOktaProvider, oktaEmailSignInResolver } from './provider'; -export type { OktaProviderOptions } from './provider'; +export { okta } from './provider'; diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 98cec6df3a..de9d128641 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -192,28 +192,6 @@ export class OktaAuthProvider implements OAuthHandlers { } } -/** - * @public - * @deprecated This type has been inlined into the create method and will be removed. - */ -export type OktaProviderOptions = { - /** - * The profile transformation function used to verify and convert the auth response - * into the profile that will be presented to the user. - */ - authHandler?: AuthHandler; - - /** - * Configure sign-in for this provider, without it the provider can not be used to sign users in. - */ - signIn?: { - /** - * Maps an auth result to a Backstage identity for the user. - */ - resolver: SignInResolver; - }; -}; - /** * Auth provider integration for Okta auth * @@ -305,16 +283,3 @@ export const okta = createAuthProviderIntegration({ }, }, }); - -/** - * @public - * @deprecated Use `providers.okta.create` instead - */ -export const createOktaProvider = okta.create; - -/** - * @public - * @deprecated Use `providers.okta.resolvers.emailMatchingUserEntityAnnotation()` instead. - */ -export const oktaEmailSignInResolver = - okta.resolvers.emailMatchingUserEntityAnnotation(); diff --git a/plugins/auth-backend/src/providers/onelogin/index.ts b/plugins/auth-backend/src/providers/onelogin/index.ts index 63cf0be3cd..3f356029fb 100644 --- a/plugins/auth-backend/src/providers/onelogin/index.ts +++ b/plugins/auth-backend/src/providers/onelogin/index.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -export { createOneLoginProvider } from './provider'; -export type { OneLoginProviderOptions } from './provider'; +export { onelogin } from './provider'; diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index d81eb3d83c..59b57b3851 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -166,28 +166,6 @@ export class OneLoginProvider implements OAuthHandlers { } } -/** - * @public - * @deprecated This type has been inlined into the create method and will be removed. - */ -export type OneLoginProviderOptions = { - /** - * The profile transformation function used to verify and convert the auth response - * into the profile that will be presented to the user. - */ - authHandler?: AuthHandler; - - /** - * Configure sign-in for this provider, without it the provider can not be used to sign users in. - */ - signIn?: { - /** - * Maps an auth result to a Backstage identity for the user. - */ - resolver: SignInResolver; - }; -}; - /** * Auth provider integration for OneLogin auth * @@ -244,9 +222,3 @@ export const onelogin = createAuthProviderIntegration({ }); }, }); - -/** - * @public - * @deprecated Use `providers.onelogin.create` instead - */ -export const createOneLoginProvider = onelogin.create; diff --git a/plugins/auth-backend/src/providers/providers.ts b/plugins/auth-backend/src/providers/providers.ts index 3d49e2bbbb..71df8223ef 100644 --- a/plugins/auth-backend/src/providers/providers.ts +++ b/plugins/auth-backend/src/providers/providers.ts @@ -14,21 +14,22 @@ * limitations under the License. */ -import { atlassian } from './atlassian/provider'; -import { auth0 } from './auth0/provider'; -import { awsAlb } from './aws-alb/provider'; -import { bitbucket } from './bitbucket/provider'; -import { gcpIap } from './gcp-iap/provider'; -import { github } from './github/provider'; -import { gitlab } from './gitlab/provider'; -import { google } from './google/provider'; -import { microsoft } from './microsoft/provider'; -import { oauth2 } from './oauth2/provider'; -import { oauth2Proxy } from './oauth2-proxy/provider'; -import { oidc } from './oidc/provider'; -import { okta } from './okta/provider'; -import { onelogin } from './onelogin/provider'; -import { saml } from './saml/provider'; +import { atlassian } from './atlassian'; +import { auth0 } from './auth0'; +import { awsAlb } from './aws-alb'; +import { bitbucket } from './bitbucket'; +import { gcpIap } from './gcp-iap'; +import { github } from './github'; +import { gitlab } from './gitlab'; +import { google } from './google'; +import { microsoft } from './microsoft'; +import { oauth2 } from './oauth2'; +import { oauth2Proxy } from './oauth2-proxy'; +import { oidc } from './oidc'; +import { okta } from './okta'; +import { onelogin } from './onelogin'; +import { saml } from './saml'; +import { AuthProviderFactory } from './types'; /** * All built-in auth provider integrations. @@ -52,3 +53,26 @@ export const providers = Object.freeze({ onelogin, saml, }); + +/** + * All auth provider factories that are installed by default. + * + * @public + */ +export const defaultAuthProviderFactories: { + [providerId: string]: AuthProviderFactory; +} = { + google: google.create(), + github: github.create(), + gitlab: gitlab.create(), + saml: saml.create(), + okta: okta.create(), + auth0: auth0.create(), + microsoft: microsoft.create(), + oauth2: oauth2.create(), + oidc: oidc.create(), + onelogin: onelogin.create(), + awsalb: awsAlb.create(), + bitbucket: bitbucket.create(), + atlassian: atlassian.create(), +}; diff --git a/plugins/auth-backend/src/providers/saml/index.ts b/plugins/auth-backend/src/providers/saml/index.ts index 8d36e17d29..d59f7bf0a7 100644 --- a/plugins/auth-backend/src/providers/saml/index.ts +++ b/plugins/auth-backend/src/providers/saml/index.ts @@ -14,8 +14,5 @@ * limitations under the License. */ -export { - createSamlProvider, - samlNameIdEntityNameSignInResolver, -} from './provider'; -export type { SamlProviderOptions, SamlAuthResult } from './provider'; +export { saml } from './provider'; +export type { SamlAuthResult } from './provider'; diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 4f5076b410..3d35f46628 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -132,28 +132,6 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { type SignatureAlgorithm = 'sha1' | 'sha256' | 'sha512'; -/** - * @public - * @deprecated This type has been inlined into the create method and will be removed. - */ -export type SamlProviderOptions = { - /** - * The profile transformation function used to verify and convert the auth response - * into the profile that will be presented to the user. - */ - authHandler?: AuthHandler; - - /** - * Configure sign-in for this provider, without it the provider can not be used to sign users in. - */ - signIn?: { - /** - * Maps an auth result to a Backstage identity for the user. - */ - resolver: SignInResolver; - }; -}; - /** * Auth provider integration for SAML auth * @@ -230,16 +208,3 @@ export const saml = createAuthProviderIntegration({ }, }, }); - -/** - * @public - * @deprecated Use `providers.saml.create` instead - */ -export const createSamlProvider = saml.create; - -/** - * @public - * @deprecated Use `providers.saml.resolvers.nameIdMatchingUserEntityName()` instead. - */ -export const samlNameIdEntityNameSignInResolver = - saml.resolvers.nameIdMatchingUserEntityName(); From ba0cb5b99b2b57e67e90fb17d98da4a617b9194c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 17 Jun 2022 12:17:11 +0200 Subject: [PATCH 039/401] auth-backend: remove deprecated provider factory fields and types Signed-off-by: Patrik Oldsberg --- plugins/auth-backend/src/providers/index.ts | 1 - plugins/auth-backend/src/providers/types.ts | 41 ++------------------- plugins/auth-backend/src/service/router.ts | 4 -- 3 files changed, 3 insertions(+), 43 deletions(-) diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index bac1143fd1..778783994f 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -35,7 +35,6 @@ export { providers, defaultAuthProviderFactories } from './providers'; export type { AuthProviderConfig, AuthProviderRouteHandlers, - AuthProviderFactoryOptions, AuthProviderFactory, AuthHandler, AuthResolverCatalogUserQuery, diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 128dea12ef..c99bdfbadb 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -14,11 +14,8 @@ * limitations under the License. */ -import { - PluginEndpointDiscovery, - TokenManager, -} from '@backstage/backend-common'; -import { CatalogApi, GetEntitiesRequest } from '@backstage/catalog-client'; +import { GetEntitiesRequest } from '@backstage/catalog-client'; +import { Entity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { BackstageIdentityResponse, @@ -26,10 +23,8 @@ import { } from '@backstage/plugin-auth-node'; import express from 'express'; import { Logger } from 'winston'; -import { TokenIssuer, TokenParams } from '../identity/types'; +import { TokenParams } from '../identity/types'; import { OAuthStartRequest } from '../lib/oauth/types'; -import { CatalogIdentityClient } from '../lib/catalog'; -import { Entity } from '@backstage/catalog-model'; /** * A query for a single user in the catalog. @@ -69,13 +64,6 @@ export type AuthResolverCatalogUserQuery = * @public */ export type AuthResolverContext = { - /** @deprecated Will be removed from the context, access it via a closure instead if needed */ - logger: Logger; - /** @deprecated Use the `issueToken` method instead */ - tokenIssuer: TokenIssuer; - /** @deprecated Use the `findCatalogUser` and `signInWithCatalogUser` methods instead, and the `getDefaultOwnershipEntityRefs` helper */ - catalogIdentityClient: CatalogIdentityClient; - /** * Issues a Backstage token using the provided parameters. */ @@ -204,35 +192,12 @@ export interface AuthProviderRouteHandlers { logout?(req: express.Request, res: express.Response): Promise; } -/** - * @deprecated This type is deprecated and will be removed in a future release. - */ -export type AuthProviderFactoryOptions = { - providerId: string; - globalConfig: AuthProviderConfig; - config: Config; - logger: Logger; - tokenManager: TokenManager; - tokenIssuer: TokenIssuer; - discovery: PluginEndpointDiscovery; - catalogApi: CatalogApi; -}; - export type AuthProviderFactory = (options: { providerId: string; globalConfig: AuthProviderConfig; config: Config; logger: Logger; resolverContext: AuthResolverContext; - - /** @deprecated This field has been deprecated and needs to be passed directly to the auth provider instead */ - tokenManager: TokenManager; - /** @deprecated This field has been deprecated and needs to be passed directly to the auth provider instead */ - tokenIssuer: TokenIssuer; - /** @deprecated This field has been deprecated and needs to be passed directly to the auth provider instead */ - discovery: PluginEndpointDiscovery; - /** @deprecated This field has been deprecated and needs to be passed directly to the auth provider instead */ - catalogApi: CatalogApi; }) => AuthProviderRouteHandlers; /** @public */ diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 80b8a24022..50577ece53 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -119,10 +119,6 @@ export async function createRouter( }, config: providersConfig.getConfig(providerId), logger, - tokenManager, - tokenIssuer, - discovery, - catalogApi, resolverContext: CatalogAuthResolverContext.create({ logger, catalogApi, From 36e4377480aff6b2939fa8a7dcb6f48328855b82 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 17 Jun 2022 12:17:29 +0200 Subject: [PATCH 040/401] auth-backend: remove deprecated identity exports Signed-off-by: Patrik Oldsberg --- plugins/auth-backend/src/identity/index.ts | 2 +- plugins/auth-backend/src/identity/types.ts | 4 +- plugins/auth-backend/src/index.ts | 2 +- .../auth-backend/src/lib/catalog/helpers.ts | 41 ------------------- plugins/auth-backend/src/lib/catalog/index.ts | 1 - .../src/lib/oauth/OAuthAdapter.ts | 8 +--- .../resolvers/CatalogAuthResolverContext.ts | 3 +- 7 files changed, 5 insertions(+), 56 deletions(-) delete mode 100644 plugins/auth-backend/src/lib/catalog/helpers.ts diff --git a/plugins/auth-backend/src/identity/index.ts b/plugins/auth-backend/src/identity/index.ts index cb2f369668..a5e0dd4b80 100644 --- a/plugins/auth-backend/src/identity/index.ts +++ b/plugins/auth-backend/src/identity/index.ts @@ -20,4 +20,4 @@ export { DatabaseKeyStore } from './DatabaseKeyStore'; export { MemoryKeyStore } from './MemoryKeyStore'; export { FirestoreKeyStore } from './FirestoreKeyStore'; export { KeyStores } from './KeyStores'; -export type { KeyStore, TokenIssuer, TokenParams } from './types'; +export type { KeyStore, TokenParams } from './types'; diff --git a/plugins/auth-backend/src/identity/types.ts b/plugins/auth-backend/src/identity/types.ts index 5a350284e5..f0959e90bd 100644 --- a/plugins/auth-backend/src/identity/types.ts +++ b/plugins/auth-backend/src/identity/types.ts @@ -37,12 +37,10 @@ export type TokenParams = { }; }; -// TODO(Rugvip): This should at least be made internal /** * A TokenIssuer is able to issue verifiable ID Tokens on demand. * - * @public - * @deprecated This interface is deprecated and will be removed in a future release. + * @internal */ export type TokenIssuer = { /** diff --git a/plugins/auth-backend/src/index.ts b/plugins/auth-backend/src/index.ts index d51cb3f1eb..f8a8580359 100644 --- a/plugins/auth-backend/src/index.ts +++ b/plugins/auth-backend/src/index.ts @@ -21,7 +21,7 @@ */ export * from './service/router'; -export type { TokenIssuer, TokenParams } from './identity'; +export type { TokenParams } from './identity'; export * from './providers'; // flow package provides 2 functions diff --git a/plugins/auth-backend/src/lib/catalog/helpers.ts b/plugins/auth-backend/src/lib/catalog/helpers.ts deleted file mode 100644 index 3ff1b5da05..0000000000 --- a/plugins/auth-backend/src/lib/catalog/helpers.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - RELATION_MEMBER_OF, - stringifyEntityRef, - UserEntity, -} from '@backstage/catalog-model'; -import { TokenParams } from '../../identity'; - -/** - * @deprecated use {@link getDefaultOwnershipEntityRefs} instead - */ -export function getEntityClaims(entity: UserEntity): TokenParams['claims'] { - const userRef = stringifyEntityRef(entity); - - const membershipRefs = - entity.relations - ?.filter( - r => r.type === RELATION_MEMBER_OF && r.targetRef.startsWith('group:'), - ) - .map(r => r.targetRef) ?? []; - - return { - sub: userRef, - ent: [userRef, ...membershipRefs], - }; -} diff --git a/plugins/auth-backend/src/lib/catalog/index.ts b/plugins/auth-backend/src/lib/catalog/index.ts index 0d1aa4a6f7..f0f5b10808 100644 --- a/plugins/auth-backend/src/lib/catalog/index.ts +++ b/plugins/auth-backend/src/lib/catalog/index.ts @@ -15,4 +15,3 @@ */ export { CatalogIdentityClient } from './CatalogIdentityClient'; -export { getEntityClaims } from './helpers'; diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index 5be4b7e3c0..9c28ed8fad 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -31,7 +31,6 @@ import { isError, NotAllowedError, } from '@backstage/errors'; -import { TokenIssuer } from '../../identity/types'; import { defaultCookieConfigurer, readState, verifyNonce } from './helpers'; import { postMessageResponse, ensuresXRequestedWith } from '../flow'; import { @@ -52,8 +51,6 @@ export type Options = { cookieDomain: string; cookiePath: string; appOrigin: string; - /** @deprecated This option is no longer needed */ - tokenIssuer?: TokenIssuer; isOriginAllowed: (origin: string) => boolean; callbackUrl: string; }; @@ -61,10 +58,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { static fromConfig( config: AuthProviderConfig, handlers: OAuthHandlers, - options: Pick< - Options, - 'providerId' | 'persistScopes' | 'tokenIssuer' | 'callbackUrl' - >, + options: Pick, ): OAuthAdapter { const { origin: appOrigin } = new URL(config.appUrl); diff --git a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts index 1a1fdd7a36..cbc2439563 100644 --- a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts +++ b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts @@ -25,8 +25,7 @@ import { } from '@backstage/catalog-model'; import { ConflictError, InputError, NotFoundError } from '@backstage/errors'; import { Logger } from 'winston'; -import { TokenIssuer } from '../..'; -import { TokenParams } from '../../identity'; +import { TokenIssuer, TokenParams } from '../../identity/types'; import { AuthResolverContext } from '../../providers'; import { AuthResolverCatalogUserQuery } from '../../providers/types'; import { CatalogIdentityClient } from '../catalog'; From 59761a0106298998547de01f415cf175250e2b0b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 17 Jun 2022 12:18:08 +0200 Subject: [PATCH 041/401] auth-backend: update API report Signed-off-by: Patrik Oldsberg --- plugins/auth-backend/api-report.md | 624 ++++++++++++----------------- 1 file changed, 253 insertions(+), 371 deletions(-) diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index bb25ff6d57..136f9a664d 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -23,9 +23,26 @@ import { TokenSet } from 'openid-client'; import { UserEntity } from '@backstage/catalog-model'; import { UserinfoResponse } from 'openid-client'; +// @public +export const atlassian: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: never; +}>; + // Warning: (ae-missing-release-tag) "AtlassianAuthProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export class AtlassianAuthProvider implements OAuthHandlers { // Warning: (ae-forgotten-export) The symbol "AtlassianAuthProviderOptions" needs to be exported by the entry point index.d.ts constructor(options: AtlassianAuthProviderOptions); @@ -45,21 +62,22 @@ export class AtlassianAuthProvider implements OAuthHandlers { start(req: OAuthStartRequest): Promise; } -// @public @deprecated (undocumented) -export type AtlassianProviderOptions = { - authHandler?: AuthHandler; - signIn?: { - resolver: SignInResolver; - }; -}; - -// @public @deprecated (undocumented) -export type Auth0ProviderOptions = { - authHandler?: AuthHandler; - signIn?: { - resolver: SignInResolver; - }; -}; +// @public +export const auth0: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: never; +}>; // @public export type AuthHandler = ( @@ -89,26 +107,8 @@ export type AuthProviderFactory = (options: { config: Config; logger: Logger; resolverContext: AuthResolverContext; - tokenManager: TokenManager; - tokenIssuer: TokenIssuer; - discovery: PluginEndpointDiscovery; - catalogApi: CatalogApi; }) => AuthProviderRouteHandlers; -// Warning: (ae-missing-release-tag) "AuthProviderFactoryOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public @deprecated (undocumented) -export type AuthProviderFactoryOptions = { - providerId: string; - globalConfig: AuthProviderConfig; - config: Config; - logger: Logger; - tokenManager: TokenManager; - tokenIssuer: TokenIssuer; - discovery: PluginEndpointDiscovery; - catalogApi: CatalogApi; -}; - // Warning: (ae-missing-release-tag) "AuthProviderRouteHandlers" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -139,9 +139,6 @@ export type AuthResolverCatalogUserQuery = // @public export type AuthResolverContext = { - logger: Logger; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; issueToken(params: TokenParams): Promise<{ token: string; }>; @@ -160,13 +157,20 @@ export type AuthResponse = { backstageIdentity?: BackstageIdentityResponse; }; -// @public @deprecated (undocumented) -export type AwsAlbProviderOptions = { - authHandler?: AuthHandler; - signIn: { - resolver: SignInResolver; - }; -}; +// @public +export const awsAlb: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn: { + resolver: SignInResolver; + }; + } + | undefined, + ) => AuthProviderFactory; + resolvers: never; +}>; // @public (undocumented) export type AwsAlbResult = { @@ -175,6 +179,26 @@ export type AwsAlbResult = { accessToken: string; }; +// @public +export const bitbucket: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: Readonly<{ + usernameMatchingUserEntityAnnotation(): SignInResolver; + userIdMatchingUserEntityAnnotation(): SignInResolver; + }>; +}>; + // Warning: (ae-missing-release-tag) "BitbucketOAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -206,20 +230,6 @@ export type BitbucketPassportProfile = Profile & { }; }; -// @public @deprecated (undocumented) -export type BitbucketProviderOptions = { - authHandler?: AuthHandler; - signIn?: { - resolver: SignInResolver; - }; -}; - -// @public @deprecated (undocumented) -export const bitbucketUserIdSignInResolver: SignInResolver; - -// @public @deprecated (undocumented) -export const bitbucketUsernameSignInResolver: SignInResolver; - // Warning: (ae-missing-release-tag) "CatalogIdentityClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -242,189 +252,6 @@ export type CookieConfigurer = (ctx: { secure: boolean; }; -// @public @deprecated (undocumented) -export const createAtlassianProvider: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn?: - | { - resolver: SignInResolver; - } - | undefined; - } - | undefined, -) => AuthProviderFactory; - -// @public @deprecated (undocumented) -export const createAuth0Provider: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn?: - | { - resolver: SignInResolver; - } - | undefined; - } - | undefined, -) => AuthProviderFactory; - -// @public @deprecated (undocumented) -export const createAwsAlbProvider: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn: { - resolver: SignInResolver; - }; - } - | undefined, -) => AuthProviderFactory; - -// @public @deprecated (undocumented) -export const createBitbucketProvider: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn?: - | { - resolver: SignInResolver; - } - | undefined; - } - | undefined, -) => AuthProviderFactory; - -// @public @deprecated (undocumented) -export const createGcpIapProvider: (options: { - authHandler?: AuthHandler | undefined; - signIn: { - resolver: SignInResolver; - }; -}) => AuthProviderFactory; - -// @public @deprecated (undocumented) -export const createGithubProvider: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn?: - | { - resolver: SignInResolver; - } - | undefined; - stateEncoder?: StateEncoder | undefined; - } - | undefined, -) => AuthProviderFactory; - -// @public @deprecated (undocumented) -export const createGitlabProvider: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn?: - | { - resolver: SignInResolver; - } - | undefined; - } - | undefined, -) => AuthProviderFactory; - -// @public @deprecated (undocumented) -export const createGoogleProvider: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn?: - | { - resolver: SignInResolver; - } - | undefined; - } - | undefined, -) => AuthProviderFactory; - -// @public @deprecated (undocumented) -export const createMicrosoftProvider: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn?: - | { - resolver: SignInResolver; - } - | undefined; - } - | undefined, -) => AuthProviderFactory; - -// @public @deprecated (undocumented) -export const createOAuth2Provider: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn?: - | { - resolver: SignInResolver; - } - | undefined; - } - | undefined, -) => AuthProviderFactory; - -// @public @deprecated (undocumented) -export const createOauth2ProxyProvider: (options: { - authHandler?: AuthHandler> | undefined; - signIn: { - resolver: SignInResolver>; - }; -}) => AuthProviderFactory; - -// @public @deprecated (undocumented) -export const createOidcProvider: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn?: - | { - resolver: SignInResolver; - } - | undefined; - } - | undefined, -) => AuthProviderFactory; - -// @public @deprecated (undocumented) -export const createOktaProvider: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn?: - | { - resolver: SignInResolver; - } - | undefined; - } - | undefined, -) => AuthProviderFactory; - -// @public @deprecated (undocumented) -export const createOneLoginProvider: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn?: - | { - resolver: SignInResolver; - } - | undefined; - } - | undefined, -) => AuthProviderFactory; - // Warning: (ae-missing-release-tag) "createOriginFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -435,23 +262,7 @@ export function createOriginFilter(config: Config): (origin: string) => boolean; // @public (undocumented) export function createRouter(options: RouterOptions): Promise; -// @public @deprecated (undocumented) -export const createSamlProvider: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn?: - | { - resolver: SignInResolver; - } - | undefined; - } - | undefined, -) => AuthProviderFactory; - -// Warning: (ae-missing-release-tag) "factories" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const defaultAuthProviderFactories: { [providerId: string]: AuthProviderFactory; }; @@ -466,13 +277,16 @@ export const encodeState: (state: OAuthState) => string; // @public (undocumented) export const ensuresXRequestedWith: (req: express.Request) => boolean; -// @public @deprecated (undocumented) -export type GcpIapProviderOptions = { - authHandler?: AuthHandler; - signIn: { - resolver: SignInResolver; - }; -}; +// @public +export const gcpIap: Readonly<{ + create: (options: { + authHandler?: AuthHandler | undefined; + signIn: { + resolver: SignInResolver; + }; + }) => AuthProviderFactory; + resolvers: never; +}>; // @public export type GcpIapResult = { @@ -489,10 +303,25 @@ export type GcpIapTokenInfo = { // @public export function getDefaultOwnershipEntityRefs(entity: Entity): string[]; -// Warning: (ae-missing-release-tag) "getEntityClaims" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public @deprecated (undocumented) -export function getEntityClaims(entity: UserEntity): TokenParams['claims']; +// @public +export const github: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + stateEncoder?: StateEncoder | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: Readonly<{ + usernameMatchingUserEntityName: () => SignInResolver; + }>; +}>; // Warning: (ae-missing-release-tag) "GithubOAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -508,60 +337,92 @@ export type GithubOAuthResult = { refreshToken?: string; }; -// @public @deprecated (undocumented) -export type GithubProviderOptions = { - authHandler?: AuthHandler; - signIn?: { - resolver: SignInResolver; - }; - stateEncoder?: StateEncoder; -}; +// @public +export const gitlab: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: never; +}>; -// @public @deprecated (undocumented) -export type GitlabProviderOptions = { - authHandler?: AuthHandler; - signIn?: { - resolver: SignInResolver; - }; -}; +// @public +export const google: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: Readonly<{ + emailLocalPartMatchingUserEntityName: () => SignInResolver; + emailMatchingUserEntityProfileEmail: () => SignInResolver; + emailMatchingUserEntityAnnotation(): SignInResolver; + }>; +}>; -// @public @deprecated (undocumented) -export const googleEmailSignInResolver: SignInResolver; +// @public +export const microsoft: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: Readonly<{ + emailLocalPartMatchingUserEntityName: () => SignInResolver; + emailMatchingUserEntityProfileEmail: () => SignInResolver; + emailMatchingUserEntityAnnotation(): SignInResolver; + }>; +}>; -// @public @deprecated (undocumented) -export type GoogleProviderOptions = { - authHandler?: AuthHandler; - signIn?: { - resolver: SignInResolver; - }; -}; +// @public +export const oauth2: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: never; +}>; -// @public @deprecated (undocumented) -export const microsoftEmailSignInResolver: SignInResolver; - -// @public @deprecated (undocumented) -export type MicrosoftProviderOptions = { - authHandler?: AuthHandler; - signIn?: { - resolver: SignInResolver; - }; -}; - -// @public @deprecated (undocumented) -export type OAuth2ProviderOptions = { - authHandler?: AuthHandler; - signIn?: { - resolver: SignInResolver; - }; -}; - -// @public @deprecated (undocumented) -export type Oauth2ProxyProviderOptions = { - authHandler: AuthHandler>; - signIn: { - resolver: SignInResolver>; - }; -}; +// @public +export const oauth2Proxy: Readonly<{ + create: (options: { + authHandler?: AuthHandler> | undefined; + signIn: { + resolver: SignInResolver>; + }; + }) => AuthProviderFactory; + resolvers: never; +}>; // @public export type OAuth2ProxyResult = { @@ -584,10 +445,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { static fromConfig( config: AuthProviderConfig, handlers: OAuthHandlers, - options: Pick< - Options, - 'providerId' | 'persistScopes' | 'tokenIssuer' | 'callbackUrl' - >, + options: Pick, ): OAuthAdapter; // (undocumented) logout(req: express.Request, res: express.Response): Promise; @@ -697,38 +555,66 @@ export type OAuthState = { scope?: string; }; +// @public +export const oidc: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: never; +}>; + // @public export type OidcAuthResult = { tokenset: TokenSet; userinfo: UserinfoResponse; }; -// @public @deprecated (undocumented) -export type OidcProviderOptions = { - authHandler?: AuthHandler; - signIn?: { - resolver: SignInResolver; - }; -}; +// @public +export const okta: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: Readonly<{ + emailLocalPartMatchingUserEntityName: () => SignInResolver; + emailMatchingUserEntityProfileEmail: () => SignInResolver; + emailMatchingUserEntityAnnotation(): SignInResolver; + }>; +}>; -// @public @deprecated (undocumented) -export const oktaEmailSignInResolver: SignInResolver; - -// @public @deprecated (undocumented) -export type OktaProviderOptions = { - authHandler?: AuthHandler; - signIn?: { - resolver: SignInResolver; - }; -}; - -// @public @deprecated (undocumented) -export type OneLoginProviderOptions = { - authHandler?: AuthHandler; - signIn?: { - resolver: SignInResolver; - }; -}; +// @public +export const onelogin: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: never; +}>; // Warning: (ae-missing-release-tag) "postMessageResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1011,22 +897,30 @@ export interface RouterOptions { tokenManager: TokenManager; } +// @public +export const saml: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: Readonly<{ + nameIdMatchingUserEntityName(): SignInResolver; + }>; +}>; + // @public (undocumented) export type SamlAuthResult = { fullProfile: any; }; -// @public @deprecated (undocumented) -export const samlNameIdEntityNameSignInResolver: SignInResolver; - -// @public @deprecated (undocumented) -export type SamlProviderOptions = { - authHandler?: AuthHandler; - signIn?: { - resolver: SignInResolver; - }; -}; - // @public export type SignInInfo = { profile: ProfileInfo; @@ -1044,14 +938,6 @@ export type StateEncoder = (req: OAuthStartRequest) => Promise<{ encodedState: string; }>; -// @public @deprecated -export type TokenIssuer = { - issueToken(params: TokenParams): Promise; - listPublicKeys(): Promise<{ - keys: AnyJWK[]; - }>; -}; - // @public export type TokenParams = { claims: { @@ -1077,8 +963,4 @@ export type WebMessageResponse = type: 'authorization_response'; error: Error; }; - -// Warnings were encountered during analysis: -// -// src/identity/types.d.ts:38:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts ``` From 4d74ec0b22b89228541b787ce397104aedfa6db7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 17 Jun 2022 12:44:24 +0200 Subject: [PATCH 042/401] auth-backend: remove direct provider exports Signed-off-by: Patrik Oldsberg --- plugins/auth-backend/api-report.md | 263 ------------------ .../src/providers/atlassian/index.ts | 3 +- .../src/providers/atlassian/provider.ts | 1 + plugins/auth-backend/src/providers/index.ts | 26 +- 4 files changed, 13 insertions(+), 280 deletions(-) diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 136f9a664d..9e567cc6ab 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -23,25 +23,6 @@ import { TokenSet } from 'openid-client'; import { UserEntity } from '@backstage/catalog-model'; import { UserinfoResponse } from 'openid-client'; -// @public -export const atlassian: Readonly<{ - create: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn?: - | { - resolver: SignInResolver; - } - | undefined; - } - | undefined, - ) => AuthProviderFactory; - resolvers: never; -}>; - -// Warning: (ae-missing-release-tag) "AtlassianAuthProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export class AtlassianAuthProvider implements OAuthHandlers { // Warning: (ae-forgotten-export) The symbol "AtlassianAuthProviderOptions" needs to be exported by the entry point index.d.ts @@ -62,23 +43,6 @@ export class AtlassianAuthProvider implements OAuthHandlers { start(req: OAuthStartRequest): Promise; } -// @public -export const auth0: Readonly<{ - create: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn?: - | { - resolver: SignInResolver; - } - | undefined; - } - | undefined, - ) => AuthProviderFactory; - resolvers: never; -}>; - // @public export type AuthHandler = ( input: TAuthResult, @@ -157,21 +121,6 @@ export type AuthResponse = { backstageIdentity?: BackstageIdentityResponse; }; -// @public -export const awsAlb: Readonly<{ - create: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn: { - resolver: SignInResolver; - }; - } - | undefined, - ) => AuthProviderFactory; - resolvers: never; -}>; - // @public (undocumented) export type AwsAlbResult = { fullProfile: Profile; @@ -179,26 +128,6 @@ export type AwsAlbResult = { accessToken: string; }; -// @public -export const bitbucket: Readonly<{ - create: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn?: - | { - resolver: SignInResolver; - } - | undefined; - } - | undefined, - ) => AuthProviderFactory; - resolvers: Readonly<{ - usernameMatchingUserEntityAnnotation(): SignInResolver; - userIdMatchingUserEntityAnnotation(): SignInResolver; - }>; -}>; - // Warning: (ae-missing-release-tag) "BitbucketOAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -277,17 +206,6 @@ export const encodeState: (state: OAuthState) => string; // @public (undocumented) export const ensuresXRequestedWith: (req: express.Request) => boolean; -// @public -export const gcpIap: Readonly<{ - create: (options: { - authHandler?: AuthHandler | undefined; - signIn: { - resolver: SignInResolver; - }; - }) => AuthProviderFactory; - resolvers: never; -}>; - // @public export type GcpIapResult = { iapToken: GcpIapTokenInfo; @@ -303,26 +221,6 @@ export type GcpIapTokenInfo = { // @public export function getDefaultOwnershipEntityRefs(entity: Entity): string[]; -// @public -export const github: Readonly<{ - create: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn?: - | { - resolver: SignInResolver; - } - | undefined; - stateEncoder?: StateEncoder | undefined; - } - | undefined, - ) => AuthProviderFactory; - resolvers: Readonly<{ - usernameMatchingUserEntityName: () => SignInResolver; - }>; -}>; - // Warning: (ae-missing-release-tag) "GithubOAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -337,93 +235,6 @@ export type GithubOAuthResult = { refreshToken?: string; }; -// @public -export const gitlab: Readonly<{ - create: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn?: - | { - resolver: SignInResolver; - } - | undefined; - } - | undefined, - ) => AuthProviderFactory; - resolvers: never; -}>; - -// @public -export const google: Readonly<{ - create: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn?: - | { - resolver: SignInResolver; - } - | undefined; - } - | undefined, - ) => AuthProviderFactory; - resolvers: Readonly<{ - emailLocalPartMatchingUserEntityName: () => SignInResolver; - emailMatchingUserEntityProfileEmail: () => SignInResolver; - emailMatchingUserEntityAnnotation(): SignInResolver; - }>; -}>; - -// @public -export const microsoft: Readonly<{ - create: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn?: - | { - resolver: SignInResolver; - } - | undefined; - } - | undefined, - ) => AuthProviderFactory; - resolvers: Readonly<{ - emailLocalPartMatchingUserEntityName: () => SignInResolver; - emailMatchingUserEntityProfileEmail: () => SignInResolver; - emailMatchingUserEntityAnnotation(): SignInResolver; - }>; -}>; - -// @public -export const oauth2: Readonly<{ - create: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn?: - | { - resolver: SignInResolver; - } - | undefined; - } - | undefined, - ) => AuthProviderFactory; - resolvers: never; -}>; - -// @public -export const oauth2Proxy: Readonly<{ - create: (options: { - authHandler?: AuthHandler> | undefined; - signIn: { - resolver: SignInResolver>; - }; - }) => AuthProviderFactory; - resolvers: never; -}>; - // @public export type OAuth2ProxyResult = { fullProfile: JWTPayload; @@ -555,67 +366,12 @@ export type OAuthState = { scope?: string; }; -// @public -export const oidc: Readonly<{ - create: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn?: - | { - resolver: SignInResolver; - } - | undefined; - } - | undefined, - ) => AuthProviderFactory; - resolvers: never; -}>; - // @public export type OidcAuthResult = { tokenset: TokenSet; userinfo: UserinfoResponse; }; -// @public -export const okta: Readonly<{ - create: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn?: - | { - resolver: SignInResolver; - } - | undefined; - } - | undefined, - ) => AuthProviderFactory; - resolvers: Readonly<{ - emailLocalPartMatchingUserEntityName: () => SignInResolver; - emailMatchingUserEntityProfileEmail: () => SignInResolver; - emailMatchingUserEntityAnnotation(): SignInResolver; - }>; -}>; - -// @public -export const onelogin: Readonly<{ - create: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn?: - | { - resolver: SignInResolver; - } - | undefined; - } - | undefined, - ) => AuthProviderFactory; - resolvers: never; -}>; - // Warning: (ae-missing-release-tag) "postMessageResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -897,25 +653,6 @@ export interface RouterOptions { tokenManager: TokenManager; } -// @public -export const saml: Readonly<{ - create: ( - options?: - | { - authHandler?: AuthHandler | undefined; - signIn?: - | { - resolver: SignInResolver; - } - | undefined; - } - | undefined, - ) => AuthProviderFactory; - resolvers: Readonly<{ - nameIdMatchingUserEntityName(): SignInResolver; - }>; -}>; - // @public (undocumented) export type SamlAuthResult = { fullProfile: any; diff --git a/plugins/auth-backend/src/providers/atlassian/index.ts b/plugins/auth-backend/src/providers/atlassian/index.ts index dc6ce77166..8dff0197a3 100644 --- a/plugins/auth-backend/src/providers/atlassian/index.ts +++ b/plugins/auth-backend/src/providers/atlassian/index.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -export { atlassian } from './provider'; -export type { AtlassianAuthProvider } from './provider'; +export { atlassian, AtlassianAuthProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/atlassian/provider.ts b/plugins/auth-backend/src/providers/atlassian/provider.ts index 82feef5369..b693d226a2 100644 --- a/plugins/auth-backend/src/providers/atlassian/provider.ts +++ b/plugins/auth-backend/src/providers/atlassian/provider.ts @@ -59,6 +59,7 @@ export const atlassianDefaultAuthHandler: AuthHandler = async ({ }); /** + * @public * @deprecated This export is deprecated and will be removed in the future. */ export class AtlassianAuthProvider implements OAuthHandlers { diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 778783994f..de410a9407 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -14,21 +14,17 @@ * limitations under the License. */ -export * from './atlassian'; -export * from './auth0'; -export * from './aws-alb'; -export * from './bitbucket'; -export * from './github'; -export * from './gitlab'; -export * from './google'; -export * from './microsoft'; -export * from './oauth2'; -export * from './oauth2-proxy'; -export * from './oidc'; -export * from './okta'; -export * from './onelogin'; -export * from './saml'; -export * from './gcp-iap'; +export { AtlassianAuthProvider } from './atlassian'; +export type { AwsAlbResult } from './aws-alb'; +export type { + BitbucketOAuthResult, + BitbucketPassportProfile, +} from './bitbucket'; +export type { GithubOAuthResult } from './github'; +export type { OAuth2ProxyResult } from './oauth2-proxy'; +export type { OidcAuthResult } from './oidc'; +export type { SamlAuthResult } from './saml'; +export type { GcpIapResult, GcpIapTokenInfo } from './gcp-iap'; export { providers, defaultAuthProviderFactories } from './providers'; From 9d4040777e760639c307d979b75022699d8e615a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 17 Jun 2022 17:40:02 +0200 Subject: [PATCH 043/401] changesets: add changeset for auth backend removals Signed-off-by: Patrik Oldsberg --- .changeset/popular-pots-yell.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/popular-pots-yell.md diff --git a/.changeset/popular-pots-yell.md b/.changeset/popular-pots-yell.md new file mode 100644 index 0000000000..f0decc9fb8 --- /dev/null +++ b/.changeset/popular-pots-yell.md @@ -0,0 +1,11 @@ +--- +'@backstage/plugin-auth-backend': minor +--- + +**BREAKING**: Removed all directly exported auth provider factories, option types, and sign-in resolvers. For example: `AwsAlbProviderOptions`, `bitbucketUserIdSignInResolver`, `createGithubProvider`. These are all still accessible via the `providers` export. For example, use `providers.github.create()` rather than `createGithubProvider()`, and `providers.bitbucket.resolvers.userIdMatchingUserEntityAnnotation()` rather than `bitbucketUserIdSignInResolver`. + +**BREAKING**: Removed the exported `AuthProviderFactoryOptions` type as well as the deprecated option fields of the `AuthProviderFactory` callback. This includes the `tokenManager`, `tokenIssuer`, `discovery`, and `catalogApi` fields. Existing usage of these should be replaced with the new utilities in the `resolverContext` field. The deprecated `TokenIssuer` type is now also removed, since it is no longer used. + +**BREAKING**: Removed `getEntityClaims`, use `getDefaultOwnershipEntityRefs` instead. + +**DEPRECATION**: Deprecated `AtlassianAuthProvider` as it was unintentionally exported. From 3faed18fd31d512218d09866e74c154631209116 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 17 Jun 2022 18:02:48 +0200 Subject: [PATCH 044/401] docs: update auth providers usage Signed-off-by: Patrik Oldsberg --- contrib/docs/tutorials/aws-alb-aad-oidc-auth.md | 7 ++----- docs/auth/bitbucket/provider.md | 12 +++++------- docs/auth/identity-resolver.md | 2 +- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md b/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md index 2b3e02c18b..3cedf5634b 100644 --- a/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md +++ b/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md @@ -122,10 +122,7 @@ When using ALB auth you can configure it as described [here](https://backstage.i - replace the content of `packages/backend/plugin/auth.ts` with the below and tweak it according to your needs. ```ts -import { - createRouter, - createAwsAlbProvider, -} from '@backstage/plugin-auth-backend'; +import { createRouter, providers } from '@backstage/plugin-auth-backend'; import { DEFAULT_NAMESPACE, stringifyEntityRef, @@ -145,7 +142,7 @@ export default async function createPlugin({ database, discovery, providerFactories: { - awsalb: createAwsAlbProvider({ + awsalb: providers.awsAlb.create({ authHandler: async ({ fullProfile }) => { let email: string | undefined = undefined; if (fullProfile.emails && fullProfile.emails.length > 0) { diff --git a/docs/auth/bitbucket/provider.md b/docs/auth/bitbucket/provider.md index c54fb75ffa..36859dab45 100644 --- a/docs/auth/bitbucket/provider.md +++ b/docs/auth/bitbucket/provider.md @@ -57,7 +57,7 @@ In order to use the Bitbucket provider for sign-in, you must configure it with a `signIn.resolver`. See the [Sign-In Resolver documentation](../identity-resolver.md) for more details on how this is done. Note that for the Bitbucket provider, you'll want to use -`bitbucket` as the provider ID, and `createBitbucketProvider` for the provider +`bitbucket` as the provider ID, and `providers.bitbucket.create` for the provider factory. The `@backstage/plugin-auth-backend` plugin also comes with two built-in @@ -74,16 +74,14 @@ same way, but uses the Bitbucket user ID instead, and matches on the The following is an example of how to use one of the built-in resolvers: ```ts -import { - createBitbucketProvider, - bitbucketUsernameSignInResolver, -} from '@backstage/plugin-auth-backend'; +import { providers } from '@backstage/plugin-auth-backend'; // ... providerFactories: { - bitbucket: createBitbucketProvider({ + bitbucket: providers.bitbucket.create({ signIn: { - resolver: bitbucketUsernameSignInResolver, + resolver: + providers.bitbucket.resolvers.usernameMatchingUserEntityAnnotation(), }, }), }, diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index b314954105..c6d4ae2a89 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -304,7 +304,7 @@ export default async function createPlugin( return await createRouter({ ... providerFactories: { - google: createGoogleProvider({ + google: providers.google.create({ authHandler: async ({ fullProfile // Type: passport.Profile, idToken // Type: (Optional) string, From 66c79ff359b2507ddb57ef6b5408e7385d2b7f11 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 20 Jun 2022 15:25:33 +0200 Subject: [PATCH 045/401] auth-backend: avoid marking TokenIssuer as internal Signed-off-by: Patrik Oldsberg --- plugins/auth-backend/src/identity/types.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/auth-backend/src/identity/types.ts b/plugins/auth-backend/src/identity/types.ts index f0959e90bd..4765b50d7d 100644 --- a/plugins/auth-backend/src/identity/types.ts +++ b/plugins/auth-backend/src/identity/types.ts @@ -39,8 +39,6 @@ export type TokenParams = { /** * A TokenIssuer is able to issue verifiable ID Tokens on demand. - * - * @internal */ export type TokenIssuer = { /** From 18dad948a10a2034959e1eb14b3c41669683e1fb Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Mon, 20 Jun 2022 15:46:06 +0200 Subject: [PATCH 046/401] add custom search error Signed-off-by: Emma Indal --- plugins/search-backend-node/src/errors.ts | 23 +++++++++++++++++++++++ plugins/search-backend-node/src/index.ts | 1 + 2 files changed, 24 insertions(+) create mode 100644 plugins/search-backend-node/src/errors.ts diff --git a/plugins/search-backend-node/src/errors.ts b/plugins/search-backend-node/src/errors.ts new file mode 100644 index 0000000000..c6128f3f2e --- /dev/null +++ b/plugins/search-backend-node/src/errors.ts @@ -0,0 +1,23 @@ +/* + * 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 { CustomErrorBase } from '@backstage/errors'; + +/** + * Failed to query documents for index that does not exist. + * @public + */ +export class MissingIndexError extends CustomErrorBase {} diff --git a/plugins/search-backend-node/src/index.ts b/plugins/search-backend-node/src/index.ts index d342bb9259..854881829c 100644 --- a/plugins/search-backend-node/src/index.ts +++ b/plugins/search-backend-node/src/index.ts @@ -34,6 +34,7 @@ export type { RegisterCollatorParameters, RegisterDecoratorParameters, } from './types'; +export * from './errors'; export * from './indexing'; export * from './test-utils'; From c30ecd900f75cfc74a48f14f74bc663bd6121f60 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Mon, 20 Jun 2022 15:48:44 +0200 Subject: [PATCH 047/401] ElasticSearch engine throw MissingIndexError if error type is index_not_found_exception Signed-off-by: Emma Indal --- .../src/engines/ElasticSearchSearchEngine.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index 31112e3446..8f321a266c 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -26,6 +26,7 @@ import { SearchEngine, SearchQuery, } from '@backstage/plugin-search-common'; +import { MissingIndexError } from '@backstage/plugin-search-backend-node'; import { Client } from '@elastic/elasticsearch'; import esb from 'elastic-builder'; import { isEmpty, isNaN as nan, isNumber } from 'lodash'; @@ -362,6 +363,9 @@ export class ElasticSearchSearchEngine implements SearchEngine { previousPageCursor, }; } catch (e) { + if (e.meta.body.error.type === 'index_not_found_exception') { + throw new MissingIndexError(`Missing index for ${queryIndices}`, e); + } this.logger.error( `Failed to query documents for indices ${queryIndices}`, e, From 1e28c4336632edf6814e66ee5bb05406c0e8e560 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Mon, 20 Jun 2022 16:24:15 +0200 Subject: [PATCH 048/401] check error, if MissingIndexError return status code 400 with clear error message Signed-off-by: Emma Indal --- plugins/search-backend/src/service/router.ts | 24 ++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/plugins/search-backend/src/service/router.ts b/plugins/search-backend/src/service/router.ts index f54c2d388f..c85b3ed797 100644 --- a/plugins/search-backend/src/service/router.ts +++ b/plugins/search-backend/src/service/router.ts @@ -33,7 +33,10 @@ import { IndexableResultSet, SearchResultSet, } from '@backstage/plugin-search-common'; -import { SearchEngine } from '@backstage/plugin-search-backend-node'; +import { + SearchEngine, + MissingIndexError, +} from '@backstage/plugin-search-backend-node'; import { AuthorizedSearchEngine } from './AuthorizedSearchEngine'; const jsonObjectSchema: z.ZodSchema = z.lazy(() => { @@ -129,7 +132,10 @@ export async function createRouter( const router = Router(); router.get( '/query', - async (req: express.Request, res: express.Response) => { + async ( + req: express.Request, + res: express.Response, + ) => { const parseResult = requestSchema.safeParse(req.query); if (!parseResult.success) { @@ -154,9 +160,19 @@ export async function createRouter( const resultSet = await engine?.query(query, { token }); res.send(filterResultSet(toSearchResults(resultSet))); - } catch (err) { + } catch (error) { + if (error instanceof MissingIndexError) { + res + .status(400) + .send( + `\nNo index found for types: ${ + query.types ? query.types.join(',') : '' + }. This means there are no documents to search through.`, + ); + } + throw new Error( - `There was a problem performing the search query. ${err}`, + `There was a problem performing the search query. ${error}`, ); } }, From 411e8e60819013fbcf847ded2be8d577abf4fc12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Tresarrieu?= Date: Mon, 20 Jun 2022 17:32:29 +0200 Subject: [PATCH 049/401] chore: add custom types for transform function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is to avoid exposing complex types from our internal deps. Signed-off-by: Côme Tresarrieu --- .../MarkdownContent/MarkdownContent.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx b/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx index 4059482b79..3052aff7d9 100644 --- a/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx +++ b/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx @@ -64,12 +64,23 @@ const useStyles = makeStyles( { name: 'BackstageMarkdownContent' }, ); +type TransformLink = ( + href: string, + // Complex type from internal react-markdown dep library (hast)[./node_modules/@types/hast/index.d.ts] + children: any[], + title: string | null, +) => string; + type Props = { content: string; dialect?: 'gfm' | 'common-mark'; - linkTarget?: Options['linkTarget']; - transformLinkUri?: Options['transformLinkUri']; - transformImageUri?: Options['transformImageUri']; + linkTarget?: React.HTMLAttributeAnchorTarget | TransformLink; + transformLinkUri?: TransformLink; + transformImageUri?: ( + src: string, + alt: string, + title: string | null, + ) => string; }; const components: Options['components'] = { From fccddd80fd865c959a459c9e20375dd4270c855e Mon Sep 17 00:00:00 2001 From: Vladimir Masarik Date: Mon, 20 Jun 2022 17:25:00 +0200 Subject: [PATCH 050/401] docs: fix prettier complaints Signed-off-by: Vladimir Masarik --- docs/features/software-templates/writing-custom-actions.md | 2 +- docs/features/software-templates/writing-templates.md | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index abff0cf7c7..a9e1d297f3 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -137,7 +137,7 @@ export default async function createPlugin( config: env.config, reader: env.reader, }); - + const actions = [...builtInActions, createNewFileAction()]; return createRouter({ diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 3173a0500a..40b735764c 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -513,14 +513,12 @@ spec: title: URL endpoint type: string description: URL endpoint at which the component can be reached - default: "https://www.example.com" + default: 'https://www.example.com' enabledDB: title: Enable Database type: boolean default: false - -... - + ... steps: - id: fetch-base name: Fetch Base From bce462d4e326f120fb18b5320537ec2628a516e9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Jun 2022 07:45:37 +0000 Subject: [PATCH 051/401] fix(deps): update dependency @microsoft/microsoft-graph-types to v2.22.0 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index bca39feaef..61a9d59a1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4183,9 +4183,9 @@ typescript "~4.6.3" "@microsoft/microsoft-graph-types@^2.6.0": - version "2.21.0" - resolved "https://registry.npmjs.org/@microsoft/microsoft-graph-types/-/microsoft-graph-types-2.21.0.tgz#3244b41374307f590948e25fb2de8dad2a8a81c9" - integrity sha512-ZbYetXyUqHZliM8exZzF7IPEKVMvR+PaY+P9bjNHpxeiacF3U9+c40YRv0TJHpF6+50GWmJmPD6BsKHVRBZiCA== + version "2.22.0" + resolved "https://registry.npmjs.org/@microsoft/microsoft-graph-types/-/microsoft-graph-types-2.22.0.tgz#7ceb222311770ed5b240d627b657d25b3979964e" + integrity sha512-iKc5L036hc/wu13DX5kGf//3/WqTAErAPTyYKG6zT3vG070xXaaP7PInODznfyZduhiOvavmrRlQb34ajeDTUA== "@microsoft/tsdoc-config@~0.16.1": version "0.16.1" From 741b6df63a59b721a6422dbecf89863f840c7e9f Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Tue, 21 Jun 2022 10:02:04 +0200 Subject: [PATCH 052/401] update api reports Signed-off-by: Emma Indal --- plugins/search-backend-node/api-report.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/search-backend-node/api-report.md b/plugins/search-backend-node/api-report.md index 7649bc5d0f..63273d7514 100644 --- a/plugins/search-backend-node/api-report.md +++ b/plugins/search-backend-node/api-report.md @@ -6,6 +6,7 @@ /// import { Config } from '@backstage/config'; +import { CustomErrorBase } from '@backstage/errors'; import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; import { DocumentDecoratorFactory } from '@backstage/plugin-search-common'; import { DocumentTypeInfo } from '@backstage/plugin-search-common'; @@ -113,6 +114,9 @@ export class LunrSearchEngineIndexer extends BatchSearchEngineIndexer { initialize(): Promise; } +// @public +export class MissingIndexError extends CustomErrorBase {} + // @public export class NewlineDelimitedJsonCollatorFactory implements DocumentCollatorFactory From c7285cac4cac785030de5b6bb58b2b7c924c0364 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 21 Jun 2022 11:25:56 +0200 Subject: [PATCH 053/401] fix(search): search modal context scope Only create a search context for the modal if there is no parent context already defined. Signed-off-by: Camila Belo --- .../SearchModal/SearchModal.test.tsx | 51 ++++++++++++++++++- .../components/SearchModal/SearchModal.tsx | 17 +++++-- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/plugins/search/src/components/SearchModal/SearchModal.test.tsx b/plugins/search/src/components/SearchModal/SearchModal.test.tsx index 842f5fb401..3ef9291585 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.test.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.test.tsx @@ -21,7 +21,10 @@ import userEvent from '@testing-library/user-event'; import { configApiRef } from '@backstage/core-plugin-api'; import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { rootRouteRef } from '../../plugin'; -import { searchApiRef } from '@backstage/plugin-search-react'; +import { + searchApiRef, + SearchContextProvider, +} from '@backstage/plugin-search-react'; import { SearchModal } from './SearchModal'; @@ -55,6 +58,52 @@ describe('SearchModal', () => { expect(query).toHaveBeenCalledTimes(1); }); + it('Should use parent search context if defined', async () => { + const initialState = { + term: 'term', + filters: { filter: '' }, + types: ['type'], + pageCursor: 'page cursor', + }; + + await renderInTestApp( + + + + , + { + mountedRoutes: { + '/search': rootRouteRef, + }, + }, + ); + + expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(query).toHaveBeenCalledWith(initialState); + }); + + it('Should create a local search context if a parent is not defined', async () => { + await renderInTestApp( + + , + { + mountedRoutes: { + '/search': rootRouteRef, + }, + }, + ); + + expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(query).toHaveBeenCalledWith({ + term: '', + filters: {}, + types: [], + pageCursor: undefined, + }); + }); + it('Should render a custom Modal correctly', async () => { await renderInTestApp( diff --git a/plugins/search/src/components/SearchModal/SearchModal.tsx b/plugins/search/src/components/SearchModal/SearchModal.tsx index 6c37f20001..aaee7667dd 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React from 'react'; +import React, { PropsWithChildren } from 'react'; import { Dialog, DialogActions, @@ -170,6 +170,17 @@ export const Modal = ({ toggleModal }: SearchModalProps) => { ); }; +const Context = ({ children }: PropsWithChildren<{}>) => { + // Checks if there is a parent context already defined and, if not, creates a new local context. + try { + // Throws an exception if there is no parent context already defined + useSearch(); + return <>{children}; + } catch { + return {children}; + } +}; + /** * @public */ @@ -194,11 +205,11 @@ export const SearchModal = ({ hidden={hidden} > {open && ( - + {(children && children({ toggleModal })) ?? ( )} - + )} ); From 509c4092f0b617de243238ac0975f7910b199e43 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 21 Jun 2022 11:51:41 +0200 Subject: [PATCH 054/401] chore: add changeset file Signed-off-by: Camila Belo --- .changeset/search-turtles-itch.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .changeset/search-turtles-itch.md diff --git a/.changeset/search-turtles-itch.md b/.changeset/search-turtles-itch.md new file mode 100644 index 0000000000..2da26fc64a --- /dev/null +++ b/.changeset/search-turtles-itch.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-search': patch +--- + +To allow people to use a global search context in the search modal, the code for the search modal has been changed to only create a local search context if there is no parent context already defined. + +If you want to continue using a local context even if you define a global one, you will have to wrap the modal in a new local context manually: + +```tsx + + + +``` From e66124284459b74259b544a63ba821c526f2ed31 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Jun 2022 11:22:42 +0000 Subject: [PATCH 055/401] fix(deps): update dependency run-script-webpack-plugin to ^0.1.0 Signed-off-by: Renovate Bot --- .changeset/renovate-4b5ff24.md | 5 +++++ packages/cli/package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 .changeset/renovate-4b5ff24.md diff --git a/.changeset/renovate-4b5ff24.md b/.changeset/renovate-4b5ff24.md new file mode 100644 index 0000000000..4867c345d8 --- /dev/null +++ b/.changeset/renovate-4b5ff24.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Updated dependency `run-script-webpack-plugin` to `^0.1.0`. diff --git a/packages/cli/package.json b/packages/cli/package.json index 08f67249f5..2cf6f4fe7b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -110,7 +110,7 @@ "rollup-plugin-esbuild": "^4.7.2", "rollup-plugin-postcss": "^4.0.0", "rollup-pluginutils": "^2.8.2", - "run-script-webpack-plugin": "^0.0.14", + "run-script-webpack-plugin": "^0.1.0", "semver": "^7.3.2", "style-loader": "^3.3.1", "sucrase": "^3.20.2", diff --git a/yarn.lock b/yarn.lock index 787ec010d7..9bc97aa36c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22775,10 +22775,10 @@ run-parallel@^1.1.9: resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== -run-script-webpack-plugin@^0.0.14: - version "0.0.14" - resolved "https://registry.npmjs.org/run-script-webpack-plugin/-/run-script-webpack-plugin-0.0.14.tgz#fe2362b32c1dab7a8af7a6f1246fc043690cedd7" - integrity sha512-DXe6lzzEVXjBr/74zd4m4yOfmz5P6GMjzhQxDDsViOmwG7cap8UCE6RgD5rT7zf4wM83a+ToHnpB3v4efUv5IA== +run-script-webpack-plugin@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/run-script-webpack-plugin/-/run-script-webpack-plugin-0.1.0.tgz#6250a4734511836628259666d56641a27526875d" + integrity sha512-PKddVrnTu1BO90ogvN93yP/zg3X8Q4XvSKKyGVZFye9VBVbgVkFllKykNOomi9IfNtnoFFzu6TkLONIuXq5URA== rxjs@7.5.5, rxjs@^7.0.0, rxjs@^7.1.0, rxjs@^7.2.0, rxjs@^7.5.1, rxjs@^7.5.5: version "7.5.5" From a620f777b33e7527ede1cdd6d4fdf687fed81b3c Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 21 Jun 2022 13:46:18 +0200 Subject: [PATCH 056/401] refactor(search): apply review suggestion Use search context checker in search modal and create local context only if needed. Signed-off-by: Camila Belo --- .../search/src/components/SearchModal/SearchModal.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/search/src/components/SearchModal/SearchModal.tsx b/plugins/search/src/components/SearchModal/SearchModal.tsx index aaee7667dd..dbb8100ec1 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.tsx @@ -35,6 +35,7 @@ import { SearchResult, SearchResultPager, useSearch, + useSearchContextCheck, } from '@backstage/plugin-search-react'; import { useRouteRef } from '@backstage/core-plugin-api'; import { Link, useContent } from '@backstage/core-components'; @@ -172,13 +173,11 @@ export const Modal = ({ toggleModal }: SearchModalProps) => { const Context = ({ children }: PropsWithChildren<{}>) => { // Checks if there is a parent context already defined and, if not, creates a new local context. - try { - // Throws an exception if there is no parent context already defined - useSearch(); + const hasParentContext = useSearchContextCheck(); + if (hasParentContext) { return <>{children}; - } catch { - return {children}; } + return {children}; }; /** From 9fef814cead88c3cd436883a25cf91020ffdf819 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 21 Jun 2022 14:17:27 +0200 Subject: [PATCH 057/401] chore: added vacation notice! Signed-off-by: blam --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5dba1fdf52..7b1923211b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ ![headline](docs/assets/headline.png) +> 🏖 From W/C 11/7 due to Summer Vacations of some of the maintainers, expect the project to move a little slower than normal, and support to be limited. Normal service will resume on W/C 8/8! 🏝 + # [Backstage](https://backstage.io) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) From d2cf9d43ccdb13df8516faf900b08d64cf198a65 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 21 Jun 2022 14:25:00 +0200 Subject: [PATCH 058/401] chore: make more human Signed-off-by: blam Signed-off-by: blam --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7b1923211b..a6e740a427 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![headline](docs/assets/headline.png) -> 🏖 From W/C 11/7 due to Summer Vacations of some of the maintainers, expect the project to move a little slower than normal, and support to be limited. Normal service will resume on W/C 8/8! 🏝 +> 🏖 From July 7th due to Summer Vacations for some of the maintainers, expect the project to move a little slower than normal, and support to be limited. Normal service will resume on August 8th 🏝 # [Backstage](https://backstage.io) From 4df6d464baa8e4f2ec4a5852983b76f97378488e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Jun 2022 12:35:11 +0000 Subject: [PATCH 059/401] fix(deps): update typescript-eslint monorepo to v5.29.0 Signed-off-by: Renovate Bot --- yarn.lock | 90 +++++++++++++++++++++++++++---------------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/yarn.lock b/yarn.lock index f19ce84ad0..a0041b7ff5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7145,13 +7145,13 @@ integrity sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw== "@typescript-eslint/eslint-plugin@^5.9.0": - version "5.27.1" - resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.27.1.tgz#fdf59c905354139046b41b3ed95d1609913d0758" - integrity sha512-6dM5NKT57ZduNnJfpY81Phe9nc9wolnMCnknb1im6brWi1RYv84nbMS3olJa27B6+irUVV1X/Wb+Am0FjJdGFw== + version "5.29.0" + resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.29.0.tgz#c67794d2b0fd0b4a47f50266088acdc52a08aab6" + integrity sha512-kgTsISt9pM53yRFQmLZ4npj99yGl3x3Pl7z4eA66OuTzAGC4bQB5H5fuLwPnqTKU3yyrrg4MIhjF17UYnL4c0w== dependencies: - "@typescript-eslint/scope-manager" "5.27.1" - "@typescript-eslint/type-utils" "5.27.1" - "@typescript-eslint/utils" "5.27.1" + "@typescript-eslint/scope-manager" "5.29.0" + "@typescript-eslint/type-utils" "5.29.0" + "@typescript-eslint/utils" "5.29.0" debug "^4.3.4" functional-red-black-tree "^1.0.1" ignore "^5.2.0" @@ -7172,13 +7172,13 @@ eslint-utils "^3.0.0" "@typescript-eslint/parser@^5.9.0": - version "5.27.1" - resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.27.1.tgz#3a4dcaa67e45e0427b6ca7bb7165122c8b569639" - integrity sha512-7Va2ZOkHi5NP+AZwb5ReLgNF6nWLGTeUJfxdkVUAPPSaAdbWNnFZzLZ4EGGmmiCTg+AwlbE1KyUYTBglosSLHQ== + version "5.29.0" + resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.29.0.tgz#41314b195b34d44ff38220caa55f3f93cfca43cf" + integrity sha512-ruKWTv+x0OOxbzIw9nW5oWlUopvP/IQDjB5ZqmTglLIoDTctLlAJpAQFpNPJP/ZI7hTT9sARBosEfaKbcFuECw== dependencies: - "@typescript-eslint/scope-manager" "5.27.1" - "@typescript-eslint/types" "5.27.1" - "@typescript-eslint/typescript-estree" "5.27.1" + "@typescript-eslint/scope-manager" "5.29.0" + "@typescript-eslint/types" "5.29.0" + "@typescript-eslint/typescript-estree" "5.29.0" debug "^4.3.4" "@typescript-eslint/scope-manager@5.20.0": @@ -7189,13 +7189,13 @@ "@typescript-eslint/types" "5.20.0" "@typescript-eslint/visitor-keys" "5.20.0" -"@typescript-eslint/scope-manager@5.27.1": - version "5.27.1" - resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.27.1.tgz#4d1504392d01fe5f76f4a5825991ec78b7b7894d" - integrity sha512-fQEOSa/QroWE6fAEg+bJxtRZJTH8NTskggybogHt4H9Da8zd4cJji76gA5SBlR0MgtwF7rebxTbDKB49YUCpAg== +"@typescript-eslint/scope-manager@5.29.0": + version "5.29.0" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.29.0.tgz#2a6a32e3416cb133e9af8dcf54bf077a916aeed3" + integrity sha512-etbXUT0FygFi2ihcxDZjz21LtC+Eps9V2xVx09zFoN44RRHPrkMflidGMI+2dUs821zR1tDS6Oc9IXxIjOUZwA== dependencies: - "@typescript-eslint/types" "5.27.1" - "@typescript-eslint/visitor-keys" "5.27.1" + "@typescript-eslint/types" "5.29.0" + "@typescript-eslint/visitor-keys" "5.29.0" "@typescript-eslint/scope-manager@5.9.0": version "5.9.0" @@ -7205,12 +7205,12 @@ "@typescript-eslint/types" "5.9.0" "@typescript-eslint/visitor-keys" "5.9.0" -"@typescript-eslint/type-utils@5.27.1": - version "5.27.1" - resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.27.1.tgz#369f695199f74c1876e395ebea202582eb1d4166" - integrity sha512-+UC1vVUWaDHRnC2cQrCJ4QtVjpjjCgjNFpg8b03nERmkHv9JV9X5M19D7UFMd+/G7T/sgFwX2pGmWK38rqyvXw== +"@typescript-eslint/type-utils@5.29.0": + version "5.29.0" + resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.29.0.tgz#241918001d164044020b37d26d5b9f4e37cc3d5d" + integrity sha512-JK6bAaaiJozbox3K220VRfCzLa9n0ib/J+FHIwnaV3Enw/TO267qe0pM1b1QrrEuy6xun374XEAsRlA86JJnyg== dependencies: - "@typescript-eslint/utils" "5.27.1" + "@typescript-eslint/utils" "5.29.0" debug "^4.3.4" tsutils "^3.21.0" @@ -7219,10 +7219,10 @@ resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.20.0.tgz#fa39c3c2aa786568302318f1cb51fcf64258c20c" integrity sha512-+d8wprF9GyvPwtoB4CxBAR/s0rpP25XKgnOvMf/gMXYDvlUC3rPFHupdTQ/ow9vn7UDe5rX02ovGYQbv/IUCbg== -"@typescript-eslint/types@5.27.1": - version "5.27.1" - resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.27.1.tgz#34e3e629501349d38be6ae97841298c03a6ffbf1" - integrity sha512-LgogNVkBhCTZU/m8XgEYIWICD6m4dmEDbKXESCbqOXfKZxRKeqpiJXQIErv66sdopRKZPo5l32ymNqibYEH/xg== +"@typescript-eslint/types@5.29.0": + version "5.29.0" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.29.0.tgz#7861d3d288c031703b2d97bc113696b4d8c19aab" + integrity sha512-X99VbqvAXOMdVyfFmksMy3u8p8yoRGITgU1joBJPzeYa0rhdf5ok9S56/itRoUSh99fiDoMtarSIJXo7H/SnOg== "@typescript-eslint/types@5.9.0": version "5.9.0" @@ -7242,13 +7242,13 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@5.27.1": - version "5.27.1" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.27.1.tgz#7621ee78607331821c16fffc21fc7a452d7bc808" - integrity sha512-DnZvvq3TAJ5ke+hk0LklvxwYsnXpRdqUY5gaVS0D4raKtbznPz71UJGnPTHEFo0GDxqLOLdMkkmVZjSpET1hFw== +"@typescript-eslint/typescript-estree@5.29.0": + version "5.29.0" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.29.0.tgz#e83d19aa7fd2e74616aab2f25dfbe4de4f0b5577" + integrity sha512-mQvSUJ/JjGBdvo+1LwC+GY2XmSYjK1nAaVw2emp/E61wEVYEyibRHCqm1I1vEKbXCpUKuW4G7u9ZCaZhJbLoNQ== dependencies: - "@typescript-eslint/types" "5.27.1" - "@typescript-eslint/visitor-keys" "5.27.1" + "@typescript-eslint/types" "5.29.0" + "@typescript-eslint/visitor-keys" "5.29.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" @@ -7268,15 +7268,15 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/utils@5.27.1": - version "5.27.1" - resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.27.1.tgz#b4678b68a94bc3b85bf08f243812a6868ac5128f" - integrity sha512-mZ9WEn1ZLDaVrhRaYgzbkXBkTPghPFsup8zDbbsYTxC5OmqrFE7skkKS/sraVsLP3TcT3Ki5CSyEFBRkLH/H/w== +"@typescript-eslint/utils@5.29.0": + version "5.29.0" + resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.29.0.tgz#775046effd5019667bd086bcf326acbe32cd0082" + integrity sha512-3Eos6uP1nyLOBayc/VUdKZikV90HahXE5Dx9L5YlSd/7ylQPXhLk1BYb29SDgnBnTp+jmSZUU0QxUiyHgW4p7A== dependencies: "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.27.1" - "@typescript-eslint/types" "5.27.1" - "@typescript-eslint/typescript-estree" "5.27.1" + "@typescript-eslint/scope-manager" "5.29.0" + "@typescript-eslint/types" "5.29.0" + "@typescript-eslint/typescript-estree" "5.29.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" @@ -7300,12 +7300,12 @@ "@typescript-eslint/types" "5.20.0" eslint-visitor-keys "^3.0.0" -"@typescript-eslint/visitor-keys@5.27.1": - version "5.27.1" - resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.27.1.tgz#05a62666f2a89769dac2e6baa48f74e8472983af" - integrity sha512-xYs6ffo01nhdJgPieyk7HAOpjhTsx7r/oB9LWEhwAXgwn33tkr+W8DI2ChboqhZlC4q3TC6geDYPoiX8ROqyOQ== +"@typescript-eslint/visitor-keys@5.29.0": + version "5.29.0" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.29.0.tgz#7a4749fa7ef5160c44a451bf060ac1dc6dfb77ee" + integrity sha512-Hpb/mCWsjILvikMQoZIE3voc9wtQcS0A9FUw3h8bhr9UxBdtI/tw1ZDZUOXHXLOVMedKCH5NxyzATwnU78bWCQ== dependencies: - "@typescript-eslint/types" "5.27.1" + "@typescript-eslint/types" "5.29.0" eslint-visitor-keys "^3.3.0" "@typescript-eslint/visitor-keys@5.9.0": From 87516675416340b2a7e3dc7c1749c1b62599b052 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Jun 2022 13:02:41 +0000 Subject: [PATCH 060/401] fix(deps): update dependency kafkajs to v2 Signed-off-by: Renovate Bot --- .changeset/renovate-149779d.md | 5 +++++ plugins/kafka-backend/package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 .changeset/renovate-149779d.md diff --git a/.changeset/renovate-149779d.md b/.changeset/renovate-149779d.md new file mode 100644 index 0000000000..58d764485e --- /dev/null +++ b/.changeset/renovate-149779d.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kafka-backend': patch +--- + +Updated dependency `kafkajs` to `^2.0.0`. diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index b78c297586..ab9c515b69 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -42,7 +42,7 @@ "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", - "kafkajs": "^1.16.0-beta.6", + "kafkajs": "^2.0.0", "lodash": "^4.17.21", "winston": "^3.2.1" }, diff --git a/yarn.lock b/yarn.lock index 2b5e08220d..7036ba99a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16844,10 +16844,10 @@ jwt-decode@*, jwt-decode@^3.1.0: resolved "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59" integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A== -kafkajs@^1.16.0-beta.6: - version "1.16.0" - resolved "https://registry.npmjs.org/kafkajs/-/kafkajs-1.16.0.tgz#bfcc3ae2b69265ca8435b53a01ee9e8787b9fee5" - integrity sha512-+Rcfu2hyQ/jv5skqRY8xA7Ra+mmRkDAzCaLDYbkGtgsNKpzxPWiLbk8ub0dgr4EbWrN1Zb4BCXHUkD6+zYfdWg== +kafkajs@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/kafkajs/-/kafkajs-2.0.2.tgz#cdfc8f57aa4fd69f6d9ca1cce4ee89bbc2a3a1f9" + integrity sha512-g6CM3fAenofOjR1bfOAqeZUEaSGhNtBscNokybSdW1rmIKYNwBPC9xQzwulFJm36u/xcxXUiCl/L/qfslapihA== keyv-memcache@^1.2.5: version "1.2.9" From 679b32172eeddb53886f078efbb3de23eb4d308a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Jun 2022 13:03:17 +0000 Subject: [PATCH 061/401] fix(deps): update dependency knex to v2 Signed-off-by: Renovate Bot --- .changeset/renovate-7438bff.md | 16 ++++++++++++++++ packages/backend-common/package.json | 2 +- packages/backend-tasks/package.json | 2 +- packages/backend-test-utils/package.json | 2 +- plugins/app-backend/package.json | 2 +- plugins/auth-backend/package.json | 2 +- plugins/bazaar-backend/package.json | 2 +- plugins/catalog-backend/package.json | 2 +- plugins/code-coverage-backend/package.json | 2 +- plugins/scaffolder-backend/package.json | 2 +- plugins/search-backend-module-pg/package.json | 2 +- plugins/tech-insights-backend/package.json | 2 +- plugins/techdocs-backend/package.json | 2 +- yarn.lock | 8 ++++---- 14 files changed, 32 insertions(+), 16 deletions(-) create mode 100644 .changeset/renovate-7438bff.md diff --git a/.changeset/renovate-7438bff.md b/.changeset/renovate-7438bff.md new file mode 100644 index 0000000000..43a4ac5016 --- /dev/null +++ b/.changeset/renovate-7438bff.md @@ -0,0 +1,16 @@ +--- +'@backstage/backend-common': patch +'@backstage/backend-tasks': patch +'@backstage/backend-test-utils': patch +'@backstage/plugin-app-backend': patch +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-bazaar-backend': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-code-coverage-backend': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-search-backend-module-pg': patch +'@backstage/plugin-tech-insights-backend': patch +'@backstage/plugin-techdocs-backend': patch +--- + +Updated dependency `knex` to `^2.0.0`. diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 9c459373ff..e8ebcb9ea2 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -65,7 +65,7 @@ "keyv": "^4.0.3", "keyv-memcache": "^1.2.5", "@keyv/redis": "^2.2.3", - "knex": "^1.0.2", + "knex": "^2.0.0", "lodash": "^4.17.21", "logform": "^2.3.2", "luxon": "^2.3.1", diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json index f99baea1d5..2cbfd0b290 100644 --- a/packages/backend-tasks/package.json +++ b/packages/backend-tasks/package.json @@ -39,7 +39,7 @@ "@backstage/types": "^1.0.0", "@types/luxon": "^2.0.4", "cron": "^2.0.0", - "knex": "^1.0.2", + "knex": "^2.0.0", "lodash": "^4.17.21", "luxon": "^2.0.2", "node-abort-controller": "^3.0.1", diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 56aa23bfd8..1dc7ddc821 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -38,7 +38,7 @@ "@backstage/cli": "^0.17.3-next.0", "@backstage/config": "^1.0.1", "better-sqlite3": "^7.5.0", - "knex": "^1.0.2", + "knex": "^2.0.0", "msw": "^0.42.0", "mysql2": "^2.2.5", "pg": "^8.3.0", diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 173b15c80c..7a62720f8b 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -43,7 +43,7 @@ "express-promise-router": "^4.1.0", "fs-extra": "10.1.0", "helmet": "^5.0.2", - "knex": "^1.0.2", + "knex": "^2.0.0", "lodash": "^4.17.21", "luxon": "^2.0.2", "winston": "^3.2.1", diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index c1fe319e3c..8403979f6f 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -53,7 +53,7 @@ "google-auth-library": "^8.0.0", "jose": "^4.6.0", "jwt-decode": "^3.1.0", - "knex": "^1.0.2", + "knex": "^2.0.0", "lodash": "^4.17.21", "luxon": "^2.0.2", "minimatch": "^5.0.0", diff --git a/plugins/bazaar-backend/package.json b/plugins/bazaar-backend/package.json index c0769fe5b8..51b4a7786b 100644 --- a/plugins/bazaar-backend/package.json +++ b/plugins/bazaar-backend/package.json @@ -29,7 +29,7 @@ "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", - "knex": "^1.0.2", + "knex": "^2.0.0", "winston": "^3.2.1", "yn": "^4.0.0" }, diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 61a8e68412..99037b09f7 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -55,7 +55,7 @@ "fs-extra": "10.1.0", "git-url-parse": "^11.6.0", "glob": "^7.1.6", - "knex": "^1.0.2", + "knex": "^2.0.0", "lodash": "^4.17.21", "luxon": "^2.0.2", "node-fetch": "^2.6.7", diff --git a/plugins/code-coverage-backend/package.json b/plugins/code-coverage-backend/package.json index 78b57974b9..d63fd88ad8 100644 --- a/plugins/code-coverage-backend/package.json +++ b/plugins/code-coverage-backend/package.json @@ -33,7 +33,7 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "express-xml-bodyparser": "^0.3.0", - "knex": "^1.0.2", + "knex": "^2.0.0", "uuid": "^8.3.2", "winston": "^3.2.1", "yn": "^4.0.0" diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index dba891bfa2..8531523034 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -59,7 +59,7 @@ "isbinaryfile": "^5.0.0", "isomorphic-git": "^1.8.0", "jsonschema": "^1.2.6", - "knex": "^1.0.2", + "knex": "^2.0.0", "lodash": "^4.17.21", "luxon": "^2.0.2", "morgan": "^1.10.0", diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index cf072f5fe3..86c30f63d3 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -27,7 +27,7 @@ "@backstage/plugin-search-backend-node": "^0.6.3-next.0", "@backstage/plugin-search-common": "^0.3.5", "lodash": "^4.17.21", - "knex": "^1.0.2" + "knex": "^2.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "^0.1.26-next.0", diff --git a/plugins/tech-insights-backend/package.json b/plugins/tech-insights-backend/package.json index 5461f22789..f0f9d0491a 100644 --- a/plugins/tech-insights-backend/package.json +++ b/plugins/tech-insights-backend/package.json @@ -45,7 +45,7 @@ "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", - "knex": "^1.0.2", + "knex": "^2.0.0", "lodash": "^4.17.21", "luxon": "^2.0.2", "semver": "^7.3.5", diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index bf29eeb9af..d42f34e1a4 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -49,7 +49,7 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "fs-extra": "10.1.0", - "knex": "^1.0.2", + "knex": "^2.0.0", "lodash": "^4.17.21", "node-fetch": "^2.6.7", "p-limit": "^3.1.0", diff --git a/yarn.lock b/yarn.lock index 2b5e08220d..8d8b0acbc7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16906,10 +16906,10 @@ kleur@^4.0.3, kleur@^4.1.4: resolved "https://registry.npmjs.org/kleur/-/kleur-4.1.4.tgz#8c202987d7e577766d039a8cd461934c01cda04d" integrity sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA== -knex@^1.0.2: - version "1.0.7" - resolved "https://registry.npmjs.org/knex/-/knex-1.0.7.tgz#965f4490efc451b140aac4c5c6efa39fd877597b" - integrity sha512-89jxuRATt4qJMb9ZyyaKBy0pQ4d5h7eOFRqiNFnUvsgU+9WZ2eIaZKrAPG1+F3mgu5UloPUnkVE5Yo2sKZUs6Q== +knex@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/knex/-/knex-2.1.0.tgz#9348aace3a08ff5be26eb1c8e838416ddf1aa216" + integrity sha512-vVsnD6UJdSJy55TvCXfFF9syfwyXNxfE9mvr2hJL/4Obciy2EPGoqjDpgRSlMruHuPWDOeYAG25nyrGvU+jJog== dependencies: colorette "2.0.16" commander "^9.1.0" From 50c28bf2fabefd1ddf8833f75e49dc6fcd5b6f77 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 21 Jun 2022 15:24:53 +0200 Subject: [PATCH 062/401] codeowners: Add techdocs-core as changeset owner Signed-off-by: Johan Haals --- .github/CODEOWNERS | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3401dc5b2b..7127fb1da2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,8 +8,7 @@ yarn.lock @backstage/reviewers @backstage-service */yarn.lock @backstage/reviewers @backstage-service /.changeset/cost-insights-* @backstage/reviewers @backstage/silver-lining -/.changeset/search-* @backstage/reviewers @backstage/techdocs-core -/.changeset/techdocs-* @backstage/reviewers @backstage/techdocs-core +/.changeset @backstage/reviewers @backstage/techdocs-core /cypress/src/integration/plugins/techdocs.spec.ts @backstage/reviewers @backstage/techdocs-core /docs/assets/search @backstage/reviewers @backstage/techdocs-core /docs/features/search @backstage/reviewers @backstage/techdocs-core From c8a502ce3e0354d87f6b5cb42acde1d744cd66b7 Mon Sep 17 00:00:00 2001 From: Daniele Mazzotta Date: Tue, 21 Jun 2022 17:36:35 +0400 Subject: [PATCH 063/401] fixed formatting Signed-off-by: Daniele Mazzotta --- plugins/apache-airflow/api-report.md | 4 ++-- .../src/components/DagTableComponent/DagTableComponent.tsx | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/plugins/apache-airflow/api-report.md b/plugins/apache-airflow/api-report.md index 09123de613..333e4543cf 100644 --- a/plugins/apache-airflow/api-report.md +++ b/plugins/apache-airflow/api-report.md @@ -5,8 +5,8 @@ ```ts /// -import {BackstagePlugin} from '@backstage/core-plugin-api'; -import {RouteRef} from '@backstage/core-plugin-api'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { RouteRef } from '@backstage/core-plugin-api'; // @public export const ApacheAirflowDagTable: ({ diff --git a/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx b/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx index 7619b06497..ff7b961c48 100644 --- a/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx +++ b/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx @@ -164,14 +164,12 @@ export const DagTableComponent = ({ dagIds }: DagTableComponentProps) => { return ( <> - {dagsNotFound ? ( + {dagsNotFound && ( {dagsNotFound.map(dagId => ( {dagId} ))} - ) : ( - '' )} From 4764e30cc8db91835c8a0ba6e446463e42018d8c Mon Sep 17 00:00:00 2001 From: planeiii Date: Mon, 20 Jun 2022 11:54:09 -0500 Subject: [PATCH 064/401] chore: updated getProjects to accept arrays of branches Signed-off-by: planeiii --- .changeset/many-vans-thank.md | 2 -- .../jenkins-backend/src/service/jenkinsApi.test.ts | 14 +++++++------- plugins/jenkins-backend/src/service/jenkinsApi.ts | 8 ++++---- plugins/jenkins-backend/src/service/router.ts | 8 ++++---- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/.changeset/many-vans-thank.md b/.changeset/many-vans-thank.md index 942aa4cc7c..16f8c8a69b 100644 --- a/.changeset/many-vans-thank.md +++ b/.changeset/many-vans-thank.md @@ -3,6 +3,4 @@ '@backstage/plugin-jenkins-backend': patch --- ---- - feature: added support for multiple branches to the `JenkinsApi` diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.test.ts b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts index ad1a09a40f..bada5e1c29 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.test.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts @@ -118,10 +118,9 @@ describe('JenkinsApi', () => { it('standard github layout', async () => { mockedJenkinsClient.job.get.mockResolvedValueOnce(project); - const result = await jenkinsApi.getProjects( - jenkinsInfo, + const result = await jenkinsApi.getProjects(jenkinsInfo, [ 'testBranchName', - ); + ]); expect(mockedJenkins).toHaveBeenCalledWith({ baseUrl: jenkinsInfo.baseUrl, @@ -138,10 +137,11 @@ describe('JenkinsApi', () => { it('supports multiple branches', async () => { mockedJenkinsClient.job.get.mockResolvedValue(project); - const result = await jenkinsApi.getProjects( - jenkinsInfo, - 'foo,bar,catpants', - ); + const result = await jenkinsApi.getProjects(jenkinsInfo, [ + 'foo', + 'bar', + 'catpants', + ]); expect(mockedJenkins).toHaveBeenCalledWith({ baseUrl: jenkinsInfo.baseUrl, diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.ts b/plugins/jenkins-backend/src/service/jenkinsApi.ts index 07469dc83a..334be5dfa9 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.ts @@ -70,17 +70,17 @@ export class JenkinsApiImpl { * Get a list of projects for the given JenkinsInfo. * @see ../../../jenkins/src/api/JenkinsApi.ts#getProjects */ - async getProjects(jenkinsInfo: JenkinsInfo, branch?: string) { + async getProjects(jenkinsInfo: JenkinsInfo, branches?: string[]) { const client = await JenkinsApiImpl.getClient(jenkinsInfo); const projects: BackstageProject[] = []; - if (branch) { + if (branches) { // Assume jenkinsInfo.jobFullName is a folder which contains one job per branch. // TODO: extract a strategy interface for this const job = await Promise.any( - branch.split(/,/g).map(name => + branches.map(branch => client.job.get({ - name: `${jenkinsInfo.jobFullName}/${name}`, + name: `${jenkinsInfo.jobFullName}/${branch}`, tree: JenkinsApiImpl.jobTreeSpec.replace(/\s/g, ''), }), ), diff --git a/plugins/jenkins-backend/src/service/router.ts b/plugins/jenkins-backend/src/service/router.ts index 2c9e268165..b4c06314e1 100644 --- a/plugins/jenkins-backend/src/service/router.ts +++ b/plugins/jenkins-backend/src/service/router.ts @@ -64,12 +64,12 @@ export async function createRouter( request.header('authorization'), ); const branch = request.query.branch; - let branchStr: string | undefined; + let branches: string[] | undefined; if (branch === undefined) { - branchStr = undefined; + branches = undefined; } else if (typeof branch === 'string') { - branchStr = branch; + branches = branch.split(/,/g); } else { // this was passed in as something weird -> 400 // https://evanhahn.com/gotchas-with-express-query-parsing-and-how-to-avoid-them/ @@ -88,7 +88,7 @@ export async function createRouter( }, backstageToken: token, }); - const projects = await jenkinsApi.getProjects(jenkinsInfo, branchStr); + const projects = await jenkinsApi.getProjects(jenkinsInfo, branches); response.json({ projects: projects, From d03b2c4ceb8328d427f170d4d93dc9b8a4eca554 Mon Sep 17 00:00:00 2001 From: Taras Date: Mon, 20 Jun 2022 16:37:38 -0400 Subject: [PATCH 065/401] Added Humanitec plugin Signed-off-by: Taras Mankovski Signed-off-by: Taras --- microsite/data/plugins/humanitec.yaml | 11 +++++++++++ microsite/static/img/humanitec-logo.png | Bin 0 -> 7088 bytes 2 files changed, 11 insertions(+) create mode 100644 microsite/data/plugins/humanitec.yaml create mode 100644 microsite/static/img/humanitec-logo.png diff --git a/microsite/data/plugins/humanitec.yaml b/microsite/data/plugins/humanitec.yaml new file mode 100644 index 0000000000..88c0cd0a08 --- /dev/null +++ b/microsite/data/plugins/humanitec.yaml @@ -0,0 +1,11 @@ +--- +title: Humanitec Platform Orchestrator +author: Frontside +authorUrl: 'https://frontside.com' +category: Deployment # A single category e.g. CI, Machine Learning, Services, Monitoring +description: | + Show workloads, environments and resources deployed by Humanitec Platform Orchestrator. + Plugin includes an Entity ComponentCard, Backend API route and scaffolder actions. +documentation: https://github.com/thefrontside/backstage/tree/main/plugins/humanitec +iconUrl: ../../static/img/humanitec-logo.png +npmPackageName: '@frontside/backstage-plugin-humanitec' diff --git a/microsite/static/img/humanitec-logo.png b/microsite/static/img/humanitec-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..69ab02e9cd481ad93d6d8539ed9d2de1677b834b GIT binary patch literal 7088 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 a3Q Date: Tue, 21 Jun 2022 16:36:59 +0200 Subject: [PATCH 066/401] Remove unused vale keyword ent Signed-off-by: Himanshu Mishra --- .github/vale/Vocab/Backstage/accept.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/vale/Vocab/Backstage/accept.txt b/.github/vale/Vocab/Backstage/accept.txt index b87f5d3881..7631640199 100644 --- a/.github/vale/Vocab/Backstage/accept.txt +++ b/.github/vale/Vocab/Backstage/accept.txt @@ -92,7 +92,6 @@ elasticsearch esbuild eslint etag -ent Expedia facto failover From 384dddd0b493aa935eb973970d6a64e88071a206 Mon Sep 17 00:00:00 2001 From: Alex Rybchenko Date: Tue, 21 Jun 2022 17:43:54 +0200 Subject: [PATCH 067/401] moved entity checkers to catalog-model Signed-off-by: Alex Rybchenko --- .../src/entity/conditions.test.ts | 172 ++++++++++++++++++ .../catalog-model/src/entity/conditions.ts | 76 ++++++++ packages/catalog-model/src/entity/index.ts | 1 + .../src/search/DefaultCatalogCollator.ts | 8 +- plugins/catalog-backend/src/search/util.ts | 10 +- .../src/components/EntitySwitch/conditions.ts | 61 +------ 6 files changed, 253 insertions(+), 75 deletions(-) create mode 100644 packages/catalog-model/src/entity/conditions.test.ts create mode 100644 packages/catalog-model/src/entity/conditions.ts diff --git a/packages/catalog-model/src/entity/conditions.test.ts b/packages/catalog-model/src/entity/conditions.test.ts new file mode 100644 index 0000000000..046f1c60e4 --- /dev/null +++ b/packages/catalog-model/src/entity/conditions.test.ts @@ -0,0 +1,172 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from './Entity'; +import { + isApiEntity, + isComponentEntity, + isDomainEntity, + isGroupEntity, + isLocationEntity, + isResourceEntity, + isSystemEntity, + isUserEntity, +} from './conditions'; + +const apiEntity: Entity = { + apiVersion: '', + kind: 'api', + metadata: { name: 'api' }, +}; +const componentEntity: Entity = { + apiVersion: '', + kind: 'component', + metadata: { name: 'Component' }, +}; +const domainEntity: Entity = { + apiVersion: '', + kind: 'domain', + metadata: { name: 'Domain' }, +}; +const groupEntity: Entity = { + apiVersion: '', + kind: 'group', + metadata: { name: 'Group' }, +}; +const locationEntity: Entity = { + apiVersion: '', + kind: 'location', + metadata: { name: 'Location' }, +}; +const resourceEntity: Entity = { + apiVersion: '', + kind: 'resource', + metadata: { name: 'Resource' }, +}; +const systemEntity: Entity = { + apiVersion: '', + kind: 'system', + metadata: { name: 'System' }, +}; +const userEntity: Entity = { + apiVersion: '', + kind: 'user', + metadata: { name: 'User' }, +}; + +describe('isApiEntity', () => { + it('should check for the intended type', () => { + expect(isApiEntity(componentEntity)).not.toBeTruthy(); + expect(isApiEntity(domainEntity)).not.toBeTruthy(); + expect(isApiEntity(groupEntity)).not.toBeTruthy(); + expect(isApiEntity(locationEntity)).not.toBeTruthy(); + expect(isApiEntity(resourceEntity)).not.toBeTruthy(); + expect(isApiEntity(systemEntity)).not.toBeTruthy(); + expect(isApiEntity(userEntity)).not.toBeTruthy(); + expect(isApiEntity(apiEntity)).toBeTruthy(); + }); +}); + +describe('isComponentEntity', () => { + it('should check for the intended type', () => { + expect(isComponentEntity(apiEntity)).not.toBeTruthy(); + expect(isComponentEntity(domainEntity)).not.toBeTruthy(); + expect(isComponentEntity(groupEntity)).not.toBeTruthy(); + expect(isComponentEntity(locationEntity)).not.toBeTruthy(); + expect(isComponentEntity(resourceEntity)).not.toBeTruthy(); + expect(isComponentEntity(systemEntity)).not.toBeTruthy(); + expect(isComponentEntity(userEntity)).not.toBeTruthy(); + expect(isComponentEntity(componentEntity)).toBeTruthy(); + }); +}); + +describe('isDomainEntity', () => { + it('should check for the intended type', () => { + expect(isDomainEntity(apiEntity)).not.toBeTruthy(); + expect(isDomainEntity(componentEntity)).not.toBeTruthy(); + expect(isDomainEntity(groupEntity)).not.toBeTruthy(); + expect(isDomainEntity(locationEntity)).not.toBeTruthy(); + expect(isDomainEntity(resourceEntity)).not.toBeTruthy(); + expect(isDomainEntity(systemEntity)).not.toBeTruthy(); + expect(isDomainEntity(userEntity)).not.toBeTruthy(); + expect(isDomainEntity(domainEntity)).toBeTruthy(); + }); +}); + +describe('isGroupEntity', () => { + it('should check for the intended type', () => { + expect(isGroupEntity(apiEntity)).not.toBeTruthy(); + expect(isGroupEntity(componentEntity)).not.toBeTruthy(); + expect(isGroupEntity(domainEntity)).not.toBeTruthy(); + expect(isGroupEntity(locationEntity)).not.toBeTruthy(); + expect(isGroupEntity(resourceEntity)).not.toBeTruthy(); + expect(isGroupEntity(systemEntity)).not.toBeTruthy(); + expect(isGroupEntity(userEntity)).not.toBeTruthy(); + expect(isGroupEntity(groupEntity)).toBeTruthy(); + }); +}); + +describe('isLocationEntity', () => { + it('should check for the intended type', () => { + expect(isLocationEntity(apiEntity)).not.toBeTruthy(); + expect(isLocationEntity(componentEntity)).not.toBeTruthy(); + expect(isLocationEntity(domainEntity)).not.toBeTruthy(); + expect(isLocationEntity(groupEntity)).not.toBeTruthy(); + expect(isLocationEntity(resourceEntity)).not.toBeTruthy(); + expect(isLocationEntity(systemEntity)).not.toBeTruthy(); + expect(isLocationEntity(userEntity)).not.toBeTruthy(); + expect(isLocationEntity(locationEntity)).toBeTruthy(); + }); +}); + +describe('isResourceEntity', () => { + it('should check for the intended type', () => { + expect(isResourceEntity(apiEntity)).not.toBeTruthy(); + expect(isResourceEntity(componentEntity)).not.toBeTruthy(); + expect(isResourceEntity(domainEntity)).not.toBeTruthy(); + expect(isResourceEntity(groupEntity)).not.toBeTruthy(); + expect(isResourceEntity(locationEntity)).not.toBeTruthy(); + expect(isResourceEntity(systemEntity)).not.toBeTruthy(); + expect(isResourceEntity(userEntity)).not.toBeTruthy(); + expect(isResourceEntity(resourceEntity)).toBeTruthy(); + }); +}); + +describe('isSystemEntity', () => { + it('should check for the intended type', () => { + expect(isSystemEntity(apiEntity)).not.toBeTruthy(); + expect(isSystemEntity(componentEntity)).not.toBeTruthy(); + expect(isSystemEntity(domainEntity)).not.toBeTruthy(); + expect(isSystemEntity(groupEntity)).not.toBeTruthy(); + expect(isSystemEntity(locationEntity)).not.toBeTruthy(); + expect(isSystemEntity(resourceEntity)).not.toBeTruthy(); + expect(isSystemEntity(userEntity)).not.toBeTruthy(); + expect(isSystemEntity(systemEntity)).toBeTruthy(); + }); +}); + +describe('isUserEntity', () => { + it('should check for the intended type', () => { + expect(isUserEntity(apiEntity)).not.toBeTruthy(); + expect(isUserEntity(componentEntity)).not.toBeTruthy(); + expect(isUserEntity(domainEntity)).not.toBeTruthy(); + expect(isUserEntity(groupEntity)).not.toBeTruthy(); + expect(isUserEntity(locationEntity)).not.toBeTruthy(); + expect(isUserEntity(resourceEntity)).not.toBeTruthy(); + expect(isUserEntity(systemEntity)).not.toBeTruthy(); + expect(isUserEntity(userEntity)).toBeTruthy(); + }); +}); diff --git a/packages/catalog-model/src/entity/conditions.ts b/packages/catalog-model/src/entity/conditions.ts new file mode 100644 index 0000000000..d761695b5e --- /dev/null +++ b/packages/catalog-model/src/entity/conditions.ts @@ -0,0 +1,76 @@ +/* + * 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 { + ApiEntity, + ComponentEntity, + DomainEntity, + GroupEntity, + LocationEntity, + ResourceEntity, + SystemEntity, + UserEntity, +} from '../kinds'; +import { Entity } from './Entity'; + +/** + * @public + */ +export function isApiEntity(entity: Entity): entity is ApiEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'API'; +} +/** + * @public + */ +export function isComponentEntity(entity: Entity): entity is ComponentEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'COMPONENT'; +} +/** + * @public + */ +export function isDomainEntity(entity: Entity): entity is DomainEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'DOMAIN'; +} +/** + * @public + */ +export function isGroupEntity(entity: Entity): entity is GroupEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'GROUP'; +} +/** + * @public + */ +export function isLocationEntity(entity: Entity): entity is LocationEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'LOCATION'; +} +/** + * @public + */ +export function isResourceEntity(entity: Entity): entity is ResourceEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'RESOURCE'; +} +/** + * @public + */ +export function isSystemEntity(entity: Entity): entity is SystemEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'SYSTEM'; +} +/** + * @public + */ +export function isUserEntity(entity: Entity): entity is UserEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'USER'; +} diff --git a/packages/catalog-model/src/entity/index.ts b/packages/catalog-model/src/entity/index.ts index 45ee673167..039529bf3e 100644 --- a/packages/catalog-model/src/entity/index.ts +++ b/packages/catalog-model/src/entity/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +export * from './conditions'; export { DEFAULT_NAMESPACE, ANNOTATION_EDIT_URL, diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts index f895ded85e..70ec0ee948 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts @@ -20,8 +20,8 @@ import { } from '@backstage/backend-common'; import { Entity, + isUserEntity, stringifyEntityRef, - UserEntity, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { @@ -93,13 +93,9 @@ export class DefaultCatalogCollator { return formatted.toLowerCase(); } - private isUserEntity(entity: Entity): entity is UserEntity { - return entity.kind.toLocaleUpperCase('en-US') === 'USER'; - } - private getDocumentText(entity: Entity): string { let documentText = entity.metadata.description || ''; - if (this.isUserEntity(entity)) { + if (isUserEntity(entity)) { if (entity.spec?.profile?.displayName && documentText) { // combine displayName and description const displayName = entity.spec?.profile?.displayName; diff --git a/plugins/catalog-backend/src/search/util.ts b/plugins/catalog-backend/src/search/util.ts index b5df2a28a7..fd54d397c9 100644 --- a/plugins/catalog-backend/src/search/util.ts +++ b/plugins/catalog-backend/src/search/util.ts @@ -14,15 +14,7 @@ * limitations under the License. */ -import { Entity, UserEntity, GroupEntity } from '@backstage/catalog-model'; - -function isUserEntity(entity: Entity): entity is UserEntity { - return entity.kind.toLocaleUpperCase('en-US') === 'USER'; -} - -function isGroupEntity(entity: Entity): entity is GroupEntity { - return entity.kind.toLocaleUpperCase('en-US') === 'GROUP'; -} +import { Entity, isUserEntity, isGroupEntity } from '@backstage/catalog-model'; export function getDocumentText(entity: Entity): string { const documentTexts: string[] = []; diff --git a/plugins/catalog/src/components/EntitySwitch/conditions.ts b/plugins/catalog/src/components/EntitySwitch/conditions.ts index b6837627c0..86fd38a900 100644 --- a/plugins/catalog/src/components/EntitySwitch/conditions.ts +++ b/plugins/catalog/src/components/EntitySwitch/conditions.ts @@ -14,17 +14,7 @@ * limitations under the License. */ -import { - ApiEntity, - ComponentEntity, - DomainEntity, - Entity, - GroupEntity, - LocationEntity, - ResourceEntity, - SystemEntity, - UserEntity, -} from '@backstage/catalog-model'; +import { ComponentEntity, Entity } from '@backstage/catalog-model'; function strCmp(a: string | undefined, b: string | undefined): boolean { return Boolean( @@ -67,52 +57,3 @@ export function isComponentType(types: string | string[]) { export function isNamespace(namespaces: string | string[]) { return (entity: Entity) => strCmpAll(entity.metadata?.namespace, namespaces); } - -/** - * @public - */ -export function isApiEntity(entity: Entity): entity is ApiEntity { - return entity.kind.toLocaleUpperCase('en-US') === 'API'; -} -/** - * @public - */ -export function isComponentEntity(entity: Entity): entity is ComponentEntity { - return entity.kind.toLocaleUpperCase('en-US') === 'COMPONENT'; -} -/** - * @public - */ -export function isDomainEntity(entity: Entity): entity is DomainEntity { - return entity.kind.toLocaleUpperCase('en-US') === 'DOMAIN'; -} -/** - * @public - */ -export function isGroupEntity(entity: Entity): entity is GroupEntity { - return entity.kind.toLocaleUpperCase('en-US') === 'GROUP'; -} -/** - * @public - */ -export function isLocationEntity(entity: Entity): entity is LocationEntity { - return entity.kind.toLocaleUpperCase('en-US') === 'LOCATION'; -} -/** - * @public - */ -export function isResourceEntity(entity: Entity): entity is ResourceEntity { - return entity.kind.toLocaleUpperCase('en-US') === 'RESOURCE'; -} -/** - * @public - */ -export function isSystemEntity(entity: Entity): entity is SystemEntity { - return entity.kind.toLocaleUpperCase('en-US') === 'SYSTEM'; -} -/** - * @public - */ -export function isUserEntity(entity: Entity): entity is UserEntity { - return entity.kind.toLocaleUpperCase('en-US') === 'USER'; -} From 919396b27aa75491dd45d372ee64e09256484b5c Mon Sep 17 00:00:00 2001 From: Alex Rybchenko Date: Tue, 21 Jun 2022 17:47:18 +0200 Subject: [PATCH 068/401] revert plugin-catalog export change Signed-off-by: Alex Rybchenko --- plugins/catalog/src/components/EntitySwitch/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog/src/components/EntitySwitch/index.ts b/plugins/catalog/src/components/EntitySwitch/index.ts index 5d0537bfa5..6cccf4be6e 100644 --- a/plugins/catalog/src/components/EntitySwitch/index.ts +++ b/plugins/catalog/src/components/EntitySwitch/index.ts @@ -16,4 +16,4 @@ export { EntitySwitch } from './EntitySwitch'; export type { EntitySwitchProps, EntitySwitchCaseProps } from './EntitySwitch'; -export * from './conditions'; +export { isKind, isNamespace, isComponentType } from './conditions'; From f1dcc6f3c684bb679a4cecb9a3e557c1115103a2 Mon Sep 17 00:00:00 2001 From: Alex Rybchenko Date: Tue, 21 Jun 2022 18:01:21 +0200 Subject: [PATCH 069/401] added changesets Signed-off-by: Alex Rybchenko --- .changeset/pretty-masks-live.md | 5 +++++ .changeset/silent-coats-brake.md | 6 ++++++ 2 files changed, 11 insertions(+) create mode 100644 .changeset/pretty-masks-live.md create mode 100644 .changeset/silent-coats-brake.md diff --git a/.changeset/pretty-masks-live.md b/.changeset/pretty-masks-live.md new file mode 100644 index 0000000000..c45ad1e77f --- /dev/null +++ b/.changeset/pretty-masks-live.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': patch +--- + +Added type predicates for all entity types, e.g. isUserEntity diff --git a/.changeset/silent-coats-brake.md b/.changeset/silent-coats-brake.md new file mode 100644 index 0000000000..b1cfbacb6a --- /dev/null +++ b/.changeset/silent-coats-brake.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-backend': patch +--- + +Use entity type predicates from catalog-model From 3db6c21390ea33c9db24f322bec45ea842dbccfd Mon Sep 17 00:00:00 2001 From: Alex Rybchenko Date: Tue, 21 Jun 2022 18:34:52 +0200 Subject: [PATCH 070/401] updated api-report Signed-off-by: Alex Rybchenko --- packages/catalog-model/api-report.md | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index fca23d6ab9..9ea7636de5 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -254,6 +254,36 @@ export { GroupEntityV1alpha1 }; // @public export const groupEntityV1alpha1Validator: KindValidator; +// @public (undocumented) +export function isApiEntity(entity: Entity): entity is ApiEntityV1alpha1; + +// @public (undocumented) +export function isComponentEntity( + entity: Entity, +): entity is ComponentEntityV1alpha1; + +// @public (undocumented) +export function isDomainEntity(entity: Entity): entity is DomainEntityV1alpha1; + +// @public (undocumented) +export function isGroupEntity(entity: Entity): entity is GroupEntityV1alpha1; + +// @public (undocumented) +export function isLocationEntity( + entity: Entity, +): entity is LocationEntityV1alpha1; + +// @public (undocumented) +export function isResourceEntity( + entity: Entity, +): entity is ResourceEntityV1alpha1; + +// @public (undocumented) +export function isSystemEntity(entity: Entity): entity is SystemEntityV1alpha1; + +// @public (undocumented) +export function isUserEntity(entity: Entity): entity is UserEntityV1alpha1; + // @public export type KindValidator = { check(entity: Entity): Promise; From 7f18a4245043d04511d1d966163b3f6dd5c7bdf3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Jun 2022 16:55:38 +0000 Subject: [PATCH 071/401] fix(deps): update dependency logform to v2.4.1 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6841fc4366..2dbf176e9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17554,9 +17554,9 @@ log-update@^4.0.0: wrap-ansi "^6.2.0" logform@^2.3.2, logform@^2.4.0: - version "2.4.0" - resolved "https://registry.npmjs.org/logform/-/logform-2.4.0.tgz#131651715a17d50f09c2a2c1a524ff1a4164bcfe" - integrity sha512-CPSJw4ftjf517EhXZGGvTHHkYobo7ZCc0kvwUoOYcjfR2UVrI66RHj8MCrfAdEitdmFqbu2BYdYs8FHHZSb6iw== + version "2.4.1" + resolved "https://registry.npmjs.org/logform/-/logform-2.4.1.tgz#512c9eaef738044d1c619790ba0f806c80d9d3a9" + integrity sha512-7XB/tqc3VRbri9pRjU6E97mQ8vC27ivJ3lct4jhyT+n0JNDd4YKldFl0D75NqDp46hk8RC7Ma1Vjv/UPf67S+A== dependencies: "@colors/colors" "1.5.0" fecha "^4.2.0" From cb37b69827818cd8cc83f3a3e6d31fdedea7ffd2 Mon Sep 17 00:00:00 2001 From: Clare Liguori Date: Tue, 21 Jun 2022 10:26:23 -0700 Subject: [PATCH 072/401] Add AWS Proton to plugin marketplace Signed-off-by: Clare Liguori --- microsite/data/plugins/aws-proton.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 microsite/data/plugins/aws-proton.yaml diff --git a/microsite/data/plugins/aws-proton.yaml b/microsite/data/plugins/aws-proton.yaml new file mode 100644 index 0000000000..022b553b24 --- /dev/null +++ b/microsite/data/plugins/aws-proton.yaml @@ -0,0 +1,9 @@ +--- +title: AWS Proton +author: Amazon Web Services +authorUrl: https://aws.amazon.com/ +category: Infrastructure +description: Create and view AWS Proton services for your components in Backstage. +documentation: https://github.com/awslabs/aws-proton-plugins-for-backstage#readme +iconUrl: https://github.com/awslabs/aws-proton-plugins-for-backstage/blob/main/docs/images/proton-logo.png?raw=true +npmPackageName: '@aws/aws-proton-plugin-for-backstage' From 7e115d42f945e57e2fbcf4d83ef69e89362753f4 Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Tue, 21 Jun 2022 16:34:55 -0400 Subject: [PATCH 073/401] refactor(MyGroupsSidebarItem): render namespaces with subtitles Signed-off-by: Phil Kuang --- .changeset/serious-zebras-joke.md | 5 +++ .changeset/shiny-turkeys-doubt.md | 2 +- packages/core-components/api-report.md | 1 + .../src/layout/Sidebar/SidebarSubmenuItem.tsx | 23 ++++++++++++- .../MyGroupsSidebarItem.tsx | 32 ++++--------------- 5 files changed, 36 insertions(+), 27 deletions(-) create mode 100644 .changeset/serious-zebras-joke.md diff --git a/.changeset/serious-zebras-joke.md b/.changeset/serious-zebras-joke.md new file mode 100644 index 0000000000..78f38ca967 --- /dev/null +++ b/.changeset/serious-zebras-joke.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Support displaying subtitle text in `SidebarSubmenuItem` diff --git a/.changeset/shiny-turkeys-doubt.md b/.changeset/shiny-turkeys-doubt.md index 0f663a4c74..edac39182c 100644 --- a/.changeset/shiny-turkeys-doubt.md +++ b/.changeset/shiny-turkeys-doubt.md @@ -2,4 +2,4 @@ '@backstage/plugin-org': patch --- -Render namespaced teams within dropdown items in `MyGroupsSidebarItem` +Render namespaces for teams with subtitles in `MyGroupsSidebarItem` diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 911ef36867..302ef7ef7d 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -1110,6 +1110,7 @@ export type SidebarSubmenuItemDropdownItem = { // @public export type SidebarSubmenuItemProps = { title: string; + subtitle?: string; to?: string; icon?: IconComponent; dropdownItems?: SidebarSubmenuItemDropdownItem[]; diff --git a/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx b/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx index 41f1a1e287..a1565a348f 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx @@ -59,6 +59,13 @@ const useStyles = makeStyles( whiteSpace: 'nowrap', overflow: 'hidden', 'text-overflow': 'ellipsis', + lineHeight: 1, + }, + subtitle: { + fontSize: 10, + whiteSpace: 'nowrap', + overflow: 'hidden', + 'text-overflow': 'ellipsis', }, dropdownArrow: { position: 'absolute', @@ -105,6 +112,7 @@ export type SidebarSubmenuItemDropdownItem = { * Holds submenu item content. * * title: Text content of submenu item + * subtitle: A subtitle displayed under the main title * to: Path to navigate to when item is clicked * icon: Icon displayed on the left of text content * dropdownItems: Optional array of dropdown items displayed when submenu item is clicked. @@ -113,6 +121,7 @@ export type SidebarSubmenuItemDropdownItem = { */ export type SidebarSubmenuItemProps = { title: string; + subtitle?: string; to?: string; icon?: IconComponent; dropdownItems?: SidebarSubmenuItemDropdownItem[]; @@ -124,7 +133,7 @@ export type SidebarSubmenuItemProps = { * @public */ export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => { - const { title, to, icon: Icon, dropdownItems } = props; + const { title, subtitle, to, icon: Icon, dropdownItems } = props; const classes = useStyles(); const { setIsHoveredOn } = useContext(SidebarItemWithSubmenuContext); const closeSubmenu = () => { @@ -158,6 +167,12 @@ export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => { {Icon && } {title} +
+ {subtitle && ( + + {subtitle} + + )}
{showDropDown ? ( @@ -210,6 +225,12 @@ export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => { {Icon && } {title} +
+ {subtitle && ( + + {subtitle} + + )}
diff --git a/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.tsx b/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.tsx index 445bf8e6fe..f3e3a6bd9a 100644 --- a/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.tsx +++ b/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.tsx @@ -17,7 +17,6 @@ import React from 'react'; import { DEFAULT_NAMESPACE, - Entity, stringifyEntityRef, } from '@backstage/catalog-model'; import { @@ -90,42 +89,25 @@ export const MyGroupsSidebarItem = (props: { ); } - const namespacedGroupsMap: { [namespace: string]: Entity[] } = {}; - groups.forEach(group => { - namespacedGroupsMap[group.metadata.namespace!] = - namespacedGroupsMap[group.metadata.namespace!] ?? []; - namespacedGroupsMap[group.metadata.namespace!].push(group); - }); - // Member of more than one group return ( - {namespacedGroupsMap[DEFAULT_NAMESPACE]?.map(group => { + {groups?.map(function groupsMap(group) { return ( ); })} - {Object.entries(namespacedGroupsMap) - .filter(([n]) => n !== DEFAULT_NAMESPACE) - .map(([namespace, namespacedGroups]) => { - return ( - ({ - title: group.metadata.title || group.metadata.name, - to: catalogEntityRoute(getCompoundEntityRef(group)), - }))} - /> - ); - })} ); From 0ae5d7858d364073caa0a37de17cfa1d7ace5913 Mon Sep 17 00:00:00 2001 From: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Date: Tue, 21 Jun 2022 17:29:35 -0500 Subject: [PATCH 074/401] Fixed Azure Discovery config example Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> --- docs/integrations/azure/discovery.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/integrations/azure/discovery.md b/docs/integrations/azure/discovery.md index c607a6550e..6a420462c0 100644 --- a/docs/integrations/azure/discovery.md +++ b/docs/integrations/azure/discovery.md @@ -45,7 +45,7 @@ catalog: anotherProviderId: # another identifier organization: myorg project: myproject - repository: '*' # this will match all repos starting with service-* + repository: '*' # this will match all repos path: /src/*/catalog-info.yaml # this will search for files deep inside the /src folder yetAotherProviderId: # guess, what? Another one :) host: selfhostedazure.yourcompany.com From 2b74a8505985b68b31d90c3bbff614ebe27af157 Mon Sep 17 00:00:00 2001 From: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Date: Tue, 21 Jun 2022 17:42:31 -0500 Subject: [PATCH 075/401] Added details about installing addons package Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> --- docs/features/techdocs/addons.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/features/techdocs/addons.md b/docs/features/techdocs/addons.md index 831729d093..33d97521d3 100644 --- a/docs/features/techdocs/addons.md +++ b/docs/features/techdocs/addons.md @@ -52,6 +52,8 @@ Addons are rendered in the order in which they are registered. ## Installing and using Addons +To start using Addons you need to add the `@backstage/plugin-techdocs-module-addons-contrib` package you your app, you can do that by running this command form the root of your project: `yarn add --cwd packages/app @backstage/plugin-techdocs-module-addons-contrib` + Addons can be installed and configured in much the same way as extensions for other Backstage plugins: by adding them underneath an extension registry component (``) under the route representing the TechDocs Reader From bf0336352b2047409bd8ab6a07832f5ff09bc644 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Jun 2022 00:40:44 +0000 Subject: [PATCH 076/401] chore(deps): update dependency @types/testing-library__jest-dom to v5.14.5 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2c1a244a1f..f0c7f2ba35 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7008,9 +7008,9 @@ terser-webpack-plugin "*" "@types/testing-library__jest-dom@^5.9.1": - version "5.14.4" - resolved "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.4.tgz#21567ec845f4efee55923842c79728dc49c1650b" - integrity sha512-EUCs9PTBOEyfRtLKkKd31YrRCx/9Wxjy2Uqb6IH/KAOr7/vP0i8iClOyxQqjm/UxMGU5r5s2vOBM7vSPQVmabg== + version "5.14.5" + resolved "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz#d113709c90b3c75fdb127ec338dad7d5f86c974f" + integrity sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ== dependencies: "@types/jest" "*" From 81c9d7fb4446e1ced660277d394783f84d1117c0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Jun 2022 00:41:30 +0000 Subject: [PATCH 077/401] fix(deps): update dependency @uiw/react-codemirror to v4.9.4 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2c1a244a1f..9e3320b676 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7317,9 +7317,9 @@ eslint-visitor-keys "^3.0.0" "@uiw/react-codemirror@^4.9.3": - version "4.9.3" - resolved "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.9.3.tgz#3d7abc434b140f01119332e25e9f49ace7c381bf" - integrity sha512-iT/daKF852ZsNJGvqmxRFJ4KTOJSikeoCMl7jhUUJTETdozUf0/DqUQhvazAoduMRzqqHYnN/5isdITCyy2P6g== + version "4.9.4" + resolved "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.9.4.tgz#4643201f279fed103f2cf15df9431931fe4e9f15" + integrity sha512-IsC5xDevpIeLMzHQQwT2W40gFFIdKeT1T0DHjzzai+s5SIrMlGe3QSHWeC1wSO7FtfNxFpFlTYMGJm5JwUviMA== dependencies: "@babel/runtime" ">=7.11.0" "@codemirror/theme-one-dark" "^6.0.0" From ed40cd0278f8ae46f488c3645a91016ac1158f3b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Jun 2022 01:51:26 +0000 Subject: [PATCH 078/401] fix(deps): update dependency google-auth-library to v8.0.3 Signed-off-by: Renovate Bot --- yarn.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2793bc7ba7..c4640b76b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13884,9 +13884,9 @@ google-auth-library@^7.14.0: lru-cache "^6.0.0" google-auth-library@^8.0.0, google-auth-library@^8.0.1, google-auth-library@^8.0.2: - version "8.0.2" - resolved "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.0.2.tgz#5fa0f2d3795c3e4019d2bb315ade4454cc9c30b5" - integrity sha512-HoG+nWFAThLovKpvcbYzxgn+nBJPTfAwtq0GxPN821nOO+21+8oP7MoEHfd1sbDulUFFGfcjJr2CnJ4YssHcyg== + version "8.0.3" + resolved "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.0.3.tgz#1e780430632d03df36dc22a3f5c89dbc251f2105" + integrity sha512-1eC6yaCrPfkv3bwtb3e0AOct7E7xR/uikDyXNo/j8Wd6a1ldRgAey5FmaDGNJnHNDPLtDiENQLYsA69eXOF5sA== dependencies: arrify "^2.0.0" base64-js "^1.3.0" @@ -13894,7 +13894,7 @@ google-auth-library@^8.0.0, google-auth-library@^8.0.1, google-auth-library@^8.0 fast-text-encoding "^1.0.0" gaxios "^5.0.0" gcp-metadata "^5.0.0" - gtoken "^5.3.2" + gtoken "^6.0.0" jws "^4.0.0" lru-cache "^6.0.0" @@ -13943,10 +13943,10 @@ google-p12-pem@^3.0.3: dependencies: node-forge "^1.0.0" -google-p12-pem@^3.1.3: - version "3.1.4" - resolved "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.4.tgz#123f7b40da204de4ed1fbf2fd5be12c047fc8b3b" - integrity sha512-HHuHmkLgwjdmVRngf5+gSmpkyaRI6QmOg77J8tkNBHhNEI62sGHyw4/+UkgyZEI7h84NbWprXDJ+sa3xOYFvTg== +google-p12-pem@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.0.tgz#f46581add1dc6ea0b96160cda6ce37ee35ab8ca3" + integrity sha512-lRTMn5ElBdDixv4a86bixejPSRk1boRtUowNepeKEVvYiFlkLuAJUVpEz6PfObDHYEKnZWq/9a2zC98xu62A9w== dependencies: node-forge "^1.3.1" @@ -14145,13 +14145,13 @@ gtoken@^5.0.4: jws "^4.0.0" mime "^2.2.0" -gtoken@^5.3.2: - version "5.3.2" - resolved "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz#deb7dc876abe002178e0515e383382ea9446d58f" - integrity sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ== +gtoken@^6.0.0: + version "6.0.1" + resolved "https://registry.npmjs.org/gtoken/-/gtoken-6.0.1.tgz#1276371d51e93c4eface76e3f30f9e8f1cad3f1a" + integrity sha512-J0vebk6u6i4rLTM0lQq25SdusCLMvujYNZeAouyPvSbGlcjw7P8L3W9INIFnlXUx+AUD7TDoM1mgdhzH+XX7DQ== dependencies: gaxios "^4.0.0" - google-p12-pem "^3.1.3" + google-p12-pem "^4.0.0" jws "^4.0.0" gzip-size@^6.0.0: From e7ce3f708442eea242e08c08d63a667744137cf3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Jun 2022 01:54:37 +0000 Subject: [PATCH 079/401] fix(deps): update dependency sucrase to v3.21.1 Signed-off-by: Renovate Bot --- storybook/yarn.lock | 17 ++++++----------- yarn.lock | 6 +++--- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/storybook/yarn.lock b/storybook/yarn.lock index 39d0e2725f..6bf41324eb 100644 --- a/storybook/yarn.lock +++ b/storybook/yarn.lock @@ -2610,7 +2610,7 @@ ansi-to-html@^0.6.11: any-promise@^1.0.0: version "1.3.0" resolved "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" - integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== anymatch@^2.0.0: version "2.0.0" @@ -7039,12 +7039,7 @@ pinkie@^2.0.0: resolved "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== -pirates@^4.0.1: - version "4.0.4" - resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.4.tgz#07df81e61028e402735cdd49db701e4885b4e6e6" - integrity sha512-ZIrVPH+A52Dw84R0L3/VS9Op04PuQ2SEoJL6bkshmiTic/HldyW9Tf7oH5mhJZBK7NmDx27vSMrYEXPXclpDKw== - -pirates@^4.0.5: +pirates@^4.0.1, pirates@^4.0.5: version "4.0.5" resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== @@ -8392,9 +8387,9 @@ style-to-object@0.3.0, style-to-object@^0.3.0: inline-style-parser "0.1.1" sucrase@^3.21.0: - version "3.21.0" - resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.21.0.tgz#6a5affdbe716b22e4dc99c57d366ad0d216444b9" - integrity sha512-FjAhMJjDcifARI7bZej0Bi1yekjWQHoEvWIXhLPwDhC6O4iZ5PtGb86WV56riW87hzpgB13wwBKO9vKAiWu5VQ== + version "3.21.1" + resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.21.1.tgz#7e29ddaca012764cf843280b00e74a843bdf790f" + integrity sha512-kxXnC9yZEav5USAu8gooZID9Ph3xqwdJxZoh+WbOWQZHTB7CHj3ANwENVMZ6mAZ9k7UtJtFxvQD9R03q3yU2YQ== dependencies: commander "^4.0.0" glob "7.1.6" @@ -8552,7 +8547,7 @@ test-exclude@^6.0.0: thenify-all@^1.0.0: version "1.6.0" resolved "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" - integrity sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY= + integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== dependencies: thenify ">= 3.1.0 < 4" diff --git a/yarn.lock b/yarn.lock index 2793bc7ba7..2a6c060e14 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24153,9 +24153,9 @@ stylis@^4.0.6: integrity sha512-OFFeUXFgwnGOKvEXaSv0D0KQ5ADP0n6g3SVONx6I/85JzNZ3u50FRwB3lVIk1QO2HNdI75tbVzc4Z66Gdp9voA== sucrase@^3.18.0, sucrase@^3.20.2: - version "3.21.0" - resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.21.0.tgz#6a5affdbe716b22e4dc99c57d366ad0d216444b9" - integrity sha512-FjAhMJjDcifARI7bZej0Bi1yekjWQHoEvWIXhLPwDhC6O4iZ5PtGb86WV56riW87hzpgB13wwBKO9vKAiWu5VQ== + version "3.21.1" + resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.21.1.tgz#7e29ddaca012764cf843280b00e74a843bdf790f" + integrity sha512-kxXnC9yZEav5USAu8gooZID9Ph3xqwdJxZoh+WbOWQZHTB7CHj3ANwENVMZ6mAZ9k7UtJtFxvQD9R03q3yU2YQ== dependencies: commander "^4.0.0" glob "7.1.6" From 403c92438b522938b21f7c1354056b9c538d8c05 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Jun 2022 02:44:30 +0000 Subject: [PATCH 080/401] chore(deps): update dependency cypress to v10.2.0 Signed-off-by: Renovate Bot --- cypress/yarn.lock | 6 +++--- yarn.lock | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cypress/yarn.lock b/cypress/yarn.lock index 0fc9da99b6..1ccce4ea24 100644 --- a/cypress/yarn.lock +++ b/cypress/yarn.lock @@ -304,9 +304,9 @@ cross-spawn@^7.0.0: which "^2.0.1" cypress@^10.0.0: - version "10.1.0" - resolved "https://registry.npmjs.org/cypress/-/cypress-10.1.0.tgz#6514a26c721822a02bc194e9a7f72c3142aea174" - integrity sha512-aQ4JVZVib4Xd9FZW8IRZfKelUvqF4y5A+oUbNvn8TlsBmEfIg3m5Xd6Mt6PVU/jHiVJ9Psl905B3ZPnrDcmyuQ== + version "10.2.0" + resolved "https://registry.npmjs.org/cypress/-/cypress-10.2.0.tgz#ca078abfceb13be2a33cbba6e0e80ded770f542a" + integrity sha512-+i9lY5ENlfi2mJwsggzR+XASOIgMd7S/Gd3/13NCpv596n3YSplMAueBTIxNLcxDpTcIksp+9pM3UaDrJDpFqA== dependencies: "@cypress/request" "^2.88.10" "@cypress/xvfb" "^1.2.4" diff --git a/yarn.lock b/yarn.lock index c4640b76b4..436199f240 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10719,9 +10719,9 @@ cypress-plugin-snapshots@^1.4.4: unidiff "1.0.2" cypress@^10.0.0: - version "10.1.0" - resolved "https://registry.npmjs.org/cypress/-/cypress-10.1.0.tgz#6514a26c721822a02bc194e9a7f72c3142aea174" - integrity sha512-aQ4JVZVib4Xd9FZW8IRZfKelUvqF4y5A+oUbNvn8TlsBmEfIg3m5Xd6Mt6PVU/jHiVJ9Psl905B3ZPnrDcmyuQ== + version "10.2.0" + resolved "https://registry.npmjs.org/cypress/-/cypress-10.2.0.tgz#ca078abfceb13be2a33cbba6e0e80ded770f542a" + integrity sha512-+i9lY5ENlfi2mJwsggzR+XASOIgMd7S/Gd3/13NCpv596n3YSplMAueBTIxNLcxDpTcIksp+9pM3UaDrJDpFqA== dependencies: "@cypress/request" "^2.88.10" "@cypress/xvfb" "^1.2.4" From c5270e31a269ba2e51e3a2cf7802a534c38f283b Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Tue, 21 Jun 2022 22:49:29 -0400 Subject: [PATCH 081/401] Improve plural handling in log output Signed-off-by: Adam Harvey --- packages/backend-common/src/config.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index 94b362d99d..608869ea93 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -53,7 +53,9 @@ const updateRedactionList = ( ); logger.info( - `${values.size} secrets found in the config which will be redacted`, + `${values.size} secret${ + values.size > 1 ? 's' : '' + } found in the config which will be redacted`, ); setRootLoggerRedactionList(Array.from(values)); From 0fc57887e86d4ccbd3764d27a63a7a1c4286a2b7 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Tue, 21 Jun 2022 22:50:34 -0400 Subject: [PATCH 082/401] Add changeset Signed-off-by: Adam Harvey --- .changeset/quiet-pens-notice.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/quiet-pens-notice.md diff --git a/.changeset/quiet-pens-notice.md b/.changeset/quiet-pens-notice.md new file mode 100644 index 0000000000..aa6aeb049d --- /dev/null +++ b/.changeset/quiet-pens-notice.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Improve plural handling in logging output for secrets From d821a2eec3269be542b27e28da5f8d1c61cb8eea Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Tue, 21 Jun 2022 23:08:41 -0400 Subject: [PATCH 083/401] Fix minor possessive typo Signed-off-by: Adam Harvey --- docs/getting-started/project-structure.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/project-structure.md b/docs/getting-started/project-structure.md index cbe84db2e0..8dca898ae7 100644 --- a/docs/getting-started/project-structure.md +++ b/docs/getting-started/project-structure.md @@ -28,7 +28,7 @@ the code. sub-folder which is used for a markdown spellchecker. - [`.yarn/`](https://github.com/backstage/backstage/tree/master/.yarn) - - Backstage ships with it's own `yarn` implementation. This allows us to have + Backstage ships with its own `yarn` implementation. This allows us to have better control over our `yarn.lock` file and hopefully avoid problems due to yarn versioning differences. From d8e116964f70da1e2480ad93157b2b630cb94e01 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Tue, 21 Jun 2022 23:17:42 -0400 Subject: [PATCH 084/401] Fix broken deployment link Signed-off-by: Adam Harvey --- docs/FAQ.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index 96d49ff060..f695a59410 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -223,8 +223,7 @@ For more information, see our No, this is not a service offering. We build the piece of software, and someone in your infrastructure team is responsible for -[deploying](https://backstage.io/docs/getting-started/deployment-k8s) and -maintaining it. +[deploying](https://backstage.io/docs/deployment) and maintaining it. ### How secure is Backstage? From 80542ee49cd308da40d7eb335a8884aa4b9d57cf Mon Sep 17 00:00:00 2001 From: nguyentranbao-ct Date: Wed, 22 Jun 2022 11:01:26 +0700 Subject: [PATCH 085/401] docs(adopters): add Cho Tot company Signed-off-by: nguyentranbao-ct --- ADOPTERS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ADOPTERS.md b/ADOPTERS.md index 44e9dc9b44..8eacb45f83 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -183,3 +183,4 @@ _If you're using Backstage in your organization, please try to add your company | [Mercedes-Benz.io](https://www.mercedes-benz.io/) | [Manuel Santos](https://github.com/manusant) | At Mercedes-Benz we use it as a developer portal with software catalog, TechDocs, Scaffolding and custom plugins. It provides an overview of our tech ecosystem to our product development teams. The portal also serves as a way to foster collaboration among the numerous companies of the Mercedes-Benz Group. | [Funding Circle](https://www.fundingcircle.com/) | [Ariel Pacciaroni](https://github.com/arielpacciaroni) | We are building the internal developer portal using Backstage project and centralizing all services information at one place. The portal helps us track down repositories ownership as well as direct access to key information on every component. | [Clarivate](https://www.clarivate.com) | [Gabriele Carteni](mailto:gabriele.carteni@clarivate.com) | We are building our Developer Portal using Backstage to have a better control over our software ecosystem, integrate SDLC tools and promote best practices | +| [Cho Tot](https://www.chotot.com) | [Chotot Team](mailto:sre@chotot.vn) | Internal developer portal, service catalog with CI/CD tools. From bf44af8b41e37139f1661fa98f20760eb6212a8a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Jun 2022 07:43:55 +0000 Subject: [PATCH 086/401] fix(deps): update dependency aws-sdk to v2.1159.0 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 436199f240..c232257cd7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8232,9 +8232,9 @@ aws-sdk-mock@^5.2.1: traverse "^0.6.6" aws-sdk@^2.1122.0, aws-sdk@^2.840.0, aws-sdk@^2.948.0: - version "2.1158.0" - resolved "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1158.0.tgz#924fb6dda1acecb1dc1c8901095cf64d6acdade4" - integrity sha512-uHYzZMGE+b50sWXaLhga4aD1SpB3+DEZclAkg9aYz2pDZlSDTOMh3uJ/ufsMBs7VcDKGS7mQRibCmCbwRGTIlg== + version "2.1159.0" + resolved "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1159.0.tgz#64d585ac608d58e6ff62678be71a4c4ca61ca5dc" + integrity sha512-zm3k/ufwZnkWc6M+HDz00CWuILot4L9kJ5VJsuDS9fwsT9To6k91Y1njCtIV4tcgcXvUru0Sbm4D0w5bc2847A== dependencies: buffer "4.9.2" events "1.1.1" From 1d7fe55f6fbf263a17ae6ebeb5aa8adfc39877c8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Jun 2022 08:06:52 +0000 Subject: [PATCH 087/401] fix(deps): update dependency graphql-modules to v2.1.0 Signed-off-by: Renovate Bot --- yarn.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/yarn.lock b/yarn.lock index 42c2af7d55..670073ba31 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2597,7 +2597,7 @@ "@graphql-tools/utils" "8.6.13" tslib "^2.4.0" -"@graphql-tools/schema@8.3.10", "@graphql-tools/schema@^8.0.0", "@graphql-tools/schema@^8.1.1", "@graphql-tools/schema@^8.1.2", "@graphql-tools/schema@^8.3.1": +"@graphql-tools/schema@8.3.10", "@graphql-tools/schema@^8.0.0", "@graphql-tools/schema@^8.1.2", "@graphql-tools/schema@^8.3.1": version "8.3.10" resolved "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.3.10.tgz#c3e373e6ad854f533fc7e55859dd8f9e81de30dd" integrity sha512-tfhjSTi3OzheDrVzG7rkPZg2BbQjmZRLM2vvQoM2b1TnUwgUIbpAgcnf+AWDLRsoCOWlezeLgij1BLeAR0Q0jg== @@ -14070,14 +14070,14 @@ graphql-language-service@^5.0.6: vscode-languageserver-types "^3.15.1" graphql-modules@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/graphql-modules/-/graphql-modules-2.0.0.tgz#5f710d86eb7295da83769c8cd6d61a049d999800" - integrity sha512-CM5CIJp428+ripgcLyrioBmAKB3ucvIEOgJG4WMjnOgScHY/eCZh/8I51cF8oc+pqebOgBCR3HH//IH5v7kv+w== + version "2.1.0" + resolved "https://registry.npmjs.org/graphql-modules/-/graphql-modules-2.1.0.tgz#d99692034b4b053fba3d0ebe49e4e66772ed2785" + integrity sha512-fOUc4i5xNLkRxqx234MIr+kolrxk1tZ2onTeRIcB73mCY4NeKxej7FMLb5St+UcNLzhqM4L6Mf47rN9MPVDmgA== dependencies: - "@graphql-tools/schema" "^8.1.1" + "@graphql-tools/schema" "^8.3.1" "@graphql-tools/wrap" "^8.3.1" "@graphql-typed-document-node/core" "^3.1.0" - ramda "^0.27.1" + ramda "^0.28.0" graphql-request@^4.0.0: version "4.2.0" @@ -21412,10 +21412,10 @@ raf@^3.4.0: dependencies: performance-now "^2.1.0" -ramda@^0.27.1: - version "0.27.2" - resolved "https://registry.npmjs.org/ramda/-/ramda-0.27.2.tgz#84463226f7f36dc33592f6f4ed6374c48306c3f1" - integrity sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA== +ramda@^0.28.0: + version "0.28.0" + resolved "https://registry.npmjs.org/ramda/-/ramda-0.28.0.tgz#acd785690100337e8b063cab3470019be427cc97" + integrity sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA== randexp@^0.5.3: version "0.5.3" From 6cf1fe4bc4b35b0cc02ed71c09082101bce76d1c Mon Sep 17 00:00:00 2001 From: Andrea Giannantonio Date: Wed, 22 Jun 2022 11:36:16 +0200 Subject: [PATCH 088/401] fix: adopters table border Signed-off-by: Andrea Giannantonio --- ADOPTERS.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/ADOPTERS.md b/ADOPTERS.md index 8eacb45f83..e8d2aeed8b 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -3,7 +3,7 @@ _If you're using Backstage in your organization, please try to add your company name to this list. This really helps the project to gain momentum and credibility. It's a small contribution back to the project with a big impact._ | Organization | Contact | Description of Use | -| --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|-----------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [Spotify](https://www.spotify.com) | [@leemills83](https://github.com/leemills83) | Main interface towards all of Spotify's infrastructure and technical documentation. | | [bol.com](https://www.bol.com) | [@sagacity](https://github.com/sagacity) | Initial work being done to unify platform tooling. | | [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. | @@ -34,9 +34,9 @@ _If you're using Backstage in your organization, please try to add your company | [Netflix](https://www.netflix.com/) | [bleathem](https://github.com/bleathem) | Our Backstage implementation will be the front door to a unified experience connecting our internal platform products across important workflows with integrated knowledge and support. | | [b.well](https://www.icanbwell.com/) | [Jacob Rosales](https://github.com/jrosales) | Foundation for our engineering portal and cloud insights. | | [PagerDuty](https://www.pagerduty.com/) | [Mark Shaw](https://github.com/markshawtoronto) | Developer portal, initially focused on software templates and tech-docs. | -| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process 🌕🚀🧑‍🚀 | +| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process 🌕🚀🧑‍🚀 | | [FundApps](https://www.fundapps.co/) | [Elliot Greenwood](https://github.com/egnwd) | Developer Portal - A place for us to keep track of our projects and documentation for all services and processes | -| [DAZN](https://dazn.com/) | [Lou Bichard](https://twitter.com/loujaybee), [Marco Crivellaro](https://github.com/crivetechie), [Alex Hollerith](mailto:alex.hollerith@dazn.com), [Kamil Wolny](https://github.com/mrwolny) | Ingesting all of DAZN's repos for the catalog, migrating our internal platform apps (pull request boards, release information, inner source marketplace etc) to Backstage plugins (where applicable). | +| [DAZN](https://dazn.com/) | [Lou Bichard](https://twitter.com/loujaybee), [Marco Crivellaro](https://github.com/crivetechie), [Alex Hollerith](mailto:alex.hollerith@dazn.com), [Kamil Wolny](https://github.com/mrwolny) | Ingesting all of DAZN's repos for the catalog, migrating our internal platform apps (pull request boards, release information, inner source marketplace etc) to Backstage plugins (where applicable). | | [HelloFresh](https://www.hellofresh.de/) | [@iammuho](https://github.com/iammuho), [@ElenaForester](https://github.com/ElenaForester), [@diegomarangoni](https://github.com/diegomarangoni) | Our developer portal at HelloFresh - Spread across an organisation of 500+ engineers globally. | | [FactSet](https://www.factset.com/) | [@kuangp](https://github.com/kuangp) | Developer portal to provide discoverability to all internal components, APIs, documentation, and scaffold templates with integrations to our internal infrastructure tools. | | [Workrise](https://www.workrise.com/) | [Michael Rode](https://github.com/michaelrode) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | @@ -161,7 +161,7 @@ _If you're using Backstage in your organization, please try to add your company | [OVHcloud](https://www.ovhcloud.com/fr/) | [Jean-Philippe Blary](https://github.com/blaryjp), [Arnaud Bauer](mailto:arnaud.bauer@ovhcloud.com), [Flavien Chantelot](https://github.com/Dorn-) | We're providing Backstage to our collaborators to ease their daily jobs, and let them extends it using plugins. | | [Procter & Gamble](https://us.pg.com/) | [Binita Nayak](https://github.com/binitan), [Josh Rose](https://github.com/joshuarose), [RJ Winkler](https://github.com/rjwink) | P&G leverages Backstage to build internal developer portal to ensure developers' happiness. This developer portal shall act as single source of information needed by development teams to seamlessly create, find and maintain their software components/resources/documentation. | | [SANS Institute](https://www.sans.org) | [Christopher Klewin](mailto:cklewin@sans.org) | Developer portal for centralized visibility, reporting, and tooling across multiple organizations. | -| [Okay](https://www.okayhq.com/) | [Tomas Barreto](mailto:tomas@okayhq.com) | Service catalog, developer portal, and technical documentation | +| [Okay](https://www.okayhq.com/) | [Tomas Barreto](mailto:tomas@okayhq.com) | Service catalog, developer portal, and technical documentation | | [Kaluza](https://www.kaluza.com) | [James Condren](mailto:james.condron@kaluza.com) | To provide an automated golden path to developers, with a focus on discovery and documentation | | [LinkedIn](https://linkedin.com) | [Joshua Lawrence](mailto:jlawrence@linkedin.com) | We are building a platform for internal web tools | | [Forto](https://forto.com) | [Rodolfo Matos](mailto:rodolfo.matos@forto.com) | Still in a experimental phase/assessing the organisational fit. We will be using it mostly a developer portal -- pretty standard use case. | @@ -171,16 +171,16 @@ _If you're using Backstage in your organization, please try to add your company | [AEB](https://www.aeb.com/) | [David Fankhänel](mailto:dfl@aeb.com) | Central developer platform for creating new apps via templates, getting an overview via software catalog, etc | | [SALTO Systems](https://saltosystems.com) | [Ian Cowley](mailto:i.cowley@saltosystems.com) | Currently using Backstage as an internal documentation portal. | | [Lummo](https://lummo.com) | [Anjul Sahu](mailto:anjul@lummo.com) | We are building the internal developer portal using Backstage and bringing up all integrations and service information at one place. | -| [Frontside](https://frontside.com) | [Taras Mankovski](mailto:taras@frontside.com) | +| [Frontside](https://frontside.com) | [Taras Mankovski](mailto:taras@frontside.com) | | | [Stepstone](https://www.stepstone.com/en/) | [Neil Kennedy](mailto:neil.kennedy@stepstone.com) | StepStone is using Backstage to solve problems around ownership and visibility of our applications. We have thousands of repos, multiple legacy systems and a growing platform that is hard to maintain. Backstage is forming the centre of our push to embrace the chaos. | | [idwall](https://idwall.co) | [Rodrigo Catão Araujo](mailto:rodrigo@idwall.co) | Developer Portal for internal engineers to access service catalog, documentation, observability, infrastructure and internal tooling. | | [Jaguar Land Rover](https://www.jaguarlandrover.com) | [Josh Walker](mailto:jwalke18@jaguarlandrover.com) | Users can request a Gitlab user, which creates a commit with the Terraform code. | -| [Glovo](http://glovoapp.com/) | [Yaser Toutah](mailto:yaser.toutah@glovoapp.com) | Developer Portal to improve our Developer Experience, identify ownership and track metadata for our services and tools. It's our Service Catalog. In addition to that, we use it for Service Creation, and much more. | +| [Glovo](http://glovoapp.com/) | [Yaser Toutah](mailto:yaser.toutah@glovoapp.com) | Developer Portal to improve our Developer Experience, identify ownership and track metadata for our services and tools. It's our Service Catalog. In addition to that, we use it for Service Creation, and much more. | | [Dixa](https://dixa.com) | [Jens Møller](mailto:jsc@dixa.com) | We are in early stages, but using it to get overview of our repositories and ownership of these. We want among many things to use it for compliance and easier access to key metrics for our repos. | | [Notino](https://notino.com) | [Jan Remunda](mailto:jan.remunda@notino.com) | Backstage is our developer portal. We use it as service catalog and for technical documentation. | | [Polarpoint](https://polarpoint.io/) | [Surj Bains](https://github.com/polarpoint-io) | We are using Backstage as our Developer portal as well as for hosting our DevOps portal for software catalog. | | [Niche](https://niche.com) | [Zach Romitz](mailto:zach.romitz@niche.com) | We are using the Software Catalog, Software Templates, API documentation, and Techdocs to try and centralize service information. | -| [Mercedes-Benz.io](https://www.mercedes-benz.io/) | [Manuel Santos](https://github.com/manusant) | At Mercedes-Benz we use it as a developer portal with software catalog, TechDocs, Scaffolding and custom plugins. It provides an overview of our tech ecosystem to our product development teams. The portal also serves as a way to foster collaboration among the numerous companies of the Mercedes-Benz Group. -| [Funding Circle](https://www.fundingcircle.com/) | [Ariel Pacciaroni](https://github.com/arielpacciaroni) | We are building the internal developer portal using Backstage project and centralizing all services information at one place. The portal helps us track down repositories ownership as well as direct access to key information on every component. -| [Clarivate](https://www.clarivate.com) | [Gabriele Carteni](mailto:gabriele.carteni@clarivate.com) | We are building our Developer Portal using Backstage to have a better control over our software ecosystem, integrate SDLC tools and promote best practices | -| [Cho Tot](https://www.chotot.com) | [Chotot Team](mailto:sre@chotot.vn) | Internal developer portal, service catalog with CI/CD tools. +| [Mercedes-Benz.io](https://www.mercedes-benz.io/) | [Manuel Santos](https://github.com/manusant) | At Mercedes-Benz we use it as a developer portal with software catalog, TechDocs, Scaffolding and custom plugins. It provides an overview of our tech ecosystem to our product development teams. The portal also serves as a way to foster collaboration among the numerous companies of the Mercedes-Benz Group. | +| [Funding Circle](https://www.fundingcircle.com/) | [Ariel Pacciaroni](https://github.com/arielpacciaroni) | We are building the internal developer portal using Backstage project and centralizing all services information at one place. The portal helps us track down repositories ownership as well as direct access to key information on every component. | +| [Clarivate](https://www.clarivate.com) | [Gabriele Carteni](mailto:gabriele.carteni@clarivate.com) | We are building our Developer Portal using Backstage to have a better control over our software ecosystem, integrate SDLC tools and promote best practices | +| [Cho Tot](https://www.chotot.com) | [Chotot Team](mailto:sre@chotot.vn) | Internal developer portal, service catalog with CI/CD tools. | From 40de38154f267faa4f7f7a5c2eeee22166249de4 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 22 Jun 2022 11:45:45 +0200 Subject: [PATCH 089/401] chore: convert to patch Signed-off-by: Johan Haals --- .changeset/two-crews-accept.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/two-crews-accept.md b/.changeset/two-crews-accept.md index deacf1559d..9540f97cb5 100644 --- a/.changeset/two-crews-accept.md +++ b/.changeset/two-crews-accept.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-catalog-backend-module-gitlab': minor +'@backstage/plugin-catalog-backend-module-gitlab': patch --- Add the possibility in the `GitlabDiscoveryEntityProvider` to scan the whole project instead of concrete groups. For that, use a configuration like this one, where the group parameter is omitted (not mandatory anymore): From 32c03c6a69f0d0157af1b80508ded0a8280b85c8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 22 Jun 2022 12:38:37 +0200 Subject: [PATCH 090/401] docs: add node release section to versioning policy Signed-off-by: Patrik Oldsberg --- docs/overview/versioning-policy.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/overview/versioning-policy.md b/docs/overview/versioning-policy.md index 7430f3c200..282906c580 100644 --- a/docs/overview/versioning-policy.md +++ b/docs/overview/versioning-policy.md @@ -135,3 +135,21 @@ package export. accessed via `/beta` or `/alpha` imports. - `@alpha` - here be dragons. Not visible in the main package entry point, alpha exports must be accessed via `/alpha` imports. + +## Node.js Releases + +The Backstage project uses [Node.js](https://nodejs.org/) for both its development +tooling and backend runtime. In order for expectations to be clear we use the +following schedule for determining the [Node.js releases](https://nodejs.org/en/about/releases/) that we support: + +- At any given point in time we support exactly two adjacent even-numbered + releases of Node.js, for example v12 and v14. +- Three months before a Node.js release becomes _Active LTS_ we switch support + to that release and the previous one. This is halfway through the _Current LTS_ + cycle for that release and occurs at the end of July every year. + +When we say _Supporting_ a Node.js release, that means the following: + +- The CI pipeline in the main Backstage repo tests towards the supported releases, and we encourage any other Backstage related projects to do the same. +- New Backstage projects created with `@backstage/create-app` will have their `engines.node` version set accordingly. +- Dropping compatibility with unsupported releases is not considered a breaking change. This includes using new syntax or APIs, as well as bumping dependencies that drop support for these versions. From dabbb286f7e345f4c1d522a2cd40c1353a8a0952 Mon Sep 17 00:00:00 2001 From: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Date: Wed, 22 Jun 2022 06:58:23 -0500 Subject: [PATCH 091/401] Corrected typos Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> --- docs/features/techdocs/addons.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/techdocs/addons.md b/docs/features/techdocs/addons.md index 33d97521d3..a4e1de7d4f 100644 --- a/docs/features/techdocs/addons.md +++ b/docs/features/techdocs/addons.md @@ -52,7 +52,7 @@ Addons are rendered in the order in which they are registered. ## Installing and using Addons -To start using Addons you need to add the `@backstage/plugin-techdocs-module-addons-contrib` package you your app, you can do that by running this command form the root of your project: `yarn add --cwd packages/app @backstage/plugin-techdocs-module-addons-contrib` +To start using Addons you need to add the `@backstage/plugin-techdocs-module-addons-contrib` package to your app. You can do that by running this command from the root of your project: `yarn add --cwd packages/app @backstage/plugin-techdocs-module-addons-contrib` Addons can be installed and configured in much the same way as extensions for other Backstage plugins: by adding them underneath an extension registry From 34c97eb35ba76c2790f33b3c8616146781c42822 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Wed, 22 Jun 2022 13:39:39 +0200 Subject: [PATCH 092/401] switch to use .json instead of .send Signed-off-by: Emma Indal --- .../src/engines/ElasticSearchSearchEngine.ts | 11 +++++++---- plugins/search-backend-node/api-report.md | 6 ++++-- plugins/search-backend-node/src/errors.ts | 18 ++++++++++++++++-- plugins/search-backend/src/service/router.ts | 8 +------- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index 8f321a266c..1eb32c882d 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -362,13 +362,16 @@ export class ElasticSearchSearchEngine implements SearchEngine { nextPageCursor, previousPageCursor, }; - } catch (e) { - if (e.meta.body.error.type === 'index_not_found_exception') { - throw new MissingIndexError(`Missing index for ${queryIndices}`, e); + } catch (error) { + if (error.meta.body.error.type === 'index_not_found_exception') { + throw new MissingIndexError( + `Missing index for ${queryIndices}. This means there are no documents to search through.`, + error, + ); } this.logger.error( `Failed to query documents for indices ${queryIndices}`, - e, + error, ); return Promise.reject({ results: [] }); } diff --git a/plugins/search-backend-node/api-report.md b/plugins/search-backend-node/api-report.md index 63273d7514..ff9480c19d 100644 --- a/plugins/search-backend-node/api-report.md +++ b/plugins/search-backend-node/api-report.md @@ -6,7 +6,6 @@ /// import { Config } from '@backstage/config'; -import { CustomErrorBase } from '@backstage/errors'; import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; import { DocumentDecoratorFactory } from '@backstage/plugin-search-common'; import { DocumentTypeInfo } from '@backstage/plugin-search-common'; @@ -115,7 +114,10 @@ export class LunrSearchEngineIndexer extends BatchSearchEngineIndexer { } // @public -export class MissingIndexError extends CustomErrorBase {} +export class MissingIndexError extends Error { + constructor(message?: string, cause?: Error | unknown); + readonly cause?: Error | undefined; +} // @public export class NewlineDelimitedJsonCollatorFactory diff --git a/plugins/search-backend-node/src/errors.ts b/plugins/search-backend-node/src/errors.ts index c6128f3f2e..00b168d460 100644 --- a/plugins/search-backend-node/src/errors.ts +++ b/plugins/search-backend-node/src/errors.ts @@ -14,10 +14,24 @@ * limitations under the License. */ -import { CustomErrorBase } from '@backstage/errors'; +import { isError } from '@backstage/errors'; /** * Failed to query documents for index that does not exist. * @public */ -export class MissingIndexError extends CustomErrorBase {} +export class MissingIndexError extends Error { + /** + * An inner error that caused this error to be thrown, if any. + */ + readonly cause?: Error | undefined; + + constructor(message?: string, cause?: Error | unknown) { + super(message); + + Error.captureStackTrace?.(this, this.constructor); + + this.name = this.constructor.name; + this.cause = isError(cause) ? cause : undefined; + } +} diff --git a/plugins/search-backend/src/service/router.ts b/plugins/search-backend/src/service/router.ts index c85b3ed797..0fbacb018e 100644 --- a/plugins/search-backend/src/service/router.ts +++ b/plugins/search-backend/src/service/router.ts @@ -162,13 +162,7 @@ export async function createRouter( res.send(filterResultSet(toSearchResults(resultSet))); } catch (error) { if (error instanceof MissingIndexError) { - res - .status(400) - .send( - `\nNo index found for types: ${ - query.types ? query.types.join(',') : '' - }. This means there are no documents to search through.`, - ); + res.status(400).json(error.message); } throw new Error( From e150231eb962bbe8616580ff3c6e1ab76c2cdfb6 Mon Sep 17 00:00:00 2001 From: Marco Crivellaro Date: Tue, 21 Jun 2022 14:23:08 +0100 Subject: [PATCH 093/401] refactor: DRY on input and output properties Signed-off-by: Marco Crivellaro --- .../builtin/github/githubRepoCreate.ts | 131 ++----------- .../actions/builtin/github/githubRepoPush.ts | 85 ++------- .../actions/builtin/github/inputProperties.ts | 161 ++++++++++++++++ .../builtin/github/outputProperties.ts | 27 +++ .../actions/builtin/publish/github.ts | 175 +++--------------- 5 files changed, 248 insertions(+), 331 deletions(-) create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/inputProperties.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/outputProperties.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts index 494a47516c..a725602e07 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts @@ -22,6 +22,8 @@ import { Octokit } from 'octokit'; import { createTemplateAction } from '../../createTemplateAction'; import { parseRepoUrl } from '../publish/util'; import { getOctokitOptions } from './helpers'; +import * as inputProps from './inputProperties'; +import * as outputProps from './outputProperties'; /** * Creates a new action that initializes a git repository @@ -67,123 +69,26 @@ export function createGithubRepoCreateAction(options: { type: 'object', required: ['repoUrl'], properties: { - repoUrl: { - title: 'Repository Location', - description: `Accepts the format 'github.com?repo=reponame&owner=owner' where 'reponame' is the new repository name and 'owner' is an organization or username`, - type: 'string', - }, - description: { - title: 'Repository Description', - type: 'string', - }, - access: { - title: 'Repository Access', - description: `Sets an admin collaborator on the repository. Can either be a user reference different from 'owner' in 'repoUrl' or team reference, eg. 'org/team-name'`, - type: 'string', - }, - requireCodeOwnerReviews: { - title: 'Require CODEOWNER Reviews?', - description: - 'Require an approved review in PR including files with a designated Code Owner', - type: 'boolean', - }, - requiredStatusCheckContexts: { - title: 'Required Status Check Contexts', - description: - 'The list of status checks to require in order to merge into this branch', - type: 'array', - items: { - type: 'string', - }, - }, - repoVisibility: { - title: 'Repository Visibility', - type: 'string', - enum: ['private', 'public', 'internal'], - }, - deleteBranchOnMerge: { - title: 'Delete Branch On Merge', - type: 'boolean', - description: `Delete the branch after merging the PR. The default value is 'false'`, - }, - gitAuthorName: { - title: 'Default Author Name', - type: 'string', - description: `Sets the default author name for the commit. The default value is 'Scaffolder'`, - }, - gitAuthorEmail: { - title: 'Default Author Email', - type: 'string', - description: `Sets the default author email for the commit.`, - }, - allowMergeCommit: { - title: 'Allow Merge Commits', - type: 'boolean', - description: `Allow merge commits. The default value is 'true'`, - }, - allowSquashMerge: { - title: 'Allow Squash Merges', - type: 'boolean', - description: `Allow squash merges. The default value is 'true'`, - }, - allowRebaseMerge: { - title: 'Allow Rebase Merges', - type: 'boolean', - description: `Allow rebase merges. The default value is 'true'`, - }, - collaborators: { - title: 'Collaborators', - description: 'Provide additional users or teams with permissions', - type: 'array', - items: { - type: 'object', - additionalProperties: false, - required: ['access'], - properties: { - access: { - type: 'string', - description: 'The type of access for the user', - enum: ['push', 'pull', 'admin', 'maintain', 'triage'], - }, - user: { - type: 'string', - description: - 'The name of the user that will be added as a collaborator', - }, - team: { - type: 'string', - description: - 'The name of the team that will be added as a collaborator', - }, - }, - oneOf: [{ required: ['user'] }, { required: ['team'] }], - }, - }, - token: { - title: 'Authentication Token', - type: 'string', - description: 'The token to use for authorization to GitHub', - }, - topics: { - title: 'Topics', - type: 'array', - items: { - type: 'string', - }, - }, + repoUrl: inputProps.repoUrl, + description: inputProps.description, + access: inputProps.access, + requireCodeOwnerReviews: inputProps.requireCodeOwnerReviews, + requiredStatusCheckContexts: inputProps.requiredStatusCheckContexts, + repoVisibility: inputProps.repoVisibility, + deleteBranchOnMerge: inputProps.deleteBranchOnMerge, + allowMergeCommit: inputProps.allowMergeCommit, + allowSquashMerge: inputProps.allowSquashMerge, + allowRebaseMerge: inputProps.allowRebaseMerge, + collaborators: inputProps.collaborators, + token: inputProps.token, + topics: inputProps.topics, }, }, output: { type: 'object', properties: { - remoteUrl: { - title: 'A URL to the repository with the provider', - type: 'string', - }, - repoContentsUrl: { - title: 'A URL to the root of the repository', - type: 'string', - }, + remoteUrl: outputProps.remoteUrl, + repoContentsUrl: outputProps.repoContentsUrl, }, }, }, @@ -212,7 +117,7 @@ export function createGithubRepoCreateAction(options: { integrations, credentialsProvider: githubCredentialsProvider, token: providedToken, - repoUrl, + repoUrl: repoUrl, }); const client = new Octokit(octokitOptions); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts index cdf9a9b4e6..df9f5463fe 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts @@ -27,6 +27,8 @@ import { } from '../helpers'; import { getRepoSourceDirectory, parseRepoUrl } from '../publish/util'; import { getOctokitOptions } from './helpers'; +import * as inputProps from './inputProperties'; +import * as outputProps from './outputProperties'; /** * Creates a new action that initializes a git repository of the content in the workspace @@ -62,82 +64,23 @@ export function createGithubRepoPushAction(options: { type: 'object', required: ['repoUrl'], properties: { - repoUrl: { - title: 'Repository Location', - description: `Accepts the format 'github.com?repo=reponame&owner=owner' where 'reponame' is the new repository name and 'owner' is an organization or username`, - type: 'string', - }, - requireCodeOwnerReviews: { - title: 'Require CODEOWNER Reviews?', - description: - 'Require an approved review in PR including files with a designated Code Owner', - type: 'boolean', - }, - requiredStatusCheckContexts: { - title: 'Required Status Check Contexts', - description: - 'The list of status checks to require in order to merge into this branch', - type: 'array', - items: { - type: 'string', - }, - }, - defaultBranch: { - title: 'Default Branch', - type: 'string', - description: `Sets the default branch on the repository. The default value is 'master'`, - }, - protectDefaultBranch: { - title: 'Protect Default Branch', - type: 'boolean', - description: `Protect the default branch after creating the repository. The default value is 'true'`, - }, - gitCommitMessage: { - title: 'Git Commit Message', - type: 'string', - description: `Sets the commit message on the repository. The default value is 'initial commit'`, - }, - gitAuthorName: { - title: 'Default Author Name', - type: 'string', - description: `Sets the default author name for the commit. The default value is 'Scaffolder'`, - }, - gitAuthorEmail: { - title: 'Default Author Email', - type: 'string', - description: `Sets the default author email for the commit.`, - }, - sourcePath: { - title: 'Source Path', - description: - 'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository.', - type: 'string', - }, - token: { - title: 'Authentication Token', - type: 'string', - description: 'The token to use for authorization to GitHub', - }, - topics: { - title: 'Topics', - type: 'array', - items: { - type: 'string', - }, - }, + repoUrl: inputProps.repoUrl, + requireCodeOwnerReviews: inputProps.requireCodeOwnerReviews, + requiredStatusCheckContexts: inputProps.requiredStatusCheckContexts, + defaultBranch: inputProps.defaultBranch, + protectDefaultBranch: inputProps.protectDefaultBranch, + gitCommitMessage: inputProps.gitCommitMessage, + gitAuthorName: inputProps.gitAuthorName, + gitAuthorEmail: inputProps.gitAuthorEmail, + sourcePath: inputProps.sourcePath, + token: inputProps.token, }, }, output: { type: 'object', properties: { - remoteUrl: { - title: 'A URL to the repository with the provider', - type: 'string', - }, - repoContentsUrl: { - title: 'A URL to the root of the repository', - type: 'string', - }, + remoteUrl: outputProps.remoteUrl, + repoContentsUrl: outputProps.repoContentsUrl, }, }, }, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/inputProperties.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/inputProperties.ts new file mode 100644 index 0000000000..0aad402f1f --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/inputProperties.ts @@ -0,0 +1,161 @@ +/* + * 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. + */ + +const repoUrl = { + title: 'Repository Location', + description: `Accepts the format 'github.com?repo=reponame&owner=owner' where 'reponame' is the new repository name and 'owner' is an organization or username`, + type: 'string', +}; +const description = { + title: 'Repository Description', + type: 'string', +}; +const access = { + title: 'Repository Access', + description: `Sets an admin collaborator on the repository. Can either be a user reference different from 'owner' in 'repoUrl' or team reference, eg. 'org/team-name'`, + type: 'string', +}; +const requireCodeOwnerReviews = { + title: 'Require CODEOWNER Reviews?', + description: + 'Require an approved review in PR including files with a designated Code Owner', + type: 'boolean', +}; +const requiredStatusCheckContexts = { + title: 'Required Status Check Contexts', + description: + 'The list of status checks to require in order to merge into this branch', + type: 'array', + items: { + type: 'string', + }, +}; +const repoVisibility = { + title: 'Repository Visibility', + type: 'string', + enum: ['private', 'public', 'internal'], +}; +const deleteBranchOnMerge = { + title: 'Delete Branch On Merge', + type: 'boolean', + description: `Delete the branch after merging the PR. The default value is 'false'`, +}; +const gitAuthorName = { + title: 'Default Author Name', + type: 'string', + description: `Sets the default author name for the commit. The default value is 'Scaffolder'`, +}; +const gitAuthorEmail = { + title: 'Default Author Email', + type: 'string', + description: `Sets the default author email for the commit.`, +}; +const allowMergeCommit = { + title: 'Allow Merge Commits', + type: 'boolean', + description: `Allow merge commits. The default value is 'true'`, +}; +const allowSquashMerge = { + title: 'Allow Squash Merges', + type: 'boolean', + description: `Allow squash merges. The default value is 'true'`, +}; +const allowRebaseMerge = { + title: 'Allow Rebase Merges', + type: 'boolean', + description: `Allow rebase merges. The default value is 'true'`, +}; +const collaborators = { + title: 'Collaborators', + description: 'Provide additional users or teams with permissions', + type: 'array', + items: { + type: 'object', + additionalProperties: false, + required: ['access'], + properties: { + access: { + type: 'string', + description: 'The type of access for the user', + enum: ['push', 'pull', 'admin', 'maintain', 'triage'], + }, + user: { + type: 'string', + description: + 'The name of the user that will be added as a collaborator', + }, + team: { + type: 'string', + description: + 'The name of the team that will be added as a collaborator', + }, + }, + oneOf: [{ required: ['user'] }, { required: ['team'] }], + }, +}; +const token = { + title: 'Authentication Token', + type: 'string', + description: 'The token to use for authorization to GitHub', +}; +const topics = { + title: 'Topics', + type: 'array', + items: { + type: 'string', + }, +}; +const defaultBranch = { + title: 'Default Branch', + type: 'string', + description: `Sets the default branch on the repository. The default value is 'master'`, +}; +const protectDefaultBranch = { + title: 'Protect Default Branch', + type: 'boolean', + description: `Protect the default branch after creating the repository. The default value is 'true'`, +}; +const gitCommitMessage = { + title: 'Git Commit Message', + type: 'string', + description: `Sets the commit message on the repository. The default value is 'initial commit'`, +}; +const sourcePath = { + title: 'Source Path', + description: + 'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository.', + type: 'string', +}; + +export { access }; +export { allowMergeCommit }; +export { allowRebaseMerge }; +export { allowSquashMerge }; +export { collaborators }; +export { defaultBranch }; +export { deleteBranchOnMerge }; +export { description }; +export { gitAuthorEmail }; +export { gitAuthorName }; +export { gitCommitMessage }; +export { protectDefaultBranch }; +export { repoUrl }; +export { repoVisibility }; +export { requireCodeOwnerReviews }; +export { requiredStatusCheckContexts }; +export { sourcePath }; +export { token }; +export { topics }; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/outputProperties.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/outputProperties.ts new file mode 100644 index 0000000000..d5d6eb108f --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/outputProperties.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +const remoteUrl = { + title: 'A URL to the repository with the provider', + type: 'string', +}; +const repoContentsUrl = { + title: 'A URL to the root of the repository', + type: 'string', +}; + +export { remoteUrl }; +export { repoContentsUrl }; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index d9a7112fc9..145827702d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -13,20 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Config } from '@backstage/config'; +import { assertError, InputError } from '@backstage/errors'; import { GithubCredentialsProvider, ScmIntegrationRegistry, } from '@backstage/integration'; +import { Octokit } from 'octokit'; +import { createTemplateAction } from '../../createTemplateAction'; +import { getOctokitOptions } from '../github/helpers'; +import * as inputProps from '../github/inputProperties'; +import * as outputProps from '../github/outputProperties'; import { enableBranchProtectionOnDefaultRepoBranch, initRepoAndPush, } from '../helpers'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; -import { createTemplateAction } from '../../createTemplateAction'; -import { Config } from '@backstage/config'; -import { assertError, InputError } from '@backstage/errors'; -import { getOctokitOptions } from '../github/helpers'; -import { Octokit } from 'octokit'; /** * Creates a new action that initializes a git repository of the content in the workspace @@ -84,153 +86,32 @@ export function createPublishGithubAction(options: { type: 'object', required: ['repoUrl'], properties: { - repoUrl: { - title: 'Repository Location', - description: `Accepts the format 'github.com?repo=reponame&owner=owner' where 'reponame' is the new repository name and 'owner' is an organization or username`, - type: 'string', - }, - description: { - title: 'Repository Description', - type: 'string', - }, - access: { - title: 'Repository Access', - description: `Sets an admin collaborator on the repository. Can either be a user reference different from 'owner' in 'repoUrl' or team reference, eg. 'org/team-name'`, - type: 'string', - }, - requireCodeOwnerReviews: { - title: 'Require CODEOWNER Reviews?', - description: - 'Require an approved review in PR including files with a designated Code Owner', - type: 'boolean', - }, - requiredStatusCheckContexts: { - title: 'Required Status Check Contexts', - description: - 'The list of status checks to require in order to merge into this branch', - type: 'array', - items: { - type: 'string', - }, - }, - repoVisibility: { - title: 'Repository Visibility', - type: 'string', - enum: ['private', 'public', 'internal'], - }, - defaultBranch: { - title: 'Default Branch', - type: 'string', - description: `Sets the default branch on the repository. The default value is 'master'`, - }, - protectDefaultBranch: { - title: 'Protect Default Branch', - type: 'boolean', - description: `Protect the default branch after creating the repository. The default value is 'true'`, - }, - deleteBranchOnMerge: { - title: 'Delete Branch On Merge', - type: 'boolean', - description: `Delete the branch after merging the PR. The default value is 'false'`, - }, - gitCommitMessage: { - title: 'Git Commit Message', - type: 'string', - description: `Sets the commit message on the repository. The default value is 'initial commit'`, - }, - gitAuthorName: { - title: 'Default Author Name', - type: 'string', - description: `Sets the default author name for the commit. The default value is 'Scaffolder'`, - }, - gitAuthorEmail: { - title: 'Default Author Email', - type: 'string', - description: `Sets the default author email for the commit.`, - }, - allowMergeCommit: { - title: 'Allow Merge Commits', - type: 'boolean', - description: `Allow merge commits. The default value is 'true'`, - }, - allowSquashMerge: { - title: 'Allow Squash Merges', - type: 'boolean', - description: `Allow squash merges. The default value is 'true'`, - }, - allowRebaseMerge: { - title: 'Allow Rebase Merges', - type: 'boolean', - description: `Allow rebase merges. The default value is 'true'`, - }, - sourcePath: { - title: 'Source Path', - description: - 'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository.', - type: 'string', - }, - collaborators: { - title: 'Collaborators', - description: 'Provide additional users or teams with permissions', - type: 'array', - items: { - type: 'object', - additionalProperties: false, - required: ['access'], - properties: { - access: { - type: 'string', - description: 'The type of access for the user', - enum: ['push', 'pull', 'admin', 'maintain', 'triage'], - }, - user: { - type: 'string', - description: - 'The name of the user that will be added as a collaborator', - }, - username: { - type: 'string', - description: - 'Deprecated. Use the `team` or `user` field instead.', - }, - team: { - type: 'string', - description: - 'The name of the team that will be added as a collaborator', - }, - }, - oneOf: [ - { required: ['user'] }, - { required: ['username'] }, - { required: ['team'] }, - ], - }, - }, - token: { - title: 'Authentication Token', - type: 'string', - description: 'The token to use for authorization to GitHub', - }, - topics: { - title: 'Topics', - type: 'array', - items: { - type: 'string', - }, - }, + repoUrl: inputProps.repoUrl, + description: inputProps.description, + access: inputProps.access, + requireCodeOwnerReviews: inputProps.requireCodeOwnerReviews, + requiredStatusCheckContexts: inputProps.requiredStatusCheckContexts, + repoVisibility: inputProps.repoVisibility, + defaultBranch: inputProps.defaultBranch, + protectDefaultBranch: inputProps.protectDefaultBranch, + deleteBranchOnMerge: inputProps.deleteBranchOnMerge, + gitCommitMessage: inputProps.gitCommitMessage, + gitAuthorName: inputProps.gitAuthorName, + gitAuthorEmail: inputProps.gitAuthorEmail, + allowMergeCommit: inputProps.allowMergeCommit, + allowSquashMerge: inputProps.allowSquashMerge, + allowRebaseMerge: inputProps.allowRebaseMerge, + sourcePath: inputProps.sourcePath, + collaborators: inputProps.collaborators, + token: inputProps.token, + topics: inputProps.topics, }, }, output: { type: 'object', properties: { - remoteUrl: { - title: 'A URL to the repository with the provider', - type: 'string', - }, - repoContentsUrl: { - title: 'A URL to the root of the repository', - type: 'string', - }, + remoteUrl: outputProps.remoteUrl, + repoContentsUrl: outputProps.repoContentsUrl, }, }, }, From 4271cc7ecced991ce8e5498330e414325a239b64 Mon Sep 17 00:00:00 2001 From: Marco Crivellaro Date: Wed, 22 Jun 2022 11:02:42 +0100 Subject: [PATCH 094/401] refactor repo create, repo push Signed-off-by: Marco Crivellaro --- .../builtin/github/githubRepoCreate.ts | 149 ++-------- .../actions/builtin/github/githubRepoPush.ts | 66 ++--- .../actions/builtin/github/helpers.ts | 221 ++++++++++++++- .../src/scaffolder/actions/builtin/helpers.ts | 18 +- .../actions/builtin/publish/github.ts | 266 ++++++------------ 5 files changed, 369 insertions(+), 351 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts index a725602e07..1e92d91a64 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { assertError, InputError } from '@backstage/errors'; +import { InputError } from '@backstage/errors'; import { GithubCredentialsProvider, ScmIntegrationRegistry, @@ -21,7 +21,10 @@ import { import { Octokit } from 'octokit'; import { createTemplateAction } from '../../createTemplateAction'; import { parseRepoUrl } from '../publish/util'; -import { getOctokitOptions } from './helpers'; +import { + createGithubRepoWithCollaboratorsAndTopics, + getOctokitOptions, +} from './helpers'; import * as inputProps from './inputProperties'; import * as outputProps from './outputProperties'; @@ -58,6 +61,11 @@ export function createGithubRepoCreateAction(options: { team: string; access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; } + | { + /** @deprecated This field is deprecated in favor of team */ + username: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + } >; token?: string; topics?: string[]; @@ -107,136 +115,37 @@ export function createGithubRepoCreateAction(options: { token: providedToken, } = ctx.input; - const { owner, repo } = parseRepoUrl(repoUrl, integrations); - - if (!owner) { - throw new InputError('Invalid repository owner provided in repoUrl'); - } - const octokitOptions = await getOctokitOptions({ integrations, credentialsProvider: githubCredentialsProvider, token: providedToken, repoUrl: repoUrl, }); - const client = new Octokit(octokitOptions); - const user = await client.rest.users.getByUsername({ - username: owner, - }); + const { owner, repo } = parseRepoUrl(repoUrl, integrations); - const repoCreationPromise = - user.data.type === 'Organization' - ? client.rest.repos.createInOrg({ - name: repo, - org: owner, - private: repoVisibility === 'private', - visibility: repoVisibility, - description: description, - delete_branch_on_merge: deleteBranchOnMerge, - allow_merge_commit: allowMergeCommit, - allow_squash_merge: allowSquashMerge, - allow_rebase_merge: allowRebaseMerge, - }) - : client.rest.repos.createForAuthenticatedUser({ - name: repo, - private: repoVisibility === 'private', - description: description, - delete_branch_on_merge: deleteBranchOnMerge, - allow_merge_commit: allowMergeCommit, - allow_squash_merge: allowSquashMerge, - allow_rebase_merge: allowRebaseMerge, - }); - - let newRepo; - - try { - newRepo = (await repoCreationPromise).data; - } catch (e) { - assertError(e); - if (e.message === 'Resource not accessible by integration') { - ctx.logger.warn( - `The GitHub app or token provided may not have the required permissions to create the ${user.data.type} repository ${owner}/${repo}.`, - ); - } - throw new Error( - `Failed to create the ${user.data.type} repository ${owner}/${repo}, ${e.message}`, - ); + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); } - if (access?.startsWith(`${owner}/`)) { - const [, team] = access.split('/'); - await client.rest.teams.addOrUpdateRepoPermissionsInOrg({ - org: owner, - team_slug: team, - owner, - repo, - permission: 'admin', - }); - // No need to add access if it's the person who owns the personal account - } else if (access && access !== owner) { - await client.rest.repos.addCollaborator({ - owner, - repo, - username: access, - permission: 'admin', - }); - } + const newRepo = await createGithubRepoWithCollaboratorsAndTopics( + client, + repo, + owner, + repoVisibility, + description, + deleteBranchOnMerge, + allowMergeCommit, + allowSquashMerge, + allowRebaseMerge, + access, + collaborators, + topics, + ctx.logger, + ); - if (collaborators) { - for (const collaborator of collaborators) { - try { - if ('user' in collaborator) { - await client.rest.repos.addCollaborator({ - owner, - repo, - username: collaborator.user, - permission: collaborator.access, - }); - } else if ('team' in collaborator) { - await client.rest.teams.addOrUpdateRepoPermissionsInOrg({ - org: owner, - team_slug: collaborator.team, - owner, - repo, - permission: collaborator.access, - }); - } - } catch (e) { - assertError(e); - const name = extractCollaboratorName(collaborator); - ctx.logger.warn( - `Skipping ${collaborator.access} access for ${name}, ${e.message}`, - ); - } - } - } - - if (topics) { - try { - await client.rest.repos.replaceAllTopics({ - owner, - repo, - names: topics.map(t => t.toLowerCase()), - }); - } catch (e) { - assertError(e); - ctx.logger.warn(`Skipping topics ${topics.join(' ')}, ${e.message}`); - } - } - - const remoteUrl = newRepo.clone_url; - - ctx.output('remoteUrl', remoteUrl); + ctx.output('remoteUrl', newRepo.clone_url); }, }); } - -function extractCollaboratorName( - collaborator: { user: string } | { team: string } | { username: string }, -) { - if ('username' in collaborator) return collaborator.username; - if ('user' in collaborator) return collaborator.user; - return collaborator.team; -} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts index df9f5463fe..942ebe578d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts @@ -14,19 +14,15 @@ * limitations under the License. */ import { Config } from '@backstage/config'; -import { assertError, InputError } from '@backstage/errors'; +import { InputError } from '@backstage/errors'; import { GithubCredentialsProvider, ScmIntegrationRegistry, } from '@backstage/integration'; import { Octokit } from 'octokit'; import { createTemplateAction } from '../../createTemplateAction'; -import { - enableBranchProtectionOnDefaultRepoBranch, - initRepoAndPush, -} from '../helpers'; -import { getRepoSourceDirectory, parseRepoUrl } from '../publish/util'; -import { getOctokitOptions } from './helpers'; +import { parseRepoUrl } from '../publish/util'; +import { getOctokitOptions, initRepoPushAndProtect } from './helpers'; import * as inputProps from './inputProperties'; import * as outputProps from './outputProperties'; @@ -117,48 +113,24 @@ export function createGithubRepoPushAction(options: { const remoteUrl = targetRepo.data.clone_url; const repoContentsUrl = `${targetRepo.data.html_url}/blob/${defaultBranch}`; - const gitAuthorInfo = { - name: gitAuthorName - ? gitAuthorName - : config.getOptionalString('scaffolder.defaultAuthor.name'), - email: gitAuthorEmail - ? gitAuthorEmail - : config.getOptionalString('scaffolder.defaultAuthor.email'), - }; - - await initRepoAndPush({ - dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath), + await initRepoPushAndProtect( remoteUrl, + octokitOptions.auth, + ctx.workspacePath, + ctx.input.sourcePath, defaultBranch, - auth: { - username: 'x-access-token', - password: octokitOptions.auth, - }, - logger: ctx.logger, - commitMessage: gitCommitMessage - ? gitCommitMessage - : config.getOptionalString('scaffolder.defaultCommitMessage'), - gitAuthorInfo, - }); - - if (protectDefaultBranch) { - try { - await enableBranchProtectionOnDefaultRepoBranch({ - owner, - client, - repoName: repo, - logger: ctx.logger, - defaultBranch, - requireCodeOwnerReviews, - requiredStatusCheckContexts, - }); - } catch (e) { - assertError(e); - ctx.logger.warn( - `Skipping: default branch protection on '${repo}', ${e.message}`, - ); - } - } + protectDefaultBranch, + owner, + client, + repo, + requireCodeOwnerReviews, + requiredStatusCheckContexts, + config, + ctx.logger, + gitCommitMessage, + gitAuthorName, + gitAuthorEmail, + ); ctx.output('remoteUrl', remoteUrl); ctx.output('repoContentsUrl', repoContentsUrl); 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 29997e5843..c8f508e4c0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts @@ -13,14 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { InputError } from '@backstage/errors'; +import { Config } from '@backstage/config'; +import { assertError, InputError } from '@backstage/errors'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrationRegistry, } from '@backstage/integration'; import { OctokitOptions } from '@octokit/core/dist-types/types'; -import { parseRepoUrl } from '../publish/util'; +import { Octokit } from 'octokit'; +import { Logger } from 'winston'; +import { + enableBranchProtectionOnDefaultRepoBranch, + initRepoAndPush, +} from '../helpers'; +import { getRepoSourceDirectory, parseRepoUrl } from '../publish/util'; const DEFAULT_TIMEOUT_MS = 60_000; @@ -83,3 +90,213 @@ export async function getOctokitOptions(options: { previews: ['nebula-preview'], }; } + +export async function createGithubRepoWithCollaboratorsAndTopics( + client: Octokit, + repo: string, + owner: string, + repoVisibility: 'private' | 'internal' | 'public', + description: string | undefined, + deleteBranchOnMerge: boolean, + allowMergeCommit: boolean, + allowSquashMerge: boolean, + allowRebaseMerge: boolean, + access: string | undefined, + collaborators: + | ( + | { + user: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + } + | { + team: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + } + | { + /** @deprecated This field is deprecated in favor of team */ + username: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + } + )[] + | undefined, + topics: string[] | undefined, + logger: Logger, +) { + const user = await client.rest.users.getByUsername({ + username: owner, + }); + + const repoCreationPromise = + user.data.type === 'Organization' + ? client.rest.repos.createInOrg({ + name: repo, + org: owner, + private: repoVisibility === 'private', + visibility: repoVisibility, + description: description, + delete_branch_on_merge: deleteBranchOnMerge, + allow_merge_commit: allowMergeCommit, + allow_squash_merge: allowSquashMerge, + allow_rebase_merge: allowRebaseMerge, + }) + : client.rest.repos.createForAuthenticatedUser({ + name: repo, + private: repoVisibility === 'private', + description: description, + delete_branch_on_merge: deleteBranchOnMerge, + allow_merge_commit: allowMergeCommit, + allow_squash_merge: allowSquashMerge, + allow_rebase_merge: allowRebaseMerge, + }); + + let newRepo; + + try { + newRepo = (await repoCreationPromise).data; + } catch (e) { + assertError(e); + if (e.message === 'Resource not accessible by integration') { + logger.warn( + `The GitHub app or token provided may not have the required permissions to create the ${user.data.type} repository ${owner}/${repo}.`, + ); + } + throw new Error( + `Failed to create the ${user.data.type} repository ${owner}/${repo}, ${e.message}`, + ); + } + + if (access?.startsWith(`${owner}/`)) { + const [, team] = access.split('/'); + await client.rest.teams.addOrUpdateRepoPermissionsInOrg({ + org: owner, + team_slug: team, + owner, + repo, + permission: 'admin', + }); + // No need to add access if it's the person who owns the personal account + } else if (access && access !== owner) { + await client.rest.repos.addCollaborator({ + owner, + repo, + username: access, + permission: 'admin', + }); + } + + if (collaborators) { + for (const collaborator of collaborators) { + try { + if ('user' in collaborator) { + await client.rest.repos.addCollaborator({ + owner, + repo, + username: collaborator.user, + permission: collaborator.access, + }); + } else if ('team' in collaborator) { + await client.rest.teams.addOrUpdateRepoPermissionsInOrg({ + org: owner, + team_slug: collaborator.team, + owner, + repo, + permission: collaborator.access, + }); + } + } catch (e) { + assertError(e); + const name = extractCollaboratorName(collaborator); + logger.warn( + `Skipping ${collaborator.access} access for ${name}, ${e.message}`, + ); + } + } + } + + if (topics) { + try { + await client.rest.repos.replaceAllTopics({ + owner, + repo, + names: topics.map(t => t.toLowerCase()), + }); + } catch (e) { + assertError(e); + logger.warn(`Skipping topics ${topics.join(' ')}, ${e.message}`); + } + } + + return newRepo; +} + +export async function initRepoPushAndProtect( + remoteUrl: string, + password: string, + workspacePath: string, + sourcePath: string | undefined, + defaultBranch: string, + protectDefaultBranch: boolean, + owner: string, + client: Octokit, + repo: string, + requireCodeOwnerReviews: boolean, + requiredStatusCheckContexts: string[], + config: Config, + logger: any, + gitCommitMessage?: string, + gitAuthorName?: string, + gitAuthorEmail?: string, +) { + const gitAuthorInfo = { + name: gitAuthorName + ? gitAuthorName + : config.getOptionalString('scaffolder.defaultAuthor.name'), + email: gitAuthorEmail + ? gitAuthorEmail + : config.getOptionalString('scaffolder.defaultAuthor.email'), + }; + + const commitMessage = gitCommitMessage + ? gitCommitMessage + : config.getOptionalString('scaffolder.defaultCommitMessage'); + + await initRepoAndPush({ + dir: getRepoSourceDirectory(workspacePath, sourcePath), + remoteUrl, + defaultBranch, + auth: { + username: 'x-access-token', + password, + }, + logger, + commitMessage, + gitAuthorInfo, + }); + + if (protectDefaultBranch) { + try { + await enableBranchProtectionOnDefaultRepoBranch({ + owner, + client, + repoName: repo, + logger, + defaultBranch, + requireCodeOwnerReviews, + requiredStatusCheckContexts, + }); + } catch (e) { + assertError(e); + logger.warn( + `Skipping: default branch protection on '${repo}', ${e.message}`, + ); + } + } +} + +function extractCollaboratorName( + collaborator: { user: string } | { team: string } | { username: string }, +) { + if ('username' in collaborator) return collaborator.username; + if ('user' in collaborator) return collaborator.user; + return collaborator.team; +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts index 529a504cba..8753d44041 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts @@ -14,12 +14,13 @@ * limitations under the License. */ -import { SpawnOptionsWithoutStdio, spawn } from 'child_process'; +import { Git } from '@backstage/backend-common'; +import { Config } from '@backstage/config'; +import { assertError } from '@backstage/errors'; +import { spawn, SpawnOptionsWithoutStdio } from 'child_process'; +import { Octokit } from 'octokit'; import { PassThrough, Writable } from 'stream'; import { Logger } from 'winston'; -import { Git } from '@backstage/backend-common'; -import { Octokit } from 'octokit'; -import { assertError } from '@backstage/errors'; /** @public */ export type RunCommandOptions = { @@ -200,3 +201,12 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({ await tryOnce(); } }; + +export function getGitCommitMessage( + gitCommitMessage: string | undefined, + config: Config, +): string | undefined { + return gitCommitMessage + ? gitCommitMessage + : config.getOptionalString('scaffolder.defaultCommitMessage'); +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index 145827702d..87219ac1e6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -14,22 +14,21 @@ * limitations under the License. */ import { Config } from '@backstage/config'; -import { assertError, InputError } from '@backstage/errors'; +import { InputError } from '@backstage/errors'; import { GithubCredentialsProvider, ScmIntegrationRegistry, } from '@backstage/integration'; import { Octokit } from 'octokit'; import { createTemplateAction } from '../../createTemplateAction'; -import { getOctokitOptions } from '../github/helpers'; +import { + createGithubRepoWithCollaboratorsAndTopics, + getOctokitOptions, + initRepoPushAndProtect, +} from '../github/helpers'; import * as inputProps from '../github/inputProperties'; import * as outputProps from '../github/outputProperties'; -import { - enableBranchProtectionOnDefaultRepoBranch, - initRepoAndPush, -} from '../helpers'; -import { getRepoSourceDirectory, parseRepoUrl } from './util'; - +import { parseRepoUrl } from './util'; /** * Creates a new action that initializes a git repository of the content in the workspace * and publishes it to GitHub. @@ -137,192 +136,103 @@ export function createPublishGithubAction(options: { token: providedToken, } = ctx.input; + const octokitOptions = await getOctokitOptions({ + integrations, + credentialsProvider: githubCredentialsProvider, + token: providedToken, + repoUrl: repoUrl, + }); + const client = new Octokit(octokitOptions); + const { owner, repo } = parseRepoUrl(repoUrl, integrations); if (!owner) { throw new InputError('Invalid repository owner provided in repoUrl'); } - const octokitOptions = await getOctokitOptions({ - integrations, - credentialsProvider: githubCredentialsProvider, - token: providedToken, - repoUrl, - }); - - const client = new Octokit(octokitOptions); - - const user = await client.rest.users.getByUsername({ - username: owner, - }); - - const repoCreationPromise = - user.data.type === 'Organization' - ? client.rest.repos.createInOrg({ - name: repo, - org: owner, - private: repoVisibility === 'private', - visibility: repoVisibility, - description: description, - delete_branch_on_merge: deleteBranchOnMerge, - allow_merge_commit: allowMergeCommit, - allow_squash_merge: allowSquashMerge, - allow_rebase_merge: allowRebaseMerge, - }) - : client.rest.repos.createForAuthenticatedUser({ - name: repo, - private: repoVisibility === 'private', - description: description, - delete_branch_on_merge: deleteBranchOnMerge, - allow_merge_commit: allowMergeCommit, - allow_squash_merge: allowSquashMerge, - allow_rebase_merge: allowRebaseMerge, - }); - - let newRepo; - - try { - newRepo = (await repoCreationPromise).data; - } catch (e) { - assertError(e); - if (e.message === 'Resource not accessible by integration') { - ctx.logger.warn( - `The GitHub app or token provided may not have the required permissions to create the ${user.data.type} repository ${owner}/${repo}.`, - ); - } - throw new Error( - `Failed to create the ${user.data.type} repository ${owner}/${repo}, ${e.message}`, - ); - } - - if (access?.startsWith(`${owner}/`)) { - const [, team] = access.split('/'); - await client.rest.teams.addOrUpdateRepoPermissionsInOrg({ - org: owner, - team_slug: team, - owner, - repo, - permission: 'admin', - }); - // No need to add access if it's the person who owns the personal account - } else if (access && access !== owner) { - await client.rest.repos.addCollaborator({ - owner, - repo, - username: access, - permission: 'admin', - }); - } - - if (collaborators) { - for (const collaborator of collaborators) { - try { - if ('user' in collaborator) { - await client.rest.repos.addCollaborator({ - owner, - repo, - username: collaborator.user, - permission: collaborator.access, - }); - } else if ('username' in collaborator) { - ctx.logger.warn( - 'The field `username` is deprecated in favor of `team` and will be removed in the future.', - ); - await client.rest.teams.addOrUpdateRepoPermissionsInOrg({ - org: owner, - team_slug: collaborator.username, - owner, - repo, - permission: collaborator.access, - }); - } else if ('team' in collaborator) { - await client.rest.teams.addOrUpdateRepoPermissionsInOrg({ - org: owner, - team_slug: collaborator.team, - owner, - repo, - permission: collaborator.access, - }); - } - } catch (e) { - assertError(e); - const name = extractCollaboratorName(collaborator); - ctx.logger.warn( - `Skipping ${collaborator.access} access for ${name}, ${e.message}`, - ); - } - } - } - - if (topics) { - try { - await client.rest.repos.replaceAllTopics({ - owner, - repo, - names: topics.map(t => t.toLowerCase()), - }); - } catch (e) { - assertError(e); - ctx.logger.warn(`Skipping topics ${topics.join(' ')}, ${e.message}`); - } - } + const newRepo = await createGithubRepoWithCollaboratorsAndTopics( + client, + repo, + owner, + repoVisibility, + description, + deleteBranchOnMerge, + allowMergeCommit, + allowSquashMerge, + allowRebaseMerge, + access, + collaborators, + topics, + ctx.logger, + ); const remoteUrl = newRepo.clone_url; const repoContentsUrl = `${newRepo.html_url}/blob/${defaultBranch}`; - const gitAuthorInfo = { - name: gitAuthorName - ? gitAuthorName - : config.getOptionalString('scaffolder.defaultAuthor.name'), - email: gitAuthorEmail - ? gitAuthorEmail - : config.getOptionalString('scaffolder.defaultAuthor.email'), - }; + // const gitAuthorInfo = { + // name: gitAuthorName + // ? gitAuthorName + // : config.getOptionalString('scaffolder.defaultAuthor.name'), + // email: gitAuthorEmail + // ? gitAuthorEmail + // : config.getOptionalString('scaffolder.defaultAuthor.email'), + // }; - await initRepoAndPush({ - dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath), + // await initRepoAndPush({ + // dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath), + // remoteUrl, + // defaultBranch, + // auth: { + // username: 'x-access-token', + // password: octokitOptions.auth, + // }, + // logger: ctx.logger, + // commitMessage: gitCommitMessage + // ? gitCommitMessage + // : config.getOptionalString('scaffolder.defaultCommitMessage'), + // gitAuthorInfo, + // }); + + // if (protectDefaultBranch) { + // try { + // await enableBranchProtectionOnDefaultRepoBranch({ + // owner, + // client, + // repoName: newRepo.name, + // logger: ctx.logger, + // defaultBranch, + // requireCodeOwnerReviews, + // requiredStatusCheckContexts, + // }); + // } catch (e) { + // assertError(e); + // ctx.logger.warn( + // `Skipping: default branch protection on '${newRepo.name}', ${e.message}`, + // ); + // } + // } + + await initRepoPushAndProtect( remoteUrl, + octokitOptions.auth, + ctx.workspacePath, + ctx.input.sourcePath, defaultBranch, - auth: { - username: 'x-access-token', - password: octokitOptions.auth, - }, - logger: ctx.logger, - commitMessage: gitCommitMessage - ? gitCommitMessage - : config.getOptionalString('scaffolder.defaultCommitMessage'), - gitAuthorInfo, - }); - - if (protectDefaultBranch) { - try { - await enableBranchProtectionOnDefaultRepoBranch({ - owner, - client, - repoName: newRepo.name, - logger: ctx.logger, - defaultBranch, - requireCodeOwnerReviews, - requiredStatusCheckContexts, - }); - } catch (e) { - assertError(e); - ctx.logger.warn( - `Skipping: default branch protection on '${newRepo.name}', ${e.message}`, - ); - } - } + protectDefaultBranch, + owner, + client, + repo, + requireCodeOwnerReviews, + requiredStatusCheckContexts, + config, + ctx.logger, + gitCommitMessage, + gitAuthorName, + gitAuthorEmail, + ); ctx.output('remoteUrl', remoteUrl); ctx.output('repoContentsUrl', repoContentsUrl); }, }); } - -function extractCollaboratorName( - collaborator: { user: string } | { team: string } | { username: string }, -) { - if ('username' in collaborator) return collaborator.username; - if ('user' in collaborator) return collaborator.user; - return collaborator.team; -} From 4345479092e7609c32787f30d6c6530ed30c0e1b Mon Sep 17 00:00:00 2001 From: Marco Crivellaro Date: Wed, 22 Jun 2022 13:43:44 +0100 Subject: [PATCH 095/401] chore: fix tests Signed-off-by: Marco Crivellaro --- .../actions/builtin/publish/github.test.ts | 28 ++++++------ .../actions/builtin/publish/github.ts | 43 ------------------- 2 files changed, 14 insertions(+), 57 deletions(-) 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 ff20d1014b..036c5d66ee 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 @@ -18,20 +18,20 @@ import { TemplateAction } from '../../types'; jest.mock('../helpers'); -import { createPublishGithubAction } from './github'; +import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; import { - ScmIntegrations, DefaultGithubCredentialsProvider, GithubCredentialsProvider, + ScmIntegrations, } from '@backstage/integration'; -import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; +import { when } from 'jest-when'; import { PassThrough } from 'stream'; import { enableBranchProtectionOnDefaultRepoBranch, initRepoAndPush, } from '../helpers'; -import { when } from 'jest-when'; +import { createPublishGithubAction } from './github'; const mockOctokit = { rest: { @@ -609,7 +609,7 @@ describe('publish:github', () => { mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { - name: 'repository', + name: 'repo', }, }); @@ -618,7 +618,7 @@ describe('publish:github', () => { expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ owner: 'owner', client: mockOctokit, - repoName: 'repository', + repoName: 'repo', logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, @@ -636,7 +636,7 @@ describe('publish:github', () => { expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ owner: 'owner', client: mockOctokit, - repoName: 'repository', + repoName: 'repo', logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: true, @@ -654,7 +654,7 @@ describe('publish:github', () => { expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ owner: 'owner', client: mockOctokit, - repoName: 'repository', + repoName: 'repo', logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, @@ -669,7 +669,7 @@ describe('publish:github', () => { mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { - name: 'repository', + name: 'repo', }, }); @@ -678,7 +678,7 @@ describe('publish:github', () => { expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ owner: 'owner', client: mockOctokit, - repoName: 'repository', + repoName: 'repo', logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, @@ -696,7 +696,7 @@ describe('publish:github', () => { expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ owner: 'owner', client: mockOctokit, - repoName: 'repository', + repoName: 'repo', logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, @@ -714,7 +714,7 @@ describe('publish:github', () => { expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ owner: 'owner', client: mockOctokit, - repoName: 'repository', + repoName: 'repo', logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, @@ -729,7 +729,7 @@ describe('publish:github', () => { mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { - name: 'repository', + name: 'repo', }, }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index 87219ac1e6..0815a2cd00 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -169,49 +169,6 @@ export function createPublishGithubAction(options: { const remoteUrl = newRepo.clone_url; const repoContentsUrl = `${newRepo.html_url}/blob/${defaultBranch}`; - // const gitAuthorInfo = { - // name: gitAuthorName - // ? gitAuthorName - // : config.getOptionalString('scaffolder.defaultAuthor.name'), - // email: gitAuthorEmail - // ? gitAuthorEmail - // : config.getOptionalString('scaffolder.defaultAuthor.email'), - // }; - - // await initRepoAndPush({ - // dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath), - // remoteUrl, - // defaultBranch, - // auth: { - // username: 'x-access-token', - // password: octokitOptions.auth, - // }, - // logger: ctx.logger, - // commitMessage: gitCommitMessage - // ? gitCommitMessage - // : config.getOptionalString('scaffolder.defaultCommitMessage'), - // gitAuthorInfo, - // }); - - // if (protectDefaultBranch) { - // try { - // await enableBranchProtectionOnDefaultRepoBranch({ - // owner, - // client, - // repoName: newRepo.name, - // logger: ctx.logger, - // defaultBranch, - // requireCodeOwnerReviews, - // requiredStatusCheckContexts, - // }); - // } catch (e) { - // assertError(e); - // ctx.logger.warn( - // `Skipping: default branch protection on '${newRepo.name}', ${e.message}`, - // ); - // } - // } - await initRepoPushAndProtect( remoteUrl, octokitOptions.auth, From f74361a5850c3d20923553221172ad947dc68352 Mon Sep 17 00:00:00 2001 From: "Daniele.Mazzotta" Date: Wed, 22 Jun 2022 17:29:58 +0400 Subject: [PATCH 096/401] removed dagsNotFound state Signed-off-by: Daniele.Mazzotta --- .../DagTableComponent/DagTableComponent.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx b/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx index ff7b961c48..46127e0b5e 100644 --- a/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx +++ b/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx @@ -31,7 +31,7 @@ import Tooltip from '@material-ui/core/Tooltip'; import Typography from '@material-ui/core/Typography'; import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser'; import Alert from '@material-ui/lab/Alert'; -import React, { useState } from 'react'; +import React from 'react'; import useAsync from 'react-use/lib/useAsync'; import { apacheAirflowApiRef } from '../../api'; import { Dag } from '../../api/types'; @@ -136,15 +136,10 @@ type DagTableComponentProps = { export const DagTableComponent = ({ dagIds }: DagTableComponentProps) => { const apiClient = useApi(apacheAirflowApiRef); - const [dagsNotFound, setDagsNotFound] = useState(); const { value, loading, error } = useAsync(async (): Promise => { if (dagIds) { - // eslint-disable-next-line @typescript-eslint/no-shadow - const { dags, dagsNotFound } = await apiClient.getDags(dagIds); - if (dagsNotFound.length) { - setDagsNotFound(dagsNotFound); - } + const { dags } = await apiClient.getDags(dagIds); return dags; } return await apiClient.listDags(); @@ -161,13 +156,16 @@ export const DagTableComponent = ({ dagIds }: DagTableComponentProps) => { id: el.dag_id, // table records require `id` attribute dagUrl: `${apiClient.baseUrl}dag_details?dag_id=${el.dag_id}`, // construct path to DAG using `baseUrl` })); - + const dagsNotFound = + dagIds && value + ? dagIds.filter(id => !value.find(d => d.dag_id === id)) + : []; return ( <> - {dagsNotFound && ( + {dagsNotFound.length && ( {dagsNotFound.map(dagId => ( - {dagId} + {dagId} ))} )} From b15904a3675ff1726a504a76e6f585eb0fe9becf Mon Sep 17 00:00:00 2001 From: Marco Crivellaro Date: Wed, 22 Jun 2022 14:44:03 +0100 Subject: [PATCH 097/401] chore: api-report update Signed-off-by: Marco Crivellaro --- plugins/scaffolder-backend/api-report.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 029000399b..a4693125d9 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -195,6 +195,10 @@ export function createGithubRepoCreateAction(options: { team: string; access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; } + | { + username: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + } )[] | undefined; token?: string | undefined; From fbfbff6bf7ce0bf169c7a03e2f9b8d8bedcc40b3 Mon Sep 17 00:00:00 2001 From: Tomasz Szuba Date: Wed, 22 Jun 2022 16:25:49 +0200 Subject: [PATCH 098/401] Add possibility to resolve relations by RDN, in addition to UUID and DN in LDAP plugin This is required to support memberUid attribute https://ldapwiki.com/wiki/MemberUid Signed-off-by: Tomasz Szuba --- .changeset/twelve-candles-jump.md | 5 + .../src/ldap/read.test.ts | 302 +++++++----------- .../src/ldap/read.ts | 2 + 3 files changed, 119 insertions(+), 190 deletions(-) create mode 100644 .changeset/twelve-candles-jump.md diff --git a/.changeset/twelve-candles-jump.md b/.changeset/twelve-candles-jump.md new file mode 100644 index 0000000000..9d0cccd0ba --- /dev/null +++ b/.changeset/twelve-candles-jump.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-ldap': patch +--- + +Add possibility to resolve relations by RDN, in addition to UUID and DN diff --git a/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts b/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts index 410ac847b3..49803b9c39 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts @@ -345,206 +345,128 @@ describe('readLdapGroups', () => { describe('resolveRelations', () => { describe('lookup', () => { - it('matches by DN', () => { - const parent = group({ - metadata: { - name: 'parent', - annotations: { [LDAP_DN_ANNOTATION]: 'pa' }, - }, - }); - const child = group({ - metadata: { - name: 'child', - annotations: { [LDAP_DN_ANNOTATION]: 'ca' }, - }, - }); - const groupMember = new Map>([ - ['pa', new Set(['ca'])], - ]); - resolveRelations([parent, child], [], new Map(), new Map(), groupMember); - expect(parent.spec.children).toEqual(['group:default/child']); - expect(child.spec.parent).toEqual('group:default/parent'); - }); - - it('matches by UUID', () => { - const parent = group({ - metadata: { - name: 'parent', - annotations: { [LDAP_UUID_ANNOTATION]: 'pa' }, - }, - }); - const child = group({ - metadata: { - name: 'child', - annotations: { [LDAP_UUID_ANNOTATION]: 'ca' }, - }, - }); - const groupMember = new Map>([ - ['pa', new Set(['ca'])], - ]); - resolveRelations([parent, child], [], new Map(), new Map(), groupMember); - expect(parent.spec.children).toEqual(['group:default/child']); - expect(child.spec.parent).toEqual('group:default/parent'); - }); + it.each([LDAP_DN_ANNOTATION, LDAP_RDN_ANNOTATION, LDAP_UUID_ANNOTATION])( + 'matches by %s', + annotation => { + const parent = group({ + metadata: { + name: 'parent', + annotations: { [annotation]: 'pa' }, + }, + }); + const child = group({ + metadata: { + name: 'child', + annotations: { [annotation]: 'ca' }, + }, + }); + const groupMember = new Map>([ + ['pa', new Set(['ca'])], + ]); + resolveRelations( + [parent, child], + [], + new Map(), + new Map(), + groupMember, + ); + expect(parent.spec.children).toEqual(['group:default/child']); + expect(child.spec.parent).toEqual('group:default/parent'); + }, + ); }); describe('userMemberOf', () => { - it('populates relations by dn', () => { - const host = group({ - metadata: { name: 'host', annotations: { [LDAP_DN_ANNOTATION]: 'ha' } }, - }); - const member = user({ - metadata: { - name: 'member', - annotations: { [LDAP_DN_ANNOTATION]: 'ma' }, - }, - }); - const userMemberOf = new Map>([ - ['ma', new Set(['ha'])], - ]); - resolveRelations([host], [member], userMemberOf, new Map(), new Map()); - expect(member.spec.memberOf).toEqual(['group:default/host']); - }); - - it('populates relations by uuid', () => { - const host = group({ - metadata: { - name: 'host', - annotations: { [LDAP_UUID_ANNOTATION]: 'ha' }, - }, - }); - const member = user({ - metadata: { - name: 'member', - annotations: { [LDAP_DN_ANNOTATION]: 'ma' }, - }, - }); - const userMemberOf = new Map>([ - ['ma', new Set(['ha'])], - ]); - resolveRelations([host], [member], userMemberOf, new Map(), new Map()); - expect(member.spec.memberOf).toEqual(['group:default/host']); - }); + it.each([LDAP_DN_ANNOTATION, LDAP_RDN_ANNOTATION, LDAP_UUID_ANNOTATION])( + 'populates relations by %s', + annotation => { + const host = group({ + metadata: { name: 'host', annotations: { [annotation]: 'ha' } }, + }); + const member = user({ + metadata: { + name: 'member', + annotations: { [annotation]: 'ma' }, + }, + }); + const userMemberOf = new Map>([ + ['ma', new Set(['ha'])], + ]); + resolveRelations([host], [member], userMemberOf, new Map(), new Map()); + expect(member.spec.memberOf).toEqual(['group:default/host']); + }, + ); }); describe('groupMemberOf', () => { - it('populates relations by dn', () => { - const parent = group({ - metadata: { - name: 'parent', - annotations: { [LDAP_DN_ANNOTATION]: 'pa' }, - }, - }); - const child = group({ - metadata: { - name: 'child', - annotations: { [LDAP_DN_ANNOTATION]: 'ca' }, - }, - }); - const groupMemberOf = new Map>([ - ['ca', new Set(['pa'])], - ]); - resolveRelations( - [parent, child], - [], - new Map(), - groupMemberOf, - new Map(), - ); - expect(parent.spec.children).toEqual(['group:default/child']); - expect(child.spec.parent).toEqual('group:default/parent'); - }); - }); - - it('populates relations by uuid', () => { - const parent = group({ - metadata: { - name: 'parent', - annotations: { [LDAP_UUID_ANNOTATION]: 'pa' }, + it.each([LDAP_DN_ANNOTATION, LDAP_RDN_ANNOTATION, LDAP_UUID_ANNOTATION])( + 'populates relations by %s', + annotation => { + const parent = group({ + metadata: { + name: 'parent', + annotations: { [annotation]: 'pa' }, + }, + }); + const child = group({ + metadata: { + name: 'child', + annotations: { [annotation]: 'ca' }, + }, + }); + const groupMemberOf = new Map>([ + ['ca', new Set(['pa'])], + ]); + resolveRelations( + [parent, child], + [], + new Map(), + groupMemberOf, + new Map(), + ); + expect(parent.spec.children).toEqual(['group:default/child']); + expect(child.spec.parent).toEqual('group:default/parent'); }, - }); - const child = group({ - metadata: { - name: 'child', - annotations: { [LDAP_UUID_ANNOTATION]: 'ca' }, - }, - }); - const groupMemberOf = new Map>([ - ['ca', new Set(['pa'])], - ]); - resolveRelations([parent, child], [], new Map(), groupMemberOf, new Map()); - expect(parent.spec.children).toEqual(['group:default/child']); - expect(child.spec.parent).toEqual('group:default/parent'); + ); }); describe('groupMember', () => { - it('populates relations by dn', () => { - const parent = group({ - metadata: { - name: 'parent', - annotations: { [LDAP_DN_ANNOTATION]: 'pa' }, - }, - }); - const child = group({ - metadata: { - name: 'child', - annotations: { [LDAP_DN_ANNOTATION]: 'ca' }, - }, - }); - const member = user({ - metadata: { - name: 'member', - annotations: { [LDAP_DN_ANNOTATION]: 'ma' }, - }, - }); - const groupMember = new Map>([ - ['pa', new Set(['ca', 'ma'])], - ]); - resolveRelations( - [parent, child], - [member], - new Map(), - new Map(), - groupMember, - ); - expect(parent.spec.children).toEqual(['group:default/child']); - expect(child.spec.parent).toEqual('group:default/parent'); - expect(member.spec.memberOf).toEqual(['group:default/parent']); - }); - - it('populates relations by uuid', () => { - const parent = group({ - metadata: { - name: 'parent', - annotations: { [LDAP_UUID_ANNOTATION]: 'pa' }, - }, - }); - const child = group({ - metadata: { - name: 'child', - annotations: { [LDAP_UUID_ANNOTATION]: 'ca' }, - }, - }); - const member = user({ - metadata: { - name: 'member', - annotations: { [LDAP_UUID_ANNOTATION]: 'ma' }, - }, - }); - const groupMember = new Map>([ - ['pa', new Set(['ca', 'ma'])], - ]); - resolveRelations( - [parent, child], - [member], - new Map(), - new Map(), - groupMember, - ); - expect(parent.spec.children).toEqual(['group:default/child']); - expect(child.spec.parent).toEqual('group:default/parent'); - expect(member.spec.memberOf).toEqual(['group:default/parent']); - }); + it.each([LDAP_DN_ANNOTATION, LDAP_RDN_ANNOTATION, LDAP_UUID_ANNOTATION])( + 'populates relations by %s', + annotation => { + const parent = group({ + metadata: { + name: 'parent', + annotations: { [annotation]: 'pa' }, + }, + }); + const child = group({ + metadata: { + name: 'child', + annotations: { [annotation]: 'ca' }, + }, + }); + const member = user({ + metadata: { + name: 'member', + annotations: { [annotation]: 'ma' }, + }, + }); + const groupMember = new Map>([ + ['pa', new Set(['ca', 'ma'])], + ]); + resolveRelations( + [parent, child], + [member], + new Map(), + new Map(), + groupMember, + ); + expect(parent.spec.children).toEqual(['group:default/child']); + expect(child.spec.parent).toEqual('group:default/parent'); + expect(member.spec.memberOf).toEqual(['group:default/parent']); + }, + ); }); }); diff --git a/plugins/catalog-backend-module-ldap/src/ldap/read.ts b/plugins/catalog-backend-module-ldap/src/ldap/read.ts index d0fe30e0e6..420ba141fc 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/read.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/read.ts @@ -365,11 +365,13 @@ export function resolveRelations( for (const user of users) { userMap.set(stringifyEntityRef(user), user); userMap.set(user.metadata.annotations![LDAP_DN_ANNOTATION], user); + userMap.set(user.metadata.annotations![LDAP_RDN_ANNOTATION], user); userMap.set(user.metadata.annotations![LDAP_UUID_ANNOTATION], user); } for (const group of groups) { groupMap.set(stringifyEntityRef(group), group); groupMap.set(group.metadata.annotations![LDAP_DN_ANNOTATION], group); + groupMap.set(group.metadata.annotations![LDAP_RDN_ANNOTATION], group); groupMap.set(group.metadata.annotations![LDAP_UUID_ANNOTATION], group); } From 166d3a6932aeee3a7b0412349c1bec1a7571c430 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Jun 2022 14:39:32 +0000 Subject: [PATCH 099/401] fix(deps): update dependency @maxim_mazurok/gapi.client.calendar to v3.0.20220617 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 82f4239fee..6b447fa257 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4286,9 +4286,9 @@ react-is "^16.8.0 || ^17.0.0" "@maxim_mazurok/gapi.client.calendar@^3.0.20220408": - version "3.0.20220610" - resolved "https://registry.npmjs.org/@maxim_mazurok/gapi.client.calendar/-/gapi.client.calendar-3.0.20220610.tgz#46e0092da54fc8eb1342a3d1af04a071a761faac" - integrity sha512-pZwIaTw+PizFSXrF5WqP4dj+b1Vlj/hNwBY4ocWpJ2uhBDoBPAVIc8lEUNwNZVDbAgtbWHD2Kl84UsP1wmBS+w== + version "3.0.20220617" + resolved "https://registry.npmjs.org/@maxim_mazurok/gapi.client.calendar/-/gapi.client.calendar-3.0.20220617.tgz#140bbfd2caa1770fedbdaec4182eb43684f2d876" + integrity sha512-Vek5Y655GUi4RmUs3cNLfo/KQeReEuvcViJ0KYHl28KmC2Pqmxb4V2YIUhsH7BGVyq84cTIT59qHSirB919Prw== dependencies: "@types/gapi.client" "*" From 119f075cd8bbedae665fb9d8bfe224c4d7974f0f Mon Sep 17 00:00:00 2001 From: planeiii Date: Wed, 22 Jun 2022 10:12:28 -0500 Subject: [PATCH 100/401] chore: added Promise.any polyfill for Node v14 Signed-off-by: planeiii --- .../jenkins-backend/src/service/jenkinsApi.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.ts b/plugins/jenkins-backend/src/service/jenkinsApi.ts index 334be5dfa9..8de2609037 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.ts @@ -30,6 +30,23 @@ import { import { jenkinsExecutePermission } from '@backstage/plugin-jenkins-common'; import { NotAllowedError } from '@backstage/errors'; +// polyfill Promise.any for Node v14 +const anyPromise = (any => { + if (any) { + // bind is required by tsc + return any.bind(Promise); + } + + // reverse promise resolution (rejects become resolves and visa versa) + const reverse = (promise: Promise) => + new Promise((resolve, reject) => + Promise.resolve(promise).then(reject, resolve), + ); + + return (iterable: Promise[]) => + reverse(Promise.all([...iterable].map(reverse))); +})(Promise.any); + export class JenkinsApiImpl { private static readonly lastBuildTreeSpec = `lastBuild[ number, @@ -77,7 +94,7 @@ export class JenkinsApiImpl { if (branches) { // Assume jenkinsInfo.jobFullName is a folder which contains one job per branch. // TODO: extract a strategy interface for this - const job = await Promise.any( + const job = await anyPromise( branches.map(branch => client.job.get({ name: `${jenkinsInfo.jobFullName}/${branch}`, From 86640214f03e7ec48ab124a35f6d25ab3b6ee7cb Mon Sep 17 00:00:00 2001 From: Julio Zynger Date: Wed, 22 Jun 2022 15:35:46 +0200 Subject: [PATCH 101/401] Upgrade @rollup/plugin-node-resolve to 13.0.6 Signed-off-by: Julio Zynger --- .changeset/metal-singers-matter.md | 5 +++++ packages/cli/package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/metal-singers-matter.md diff --git a/.changeset/metal-singers-matter.md b/.changeset/metal-singers-matter.md new file mode 100644 index 0000000000..2c8d370790 --- /dev/null +++ b/.changeset/metal-singers-matter.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Upgrade `@rollup/plugin-node-resolve` to `^13.0.6` diff --git a/packages/cli/package.json b/packages/cli/package.json index 2cf6f4fe7b..42e8953d1e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -43,7 +43,7 @@ "@octokit/request": "^5.4.12", "@rollup/plugin-commonjs": "^22.0.0", "@rollup/plugin-json": "^4.1.0", - "@rollup/plugin-node-resolve": "^13.0.0", + "@rollup/plugin-node-resolve": "^13.0.6", "@rollup/plugin-yaml": "^3.1.0", "@spotify/eslint-config-base": "^13.0.0", "@spotify/eslint-config-react": "^13.0.0", From afb57872914b293758b545715739a8a86f58a33a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Jun 2022 15:47:22 +0000 Subject: [PATCH 102/401] fix(deps): update dependency keyv to v4.3.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 6b447fa257..94120932df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16865,9 +16865,9 @@ keyv@^3.0.0: json-buffer "3.0.0" keyv@^4.0.0, keyv@^4.0.3: - version "4.3.1" - resolved "https://registry.npmjs.org/keyv/-/keyv-4.3.1.tgz#7970672f137d987945821b1a07b524ce5a4edd27" - integrity sha512-nwP7AQOxFzELXsNq3zCx/oh81zu4DHWwCE6W9RaeHb7OHO0JpmKS8n801ovVQC7PTsZDWtPA5j1QY+/WWtARYg== + version "4.3.2" + resolved "https://registry.npmjs.org/keyv/-/keyv-4.3.2.tgz#e839df676a0c7ee594c8835e7c1c83742558e5c2" + integrity sha512-kn8WmodVBe12lmHpA6W8OY7SNh6wVR+Z+wZESF4iF5FCazaVXGWOtnbnvX0tMQ1bO+/TmOD9LziuYMvrIIs0xw== dependencies: compress-brotli "^1.3.8" json-buffer "3.0.1" From 606424388245ac0bec4bf7fbbf69b6662328381d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Frinnstr=C3=B6m?= Date: Wed, 22 Jun 2022 18:19:37 +0200 Subject: [PATCH 103/401] Add catalog-backend-module-openapi plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mattias Frinnström --- .../.eslintrc.js | 1 + .../catalog-backend-module-openapi/README.md | 27 ++++ .../api-report.md | 36 ++++++ .../package.json | 53 ++++++++ .../src/OpenApiRefProcessor.test.ts | 101 +++++++++++++++ .../src/OpenApiRefProcessor.ts | 94 ++++++++++++++ .../src/index.ts | 16 +++ .../src/lib/bundle.test.ts | 117 ++++++++++++++++++ .../src/lib/bundle.ts | 69 +++++++++++ .../src/lib/index.ts | 16 +++ .../src/setupTests.ts | 17 +++ yarn.lock | 37 +++++- 12 files changed, 582 insertions(+), 2 deletions(-) create mode 100644 plugins/catalog-backend-module-openapi/.eslintrc.js create mode 100644 plugins/catalog-backend-module-openapi/README.md create mode 100644 plugins/catalog-backend-module-openapi/api-report.md create mode 100644 plugins/catalog-backend-module-openapi/package.json create mode 100644 plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.test.ts create mode 100644 plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.ts create mode 100644 plugins/catalog-backend-module-openapi/src/index.ts create mode 100644 plugins/catalog-backend-module-openapi/src/lib/bundle.test.ts create mode 100644 plugins/catalog-backend-module-openapi/src/lib/bundle.ts create mode 100644 plugins/catalog-backend-module-openapi/src/lib/index.ts create mode 100644 plugins/catalog-backend-module-openapi/src/setupTests.ts diff --git a/plugins/catalog-backend-module-openapi/.eslintrc.js b/plugins/catalog-backend-module-openapi/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/catalog-backend-module-openapi/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-backend-module-openapi/README.md b/plugins/catalog-backend-module-openapi/README.md new file mode 100644 index 0000000000..1a133521c6 --- /dev/null +++ b/plugins/catalog-backend-module-openapi/README.md @@ -0,0 +1,27 @@ +# Catalog Backend Module for OpenAPI specifications + +This is an extension module to the plugin-catalog-backend plugin, providing extensions targeted at OpenAPI specifications. + +With this you can split your OpenAPI definition into multiple files and reference them. They will be bundled, using an UrlReader, during processing and stored as a single specification. + +## Installation + +### Install the package + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-openapi +``` + +### Adding the plugin to your `packages/backend` + +The processor can be added by importing `OpenApiRefProcessor` in `src/plugins/catalog.ts` in your `backend` package and adding the following. + +```ts +builder.addProcessor( + OpenApiRefProcessor.fromConfig(env.config, { + logger: env.logger, + reader: env.reader, + }), +); +``` diff --git a/plugins/catalog-backend-module-openapi/api-report.md b/plugins/catalog-backend-module-openapi/api-report.md new file mode 100644 index 0000000000..92ddd1e3cd --- /dev/null +++ b/plugins/catalog-backend-module-openapi/api-report.md @@ -0,0 +1,36 @@ +## API Report File for "@backstage/plugin-catalog-backend-module-openapi" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; +import { Config } from '@backstage/config'; +import { Entity } from '@backstage/catalog-model'; +import { LocationSpec } from '@backstage/plugin-catalog-backend'; +import { Logger } from 'winston'; +import { ScmIntegrations } from '@backstage/integration'; +import { UrlReader } from '@backstage/backend-common'; + +// @public (undocumented) +export class OpenApiRefProcessor implements CatalogProcessor { + constructor(options: { + integrations: ScmIntegrations; + logger: Logger; + reader: UrlReader; + }); + // (undocumented) + static fromConfig( + config: Config, + options: { + logger: Logger; + reader: UrlReader; + }, + ): OpenApiRefProcessor; + // (undocumented) + getProcessorName(): string; + // (undocumented) + preProcessEntity(entity: Entity, location: LocationSpec): Promise; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/catalog-backend-module-openapi/package.json b/plugins/catalog-backend-module-openapi/package.json new file mode 100644 index 0000000000..228147809e --- /dev/null +++ b/plugins/catalog-backend-module-openapi/package.json @@ -0,0 +1,53 @@ +{ + "name": "@backstage/plugin-catalog-backend-module-openapi", + "description": "A Backstage catalog backend module that helps with OpenAPI specifications", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/catalog-backend-module-openapi" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" + }, + "dependencies": { + "@apidevtools/swagger-parser": "^10.1.0", + "@backstage/backend-common": "^0.14.1-next.0", + "@backstage/catalog-model": "^1.1.0-next.0", + "@backstage/config": "^1.0.1", + "@backstage/integration": "^1.2.2-next.0", + "@backstage/plugin-catalog-backend": "^1.2.1-next.0", + "winston": "^3.2.1", + "yaml": "^2.1.1" + }, + "devDependencies": { + "@backstage/backend-test-utils": "^0.1.26-next.0", + "@backstage/cli": "^0.17.3-next.0", + "openapi-types": "^11.0.1" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.test.ts b/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.test.ts new file mode 100644 index 0000000000..8f2cc571c2 --- /dev/null +++ b/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.test.ts @@ -0,0 +1,101 @@ +/* + * 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 { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { LocationSpec } from '@backstage/plugin-catalog-backend'; +import { OpenApiRefProcessor } from './OpenApiRefProcessor'; +import { bundleOpenApiSpecification } from './lib'; + +jest.mock('./lib', () => ({ + bundleOpenApiSpecification: jest.fn(), +})); + +const bundledSpecification = ''; + +describe('OpenApiRefProcessor', () => { + const mockLocation = (): LocationSpec => ({ + type: 'url', + target: `https://github.com/owner/repo/blob/main/catalog-info.yaml`, + }); + + beforeEach(() => { + (bundleOpenApiSpecification as any).mockResolvedValue(bundledSpecification); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('preProcessEntity', () => { + const setupTest = ({ kind = 'API', spec = {} } = {}) => { + const entity = { + kind, + spec: { definition: '', ...spec }, + }; + const config = new ConfigReader({}); + const reader = { + read: jest.fn(), + readTree: jest.fn(), + search: jest.fn(), + }; + const processor = OpenApiRefProcessor.fromConfig(config, { + logger: getVoidLogger(), + reader, + }); + + return { entity, processor }; + }; + + it('should bundle OpenAPI specifications', async () => { + const { entity, processor } = setupTest({ + kind: 'API', + spec: { type: 'openapi' }, + }); + + const result = await processor.preProcessEntity( + entity as any, + mockLocation(), + ); + + expect(result.spec?.definition).toEqual(bundledSpecification); + }); + + it('should ignore other kinds', async () => { + const { entity, processor } = setupTest({ kind: 'Group' }); + + const result = await processor.preProcessEntity( + entity as any, + mockLocation(), + ); + + expect(result).toEqual(entity); + }); + + it('should ignore other specification types', async () => { + const { entity, processor } = setupTest({ + kind: 'Group', + spec: { type: 'asyncapi' }, + }); + + const result = await processor.preProcessEntity( + entity as any, + mockLocation(), + ); + + expect(result).toEqual(entity); + }); + }); +}); diff --git a/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.ts b/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.ts new file mode 100644 index 0000000000..01875e0779 --- /dev/null +++ b/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.ts @@ -0,0 +1,94 @@ +/* + * 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 { UrlReader } from '@backstage/backend-common'; +import { Entity } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; +import { + CatalogProcessor, + LocationSpec, +} from '@backstage/plugin-catalog-backend'; +import { bundleOpenApiSpecification } from './lib'; +import { Logger } from 'winston'; + +/** @public */ +export class OpenApiRefProcessor implements CatalogProcessor { + private readonly integrations: ScmIntegrations; + private readonly logger: Logger; + private readonly reader: UrlReader; + + static fromConfig( + config: Config, + options: { logger: Logger; reader: UrlReader }, + ) { + const integrations = ScmIntegrations.fromConfig(config); + + return new OpenApiRefProcessor({ + ...options, + integrations, + }); + } + + constructor(options: { + integrations: ScmIntegrations; + logger: Logger; + reader: UrlReader; + }) { + this.integrations = options.integrations; + this.logger = options.logger; + this.reader = options.reader; + } + + getProcessorName(): string { + return 'OpenApiRefProcessor'; + } + + async preProcessEntity( + entity: Entity, + location: LocationSpec, + ): Promise { + if ( + !entity || + entity.kind !== 'API' || + (entity.spec && entity.spec.type !== 'openapi') + ) { + return entity; + } + + const scmIntegration = this.integrations.byUrl(location.target); + if (!scmIntegration) { + return entity; + } + + this.logger.debug(`Bundling OpenAPI specification from ${location.target}`); + try { + const bundledSpec = await bundleOpenApiSpecification( + entity.spec!.definition?.toString(), + location.target, + this.reader, + scmIntegration, + ); + + return { + ...entity, + spec: { ...entity.spec, definition: bundledSpec }, + }; + } catch (error) { + this.logger.error(`Unable to bundle OpenAPI specification`, error); + return entity; + } + } +} diff --git a/plugins/catalog-backend-module-openapi/src/index.ts b/plugins/catalog-backend-module-openapi/src/index.ts new file mode 100644 index 0000000000..f98bee6bac --- /dev/null +++ b/plugins/catalog-backend-module-openapi/src/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { OpenApiRefProcessor } from './OpenApiRefProcessor'; diff --git a/plugins/catalog-backend-module-openapi/src/lib/bundle.test.ts b/plugins/catalog-backend-module-openapi/src/lib/bundle.test.ts new file mode 100644 index 0000000000..4505f158b5 --- /dev/null +++ b/plugins/catalog-backend-module-openapi/src/lib/bundle.test.ts @@ -0,0 +1,117 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; +import { bundleOpenApiSpecification } from './bundle'; + +const specification = ` +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + $ref: "./paths/pets/list.yaml" +`; + +const list = ` +--- +summary: List all pets +operationId: listPets +tags: + - pets +responses: + '200': + description: A paged array of pets + content: + application/json: + schema: + type: string +`; + +const expectedResult = ` +openapi: 3.0.0 +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + responses: + "200": + description: A paged array of pets + content: + application/json: + schema: + type: string +`; + +describe('bundleOpenApiSpecification', () => { + const readUrl = jest.fn(); + const reader = { + readUrl, + read: jest.fn(), + readTree: jest.fn(), + search: jest.fn(), + }; + + const scmIntegration = ScmIntegrations.fromConfig(new ConfigReader({})).byUrl( + 'https://github.com/owner/repo/blob/main/openapi.yaml', + ); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return undefined if no specification is supplied', async () => { + expect( + await bundleOpenApiSpecification( + undefined, + 'https://github.com/owner/repo/blob/main/openapi.yaml', + reader, + scmIntegration as any, + ), + ).toBeUndefined(); + }); + + it('should return the bundled specification', async () => { + readUrl.mockResolvedValue({ + buffer: jest.fn().mockResolvedValue(list), + }); + + const result = await bundleOpenApiSpecification( + specification, + 'https://github.com/owner/repo/blob/main/openapi.yaml', + reader, + scmIntegration as any, + ); + + expect(result).toEqual(expectedResult.trimStart()); + }); +}); diff --git a/plugins/catalog-backend-module-openapi/src/lib/bundle.ts b/plugins/catalog-backend-module-openapi/src/lib/bundle.ts new file mode 100644 index 0000000000..3d35429d8b --- /dev/null +++ b/plugins/catalog-backend-module-openapi/src/lib/bundle.ts @@ -0,0 +1,69 @@ +/* + * 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 { UrlReader } from '@backstage/backend-common'; +import { ScmIntegration } from '@backstage/integration'; +import SwaggerParser from '@apidevtools/swagger-parser'; +import { parse, stringify } from 'yaml'; +import * as path from 'path'; + +const protocolPattern = /^(\w{2,}):\/\//i; +const getProtocol = (refPath: string) => { + const match = protocolPattern.exec(refPath); + if (match) { + return match[1].toLowerCase(); + } + return undefined; +}; + +export async function bundleOpenApiSpecification( + specification: string | undefined, + targetUrl: string, + reader: UrlReader, + scmIntegration: ScmIntegration, +): Promise { + const fileUrlReaderResolver: SwaggerParser.ResolverOptions = { + canRead: file => { + const protocol = getProtocol(file.url); + return protocol === undefined || protocol === 'file'; + }, + read: async file => { + const relativePath = path.relative('.', file.url); + const url = scmIntegration.resolveUrl({ + base: targetUrl, + url: relativePath, + }); + if (reader.readUrl) { + const data = await reader.readUrl(url); + return data.buffer(); + } + throw new Error('UrlReader has no readUrl method defined'); + }, + }; + + if (!specification) { + return undefined; + } + + const options: SwaggerParser.Options = { + resolve: { + file: fileUrlReaderResolver, + http: true, + }, + }; + const specObject = parse(specification); + const bundledSpec = await SwaggerParser.bundle(specObject, options); + return stringify(bundledSpec); +} diff --git a/plugins/catalog-backend-module-openapi/src/lib/index.ts b/plugins/catalog-backend-module-openapi/src/lib/index.ts new file mode 100644 index 0000000000..805569181a --- /dev/null +++ b/plugins/catalog-backend-module-openapi/src/lib/index.ts @@ -0,0 +1,16 @@ +/* + * 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 * from './bundle'; diff --git a/plugins/catalog-backend-module-openapi/src/setupTests.ts b/plugins/catalog-backend-module-openapi/src/setupTests.ts new file mode 100644 index 0000000000..d3232290a7 --- /dev/null +++ b/plugins/catalog-backend-module-openapi/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export {}; diff --git a/yarn.lock b/yarn.lock index 6b447fa257..8b6112a39a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,7 +16,7 @@ dependencies: "@jridgewell/trace-mapping" "^0.3.0" -"@apidevtools/json-schema-ref-parser@^9.0.6": +"@apidevtools/json-schema-ref-parser@9.0.6", "@apidevtools/json-schema-ref-parser@^9.0.6": version "9.0.6" resolved "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz#5d9000a3ac1fd25404da886da6b266adcd99cf1c" integrity sha512-M3YgsLjI0lZxvrpeGVk9Ap032W6TPQkH6pRAZz81Ac3WUNF79VQooAFnp8umjvVzUmD93NkogxEwbSce7qMsUg== @@ -25,6 +25,29 @@ call-me-maybe "^1.0.1" js-yaml "^3.13.1" +"@apidevtools/openapi-schemas@^2.1.0": + version "2.1.0" + resolved "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz#9fa08017fb59d80538812f03fc7cac5992caaa17" + integrity sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ== + +"@apidevtools/swagger-methods@^3.0.2": + version "3.0.2" + resolved "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz#b789a362e055b0340d04712eafe7027ddc1ac267" + integrity sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg== + +"@apidevtools/swagger-parser@^10.1.0": + version "10.1.0" + resolved "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.1.0.tgz#a987d71e5be61feb623203be0c96e5985b192ab6" + integrity sha512-9Kt7EuS/7WbMAUv2gSziqjvxwDbFSg3Xeyfuj5laUODX8o/k/CpsAKiQ8W7/R88eXFTMbJYg6+7uAmOWNKmwnw== + dependencies: + "@apidevtools/json-schema-ref-parser" "9.0.6" + "@apidevtools/openapi-schemas" "^2.1.0" + "@apidevtools/swagger-methods" "^3.0.2" + "@jsdevtools/ono" "^7.1.3" + ajv "^8.6.3" + ajv-draft-04 "^1.0.0" + call-me-maybe "^1.0.1" + "@apollo/protobufjs@1.2.2": version "1.2.2" resolved "https://registry.npmjs.org/@apollo/protobufjs/-/protobufjs-1.2.2.tgz#4bd92cd7701ccaef6d517cdb75af2755f049f87c" @@ -7613,6 +7636,11 @@ aggregate-error@^3.0.0, aggregate-error@^3.1.0: clean-stack "^2.0.0" indent-string "^4.0.0" +ajv-draft-04@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz#3b64761b268ba0b9e668f0b41ba53fce0ad77fc8" + integrity sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw== + ajv-formats@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" @@ -7642,7 +7670,7 @@ ajv@^6.10.0, ajv@^6.10.1, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.5.5, ajv json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.10.0, ajv@^8.8.0: +ajv@^8.0.0, ajv@^8.10.0, ajv@^8.6.3, ajv@^8.8.0: version "8.11.0" resolved "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== @@ -19627,6 +19655,11 @@ openapi-sampler@^1.2.1: "@types/json-schema" "^7.0.7" json-pointer "0.6.2" +openapi-types@^11.0.1: + version "11.1.0" + resolved "https://registry.npmjs.org/openapi-types/-/openapi-types-11.1.0.tgz#037969f3dfa5999423ee33bf889fb0d12984277e" + integrity sha512-ZW+Jf12flFF6DXSij8DGL3svDA4RtSyHXjC/xB/JAh18gg3uVfVIFLvCfScUMowrpvlkxsMMbErakbth2g3/iQ== + openid-client@^4.1.1: version "4.9.0" resolved "https://registry.npmjs.org/openid-client/-/openid-client-4.9.0.tgz#bdfc9194435316df419f759ce177635146b43074" From 67503d159ea09572bf89a119e280f5b32d226691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Frinnstr=C3=B6m?= Date: Wed, 22 Jun 2022 18:31:20 +0200 Subject: [PATCH 104/401] Add changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mattias Frinnström --- .changeset/shy-cameras-develop.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/shy-cameras-develop.md diff --git a/.changeset/shy-cameras-develop.md b/.changeset/shy-cameras-develop.md new file mode 100644 index 0000000000..c62a9a56e3 --- /dev/null +++ b/.changeset/shy-cameras-develop.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-backend-module-openapi': minor +--- + +Add basic OpenAPI \$ref support. + +For more information see [here](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend-module-openapi). From 3524900dfd8a301c1240c0cf69be694b556d8568 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Jun 2022 16:57:48 +0000 Subject: [PATCH 105/401] fix(deps): update dependency run-script-webpack-plugin to v0.1.1 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 94120932df..9b93d7945b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22776,9 +22776,9 @@ run-parallel@^1.1.9: integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== run-script-webpack-plugin@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/run-script-webpack-plugin/-/run-script-webpack-plugin-0.1.0.tgz#6250a4734511836628259666d56641a27526875d" - integrity sha512-PKddVrnTu1BO90ogvN93yP/zg3X8Q4XvSKKyGVZFye9VBVbgVkFllKykNOomi9IfNtnoFFzu6TkLONIuXq5URA== + version "0.1.1" + resolved "https://registry.npmjs.org/run-script-webpack-plugin/-/run-script-webpack-plugin-0.1.1.tgz#dad3114be32eb864d2160306e4d9c52a2c1cfd59" + integrity sha512-PrxBRLv1K9itDKMlootSCyGhdTU+KbKGJ2wF6/k0eyo6M0YGPC58HYbS/J/QsDiwM0t7G99WcuCqto0J7omOXA== rxjs@7.5.5, rxjs@^7.0.0, rxjs@^7.1.0, rxjs@^7.2.0, rxjs@^7.5.1, rxjs@^7.5.5: version "7.5.5" From 64c7abbb160b2e7497ad5aa98ce31e7070207328 Mon Sep 17 00:00:00 2001 From: Taras Date: Wed, 22 Jun 2022 14:03:13 -0400 Subject: [PATCH 106/401] Added missing logos Signed-off-by: Taras Mankovski --- microsite/data/plugins/gke-usage.yaml | 2 +- microsite/data/plugins/harbor.yaml | 2 +- microsite/data/plugins/humanitec.yaml | 2 +- microsite/static/img/gke-logo.png | Bin 0 -> 10452 bytes microsite/static/img/harbor-logo.png | Bin 0 -> 18741 bytes 5 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 microsite/static/img/gke-logo.png create mode 100644 microsite/static/img/harbor-logo.png diff --git a/microsite/data/plugins/gke-usage.yaml b/microsite/data/plugins/gke-usage.yaml index e092f84973..9668b18fdf 100644 --- a/microsite/data/plugins/gke-usage.yaml +++ b/microsite/data/plugins/gke-usage.yaml @@ -5,7 +5,7 @@ authorUrl: https://bestsellerit.com category: Discovery description: This plugin will show you the cost and resource usage of your application within Google Kubernetes Engine (GKE). documentation: https://github.com/BESTSELLER/backstage-plugin-gkeusage/blob/master/README.md -iconUrl: https://bestsellerit.com/img/google-container-engine_avatar.svg +iconUrl: img/gke-logo.png npmPackageName: '@bestsellerit/backstage-plugin-gkeusage' tags: - gke diff --git a/microsite/data/plugins/harbor.yaml b/microsite/data/plugins/harbor.yaml index 9023e0c434..64b2033021 100644 --- a/microsite/data/plugins/harbor.yaml +++ b/microsite/data/plugins/harbor.yaml @@ -5,7 +5,7 @@ authorUrl: https://bestsellerit.com category: Discovery description: This plugin will show you information about Docker images within the Harbor cloud native registry. documentation: https://github.com/BESTSELLER/backstage-plugin-harbor/blob/master/README.md -iconUrl: https://bestsellerit.com/img/terraform-harbor/goharbor.jpeg +iconUrl: img/harbor-logo.png npmPackageName: '@bestsellerit/backstage-plugin-harbor' tags: - goharbor diff --git a/microsite/data/plugins/humanitec.yaml b/microsite/data/plugins/humanitec.yaml index 88c0cd0a08..bd5dec9dc6 100644 --- a/microsite/data/plugins/humanitec.yaml +++ b/microsite/data/plugins/humanitec.yaml @@ -7,5 +7,5 @@ description: | Show workloads, environments and resources deployed by Humanitec Platform Orchestrator. Plugin includes an Entity ComponentCard, Backend API route and scaffolder actions. documentation: https://github.com/thefrontside/backstage/tree/main/plugins/humanitec -iconUrl: ../../static/img/humanitec-logo.png +iconUrl: img/humanitec-logo.png npmPackageName: '@frontside/backstage-plugin-humanitec' diff --git a/microsite/static/img/gke-logo.png b/microsite/static/img/gke-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..5af1abbf9a831ebe8b9a9ff87d17e78d2789236c GIT binary patch literal 10452 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 literal 0 HcmV?d00001 From fea3933d976081fd3adad8bf7a684e3dce83438a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jun 2022 02:34:54 +0000 Subject: [PATCH 107/401] chore(deps): update dependency msw to v0.42.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 9b93d7945b..cf4f33b1d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18805,9 +18805,9 @@ msw@^0.39.2: yargs "^17.3.1" msw@^0.42.0: - version "0.42.1" - resolved "https://registry.npmjs.org/msw/-/msw-0.42.1.tgz#2496d3e191754b68686e2530de459a2e102f85c4" - integrity sha512-LZZuz7VddL45gCBgfBWHyXj6a4W7OTJY0mZPoipJ3P/xwbuJwrtwB3IJrWlqBM8aink/eTKlRxwzmtIAwCj5yQ== + version "0.42.3" + resolved "https://registry.npmjs.org/msw/-/msw-0.42.3.tgz#150c475e2cb6d53c67503bd0e3f6251bfd075328" + integrity sha512-zrKBIGCDsNUCZLd3DLSeUtRruZ0riwJgORg9/bSDw3D0PTI8XUGAK3nC0LJA9g0rChGuKaWK/SwObA8wpFrz4g== dependencies: "@mswjs/cookies" "^0.2.0" "@mswjs/interceptors" "^0.16.3" From 007cba9eb12304057f3663ec48de6754c7ab7414 Mon Sep 17 00:00:00 2001 From: "Daniele.Mazzotta" Date: Thu, 23 Jun 2022 10:07:08 +0400 Subject: [PATCH 108/401] fixed issue where error message would be 0 instead of empty element Signed-off-by: Daniele.Mazzotta --- .../src/components/DagTableComponent/DagTableComponent.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx b/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx index 46127e0b5e..2514d70d45 100644 --- a/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx +++ b/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx @@ -162,12 +162,14 @@ export const DagTableComponent = ({ dagIds }: DagTableComponentProps) => { : []; return ( <> - {dagsNotFound.length && ( + {dagsNotFound.length ? ( {dagsNotFound.map(dagId => ( {dagId} ))} + ) : ( + '' )} From aed6b691cb8c8f2b85b3edf999f6b57b39e66b58 Mon Sep 17 00:00:00 2001 From: "Daniele.Mazzotta" Date: Thu, 23 Jun 2022 10:29:10 +0400 Subject: [PATCH 109/401] fixed failing test while fetching getDags Signed-off-by: Daniele.Mazzotta --- .../apache-airflow/src/api/ApacheAirflowClient.test.ts | 9 +++++++++ plugins/apache-airflow/src/api/ApacheAirflowClient.ts | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts b/plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts index 329f89ee52..b9bdb02283 100644 --- a/plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts +++ b/plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts @@ -107,6 +107,15 @@ describe('ApacheAirflowClient', () => { ); }), + rest.get(`${mockBaseUrl}/airflow/dags/:dag_id`, (req, res, ctx) => { + const { dag_id } = req.params; + const dag = dags.find(d => d.dag_id === dag_id); + if (dag) { + return res(ctx.json(dag)); + } + return res(ctx.status(404)); + }), + rest.patch(`${mockBaseUrl}/airflow/dags/:dag_id`, (req, res, ctx) => { const { dag_id } = req.params; const body = JSON.parse(req.body as string); diff --git a/plugins/apache-airflow/src/api/ApacheAirflowClient.ts b/plugins/apache-airflow/src/api/ApacheAirflowClient.ts index bf095a96ba..b6c0a09c3a 100644 --- a/plugins/apache-airflow/src/api/ApacheAirflowClient.ts +++ b/plugins/apache-airflow/src/api/ApacheAirflowClient.ts @@ -82,7 +82,7 @@ export class ApacheAirflowClient implements ApacheAirflowApi { const response = await Promise.all( dagIds.map(id => { return this.fetch(`/dags/${id}`).catch(e => { - if (e.message === 'NOT FOUND') { + if (e.message === 'Not Found') { dagsNotFound.push(id); } else { throw e; From 7bc7b1d37617b5de113547979c224f27a10f3ad8 Mon Sep 17 00:00:00 2001 From: "Daniele.Mazzotta" Date: Thu, 23 Jun 2022 10:35:50 +0400 Subject: [PATCH 110/401] case-insensitive NOT FOUND error catching in getDags Signed-off-by: Daniele.Mazzotta --- plugins/apache-airflow/src/api/ApacheAirflowClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/apache-airflow/src/api/ApacheAirflowClient.ts b/plugins/apache-airflow/src/api/ApacheAirflowClient.ts index b6c0a09c3a..c722f5977d 100644 --- a/plugins/apache-airflow/src/api/ApacheAirflowClient.ts +++ b/plugins/apache-airflow/src/api/ApacheAirflowClient.ts @@ -82,7 +82,7 @@ export class ApacheAirflowClient implements ApacheAirflowApi { const response = await Promise.all( dagIds.map(id => { return this.fetch(`/dags/${id}`).catch(e => { - if (e.message === 'Not Found') { + if (e.message.toUpperCase('en-US') === 'NOT FOUND') { dagsNotFound.push(id); } else { throw e; From 9592a6cefd7acd7e7917fa2e4c2fb16c617981b8 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 23 Jun 2022 10:54:40 +0200 Subject: [PATCH 111/401] fix(search): race condition on search bar test Signed-off-by: Camila Belo --- .../components/SearchBar/SearchBar.test.tsx | 337 ++++++++++-------- 1 file changed, 183 insertions(+), 154 deletions(-) diff --git a/plugins/search/src/components/SearchBar/SearchBar.test.tsx b/plugins/search/src/components/SearchBar/SearchBar.test.tsx index c48c3d3ed2..4d8a46024c 100644 --- a/plugins/search/src/components/SearchBar/SearchBar.test.tsx +++ b/plugins/search/src/components/SearchBar/SearchBar.test.tsx @@ -18,59 +18,60 @@ import React from 'react'; import { screen, render, waitFor, act } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { configApiRef, analyticsApiRef } from '@backstage/core-plugin-api'; -import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; -import { MockAnalyticsApi, TestApiRegistry } from '@backstage/test-utils'; +import { ConfigReader } from '@backstage/core-app-api'; +import { + MockAnalyticsApi, + TestApiProvider, + renderWithEffects, +} from '@backstage/test-utils'; import { SearchContextProvider, searchApiRef, + SearchBar, } from '@backstage/plugin-search-react'; -import { SearchBar } from './SearchBar'; - -jest.mock('@backstage/core-plugin-api', () => ({ - ...jest.requireActual('@backstage/core-plugin-api'), -})); +const createInitialState = ({ + term = '', + filters = {}, + types = ['*'], + pageCursor = '', +} = {}) => ({ + term, + filters, + types, + pageCursor, +}); describe('SearchBar', () => { - const initialState = { - term: '', - filters: {}, - types: ['*'], - pageCursor: '', - }; + const query = jest.fn().mockResolvedValue({ results: [] }); - const query = jest.fn().mockResolvedValue({}); - const analyticsApiSpy = new MockAnalyticsApi(); - let apiRegistry: TestApiRegistry; + const analyticsApiMock = new MockAnalyticsApi(); - apiRegistry = TestApiRegistry.from( - [ - configApiRef, - new ConfigReader({ - app: { title: 'Mock title' }, - }), - ], - [searchApiRef, { query }], - ); + const configApiMock = new ConfigReader({ + app: { title: 'Mock title' }, + }); - const name = 'Search'; - const term = 'term'; + const searchApiMock = { query }; - afterAll(() => { - jest.resetAllMocks(); + beforeEach(() => { + jest.clearAllMocks(); }); it('Renders without exploding', async () => { - render( - - + await renderWithEffects( + + - , + , ); await waitFor(() => { - expect(screen.getByRole('textbox', { name })).toBeInTheDocument(); expect( screen.getByPlaceholderText('Search in Mock title'), ).toBeInTheDocument(); @@ -78,59 +79,72 @@ describe('SearchBar', () => { }); it('Renders with custom placeholder', async () => { - render( - - - + const placeholder = 'placeholder'; + + await renderWithEffects( + + + - , - , + , ); await waitFor(() => { - expect( - screen.getByPlaceholderText('This is a custom placeholder'), - ).toBeInTheDocument(); + expect(screen.getByPlaceholderText(placeholder)).toBeInTheDocument(); }); }); it('Renders based on initial search', async () => { + const term = 'term'; + render( - - + + - , - , + , ); await waitFor(() => { - expect(screen.getByRole('textbox', { name })).toHaveValue(term); + expect(screen.getByRole('textbox', { name: 'Search' })).toHaveValue(term); }); }); it('Updates term state when text is entered', async () => { - const user = userEvent.setup({ delay: null }); jest.useFakeTimers(); - const defaultDebounceTime = 200; + const user = userEvent.setup({ delay: null }); - render( - - + await renderWithEffects( + + - , - , + , ); - const textbox = screen.getByRole('textbox', { name }); + const textbox = screen.getByRole('textbox', { name: 'Search' }); const value = 'value'; await user.type(textbox, value); act(() => { - jest.advanceTimersByTime(defaultDebounceTime); + jest.advanceTimersByTime(200); }); await waitFor(() => { @@ -140,26 +154,36 @@ describe('SearchBar', () => { expect(query).toHaveBeenLastCalledWith( expect.objectContaining({ term: value }), ); + jest.useRealTimers(); }); it('Clear button clears term state', async () => { - render( - - + const term = 'term'; + + await renderWithEffects( + + - , + , ); + const textbox = screen.getByRole('textbox', { name: 'Search' }); + await waitFor(() => { - expect(screen.getByRole('textbox', { name })).toHaveValue(term); + expect(textbox).toHaveValue(term); }); await userEvent.click(screen.getByRole('button', { name: 'Clear' })); await waitFor(() => { - expect(screen.getByRole('textbox', { name })).toHaveValue(''); + expect(textbox).toHaveValue(''); }); expect(query).toHaveBeenLastCalledWith( @@ -168,12 +192,19 @@ describe('SearchBar', () => { }); it('Should not show clear button', async () => { - render( - - + const term = 'term'; + + await renderWithEffects( + + - , + , ); await waitFor(() => { @@ -184,170 +215,168 @@ describe('SearchBar', () => { }); it('Adheres to provided debounceTime', async () => { - const user = userEvent.setup({ delay: null }); jest.useFakeTimers(); - + const user = userEvent.setup({ delay: null }); const debounceTime = 600; - render( - - + await renderWithEffects( + + - , - , + , ); - await waitFor(() => { - expect(screen.getByRole('textbox', { name })).toBeInTheDocument(); - }); - - const textbox = screen.getByRole('textbox', { name }); + const textbox = await screen.findByRole('textbox', { name: 'Search' }); const value = 'value'; await user.type(textbox, value); - expect(query).not.toHaveBeenLastCalledWith( - expect.objectContaining({ term: value }), - ); + await waitFor(() => { + expect(query).not.toHaveBeenLastCalledWith( + expect.objectContaining({ term: value }), + ); + }); act(() => { jest.advanceTimersByTime(debounceTime); }); - expect(textbox).toHaveValue(value); + + await waitFor(() => { + expect(textbox).toHaveValue(value); + }); expect(query).toHaveBeenLastCalledWith( expect.objectContaining({ term: value }), ); + jest.useRealTimers(); }); it('does not capture analytics event if not enabled in app', async () => { - const user = userEvent.setup({ delay: null }); jest.useFakeTimers(); + const user = userEvent.setup({ delay: null }); - const debounceTime = 600; - - render( - - - + await renderWithEffects( + + + - , - , + , ); - await waitFor(() => { - expect(screen.getByRole('textbox', { name })).toBeInTheDocument(); - }); - - const textbox = screen.getByRole('textbox', { name }); + const textbox = await screen.findByRole('textbox', { name: 'Search' }); const value = 'value'; await user.type(textbox, value); act(() => { - jest.advanceTimersByTime(debounceTime); + jest.advanceTimersByTime(200); }); - await waitFor(() => expect(textbox).toHaveValue(value)); + await waitFor(() => { + expect(textbox).toHaveValue(value); + }); + + expect(analyticsApiMock.getEvents()).toHaveLength(0); - expect(analyticsApiSpy.getEvents()).toHaveLength(0); jest.useRealTimers(); }); it('captures analytics events if enabled in app', async () => { - const user = userEvent.setup({ delay: null }); jest.useFakeTimers(); + const user = userEvent.setup({ delay: null }); + const types = ['techdocs', 'software-catalog']; - const debounceTime = 600; - - apiRegistry = TestApiRegistry.from( - [analyticsApiRef, analyticsApiSpy], - [ - configApiRef, - new ConfigReader({ - app: { - title: 'Mock title', - analytics: { - ga: { - trackingId: 'xyz123', + await renderWithEffects( + - - + }), + ], + ]} + > + + -
, + , ); - await waitFor(() => { - expect(screen.getByRole('textbox', { name })).toBeInTheDocument(); - }); + const textbox = await screen.findByRole('textbox', { name: 'Search' }); - const textbox = screen.getByRole('textbox', { name }); - - const value = 'value'; + let value = 'value'; await user.type(textbox, value); - expect(analyticsApiSpy.getEvents()).toHaveLength(0); + expect(analyticsApiMock.getEvents()).toHaveLength(0); act(() => { - jest.advanceTimersByTime(debounceTime); + jest.advanceTimersByTime(200); }); await waitFor(() => expect(textbox).toHaveValue(value)); - expect(analyticsApiSpy.getEvents()).toHaveLength(1); - expect(analyticsApiSpy.getEvents()[0]).toEqual({ + expect(analyticsApiMock.getEvents()).toHaveLength(1); + expect(analyticsApiMock.getEvents()[0]).toEqual({ action: 'search', context: { - extension: 'App', - pluginId: 'root', + extension: 'SearchBar', + pluginId: 'search', routeRef: 'unknown', - searchTypes: 'software-catalog,techdocs', + searchTypes: types.toString(), }, - subject: 'value', + subject: value, }); await user.clear(textbox); + value = 'new value'; + // make sure new term is captured - await user.type(textbox, 'new value'); + await user.type(textbox, value); act(() => { - jest.advanceTimersByTime(debounceTime); + jest.advanceTimersByTime(200); }); - await waitFor(() => expect(textbox).toHaveValue('new value')); + await waitFor(() => expect(textbox).toHaveValue(value)); - expect(analyticsApiSpy.getEvents()).toHaveLength(2); - expect(analyticsApiSpy.getEvents()[1]).toEqual({ + expect(analyticsApiMock.getEvents()).toHaveLength(2); + expect(analyticsApiMock.getEvents()[1]).toEqual({ action: 'search', context: { - extension: 'App', - pluginId: 'root', + extension: 'SearchBar', + pluginId: 'search', routeRef: 'unknown', - searchTypes: 'software-catalog,techdocs', + searchTypes: types.toString(), }, - subject: 'new value', + subject: value, }); + jest.useRealTimers(); }); }); From 7d9dc8f271c26c964034a3bcbfcf58eef58b3454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Frinnstr=C3=B6m?= Date: Thu, 23 Jun 2022 11:00:50 +0200 Subject: [PATCH 112/401] Change version to 0.0.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mattias Frinnström --- plugins/catalog-backend-module-openapi/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog-backend-module-openapi/package.json b/plugins/catalog-backend-module-openapi/package.json index 228147809e..1c99618ff8 100644 --- a/plugins/catalog-backend-module-openapi/package.json +++ b/plugins/catalog-backend-module-openapi/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-openapi", "description": "A Backstage catalog backend module that helps with OpenAPI specifications", - "version": "0.1.0", + "version": "0.0.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", From 5d7a1cca09a1a198dd1eb01a1568bb25f9f7d4ad Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Thu, 23 Jun 2022 11:03:12 +0200 Subject: [PATCH 113/401] point to config schema from package.json Signed-off-by: Emma Indal --- plugins/stack-overflow/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/stack-overflow/package.json b/plugins/stack-overflow/package.json index 922277c74a..0fe8ae476f 100644 --- a/plugins/stack-overflow/package.json +++ b/plugins/stack-overflow/package.json @@ -55,5 +55,6 @@ "files": [ "dist", "config" - ] + ], + "configSchema": "config.d.ts" } From 52b4f796e3432b373026877fd3dfaca01beaee0a Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Thu, 23 Jun 2022 11:06:52 +0200 Subject: [PATCH 114/401] changeset Signed-off-by: Emma Indal --- .changeset/strange-tables-flash.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/strange-tables-flash.md diff --git a/.changeset/strange-tables-flash.md b/.changeset/strange-tables-flash.md new file mode 100644 index 0000000000..fd13289752 --- /dev/null +++ b/.changeset/strange-tables-flash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-stack-overflow': patch +--- + +app-config is now picked up properly. From 6e948bc6ac3518c6b64e29827ee2bfb655c3412e Mon Sep 17 00:00:00 2001 From: Jorge Lainfiesta Date: Tue, 21 Jun 2022 17:16:04 +0200 Subject: [PATCH 115/401] Adds Community page update proposal Signed-off-by: Jorge Lainfiesta --- microsite/pages/en/community.js | 215 ++++++++++++------ .../static/img/partner-logo-frontside.png | Bin 0 -> 5860 bytes microsite/static/img/partner-logo-roadie.png | Bin 0 -> 6008 bytes .../static/img/partner-logo-thoughtworks.png | Bin 0 -> 7492 bytes microsite/static/img/partner-logo-vmware.png | Bin 0 -> 6573 bytes 5 files changed, 147 insertions(+), 68 deletions(-) create mode 100644 microsite/static/img/partner-logo-frontside.png create mode 100644 microsite/static/img/partner-logo-roadie.png create mode 100644 microsite/static/img/partner-logo-thoughtworks.png create mode 100644 microsite/static/img/partner-logo-vmware.png diff --git a/microsite/pages/en/community.js b/microsite/pages/en/community.js index 26366b25df..3d32226c60 100644 --- a/microsite/pages/en/community.js +++ b/microsite/pages/en/community.js @@ -8,6 +8,27 @@ const React = require('react'); const Components = require(`${process.cwd()}/core/Components.js`); const Block = Components.Block; +const Breakpoint = Components.Breakpoint; +const ActionBlock = Components.ActionBlock; +const BulletLine = Components.BulletLine; + +const PARTNERS = [{ + name: "Frontside Software", + url: "https://frontside.com/backstage/", + logo: "img/partner-logo-frontside.png" +}, { + name: "Roadie", + url: "https://roadie.io/", + logo: "img/partner-logo-roadie.png" +}, { + name: "ThoughtWorks", + url: "https://www.thoughtworks.com", + logo: "img/partner-logo-thoughtworks.png" +}, { + name: "VMWare", + url: "https://www.vmware.com", + logo: "img/partner-logo-vmware.png" +}]; const Background = props => { const { config: siteConfig } = props; @@ -17,14 +38,10 @@ const Background = props => { - Backstage Community - - + Backstage Community - What's the use of having fun if you can't share it? Exactly. Join - the vibrant community around the Backstage project. Be it on - GitHub, social media, Discord... You'll find a welcoming - environment. To ensure this, we follow the{' '} + Join + the vibrant community around the Backstage project through social media and different meetups. To ensure you have a welcoming environment, we follow {' '} {' '} CNCF Code of Conduct @@ -32,84 +49,146 @@ const Background = props => { in everything we do. - + - Main community channels -
- Chat and get support on our{' '} -
Discord -
- Get into contributing with the{' '} - - Good First Issues - -
- Subscribe to the{' '} - - Community newsletter - -
- Join the{' '} - - Twitter community - -
+ + Get started in our community! + +
- - - - Backstage Community Sessions - - Missed a meetup? Wondering when the next one is coming up? We've - got you covered! Check out our all-new Meetups page. - - Meetups - - - Adopter Community Sessions - - Backstage Community Sessions is the monthly meetup where we all - come together to listen to the latest maintainer updates, learn - from each other about adopting, share exciting new demos or - discuss any relevant topic like developer effectiveness, developer - experience, developer portals, etc. Have something to share, or a - burning question? Add it to the{' '} - issue. - - - - Contributor Community Sessions - - Discuss all things contributing, diving deep under the hood of - Backstage (Backstage of Backstage? Backerstage?). An open - discussion with maintainers and contributors of Backstage. If you - like Backstage, this is your favorite Zoom meeting of the month, - guaranteed! Have something to share, or a burning question? Add it - to the{' '} - issue. - + + + + Offical Backstage initiatives + Stay tuned to the latest developments - + - - Backstage official Newsletter - - + + + + Community sessions + - The official monthly Backstage newsletter. Containing the latest - news from your favorite project. + Maintainers and adopters meet monthly to share updates, + demos, and ideas. Yep, all sessions are recorded! - - Subscribe + Join a session + + + + + Newsletter + + + The official monthly Backstage newsletter. Don't miss + the latest news from your favorite project! + + Subscribe + + + + + Contributor Spotlight + + + A recognition for valuable community work. Nominate contributing members for + their efforts! We'll put them in the spotlight ❤️ + + Nominate now + + + + + + + + Community initiatives + + + + Open Mic Meetup + + + A casual get together of Backstage users sharing their + experiences and helping each other. Hosted by{' '} + Roadie.io and{' '} + Frontside Software. + + Learn more + + + + Backstage Weekly Newsletter + + + A weekly newsletter with news, updates and things community from + your friends at Roadie.io. + + + Learn more - + + + + Commercial Partners + + {PARTNERS.map((partner) => ( + + + + {partner.name} + + + + ))} + + + + + {/* Spotlight @@ -145,7 +224,7 @@ const Background = props => { - + */} ); }; diff --git a/microsite/static/img/partner-logo-frontside.png b/microsite/static/img/partner-logo-frontside.png new file mode 100644 index 0000000000000000000000000000000000000000..c8cad6bce13eec4f0c79c0c259b66d9111a96e6a GIT binary patch literal 5860 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 VpV{tO9%mP1dELgW{MuhJ{|yTX@3H^@ literal 0 HcmV?d00001 diff --git a/microsite/static/img/partner-logo-roadie.png b/microsite/static/img/partner-logo-roadie.png new file mode 100644 index 0000000000000000000000000000000000000000..41b169f55b6415eaa08853ada49060f9955717e2 GIT binary patch literal 6008 zcmeI0`9D?JO!(eDuh;h<`2PI);oj#i=XuU^&w1`S=Xs~tSX~j`e{??p0K#Th zO>6;xy9K=N7UT!d`*YNZV6*S;Rp(Fukd*s(aGBYjUIhS%qM6Ah`>0pT6U+CeyT`x$ z@-Nh7^ob>E9(tM9o+r4JcJsrb1IH5Y@%mpm7sAU-xoN^nGC3#uQSH!XOyl)mULL21 zRO(&!JSAq``7MOI?^c9IvV&JvR=7cZUhDDhT3`#TrnCzIK*GC2QCt8Zs>!ViJ}bUF z$p-+2BZB{?`Q&IK1Z=LKS_S)^@ZQl6a?DDM<5YmtdW^*u`TB- zdv>{21pwag@p>HSF_8>u!|v_53?DSBDei&*2|ha;ySw!8V*ta}xrVi#+ofk7ow@dt zpa=Fa<@NZaoWVCiJ=#{6xz0Ps%I5)qCPOnGuk)GbQKJjwea)F+atrw-gm)`^z={29 zt0!T(SmmkSp^a95I}Yf|=qK%|H%f4CkTbWamFo=^pxB#a!It@E6UWxo*|pABH>%kv zd@bnbNx?)&tY#+a=(Qi!F-4eMyf&aBjyhfx^$=MYuHGiLxBr@28aNW8gxGP&=qINfce9EErOOOnQ}WtyN}&_qDvsf4WO0! zhdNVjvPoR=%Z*a{$_S-)i3u1FkZ`Lw86w*aCl;ywlf}e$a|aLmS^$p}GlcsKETS3B zeU!bNuo2iB_WYD7+IGuTL>oSUqX#S^xkiXh>u1bM6UISn7@ZVqij=~?wNM&ElkK{e zfx&`|rC#fatX;?$N)3=W6g5-^Me~SAtP|VsO8B+D5GjJZOKeYVfa9e5^aoldzeSFv zv=`4r06tgb7pO_pO=YW`M03HGRh3V(3C0^|kVMcy(+z{OQh>(T4ME_}ISZbcBR!JO zH=Q0$=t{#qA35Xu0Dc4}UE|bwVyVQ2R!)AUJ03US0^AXen(DOQxgnx$?}W1`2pvN5 zd^!`}sFI^Dpm<`nqZH{p7%Td4*sU?6kHJxlzL)T_@h?(-m0RL(Ig|f{qL|v5zIqh4 zoDyjnj0r+!F+SXX{YQk#wI;04X$Y>!6K<-wtS2!3+EU!>Et|jDd<-LUXWt3^$y!s2 zFtDAF$$#RahFKelzgk#xexlp2z0GX9MtO}MIexOe!1N83yV!J+uj768S~3=oB5gOb zPVbz$U2Ay!sUe=vB(+^AELbi-K2n+4^XVQ1=fN;)Fwq>mBIG>QU+Qr@fafqTx9VUO z#Cfp5uW?q$lGYpjvF%8W@+O&#?fxttY=y6 zUJklzcJj75(N<*l=ih4J7D+3WM}|jh1&2>nT|FFTa&tS7#tj(k_dj zp0Vdahp$9@&4-~&ARewGZn)MogaTstSM>bvt*a3@nA72|0L;va>DbTWdh(~zi_l@Q z^o7oQ&lACb9P~9T>gjuX$%Nqkzq^$673u z*)UtytD^6woM+LA48ApWtk%B8IX6<-==MpZ6+y&v_HL1~P?OWnTu7V0%&K)^<>7c3&<9#&sn&f#mzEm9ghw6Ie*D1?U zWVz5-XnuD`q0-FbHgs?FnVN$7A1zAGS6_8MFDgTD}@W~Ga|;R=rK`g3Jn+JlD^A=*K5`L~saf?l3shRxyV zMX(JW@j!z)n6ZPDrO|XHM#Y+%bZoQ+-6uik3`gcJncEBIy%yw36OBsj^ne$IHbutE z3pE`KVHk%56dK!x)wbCfJ7gKYK>DT}^9+xMQUb-cRJ;!tBa_4;EVTA>UYeSKi>x}g zgn{@N(Po8z5qqn#It!e>tlyt4r(79=e>l&jg06^LE>mi=65@?nXK3CtRxJ!4u0+T= zi?NXQiio*X%{?(CX&_=PV;_}#qYsDDc$n1Bvra$!mbP+xGm6Y)RFt=NAOW4*jjM-q|c<|f;^ zbBhoM=BzLgJ2k>^Bs-qspY{kcAl=5PV;N+;0hvu0rnFhE0ZLU|`4%*NXekt-r@WOI zJKWi<6xmqyIfJ8-)ZkWq9CT8sMBixet@5iS0cuELFi!PFM`tVNQ`hi& zP=U3(Rokaj*pylZpRSC8O)iFy?k2;tV!P3ljWM?ZM(h-4{`xjls6Lg*v2zwZ%f2s{zcXcX z-?UH;(F$UF`1=6L>%VR&li8MV675Ux>^>96DK)#Xx!ly)Ls3)5!8&brCY4jZWbxs9vq1*m8*icv zJ~PfN+VS|qv1$a*ZWC@~%J60McWW^2QjGE~*ihqOjNhzG1+Bn@*z?@7Pzciax@gAc z`-G!M?7l!Xeka>2)U9g2uIp1i@mj?{grv{;2shHqVadTd^Hug_{yp1zi&#&Dr}6<> zIHkYP?Dn?=I8;)bGEBCo!*c^%aQ1_Usg1R3Wcee9}g_=KObozwOp6B zuuttAm_pDxWF_hc*rs_zuI9HXy4NeT41qC=p1(HA$R}XGP#i3Sv*t3ox#PF&_WK7p zQn2OTl&-R$*#HR5Gx`Z|=KEhn`kmhrZP>)oBR`PN#a zL4uZWPn^Mz4WVhPwJ`ho;f2?FXxTTbe>d&=`RbN!^OE^WHekGTLRjGu~Fp|?u68#PNA3&*;R)M{j{Rw zaYq0*%EIe$1*;1mXbWnjKL>yHI;kvQoni+S+G*2r2E>E_J+K6VIpVJGrbO_VIG0~Q zb7sB3k>~U<^y7G;WR8`;zW%+&l~MYh^+gfg=teC1z_n4w-{WyDN~u+Q(xkqg@8-kA{^ZR_ z0gmnuS{$>iVKR7%ZnX;4jx~^f5&zIxUm`fCJ(9T}=A7XR|n6cgn3uLcMhd z3lW64R4Rh5*{a7J%(j@_xG^3lUGX;1abI&Q0sHWGc>hdNHbP81c|3)WKj2 zXpvp@diQgCQ}!Zi{q+{+o0QCVg*>D!k;}Z=TD?rylcSTv8BXug34mv*qNA6n|7dcVaI8J;eziQK+8SJ(Y~uh6!ev4u{ z=B;KW=gl5AOQz=mD=vMw9_z8m*=9>hkBjFi5Zq78I<32q-GX?67ouZBi7jjTo=Pj0 zVJ$iJSbz2o?I%vxLq=i0)>C}gRJI!no!dH!P&k0c)xLTdnu*NBh`OSQnOZZkSW6Y&-{! zInSr<)QND(f>DSAUC7cWFO=bGSMeBurR4P$`15f|OvTJHI10?cU~cQ8y^@K+5?zq6_0Hcw&dGq0Z=)j4+9zEEsXDpJ-G_Qu*Lx|r)MVq*u5ll9z@rDcH6X!w zv{?#EVfWLz%A_||TnOCb@7eK1yS!`+T|C~*np%cCx@t4= z{SGx&cnlBZ09d(qn`?oFVH9NjtV@5=H)qF?q_ zixU(ReaG{qvta4#kTDKAW#D31LVIfUHAEtgl<|EQQE$>rbOP<*R*vQgY8t><;qSIA z>nr^zcdY9N8e)^MUF&;@by_v|#{eSH`giDYaoh>^yPqHNScQWsqEL9^LKgOKNzWfn zH3|96+pgOq-5Tfp&9o-iS@C!IQt%nEOAx8;<((ZO69!Z$-)v%;>LJc@)ERpnByylH z&S+dG9S2BGqXUW;Q(>?El--u?K^$x7o88c~Ms)RGZC08}L&&KrE$ig-VE|ABc}Yed zHb((~`0OZMf@LO=bt^0S$mxX~KXDNLLBOK&#r$9O&lb+e8I1TKN2$Q*SK`z{G0I53p_gR9;y>i@;6|9`%`$5SPb V5$GNim;|i|n3-CcR2$uV@V_5(L_`1p literal 0 HcmV?d00001 diff --git a/microsite/static/img/partner-logo-thoughtworks.png b/microsite/static/img/partner-logo-thoughtworks.png new file mode 100644 index 0000000000000000000000000000000000000000..53c3bc2a9ed1e0d7a47b6543bde88442026c15d5 GIT binary patch literal 7492 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 Date: Tue, 21 Jun 2022 17:39:38 +0200 Subject: [PATCH 116/401] Cleans up page code Signed-off-by: Jorge Lainfiesta --- microsite/pages/en/community.js | 135 +++++++++++--------------------- 1 file changed, 45 insertions(+), 90 deletions(-) diff --git a/microsite/pages/en/community.js b/microsite/pages/en/community.js index 3d32226c60..15db10e420 100644 --- a/microsite/pages/en/community.js +++ b/microsite/pages/en/community.js @@ -8,8 +8,6 @@ const React = require('react'); const Components = require(`${process.cwd()}/core/Components.js`); const Block = Components.Block; -const Breakpoint = Components.Breakpoint; -const ActionBlock = Components.ActionBlock; const BulletLine = Components.BulletLine; const PARTNERS = [{ @@ -41,7 +39,7 @@ const Background = props => { Backstage Community Join - the vibrant community around the Backstage project through social media and different meetups. To ensure you have a welcoming environment, we follow {' '} + the vibrant community around Backstage through social media and different meetups. To ensure you have a welcoming environment, we follow {' '} {' '} CNCF Code of Conduct @@ -49,20 +47,20 @@ const Background = props => { in everything we do. - + - - Get started in our community! + + Get started in our community!