From d8c7e5eb6502d03ef79f9f085351fba975cdf0c9 Mon Sep 17 00:00:00 2001 From: Josh Uvi Date: Wed, 7 Feb 2024 12:30:04 +0000 Subject: [PATCH 001/204] feat: adds last commit and its status to the github-pull-requests-board Signed-off-by: Josh Uvi --- packages/app/package.json | 1 + packages/app/src/components/catalog/EntityPage.tsx | 4 ++++ .../src/api/useGetPullRequestDetails.ts | 9 +++++++++ .../src/components/Card/Card.tsx | 5 ++++- .../src/components/Card/CardHeader.tsx | 10 +++++++++- .../EntityTeamPullRequestsContent.tsx | 5 +++++ .../components/PullRequestCard/PullRequestCard.tsx | 5 ++++- .../github-pull-requests-board/src/utils/types.tsx | 11 +++++++++++ yarn.lock | 3 ++- 9 files changed, 49 insertions(+), 4 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 43b12a1cb7..769a28eec1 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -45,6 +45,7 @@ "@backstage/plugin-gcalendar": "workspace:^", "@backstage/plugin-gcp-projects": "workspace:^", "@backstage/plugin-github-actions": "workspace:^", + "@backstage/plugin-github-pull-requests-board": "workspace:^", "@backstage/plugin-gocd": "workspace:^", "@backstage/plugin-graphiql": "workspace:^", "@backstage/plugin-home": "workspace:^", diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 4d5f65cbbe..b0b227be92 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -185,6 +185,7 @@ import { isLinguistAvailable, EntityLinguistCard, } from '@backstage/plugin-linguist'; +import { EntityTeamPullRequestsContent } from '@backstage/plugin-github-pull-requests-board'; const customEntityFilterKind = ['Component', 'API', 'System']; @@ -809,6 +810,9 @@ const groupPage = ( + + + ); diff --git a/plugins/github-pull-requests-board/src/api/useGetPullRequestDetails.ts b/plugins/github-pull-requests-board/src/api/useGetPullRequestDetails.ts index 75f4d37cf4..8b919ce3d6 100644 --- a/plugins/github-pull-requests-board/src/api/useGetPullRequestDetails.ts +++ b/plugins/github-pull-requests-board/src/api/useGetPullRequestDetails.ts @@ -57,6 +57,15 @@ export const useGetPullRequestDetails = () => { state } } + commits(last: 1) { + nodes { + commit { + statusCheckRollup { + state + } + } + } + } mergeable state reviewDecision diff --git a/plugins/github-pull-requests-board/src/components/Card/Card.tsx b/plugins/github-pull-requests-board/src/components/Card/Card.tsx index 4fe833fa67..09c6352b60 100644 --- a/plugins/github-pull-requests-board/src/components/Card/Card.tsx +++ b/plugins/github-pull-requests-board/src/components/Card/Card.tsx @@ -16,7 +16,7 @@ import React, { PropsWithChildren, FunctionComponent } from 'react'; import { Box, Paper, CardActionArea } from '@material-ui/core'; import CardHeader from './CardHeader'; -import { Label } from '../../utils/types'; +import { Label, Status } from '../../utils/types'; type Props = { title: string; @@ -29,6 +29,7 @@ type Props = { isDraft: boolean; repositoryIsArchived: boolean; labels?: Label[]; + status: Status; }; const Card: FunctionComponent> = ( @@ -45,6 +46,7 @@ const Card: FunctionComponent> = ( isDraft, repositoryIsArchived, labels, + status, children, } = props; @@ -63,6 +65,7 @@ const Card: FunctionComponent> = ( isDraft={isDraft} repositoryIsArchived={repositoryIsArchived} labels={labels} + status={status} /> {children} diff --git a/plugins/github-pull-requests-board/src/components/Card/CardHeader.tsx b/plugins/github-pull-requests-board/src/components/Card/CardHeader.tsx index c59349b95f..98d53f781c 100644 --- a/plugins/github-pull-requests-board/src/components/Card/CardHeader.tsx +++ b/plugins/github-pull-requests-board/src/components/Card/CardHeader.tsx @@ -19,7 +19,7 @@ import { getElapsedTime } from '../../utils/functions'; import { UserHeader } from '../UserHeader'; import { DraftPrIcon } from '../icons/DraftPr'; import UnarchiveIcon from '@material-ui/icons/Unarchive'; -import { Label } from '../../utils/types'; +import { Label, Status } from '../../utils/types'; import { useFormClasses } from './styles'; type Props = { @@ -32,6 +32,7 @@ type Props = { isDraft: boolean; repositoryIsArchived: boolean; labels?: Label[]; + status: Status; }; const CardHeader: FunctionComponent = (props: Props) => { @@ -47,6 +48,7 @@ const CardHeader: FunctionComponent = (props: Props) => { isDraft, repositoryIsArchived, labels, + status: commitStatus, } = props; return ( @@ -86,6 +88,12 @@ const CardHeader: FunctionComponent = (props: Props) => { )} + + + Commit Status:{' '} + {commitStatus.commit.statusCheckRollup.state} + + {labels && ( {labels.map(data => { diff --git a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.tsx b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.tsx index f901acb119..6109ddb63c 100644 --- a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.tsx +++ b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.tsx @@ -87,6 +87,9 @@ const EntityTeamPullRequestsContent = ( return ; } + // eslint-disable-next-line no-console + console.log('pull - ', pullRequests); + return ( {pullRequests.length ? ( @@ -103,6 +106,7 @@ const EntityTeamPullRequestsContent = ( author, url, latestReviews, + commits, repository, isDraft, labels, @@ -125,6 +129,7 @@ const EntityTeamPullRequestsContent = ( author={author} url={url} reviews={latestReviews.nodes} + status={commits.nodes} repositoryName={repository.name} repositoryIsArchived={repository.isArchived} isDraft={isDraft} diff --git a/plugins/github-pull-requests-board/src/components/PullRequestCard/PullRequestCard.tsx b/plugins/github-pull-requests-board/src/components/PullRequestCard/PullRequestCard.tsx index e9ad2aeab6..8736644f09 100644 --- a/plugins/github-pull-requests-board/src/components/PullRequestCard/PullRequestCard.tsx +++ b/plugins/github-pull-requests-board/src/components/PullRequestCard/PullRequestCard.tsx @@ -19,7 +19,7 @@ import { getChangeRequests, getCommentedReviews, } from '../../utils/functions'; -import { Reviews, Author, Label } from '../../utils/types'; +import { Reviews, Author, Label, Status } from '../../utils/types'; import { Card } from '../Card'; import { UserHeaderList } from '../UserHeaderList'; @@ -30,6 +30,7 @@ type Props = { author: Author; url: string; reviews: Reviews; + status: Status; repositoryName: string; repositoryIsArchived: boolean; isDraft: boolean; @@ -44,6 +45,7 @@ const PullRequestCard: FunctionComponent = (props: Props) => { author, url, reviews, + status, repositoryName, repositoryIsArchived, isDraft, @@ -66,6 +68,7 @@ const PullRequestCard: FunctionComponent = (props: Props) => { isDraft={isDraft} repositoryIsArchived={repositoryIsArchived} labels={labels} + status={status} > {!!approvedReviews.length && ( Date: Wed, 7 Feb 2024 11:43:08 +0000 Subject: [PATCH 002/204] Ability to fetch the README file from a different AZD path, using an annotation on the entity Signed-off-by: David Roberts --- .../azure-devops-backend/src/api/AzureDevOpsApi.ts | 3 ++- plugins/azure-devops-backend/src/service/router.ts | 2 ++ plugins/azure-devops-common/src/constants.ts | 2 ++ plugins/azure-devops-common/src/types.ts | 1 + plugins/azure-devops/src/api/AzureDevOpsClient.ts | 3 +++ plugins/azure-devops/src/hooks/useReadme.ts | 11 +++++++++-- .../src/utils/getAnnotationValuesFromEntity.ts | 7 +++++++ 7 files changed, 26 insertions(+), 3 deletions(-) diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts index d50888006b..cc5cad92b7 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts @@ -527,11 +527,12 @@ export class AzureDevOpsApi { org: string, project: string, repo: string, + path: string, ): Promise<{ url: string; content: string; }> { - const url = buildEncodedUrl(host, org, project, repo, 'README.md'); + const url = buildEncodedUrl(host, org, project, repo, path); const response = await this.urlReader.readUrl(url); const buffer = await response.buffer(); const content = await replaceReadme( diff --git a/plugins/azure-devops-backend/src/service/router.ts b/plugins/azure-devops-backend/src/service/router.ts index 9a71c8f03d..a00b0ce910 100644 --- a/plugins/azure-devops-backend/src/service/router.ts +++ b/plugins/azure-devops-backend/src/service/router.ts @@ -216,12 +216,14 @@ export async function createRouter( req.query.host?.toString() ?? config.getString('azureDevOps.host'); const org = req.query.org?.toString() ?? config.getString('azureDevOps.organization'); + const path = req.query.path?.toString() ?? 'README.md'; const { projectName, repoName } = req.params; const readme = await azureDevOpsApi.getReadme( host, org, projectName, repoName, + path, ); res.status(200).json(readme); }); diff --git a/plugins/azure-devops-common/src/constants.ts b/plugins/azure-devops-common/src/constants.ts index 5cc6b54592..05eb430cc9 100644 --- a/plugins/azure-devops-common/src/constants.ts +++ b/plugins/azure-devops-common/src/constants.ts @@ -22,6 +22,8 @@ export const AZURE_DEVOPS_HOST_ORG_ANNOTATION = 'dev.azure.com/host-org'; /** @public */ export const AZURE_DEVOPS_PROJECT_ANNOTATION = 'dev.azure.com/project'; /** @public */ +export const AZURE_DEVOPS_README_ANNOTATION = 'dev.azure.com/readme-path'; +/** @public */ export const AZURE_DEVOPS_REPO_ANNOTATION = 'dev.azure.com/project-repo'; /** @public */ export const AZURE_DEVOPS_DEFAULT_TOP: number = 10; diff --git a/plugins/azure-devops-common/src/types.ts b/plugins/azure-devops-common/src/types.ts index afb6083867..0390bb1f6e 100644 --- a/plugins/azure-devops-common/src/types.ts +++ b/plugins/azure-devops-common/src/types.ts @@ -212,6 +212,7 @@ export interface ReadmeConfig { repo: string; host?: string; org?: string; + path?: string; } /** @public */ diff --git a/plugins/azure-devops/src/api/AzureDevOpsClient.ts b/plugins/azure-devops/src/api/AzureDevOpsClient.ts index 76e38b064c..a10b10ff14 100644 --- a/plugins/azure-devops/src/api/AzureDevOpsClient.ts +++ b/plugins/azure-devops/src/api/AzureDevOpsClient.ts @@ -189,6 +189,9 @@ export class AzureDevOpsClient implements AzureDevOpsApi { if (opts.org) { queryString.append('org', opts.org); } + if (opts.path) { + queryString.append('path', opts.path); + } return await this.get( `readme/${encodeURIComponent(opts.project)}/${encodeURIComponent( opts.repo, diff --git a/plugins/azure-devops/src/hooks/useReadme.ts b/plugins/azure-devops/src/hooks/useReadme.ts index 348b4219f0..9aa376e0bd 100644 --- a/plugins/azure-devops/src/hooks/useReadme.ts +++ b/plugins/azure-devops/src/hooks/useReadme.ts @@ -30,8 +30,15 @@ export function useReadme(entity: Entity): { const api = useApi(azureDevOpsApiRef); const { value, loading, error } = useAsync(() => { - const { project, repo, host, org } = getAnnotationValuesFromEntity(entity); - return api.getReadme({ project, repo: repo as string, host, org }); + const { project, repo, host, org, readmePath } = + getAnnotationValuesFromEntity(entity); + return api.getReadme({ + project, + repo: repo as string, + host, + org, + path: readmePath, + }); }, [api]); return { diff --git a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts index 7533c473ed..fb38a531e1 100644 --- a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts @@ -18,6 +18,7 @@ import { Entity } from '@backstage/catalog-model'; import { AZURE_DEVOPS_PROJECT_ANNOTATION, AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION, + AZURE_DEVOPS_README_ANNOTATION, AZURE_DEVOPS_REPO_ANNOTATION, AZURE_DEVOPS_HOST_ORG_ANNOTATION, } from '@backstage/plugin-azure-devops-common'; @@ -28,6 +29,7 @@ export function getAnnotationValuesFromEntity(entity: Entity): { definition?: string; host?: string; org?: string; + readmePath?: string; } { const hostOrg = getHostOrg(entity.metadata.annotations); const projectRepo = getProjectRepo(entity.metadata.annotations); @@ -35,12 +37,15 @@ export function getAnnotationValuesFromEntity(entity: Entity): { entity.metadata.annotations?.[AZURE_DEVOPS_PROJECT_ANNOTATION]; const definition = entity.metadata.annotations?.[AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION]; + const readmePath = + entity.metadata.annotations?.[AZURE_DEVOPS_README_ANNOTATION]; if (definition) { if (project) { return { project, definition, + readmePath: readmePath, ...hostOrg, }; } @@ -49,6 +54,7 @@ export function getAnnotationValuesFromEntity(entity: Entity): { project: projectRepo.project, repo: projectRepo.repo, definition, + readmePath: readmePath, ...hostOrg, }; } @@ -60,6 +66,7 @@ export function getAnnotationValuesFromEntity(entity: Entity): { return { project: projectRepo.project, repo: projectRepo.repo, + readmePath: readmePath, ...hostOrg, }; } From 9fdb86a91f5d24ad9e12b756dcf53eb614c8a7a4 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Wed, 7 Feb 2024 11:56:28 +0000 Subject: [PATCH 003/204] Document the changes Signed-off-by: David Roberts --- .changeset/itchy-news-drive.md | 15 +++++++++++++++ plugins/azure-devops/README.md | 10 +++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 .changeset/itchy-news-drive.md diff --git a/.changeset/itchy-news-drive.md b/.changeset/itchy-news-drive.md new file mode 100644 index 0000000000..b5bcdaec73 --- /dev/null +++ b/.changeset/itchy-news-drive.md @@ -0,0 +1,15 @@ +--- +'@backstage/plugin-azure-devops-backend': minor +'@backstage/plugin-azure-devops-common': minor +'@backstage/plugin-azure-devops': minor +--- + +Ability to fetch the README file from a different AZD path. + +Defaults to the current, AZD default behaviour (`README.md` in the root of the git repo); to use a different path, add the annotation `dev.azure.com/readme-path` + +Example: + +```yaml +dev.azure.com/readme-path: /my-path/CHANGELOG.md +``` diff --git a/plugins/azure-devops/README.md b/plugins/azure-devops/README.md index f897588987..c1191b1566 100644 --- a/plugins/azure-devops/README.md +++ b/plugins/azure-devops/README.md @@ -63,13 +63,21 @@ spec: #### Mono repos -If you have multiple entities within a single repo, you will need to specify which pipelines belong to each entity. +If you have multiple entities within a single repo, you will need to specify which pipelines belong to each entity: ```yaml dev.azure.com/project-repo: / dev.azure.com/build-definition: ``` +...and which README file belongs to each entity. + +Example: + +```yaml +dev.azure.com/readme-path: //.md +``` + #### Pipeline in different project to repo If your pipeline is in a different project to the source code, you will need to specify this in the project annotation. From 0081922e4710095ca156606a0c7f4cb6db3c410b Mon Sep 17 00:00:00 2001 From: David Roberts Date: Wed, 7 Feb 2024 12:11:16 +0000 Subject: [PATCH 004/204] Update the API reports Signed-off-by: David Roberts --- plugins/azure-devops-backend/api-report.md | 1 + plugins/azure-devops-common/api-report.md | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/plugins/azure-devops-backend/api-report.md b/plugins/azure-devops-backend/api-report.md index 6dead7f11b..d98d178cff 100644 --- a/plugins/azure-devops-backend/api-report.md +++ b/plugins/azure-devops-backend/api-report.md @@ -124,6 +124,7 @@ export class AzureDevOpsApi { org: string, project: string, repo: string, + path: string, ): Promise<{ url: string; content: string; diff --git a/plugins/azure-devops-common/api-report.md b/plugins/azure-devops-common/api-report.md index 5bd1674fc8..e8343178e3 100644 --- a/plugins/azure-devops-common/api-report.md +++ b/plugins/azure-devops-common/api-report.md @@ -16,6 +16,9 @@ export const AZURE_DEVOPS_HOST_ORG_ANNOTATION = 'dev.azure.com/host-org'; // @public (undocumented) export const AZURE_DEVOPS_PROJECT_ANNOTATION = 'dev.azure.com/project'; +// @public (undocumented) +export const AZURE_DEVOPS_README_ANNOTATION = 'dev.azure.com/readme-path'; + // @public (undocumented) export const AZURE_DEVOPS_REPO_ANNOTATION = 'dev.azure.com/project-repo'; @@ -228,6 +231,8 @@ export interface ReadmeConfig { // (undocumented) org?: string; // (undocumented) + path?: string; + // (undocumented) project: string; // (undocumented) repo: string; From ded6e4df61f1b6b367248d15417f039321409cbb Mon Sep 17 00:00:00 2001 From: David Roberts Date: Wed, 7 Feb 2024 13:39:01 +0000 Subject: [PATCH 005/204] Update the tests to reflect the new parameter and default Signed-off-by: David Roberts --- .../src/service/router.test.ts | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/plugins/azure-devops-backend/src/service/router.test.ts b/plugins/azure-devops-backend/src/service/router.test.ts index 65cb27271e..9e8f968826 100644 --- a/plugins/azure-devops-backend/src/service/router.test.ts +++ b/plugins/azure-devops-backend/src/service/router.test.ts @@ -482,7 +482,7 @@ describe('createRouter', () => { }); describe('GET /readme/:projectName/:repoName', () => { - it('fetches readme file', async () => { + it('fetches default readme file', async () => { const content = getReadmeMock(); const url = `https://host.com/myOrg/myProject/_git/myRepo?path=README.md`; @@ -491,14 +491,41 @@ describe('createRouter', () => { url, }); + const response = await request(app).get('/readme/myProject/myRepo'); + expect(azureDevOpsApi.getReadme).toHaveBeenCalledWith( + 'host.com', + 'myOrg', + 'myProject', + 'myRepo', + 'README.md', + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + content, + url, + }); + }); + }); + + describe('GET /readme/:projectName/:repoName with readme path', () => { + it('fetches specified readme file', async () => { + const content = getReadmeMock(); + const url = `https://host.com/myOrg/myProject/_git/myRepo?path=README_NOT_DEFAULT.md`; + + azureDevOpsApi.getReadme.mockResolvedValueOnce({ + content, + url, + }); + const response = await request(app).get( - '/readme/myProject/myRepo?path=README.md', + '/readme/myProject/myRepo?path=README_NOT_DEFAULT.md', ); expect(azureDevOpsApi.getReadme).toHaveBeenCalledWith( 'host.com', 'myOrg', 'myProject', 'myRepo', + 'README_NOT_DEFAULT.md', ); expect(response.status).toEqual(200); expect(response.body).toEqual({ From d294557f5ec2727e3485fb2f4936e1bd312bade8 Mon Sep 17 00:00:00 2001 From: David Roberts <61826520+DavidRobertsOrbis@users.noreply.github.com> Date: Mon, 12 Feb 2024 11:27:53 +0000 Subject: [PATCH 006/204] Better example annotation value Co-authored-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Signed-off-by: David Roberts <61826520+DavidRobertsOrbis@users.noreply.github.com> --- .changeset/itchy-news-drive.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/itchy-news-drive.md b/.changeset/itchy-news-drive.md index b5bcdaec73..4cabc647f0 100644 --- a/.changeset/itchy-news-drive.md +++ b/.changeset/itchy-news-drive.md @@ -11,5 +11,5 @@ Defaults to the current, AZD default behaviour (`README.md` in the root of the g Example: ```yaml -dev.azure.com/readme-path: /my-path/CHANGELOG.md +dev.azure.com/readme-path: /my-path/README.md ``` From ad528cc4957f45eab617c7f551efe57a83019ff9 Mon Sep 17 00:00:00 2001 From: David Roberts <61826520+DavidRobertsOrbis@users.noreply.github.com> Date: Mon, 12 Feb 2024 11:28:39 +0000 Subject: [PATCH 007/204] Better instructions for implementation Co-authored-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Signed-off-by: David Roberts <61826520+DavidRobertsOrbis@users.noreply.github.com> --- plugins/azure-devops/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/azure-devops/README.md b/plugins/azure-devops/README.md index c1191b1566..6521f0e237 100644 --- a/plugins/azure-devops/README.md +++ b/plugins/azure-devops/README.md @@ -63,7 +63,7 @@ spec: #### Mono repos -If you have multiple entities within a single repo, you will need to specify which pipelines belong to each entity: +If you have multiple entities within a single repo, you will need to specify which pipelines belong to each entity, like this: ```yaml dev.azure.com/project-repo: / From b0002dfca3ba023ab0753e9fc3804ca93826e4d1 Mon Sep 17 00:00:00 2001 From: David Roberts <61826520+DavidRobertsOrbis@users.noreply.github.com> Date: Mon, 12 Feb 2024 13:22:53 +0000 Subject: [PATCH 008/204] Replace acronym AZD with the full name Co-authored-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Signed-off-by: David Roberts <61826520+DavidRobertsOrbis@users.noreply.github.com> --- .changeset/itchy-news-drive.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/itchy-news-drive.md b/.changeset/itchy-news-drive.md index 4cabc647f0..44963ec773 100644 --- a/.changeset/itchy-news-drive.md +++ b/.changeset/itchy-news-drive.md @@ -6,7 +6,7 @@ Ability to fetch the README file from a different AZD path. -Defaults to the current, AZD default behaviour (`README.md` in the root of the git repo); to use a different path, add the annotation `dev.azure.com/readme-path` +Defaults to the current, Azure DevOps default behaviour (`README.md` in the root of the git repo); to use a different path, add the annotation `dev.azure.com/readme-path` Example: From fed13da396381f3678becf864ee5d6f8ed781c3b Mon Sep 17 00:00:00 2001 From: David Roberts <61826520+DavidRobertsOrbis@users.noreply.github.com> Date: Mon, 12 Feb 2024 13:23:05 +0000 Subject: [PATCH 009/204] Replace acronym AZD with the full name Co-authored-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Signed-off-by: David Roberts <61826520+DavidRobertsOrbis@users.noreply.github.com> --- .changeset/itchy-news-drive.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/itchy-news-drive.md b/.changeset/itchy-news-drive.md index 44963ec773..97cca5b155 100644 --- a/.changeset/itchy-news-drive.md +++ b/.changeset/itchy-news-drive.md @@ -4,7 +4,7 @@ '@backstage/plugin-azure-devops': minor --- -Ability to fetch the README file from a different AZD path. +Ability to fetch the README file from a different Azure DevOps path. Defaults to the current, Azure DevOps default behaviour (`README.md` in the root of the git repo); to use a different path, add the annotation `dev.azure.com/readme-path` From d8294546d8edca6f4c87d2027510d4bfc71ccdb6 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Mon, 12 Feb 2024 15:51:32 +0000 Subject: [PATCH 010/204] add a test for subfolder as well as filename Signed-off-by: David Roberts --- .../src/service/router.test.ts | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/plugins/azure-devops-backend/src/service/router.test.ts b/plugins/azure-devops-backend/src/service/router.test.ts index 9e8f968826..8793860708 100644 --- a/plugins/azure-devops-backend/src/service/router.test.ts +++ b/plugins/azure-devops-backend/src/service/router.test.ts @@ -482,7 +482,7 @@ describe('createRouter', () => { }); describe('GET /readme/:projectName/:repoName', () => { - it('fetches default readme file', async () => { + it('fetches default default readme file', async () => { const content = getReadmeMock(); const url = `https://host.com/myOrg/myProject/_git/myRepo?path=README.md`; @@ -507,7 +507,7 @@ describe('createRouter', () => { }); }); - describe('GET /readme/:projectName/:repoName with readme path', () => { + describe('GET /readme/:projectName/:repoName with readme filename', () => { it('fetches specified readme file', async () => { const content = getReadmeMock(); const url = `https://host.com/myOrg/myProject/_git/myRepo?path=README_NOT_DEFAULT.md`; @@ -534,6 +534,34 @@ describe('createRouter', () => { }); }); }); + + describe('GET /readme/:projectName/:repoName with readme path', () => { + it('fetches specified readme file from subfolder', async () => { + const content = getReadmeMock(); + const url = `https://host.com/myOrg/myProject/_git/myRepo?path=/my-path/README.md`; + + azureDevOpsApi.getReadme.mockResolvedValueOnce({ + content, + url, + }); + + const response = await request(app).get( + '/readme/myProject/myRepo?path=/my-path/README.md', + ); + expect(azureDevOpsApi.getReadme).toHaveBeenCalledWith( + 'host.com', + 'myOrg', + 'myProject', + 'myRepo', + '/my-path/README.md', + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + content, + url, + }); + }); + }); }); function getReadmeMock() { From 5bda681a1cc8ee5dd24753c1a5450af1073106b7 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Mon, 12 Feb 2024 16:13:24 +0000 Subject: [PATCH 011/204] add tests for readme extraction Signed-off-by: David Roberts --- .../getAnnotationValuesFromEntity.test.ts | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts index 9325a79039..e196e9dfc7 100644 --- a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts @@ -52,6 +52,7 @@ describe('getAnnotationValuesFromEntity', () => { project: 'projectName', repo: 'repoName', definition: undefined, + readmePath: undefined, host: undefined, org: undefined, }); @@ -149,6 +150,7 @@ describe('getAnnotationValuesFromEntity', () => { project: 'projectName', repo: undefined, definition: 'buildDefinitionName', + readmePath: undefined, host: undefined, org: undefined, }); @@ -220,6 +222,7 @@ describe('getAnnotationValuesFromEntity', () => { project: 'projectName', repo: 'repoName', definition: undefined, + readmePath: undefined, host: 'hostName', org: 'organizationName', }); @@ -246,6 +249,7 @@ describe('getAnnotationValuesFromEntity', () => { project: 'projectName', repo: undefined, definition: 'buildDefinitionName', + readmePath: undefined, host: 'hostName', org: 'organizationName', }); @@ -344,6 +348,7 @@ describe('getAnnotationValuesFromEntity', () => { project: 'projectName', repo: 'repoName', definition: undefined, + readmePath: undefined, host: 'company.com/tfs', org: 'organizationName', }); @@ -417,6 +422,7 @@ describe('getAnnotationValuesFromEntity', () => { project: 'projectName', repo: 'repoName', definition: 'buildDefinitionName', + readmePath: undefined, host: undefined, org: undefined, }); @@ -443,6 +449,87 @@ describe('getAnnotationValuesFromEntity', () => { project: 'projectName', repo: undefined, definition: 'buildDefinitionName', + readmePath: undefined, + host: undefined, + org: undefined, + }); + }); + }); + + describe('definition, project and readme', () => { + it('returns with the readme path', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project': 'projectName', + 'dev.azure.com/build-definition': 'buildDefinitionName', + 'dev.azure.com/readme-path': 'readme/path.md', + }, + }, + }; + const values = getAnnotationValuesFromEntity(entity); + expect(values).toEqual({ + project: 'projectName', + repo: undefined, + definition: 'buildDefinitionName', + readmePath: 'readme/path.md', + host: undefined, + org: undefined, + }); + }); + }); + + describe('definition, projectRepo and readme', () => { + it('returns with the readme path', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'projectName/repoName', + 'dev.azure.com/build-definition': 'buildDefinitionName', + 'dev.azure.com/readme-path': 'readme/path.md', + }, + }, + }; + const values = getAnnotationValuesFromEntity(entity); + expect(values).toEqual({ + project: 'projectName', + repo: 'repoName', + definition: 'buildDefinitionName', + readmePath: 'readme/path.md', + host: undefined, + org: undefined, + }); + }); + }); + + describe('projectRepo and readme', () => { + it('returns with the readme path', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'projectName/repoName', + 'dev.azure.com/readme-path': 'readme/path.md', + }, + }, + }; + const values = getAnnotationValuesFromEntity(entity); + expect(values).toEqual({ + project: 'projectName', + repo: 'repoName', + definition: undefined, + readmePath: 'readme/path.md', host: undefined, org: undefined, }); From a29f6833bb82476641d54af959fe599f92531b1f Mon Sep 17 00:00:00 2001 From: David Roberts Date: Mon, 12 Feb 2024 16:31:36 +0000 Subject: [PATCH 012/204] explicit rejection of bad parameter types Signed-off-by: David Roberts --- .../azure-devops-backend/src/service/router.test.ts | 10 ++++++++++ plugins/azure-devops-backend/src/service/router.ts | 13 ++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/plugins/azure-devops-backend/src/service/router.test.ts b/plugins/azure-devops-backend/src/service/router.test.ts index 8793860708..7bf558612f 100644 --- a/plugins/azure-devops-backend/src/service/router.test.ts +++ b/plugins/azure-devops-backend/src/service/router.test.ts @@ -562,6 +562,16 @@ describe('createRouter', () => { }); }); }); + + describe('GET /readme/:projectName/:repoName with a bad readme path (multiple values)', () => { + it('throws InputError', async () => { + const response = await request(app).get( + '/readme/myProject/myRepo?path=1&path=2', + ); + expect(azureDevOpsApi.getReadme).not.toHaveBeenCalled(); + expect(response.status).toEqual(400); + }); + }); }); function getReadmeMock() { diff --git a/plugins/azure-devops-backend/src/service/router.ts b/plugins/azure-devops-backend/src/service/router.ts index a00b0ce910..230ff08c8b 100644 --- a/plugins/azure-devops-backend/src/service/router.ts +++ b/plugins/azure-devops-backend/src/service/router.ts @@ -26,6 +26,7 @@ import { Logger } from 'winston'; import { PullRequestsDashboardProvider } from '../api/PullRequestsDashboardProvider'; import Router from 'express-promise-router'; import { errorHandler, UrlReader } from '@backstage/backend-common'; +import { InputError } from '@backstage/errors'; import express from 'express'; const DEFAULT_TOP = 10; @@ -216,7 +217,17 @@ export async function createRouter( req.query.host?.toString() ?? config.getString('azureDevOps.host'); const org = req.query.org?.toString() ?? config.getString('azureDevOps.organization'); - const path = req.query.path?.toString() ?? 'README.md'; + let path = req.query.path; + + if (path === undefined) { + // if the annotation is missing, default to the previous behaviour (look for README.md in the root of the repo) + path = 'README.md'; + } + + if (typeof path !== 'string') { + throw new InputError('Invalid path param'); + } + const { projectName, repoName } = req.params; const readme = await azureDevOpsApi.getReadme( host, From 184c8877c99a4d2338e82cd1be8bed72eb9c4784 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Mon, 12 Feb 2024 16:49:47 +0000 Subject: [PATCH 013/204] throw if we are passed the empty string - we know this won't work Signed-off-by: David Roberts --- plugins/azure-devops-backend/src/service/router.test.ts | 8 ++++++++ plugins/azure-devops-backend/src/service/router.ts | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/plugins/azure-devops-backend/src/service/router.test.ts b/plugins/azure-devops-backend/src/service/router.test.ts index 7bf558612f..c12b13a5aa 100644 --- a/plugins/azure-devops-backend/src/service/router.test.ts +++ b/plugins/azure-devops-backend/src/service/router.test.ts @@ -572,6 +572,14 @@ describe('createRouter', () => { expect(response.status).toEqual(400); }); }); + + describe('GET /readme/:projectName/:repoName with a bad readme path (empty string)', () => { + it('throws InputError', async () => { + const response = await request(app).get('/readme/myProject/myRepo?path='); + expect(azureDevOpsApi.getReadme).not.toHaveBeenCalled(); + expect(response.status).toEqual(400); + }); + }); }); function getReadmeMock() { diff --git a/plugins/azure-devops-backend/src/service/router.ts b/plugins/azure-devops-backend/src/service/router.ts index 230ff08c8b..bc13a4eee2 100644 --- a/plugins/azure-devops-backend/src/service/router.ts +++ b/plugins/azure-devops-backend/src/service/router.ts @@ -228,6 +228,10 @@ export async function createRouter( throw new InputError('Invalid path param'); } + if (path === '') { + throw new InputError('If present, the path param should not be empty'); + } + const { projectName, repoName } = req.params; const readme = await azureDevOpsApi.getReadme( host, From 1f60f53d247094475a3f73df25e7da5386109643 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Mon, 12 Feb 2024 17:32:08 +0000 Subject: [PATCH 014/204] add explicit dependency on the errors package Signed-off-by: David Roberts --- plugins/azure-devops-backend/package.json | 1 + yarn.lock | 1 + 2 files changed, 2 insertions(+) diff --git a/plugins/azure-devops-backend/package.json b/plugins/azure-devops-backend/package.json index 41b1839ce4..22889db6a1 100644 --- a/plugins/azure-devops-backend/package.json +++ b/plugins/azure-devops-backend/package.json @@ -32,6 +32,7 @@ "@backstage/backend-plugin-api": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", + "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/plugin-azure-devops-common": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", diff --git a/yarn.lock b/yarn.lock index 0e90b16d15..2415b3a4fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5020,6 +5020,7 @@ __metadata: "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" + "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-azure-devops-common": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" From 59d1a43b468e914d263b53badfb976048bb3162d Mon Sep 17 00:00:00 2001 From: Josh Uvi Date: Tue, 13 Feb 2024 09:36:32 +0000 Subject: [PATCH 015/204] fix: failing test relating to changes made to github-pull-requests-board Signed-off-by: Josh Uvi --- .../EntityTeamPullRequestsCard.test.tsx | 62 ++++- .../EntityTeamPullRequestsContent.test.tsx | 232 ++++++++++++------ 2 files changed, 214 insertions(+), 80 deletions(-) diff --git a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.test.tsx b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.test.tsx index 219e587971..a8ef9a9e2c 100644 --- a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.test.tsx +++ b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; import { EntityTeamPullRequestsCard } from '../EntityTeamPullRequestsCard'; -import { PullRequestsColumn } from '../../utils/types'; +import { PullRequestsColumn, Status } from '../../utils/types'; import { render } from '@testing-library/react'; import { fireEvent } from '@testing-library/react'; @@ -39,6 +39,7 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { repoName: string, isDraft: boolean, isArchived: boolean, + status: Status, ) => { return { id: 'id', @@ -62,6 +63,9 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { labels: { nodes: [], }, + commits: { + nodes: status, + }, isDraft: isDraft, author: { login: authorLogin, @@ -83,6 +87,13 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { 'team-repo', false, false, + { + commit: { + statusCheckRollup: { + state: 'FAILURE', + }, + }, + }, ), buildPullRequest( 'non-team-non-draft-is-archive', @@ -90,6 +101,13 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { 'team-repo', false, true, + { + commit: { + statusCheckRollup: { + state: 'FAILURE', + }, + }, + }, ), buildPullRequest( 'non-team-is-draft-non-archive', @@ -97,6 +115,13 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { 'team-repo', true, false, + { + commit: { + statusCheckRollup: { + state: 'FAILURE', + }, + }, + }, ), buildPullRequest( 'non-team-is-draft-is-archive', @@ -104,6 +129,13 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { 'team-repo', true, true, + { + commit: { + statusCheckRollup: { + state: 'SUCCESS', + }, + }, + }, ), buildPullRequest( 'is-team-non-draft-non-archive', @@ -111,6 +143,13 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { 'non-team-repo', false, false, + { + commit: { + statusCheckRollup: { + state: 'FAILURE', + }, + }, + }, ), buildPullRequest( 'is-team-non-draft-is-archive', @@ -118,6 +157,13 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { 'non-team-repo', false, true, + { + commit: { + statusCheckRollup: { + state: 'FAILURE', + }, + }, + }, ), buildPullRequest( 'is-team-is-draft-non-archive', @@ -125,6 +171,13 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { 'non-team-repo', true, false, + { + commit: { + statusCheckRollup: { + state: 'FAILURE', + }, + }, + }, ), buildPullRequest( 'is-team-is-draft-is-archive', @@ -132,6 +185,13 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { 'non-team-repo', true, true, + { + commit: { + statusCheckRollup: { + state: 'SUCCESS', + }, + }, + }, ), ], }, diff --git a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.test.tsx b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.test.tsx index 35c323a4f1..dc02030f47 100644 --- a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.test.tsx +++ b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; import { EntityTeamPullRequestsContent } from '../EntityTeamPullRequestsContent'; -import { PullRequestsColumn } from '../../utils/types'; +import { PullRequestsColumn, Status } from '../../utils/types'; import { render } from '@testing-library/react'; import { fireEvent } from '@testing-library/react'; @@ -33,13 +33,21 @@ jest.mock('../../hooks/useUserRepositoriesAndTeam', () => { }); jest.mock('../../hooks/usePullRequestsByTeam', () => { - const buildPullRequest = ( - prTitle: string, - authorLogin: string, - repoName: string, - isDraft: boolean, - isArchived: boolean, - ) => { + const buildPullRequest = ({ + prTitle, + authorLogin, + repoName, + isDraft, + isArchived, + status, + }: { + prTitle: string; + authorLogin: string; + repoName: string; + isDraft: boolean; + isArchived: boolean; + status: Status; + }) => { return { id: 'id', title: prTitle, @@ -62,6 +70,9 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { labels: { nodes: [], }, + commits: { + nodes: status, + }, isDraft: isDraft, author: { login: authorLogin, @@ -77,62 +88,118 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { { title: 'column', content: [ - buildPullRequest( - 'non-team-non-draft-non-archive', - 'non-team-member', - 'team-repo', - false, - false, - ), - buildPullRequest( - 'non-team-non-draft-is-archive', - 'non-team-member', - 'team-repo', - false, - true, - ), - buildPullRequest( - 'non-team-is-draft-non-archive', - 'non-team-member', - 'team-repo', - true, - false, - ), - buildPullRequest( - 'non-team-is-draft-is-archive', - 'non-team-member', - 'team-repo', - true, - true, - ), - buildPullRequest( - 'is-team-non-draft-non-archive', - 'team-member', - 'non-team-repo', - false, - false, - ), - buildPullRequest( - 'is-team-non-draft-is-archive', - 'team-member', - 'non-team-repo', - false, - true, - ), - buildPullRequest( - 'is-team-is-draft-non-archive', - 'team-member', - 'non-team-repo', - true, - false, - ), - buildPullRequest( - 'is-team-is-draft-is-archive', - 'team-member', - 'non-team-repo', - true, - true, - ), + buildPullRequest({ + prTitle: 'non-team-non-draft-non-archive', + authorLogin: 'non-team-member', + repoName: 'team-repo', + isDraft: false, + isArchived: false, + status: { + commit: { + statusCheckRollup: { + state: 'FAILURE', + }, + }, + }, + }), + buildPullRequest({ + prTitle: 'non-team-non-draft-is-archive', + authorLogin: 'non-team-member', + repoName: 'team-repo', + isDraft: false, + isArchived: true, + status: { + commit: { + statusCheckRollup: { + state: 'FAILURE', + }, + }, + }, + }), + buildPullRequest({ + prTitle: 'non-team-is-draft-non-archive', + authorLogin: 'non-team-member', + repoName: 'team-repo', + isDraft: true, + isArchived: false, + status: { + commit: { + statusCheckRollup: { + state: 'FAILURE', + }, + }, + }, + }), + buildPullRequest({ + prTitle: 'non-team-is-draft-is-archive', + authorLogin: 'non-team-member', + repoName: 'team-repo', + isDraft: true, + isArchived: true, + status: { + commit: { + statusCheckRollup: { + state: 'SUCCESS', + }, + }, + }, + }), + buildPullRequest({ + prTitle: 'is-team-non-draft-non-archive', + authorLogin: 'team-member', + repoName: 'non-team-repo', + isDraft: false, + isArchived: false, + status: { + commit: { + statusCheckRollup: { + state: 'FAILURE', + }, + }, + }, + }), + buildPullRequest({ + prTitle: 'is-team-non-draft-is-archive', + authorLogin: 'team-member', + repoName: 'non-team-repo', + isDraft: false, + isArchived: true, + status: { + commit: { + statusCheckRollup: { + state: 'FAILURE', + }, + }, + }, + }), + buildPullRequest({ + prTitle: 'is-team-is-draft-non-archive', + authorLogin: 'team-member', + repoName: 'non-team-repo', + isDraft: true, + isArchived: false, + status: { + commit: { + statusCheckRollup: { + state: 'FAILURE', + }, + }, + }, + }), + buildPullRequest({ + prTitle: 'is-team-is-draft-is-archive', + authorLogin: 'team-member', + repoName: 'non-team-repo', + isDraft: true, + isArchived: true, + status: { + commit: { + statusCheckRollup: { + state: 'SUCCESS', + }, + }, + }, + }), ], }, ]; @@ -152,7 +219,7 @@ describe('EntityTeamPullRequestsContent', () => { describe('non-team PRs', () => { describe('non-draft PRs', () => { it('should show non-team PRs for un-archived repos when archived option is not checked', async () => { - const { getByText, getAllByText, queryAllByTitle } = await render( + const { getByText, getAllByText, queryAllByTitle } = render( , ); expect(getByText('non-team-non-draft-non-archive')).toBeInTheDocument(); @@ -162,8 +229,9 @@ describe('EntityTeamPullRequestsContent', () => { }); it('should show non-team PRs for archived repos when archived option is checked', async () => { - const { getByText, getAllByText, getByTitle, queryAllByTitle } = - await render(); + const { getByText, getAllByText, getByTitle, queryAllByTitle } = render( + , + ); const archiveToggle = getByTitle('Show archived repos'); fireEvent.click(archiveToggle); expect(getByText('non-team-non-draft-is-archive')).toBeInTheDocument(); @@ -175,8 +243,9 @@ describe('EntityTeamPullRequestsContent', () => { describe('draft PRs', () => { it('should show draft non-team PRs for un-archived repos when archived option is not checked', async () => { - const { getByText, getAllByText, getByTitle, queryAllByTitle } = - await render(); + const { getByText, getAllByText, getByTitle, queryAllByTitle } = render( + , + ); const draftToggle = getByTitle('Show draft PRs'); fireEvent.click(draftToggle); expect(getByText('non-team-is-draft-non-archive')).toBeInTheDocument(); @@ -186,8 +255,9 @@ describe('EntityTeamPullRequestsContent', () => { }); it('should show draft non-team PRs for archived repos when archived option is checked', async () => { - const { getByText, getAllByText, getByTitle, queryAllByTitle } = - await render(); + const { getByText, getAllByText, getByTitle, queryAllByTitle } = render( + , + ); const draftToggle = getByTitle('Show draft PRs'); fireEvent.click(draftToggle); const archiveToggle = getByTitle('Show archived repos'); @@ -203,8 +273,9 @@ describe('EntityTeamPullRequestsContent', () => { describe('team PRs', () => { describe('non-draft PRs', () => { it('should show team PRs for un-archived repos when archived option is not checked', async () => { - const { getByText, getAllByText, getByTitle, queryAllByTitle } = - await render(); + const { getByText, getAllByText, getByTitle, queryAllByTitle } = render( + , + ); const teamToggle = getByTitle('Show PRs from your team'); fireEvent.click(teamToggle); expect(getByText('is-team-non-draft-non-archive')).toBeInTheDocument(); @@ -214,8 +285,9 @@ describe('EntityTeamPullRequestsContent', () => { }); it('should show team PRs for archived repos when archived option is checked', async () => { - const { getByText, getAllByText, getByTitle, queryAllByTitle } = - await render(); + const { getByText, getAllByText, getByTitle, queryAllByTitle } = render( + , + ); const teamToggle = getByTitle('Show PRs from your team'); fireEvent.click(teamToggle); const archiveToggle = getByTitle('Show archived repos'); @@ -229,8 +301,9 @@ describe('EntityTeamPullRequestsContent', () => { describe('draft PRs', () => { it('should show draft team PRs for un-archived repos when archived option is not checked', async () => { - const { getByText, getAllByText, getByTitle, queryAllByTitle } = - await render(); + const { getByText, getAllByText, getByTitle, queryAllByTitle } = render( + , + ); const teamToggle = getByTitle('Show PRs from your team'); fireEvent.click(teamToggle); const draftToggle = getByTitle('Show draft PRs'); @@ -242,8 +315,9 @@ describe('EntityTeamPullRequestsContent', () => { }); it('should show draft team PRs for archived repos when archived option is checked', async () => { - const { getByText, getAllByText, getByTitle, queryAllByTitle } = - await render(); + const { getByText, getAllByText, getByTitle, queryAllByTitle } = render( + , + ); const teamToggle = getByTitle('Show PRs from your team'); fireEvent.click(teamToggle); const draftToggle = getByTitle('Show draft PRs'); From 6899c2dcf5f5b727484a7f310c9dd8dfa802fd2c Mon Sep 17 00:00:00 2001 From: Josh Uvi Date: Tue, 13 Feb 2024 12:57:29 +0000 Subject: [PATCH 016/204] feat: adds last commit status to PR cardheader component and changeset Signed-off-by: Josh Uvi --- .../src/components/Card/CardHeader.test.tsx | 13 ++ .../src/components/Card/CardHeader.tsx | 4 +- .../EntityTeamPullRequestsCard.test.tsx | 151 +++++++++--------- .../EntityTeamPullRequestsCard.tsx | 2 + .../EntityTeamPullRequestsContent.tsx | 3 - .../src/utils/types.tsx | 2 +- 6 files changed, 97 insertions(+), 78 deletions(-) diff --git a/plugins/github-pull-requests-board/src/components/Card/CardHeader.test.tsx b/plugins/github-pull-requests-board/src/components/Card/CardHeader.test.tsx index 3171bff10f..0ca1bfd43f 100644 --- a/plugins/github-pull-requests-board/src/components/Card/CardHeader.test.tsx +++ b/plugins/github-pull-requests-board/src/components/Card/CardHeader.test.tsx @@ -37,6 +37,13 @@ const props = { name: 'documentation', }, ], + status: { + commit: { + statusCheckRollup: { + state: 'SUCCESS', + }, + }, + }, }; describe('', () => { @@ -54,4 +61,10 @@ describe('', () => { await renderInTestApp(); expect(screen.queryByRole('listitem')).not.toBeInTheDocument(); }); + + it('finds commit status in PR Card Header', async () => { + await renderInTestApp(); + expect(screen.getByText('Commit Status:')).toBeInTheDocument(); + expect(props.status.commit.statusCheckRollup.state).toBeTruthy(); + }); }); diff --git a/plugins/github-pull-requests-board/src/components/Card/CardHeader.tsx b/plugins/github-pull-requests-board/src/components/Card/CardHeader.tsx index 98d53f781c..9a47938139 100644 --- a/plugins/github-pull-requests-board/src/components/Card/CardHeader.tsx +++ b/plugins/github-pull-requests-board/src/components/Card/CardHeader.tsx @@ -48,7 +48,7 @@ const CardHeader: FunctionComponent = (props: Props) => { isDraft, repositoryIsArchived, labels, - status: commitStatus, + status, } = props; return ( @@ -91,7 +91,7 @@ const CardHeader: FunctionComponent = (props: Props) => { Commit Status:{' '} - {commitStatus.commit.statusCheckRollup.state} + {status.commit.statusCheckRollup.state} {labels && ( diff --git a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.test.tsx b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.test.tsx index a8ef9a9e2c..a4e0240cbd 100644 --- a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.test.tsx +++ b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.test.tsx @@ -33,14 +33,21 @@ jest.mock('../../hooks/useUserRepositoriesAndTeam', () => { }); jest.mock('../../hooks/usePullRequestsByTeam', () => { - const buildPullRequest = ( - prTitle: string, - authorLogin: string, - repoName: string, - isDraft: boolean, - isArchived: boolean, - status: Status, - ) => { + const buildPullRequest = ({ + prTitle, + authorLogin, + repoName, + isDraft, + isArchived, + status, + }: { + prTitle: string; + authorLogin: string; + repoName: string; + isDraft: boolean; + isArchived: boolean; + status: Status; + }) => { return { id: 'id', title: prTitle, @@ -81,118 +88,118 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { { title: 'column', content: [ - buildPullRequest( - 'non-team-non-draft-non-archive', - 'non-team-member', - 'team-repo', - false, - false, - { + buildPullRequest({ + prTitle: 'non-team-non-draft-non-archive', + authorLogin: 'non-team-member', + repoName: 'team-repo', + isDraft: false, + isArchived: false, + status: { commit: { statusCheckRollup: { state: 'FAILURE', }, }, }, - ), - buildPullRequest( - 'non-team-non-draft-is-archive', - 'non-team-member', - 'team-repo', - false, - true, - { + }), + buildPullRequest({ + prTitle: 'non-team-non-draft-is-archive', + authorLogin: 'non-team-member', + repoName: 'team-repo', + isDraft: false, + isArchived: true, + status: { commit: { statusCheckRollup: { state: 'FAILURE', }, }, }, - ), - buildPullRequest( - 'non-team-is-draft-non-archive', - 'non-team-member', - 'team-repo', - true, - false, - { + }), + buildPullRequest({ + prTitle: 'non-team-is-draft-non-archive', + authorLogin: 'non-team-member', + repoName: 'team-repo', + isDraft: true, + isArchived: false, + status: { commit: { statusCheckRollup: { state: 'FAILURE', }, }, }, - ), - buildPullRequest( - 'non-team-is-draft-is-archive', - 'non-team-member', - 'team-repo', - true, - true, - { + }), + buildPullRequest({ + prTitle: 'non-team-is-draft-is-archive', + authorLogin: 'non-team-member', + repoName: 'team-repo', + isDraft: true, + isArchived: true, + status: { commit: { statusCheckRollup: { state: 'SUCCESS', }, }, }, - ), - buildPullRequest( - 'is-team-non-draft-non-archive', - 'team-member', - 'non-team-repo', - false, - false, - { + }), + buildPullRequest({ + prTitle: 'is-team-non-draft-non-archive', + authorLogin: 'team-member', + repoName: 'non-team-repo', + isDraft: false, + isArchived: false, + status: { commit: { statusCheckRollup: { state: 'FAILURE', }, }, }, - ), - buildPullRequest( - 'is-team-non-draft-is-archive', - 'team-member', - 'non-team-repo', - false, - true, - { + }), + buildPullRequest({ + prTitle: 'is-team-non-draft-is-archive', + authorLogin: 'team-member', + repoName: 'non-team-repo', + isDraft: false, + isArchived: true, + status: { commit: { statusCheckRollup: { state: 'FAILURE', }, }, }, - ), - buildPullRequest( - 'is-team-is-draft-non-archive', - 'team-member', - 'non-team-repo', - true, - false, - { + }), + buildPullRequest({ + prTitle: 'is-team-is-draft-non-archive', + authorLogin: 'team-member', + repoName: 'non-team-repo', + isDraft: true, + isArchived: false, + status: { commit: { statusCheckRollup: { state: 'FAILURE', }, }, }, - ), - buildPullRequest( - 'is-team-is-draft-is-archive', - 'team-member', - 'non-team-repo', - true, - true, - { + }), + buildPullRequest({ + prTitle: 'is-team-is-draft-is-archive', + authorLogin: 'team-member', + repoName: 'non-team-repo', + isDraft: true, + isArchived: true, + status: { commit: { statusCheckRollup: { state: 'SUCCESS', }, }, }, - ), + }), ], }, ]; diff --git a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.tsx b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.tsx index 25ef32362b..9285d6b032 100644 --- a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.tsx +++ b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.tsx @@ -114,6 +114,7 @@ const EntityTeamPullRequestsCard = (props: EntityTeamPullRequestsCardProps) => { repository, isDraft, labels, + commits, }, index, ) => @@ -137,6 +138,7 @@ const EntityTeamPullRequestsCard = (props: EntityTeamPullRequestsCardProps) => { repositoryIsArchived={repository.isArchived} isDraft={isDraft} labels={labels.nodes} + status={commits.nodes} /> ), )} diff --git a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.tsx b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.tsx index 6109ddb63c..668995a306 100644 --- a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.tsx +++ b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.tsx @@ -87,9 +87,6 @@ const EntityTeamPullRequestsContent = ( return ; } - // eslint-disable-next-line no-console - console.log('pull - ', pullRequests); - return ( {pullRequests.length ? ( diff --git a/plugins/github-pull-requests-board/src/utils/types.tsx b/plugins/github-pull-requests-board/src/utils/types.tsx index cdabcf0713..88b05219d6 100644 --- a/plugins/github-pull-requests-board/src/utils/types.tsx +++ b/plugins/github-pull-requests-board/src/utils/types.tsx @@ -84,7 +84,7 @@ export type Label = { export type Status = { commit: { statusCheckRollup: { - state: 'SUCCESS' | 'FAILURE' | 'ERROR' | 'EXPECTED' | 'PENDING'; + state: string; }; }; }; From 3c2d7c0e720a9c412345bb5868857811bdf77d97 Mon Sep 17 00:00:00 2001 From: Josh Uvi Date: Tue, 13 Feb 2024 13:22:36 +0000 Subject: [PATCH 017/204] adds changeset Signed-off-by: Josh Uvi --- .changeset/chilled-dolphins-tap.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/chilled-dolphins-tap.md diff --git a/.changeset/chilled-dolphins-tap.md b/.changeset/chilled-dolphins-tap.md new file mode 100644 index 0000000000..92549aaef0 --- /dev/null +++ b/.changeset/chilled-dolphins-tap.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-github-pull-requests-board': patch +--- + +The cardheader component in the github-pull-requests-board plugin now requires that a `status` is passed to the component. From c71e8770e243a71d6013b14b443c836eb0ba9f10 Mon Sep 17 00:00:00 2001 From: Matt Wise Date: Tue, 13 Feb 2024 09:12:48 -0600 Subject: [PATCH 018/204] update tests to include subdirectory validation Signed-off-by: Matt Wise --- .../awsS3/awsS3-mock-object3.yaml | 1 + .../tree/ReadableArrayResponse.test.ts | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 packages/backend-common/src/reading/__fixtures__/awsS3/awsS3-mock-object3.yaml diff --git a/packages/backend-common/src/reading/__fixtures__/awsS3/awsS3-mock-object3.yaml b/packages/backend-common/src/reading/__fixtures__/awsS3/awsS3-mock-object3.yaml new file mode 100644 index 0000000000..b622af142a --- /dev/null +++ b/packages/backend-common/src/reading/__fixtures__/awsS3/awsS3-mock-object3.yaml @@ -0,0 +1 @@ +site_name: Test3 diff --git a/packages/backend-common/src/reading/tree/ReadableArrayResponse.test.ts b/packages/backend-common/src/reading/tree/ReadableArrayResponse.test.ts index e2bfe1af59..7420f14d70 100644 --- a/packages/backend-common/src/reading/tree/ReadableArrayResponse.test.ts +++ b/packages/backend-common/src/reading/tree/ReadableArrayResponse.test.ts @@ -31,6 +31,12 @@ const file2 = fs.readFileSync( path.resolve(__filename, '../../__fixtures__/awsS3/awsS3-mock-object2.yaml'), ); +const dir1 = 'dir1'; +const name3 = `file3.yaml`; +const file3 = fs.readFileSync( + path.resolve(__filename, '../../__fixtures__/awsS3/awsS3-mock-object3.yaml'), +); + describe('ReadableArrayResponse', () => { const sourceDir = createMockDirectory(); const targetDir = createMockDirectory(); @@ -39,6 +45,9 @@ describe('ReadableArrayResponse', () => { sourceDir.setContent({ [name1]: file1, [name2]: file2, + [dir1]: { + [name3]: file3, + }, }); targetDir.clear(); }); @@ -56,11 +65,13 @@ describe('ReadableArrayResponse', () => { const path1 = sourceDir.resolve(name1); const path2 = sourceDir.resolve(name2); + const path3 = sourceDir.resolve(`${dir1}/${name3}`); it('should read files', async () => { const arr: FromReadableArrayOptions = [ { data: createReadStream(path1), path: path1 }, { data: createReadStream(path2), path: path2 }, + { data: createReadStream(path3), path: path3 }, ]; const res = new ReadableArrayResponse(arr, targetDir.path, 'etag'); @@ -69,18 +80,21 @@ describe('ReadableArrayResponse', () => { expect(files).toEqual([ { path: path1, content: expect.any(Function) }, { path: path2, content: expect.any(Function) }, + { path: path3, content: expect.any(Function) }, ]); const contents = await Promise.all(files.map(f => f.content())); - expect(contents).toEqual([file1, file2]); + expect(contents).toEqual([file1, file2, file3]); }); it('should extract entire archive into directory', async () => { const relativePath1 = relative(sourceDir.path, path1); const relativePath2 = relative(sourceDir.path, path2); + const relativePath3 = relative(sourceDir.path, path3); const arr: FromReadableArrayOptions = [ { data: createReadStream(path1), path: relativePath1 }, { data: createReadStream(path2), path: relativePath2 }, + { data: createReadStream(path3), path: relativePath3 }, ]; const res = new ReadableArrayResponse(arr, targetDir.path, 'etag'); @@ -89,6 +103,9 @@ describe('ReadableArrayResponse', () => { expect(targetDir.content({ path: dir })).toEqual({ [name1]: file1.toString('utf8'), [name2]: file2.toString('utf8'), + [dir1]: { + [name3]: file3.toString('utf8'), + }, }); }); }); From c04d4bcc9dde841803ac83ada3ce98c3b5a9efe8 Mon Sep 17 00:00:00 2001 From: Tyler Wray Date: Tue, 13 Feb 2024 15:30:53 -0700 Subject: [PATCH 019/204] Update descriptor-format.md Adding table for profile fields. Signed-off-by: Tyler Wray --- .../software-catalog/descriptor-format.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index cc2b3d1c7b..9adfe47841 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -955,6 +955,14 @@ some form, that the group may wish to be used for contacting them. The picture is expected to be a URL pointing to an image that's representative of the group, and that a browser could fetch and render on a group page or similar. +The fields of a profile are: + +| Field | Type | Description | +| ------------------------ | ------ | -------------------------------------------------------------- | +| `displayName` (optional) | String | A human-readable name for the group. | +| `email` (optional) | String | An email the group may wish to be used for contacting them. | +| `picture` (optional) | String | A URL pointing to an image that's representative of the group. | + ### `spec.parent` [optional] The immediate parent group in the hierarchy, if any. Not all groups must have a @@ -1040,6 +1048,14 @@ of some form, that the user may wish to be used for contacting them. The picture is expected to be a URL pointing to an image that's representative of the user, and that a browser could fetch and render on a profile page or similar. +The fields of a profile are: + +| Field | Type | Description | +| ------------------------ | ------ | -------------------------------------------------------------- | +| `displayName` (optional) | String | A human-readable name for the group. | +| `email` (optional) | String | An email the group may wish to be used for contacting them. | +| `picture` (optional) | String | A URL pointing to an image that's representative of the group. | + ### `spec.memberOf` [required] The list of groups that the user is a direct member of (i.e., no transitive From 9bb9a4806c2a0e39db195e1ff20fb683dbb00d03 Mon Sep 17 00:00:00 2001 From: Tyler Wray Date: Tue, 13 Feb 2024 19:19:30 -0700 Subject: [PATCH 020/204] Update docs/features/software-catalog/descriptor-format.md Co-authored-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Signed-off-by: Tyler Wray --- docs/features/software-catalog/descriptor-format.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 9adfe47841..4b54f648f8 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -1054,7 +1054,7 @@ The fields of a profile are: | ------------------------ | ------ | -------------------------------------------------------------- | | `displayName` (optional) | String | A human-readable name for the group. | | `email` (optional) | String | An email the group may wish to be used for contacting them. | -| `picture` (optional) | String | A URL pointing to an image that's representative of the group. | +| `picture` (optional) | String | A URL pointing to an image that's representative of the user. | ### `spec.memberOf` [required] From f095b4b892971011676930aebfeaa0fb1d7e318b Mon Sep 17 00:00:00 2001 From: Tyler Wray Date: Tue, 13 Feb 2024 19:19:33 -0700 Subject: [PATCH 021/204] Update docs/features/software-catalog/descriptor-format.md Co-authored-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Signed-off-by: Tyler Wray --- docs/features/software-catalog/descriptor-format.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 4b54f648f8..b0d74f8ab2 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -1053,7 +1053,7 @@ The fields of a profile are: | Field | Type | Description | | ------------------------ | ------ | -------------------------------------------------------------- | | `displayName` (optional) | String | A human-readable name for the group. | -| `email` (optional) | String | An email the group may wish to be used for contacting them. | +| `email` (optional) | String | An email the user may wish to be used for contacting them. | | `picture` (optional) | String | A URL pointing to an image that's representative of the user. | ### `spec.memberOf` [required] From f321ad476632eef5a5b9271c3a55d174f20fcf3d Mon Sep 17 00:00:00 2001 From: Tyler Wray Date: Tue, 13 Feb 2024 19:19:41 -0700 Subject: [PATCH 022/204] Update docs/features/software-catalog/descriptor-format.md Co-authored-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Signed-off-by: Tyler Wray --- docs/features/software-catalog/descriptor-format.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index b0d74f8ab2..41bd81019b 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -1052,7 +1052,7 @@ The fields of a profile are: | Field | Type | Description | | ------------------------ | ------ | -------------------------------------------------------------- | -| `displayName` (optional) | String | A human-readable name for the group. | +| `displayName` (optional) | String | A human-readable name for the user. | | `email` (optional) | String | An email the user may wish to be used for contacting them. | | `picture` (optional) | String | A URL pointing to an image that's representative of the user. | From a3ef72c05582f70d70ea1f4bffe0150856669e01 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 19 Jan 2024 11:35:06 +0100 Subject: [PATCH 023/204] docs: frontend plugin migration Signed-off-by: Vincenzo Scamporlino --- .../building-plugins/migrating.md | 125 ++++++++++++++++++ microsite/sidebars.json | 3 +- 2 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 docs/frontend-system/building-plugins/migrating.md diff --git a/docs/frontend-system/building-plugins/migrating.md b/docs/frontend-system/building-plugins/migrating.md new file mode 100644 index 0000000000..3131f51a43 --- /dev/null +++ b/docs/frontend-system/building-plugins/migrating.md @@ -0,0 +1,125 @@ +--- +id: migrating +title: Migrating an existing Frontend Plugin to the New Frontend System +sidebar_label: Migration Guide +# prettier-ignore +description: How to migrate an existing frontend plugin to the new frontend system +--- + +This guide allows you to migrate a frontend plugin and its own components, routes, apis to the new frontend system. + +The main concept is that routes, components, apis are now extensions. You can use the appropriate extension creators to migrate all of them to extensions. + +## Pages + +Pages that were previously created using the `createRoutableExtension` extension function can be migrated to the new Frontend System using the `createPageExtension` extension creator, exported by `@backstage/frontend-plugin-api`. + +For example, given the following page: + +```ts +export const HomepageRoot = homePlugin.provide( + createRoutableExtension({ + name: 'HomepageRoot', + component: () => import('./components').then(m => m.HomepageRoot), + mountPoint: rootRouteRef, + }), +); +``` + +it can be migrated as the following: + +```tsx +const homePage = createPageExtension({ + defaultPath: '/home', + // you can reuse the existing routeRef + // by wrapping into the convertLegacyRouteRef. + routeRef: convertLegacyRouteRef(rootRouteRef), + // these inputs usually match the props required by the component. + inputs: { + props: createExtensionInput( + { + children: coreExtensionData.reactElement.optional(), + title: titleExtensionDataRef.optional(), + }, + + { + singleton: true, + optional: true, + }, + ), + }, + loader: ({ inputs }) => + import('./components/').then(m => + // The compatWrapper utility allows you to use the existing + // legacy frontend utilities used internally by the components. + compatWrapper( + , + ), + ), +}); +``` + +## Components + +TODO + +## APIs + +Let's imagine we have the following API: + +```ts +const myApi = createApiFactory(myApiRef, new SampleMyApi()); +``` + +you can transform the API above to an extension using the `createApiExtension` creator: + +```ts +export const myApi = createApiExtension({ + factory: createApiFactory(myApiRef, new SampleMyApi()), +}); +``` + +## Plugin + +In the legacy frontend system a plugin was defined in its own `plugin.ts` file as following: + +```ts title="my-plugin/src/plugin.ts" + import { createPlugin } from '@backstage/core-plugin-api'; + + export const myPlugin = createPlugin({ + id: 'my-plugin', + apis: [myApi], + routes: { + ... + }, + externalRoutes: { + ... + }, + }); +``` + +In order to migrate the actual definition of the plugin you need to recreate the plugin using the new `createPlugin` utility exported by `@backstage/frontend-plugin-api`. +The new `createPlugin` function doesn't accept apis anymore as apis are now extensions. + +```ts title="my-plugin/src/index.ts" + import { createPlugin } from '@backstage/frontend-plugin-api'; + + export default createPlugin({ + id: 'my-plugin', + // bind all the extensions to the plugin + /* highlight-next-line */ + extensions: [homePage, myApi], + routes: { + ... + }, + externalRoutes: { + ... + }, + }); +``` + +The code above binds all the extensions to the plugin. _Important_: Make sure to export the plugin as default export of your package in `src/index.ts`. diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 146d8db059..f9c658feee 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -414,7 +414,8 @@ "frontend-system/building-plugins/index", "frontend-system/building-plugins/testing", "frontend-system/building-plugins/extension-types", - "frontend-system/building-plugins/built-in-data-refs" + "frontend-system/building-plugins/built-in-data-refs", + "frontend-system/building-plugins/migrating" ] }, { From 07f922905ed32e1729fe5a746f00af0afa8cfc47 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 19 Jan 2024 12:56:54 +0100 Subject: [PATCH 024/204] docs: move over utility api migration to frontend plugin Signed-off-by: Vincenzo Scamporlino --- .../building-plugins/migrating.md | 94 +++++++++++-- .../utility-apis/05-migrating.md | 126 ------------------ microsite/sidebars.json | 3 +- 3 files changed, 85 insertions(+), 138 deletions(-) delete mode 100644 docs/frontend-system/utility-apis/05-migrating.md diff --git a/docs/frontend-system/building-plugins/migrating.md b/docs/frontend-system/building-plugins/migrating.md index 3131f51a43..2fb64e0eab 100644 --- a/docs/frontend-system/building-plugins/migrating.md +++ b/docs/frontend-system/building-plugins/migrating.md @@ -69,20 +69,94 @@ TODO ## APIs -Let's imagine we have the following API: +There are a few things to keep in mind in regards to utility APIs. -```ts -const myApi = createApiFactory(myApiRef, new SampleMyApi()); -``` +### React package interface and ref changes -you can transform the API above to an extension using the `createApiExtension` creator: +Let's begin with [your `-react` package](../../architecture-decisions/adr011-plugin-package-structure.md). The act of exporting TypeScript interfaces and API refs have not changed from the old system. You can typically keep those as-is. For illustrative purposes, this is an example of an interface and its API ref: -```ts -export const myApi = createApiExtension({ - factory: createApiFactory(myApiRef, new SampleMyApi()), +```tsx title="in @internal/plugin-example-react" +import { createApiRef } from '@backstage/frontend-plugin-api'; + +/** + * Performs some work. + * @public + */ +export interface WorkApi { + doWork(): Promise; +} + +/** + * The work interface for the Example plugin. + * @public + */ +export const workApiRef = createApiRef({ + id: 'plugin.example.work', }); ``` +In this example, the plugin ID already follows the [Frontend System Naming Patterns](../architecture/naming-patterns). If it doesn't, you may want to consider renaming that ID at this point. Don't worry, this won't hurt consumers in the old frontend system since the ID is mostly used for debugging purposes there. In the new system, it's much more important and appears in app-config files and similar. + +Note at the top of the file that it uses the updated import from `@backstage/frontend-plugin-api` that we migrated in the previous section, instead of the old `@backstage/core-plugin-api`. + +### Plugin package changes + +Now let's turn to the main plugin package where the plugin itself is exported. You will probably already have a `createPlugin` call in here. Before we changed the `core-plugin-api` imports it'll have looked somewhat similar to the following: + +```tsx title="in @internal/plugin-example, NOTE THIS IS LEGACY CODE" +import { + storageApiRef, + createPlugin, + createApiFactory, +} from '@backstage/core-plugin-api'; +import { workApiRef } from '@internal/plugin-example-react'; +import { WorkImpl } from './WorkImpl'; + +const exampleWorkApi = createApiFactory({ + api: workApiRef, + deps: { storageApi: storageApiRef }, + factory: ({ storageApi }) => new WorkImpl({ storageApi }), +}); + +/** @public */ +export const catalogPlugin = createPlugin({ + id: 'example', + apis: [exampleWorkApi], +}); +``` + +The major changes we'll make are + +- Optionally change the old imports to the new package as per the top section of this guide +- Wrap the existing API factory in a `createApiExtension` +- Change to the new version of `createPlugin` which exports this extension +- Change the plugin export to be the default instead + +The end result, after simplifying imports and cleaning up a bit, might look like this: + +```tsx title="in @internal/plugin-example" +import { + storageApiRef, + createPlugin, + createApiFactory, + createApiExtension, +} from '@backstage/frontend-plugin-api'; +import { workApiRef } from '@internal/plugin-example-react'; +import { WorkImpl } from './WorkImpl'; + +const exampleWorkApi = createApiExtension({ + factory: createApiFactory({ + api: workApiRef, + deps: { storageApi: storageApiRef }, + factory: ({ storageApi }) => new WorkImpl({ storageApi }), + }), +}); +``` + +### Further work + +Since utility APIs are now complete extensions, you may want to take a bigger look at how they used to be used, and what the new frontend system offers. You may for example consider [adding configurability or inputs](../utility-apis/02-creating.md) to your API, if that makes sense for your current application. + ## Plugin In the legacy frontend system a plugin was defined in its own `plugin.ts` file as following: @@ -92,7 +166,7 @@ In the legacy frontend system a plugin was defined in its own `plugin.ts` file a export const myPlugin = createPlugin({ id: 'my-plugin', - apis: [myApi], + apis: [exampleWorkApi], routes: { ... }, @@ -112,7 +186,7 @@ The new `createPlugin` function doesn't accept apis anymore as apis are now exte id: 'my-plugin', // bind all the extensions to the plugin /* highlight-next-line */ - extensions: [homePage, myApi], + extensions: [homePage, exampleWorkApi], routes: { ... }, diff --git a/docs/frontend-system/utility-apis/05-migrating.md b/docs/frontend-system/utility-apis/05-migrating.md deleted file mode 100644 index a81dc796ac..0000000000 --- a/docs/frontend-system/utility-apis/05-migrating.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -id: migrating -title: Migrating Utility APIs from the old frontend system -sidebar_label: Migrating -# prettier-ignore -description: Migrating Utility APIs from the old frontend system ---- - -> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.** - -If you are migrating your plugins or app over from the old frontend system, there are a few things to keep in mind in regards to utility APIs. - -## Overview - -- Migrate your repo overall to the latest release of Backstage -- Follow the plugin migration guide -- Optionally change your package dependencies and code from `core-*-api` to `frontend-*-api` -- Keep the TypeScript interface and API ref exported as they were, except possibly reconsidering the choice of ID of the latter -- Wrap the old API factory call in an extension using `createApiExtension` -- Make sure that this extension is referenced by your migrated plugin - -## Prerequisites - -This guide assumes that you first [upgrade your repo](../../getting-started/keeping-backstage-updated.md) to the latest release of Backstage. This ensures that you do not have to fight several types of incompatibilities and updates at the same time. - -## Dependency changes - -In this article we will discuss some old interfaces that you used to import from the `@backstage/core-plugin-api` package. Those are now generally lifted over to `@backstage/frontend-plugin-api`, next to the new interfaces that are specific to the new frontend system. If you want to, you can already update your `package.json` and code imports to only use the new plugin API, but for the time being you don't have to. The old core exports will continue to work for the foreseeable future. - -To at least get access to the new interfaces, you'll need to run the following command. Note that it's just an example! It refers to `plugins/example`, which you'll have to change to the actual folder name that your package to migrate is in. - -```bash title="from your repo root" -yarn --cwd plugins/example add @backstage/frontend-plugin-api -``` - -## React package interface and ref changes - -Let's begin with [your `-react` package](../../architecture-decisions/adr011-plugin-package-structure.md). The act of exporting TypeScript interfaces and API refs have not changed from the old system. You can typically keep those as-is. For illustrative purposes, this is an example of an interface and its API ref: - -```tsx title="in @internal/plugin-example-react" -import { createApiRef } from '@backstage/frontend-plugin-api'; - -/** - * Performs some work. - * @public - */ -export interface WorkApi { - doWork(): Promise; -} - -/** - * The work interface for the Example plugin. - * @public - */ -export const workApiRef = createApiRef({ - id: 'plugin.example.work', -}); -``` - -In this example, the plugin ID already follows the common naming convention. If it doesn't, you may want to consider renaming that ID at this point. Don't worry, this won't hurt consumers in the old frontend system since the ID is mostly used for debugging purposes there. In the new system, it's much more important and appears in app-config files and similar. - -Note at the top of the file that it uses the updated import from `@backstage/frontend-plugin-api` that we migrated in the previous section, instead of the old `@backstage/core-plugin-api`. - -## Plugin package changes - -Now let's turn to the main plugin package where the plugin itself is exported. You will probably already have a `createPlugin` call in here. Before we changed the `core-plugin-api` imports it'll have looked somewhat similar to the following: - -```tsx title="in @internal/plugin-example, NOTE THIS IS LEGACY CODE" -import { - storageApiRef, - createPlugin, - createApiFactory, -} from '@backstage/core-plugin-api'; -import { workApiRef } from '@internal/plugin-example-react'; -import { WorkImpl } from './WorkImpl'; - -const exampleWorkApi = createApiFactory({ - api: workApiRef, - deps: { storageApi: storageApiRef }, - factory: ({ storageApi }) => new WorkImpl({ storageApi }), -}); - -/** @public */ -export const catalogPlugin = createPlugin({ - id: 'example', - apis: [exampleWorkApi], -}); -``` - -The major changes we'll make are - -- Optionally change the old imports to the new package as per the top section of this guide -- Wrap the existing API factory in a `createApiExtension` -- Change to the new version of `createPlugin` which exports this extension -- Change the plugin export to be the default instead - -The end result, after simplifying imports and cleaning up a bit, might look like this: - -```tsx title="in @internal/plugin-example" -import { - storageApiRef, - createPlugin, - createApiFactory, - createApiExtension, -} from '@backstage/frontend-plugin-api'; -import { workApiRef } from '@internal/plugin-example-react'; -import { WorkImpl } from './WorkImpl'; - -const exampleWorkApi = createApiExtension({ - factory: createApiFactory({ - api: workApiRef, - deps: { storageApi: storageApiRef }, - factory: ({ storageApi }) => new WorkImpl({ storageApi }), - }), -}); - -/** @public */ -export default createPlugin({ - id: 'example', - extensions: [exampleWorkApi], -}); -``` - -## Further work - -Since utility APIs are now complete extensions, you may want to take a bigger look at how they used to be used, and what the new frontend system offers. You may for example consider [adding configurability or inputs](./02-creating.md) to your API, if that makes sense for your current application. diff --git a/microsite/sidebars.json b/microsite/sidebars.json index f9c658feee..46c94b3f18 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -433,8 +433,7 @@ "frontend-system/utility-apis/index", "frontend-system/utility-apis/creating", "frontend-system/utility-apis/consuming", - "frontend-system/utility-apis/configuring", - "frontend-system/utility-apis/migrating" + "frontend-system/utility-apis/configuring" ] } ], From c49f807ac6bb343ca9417f98e521dccd934a4e0c Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Mon, 22 Jan 2024 14:39:15 +0100 Subject: [PATCH 025/204] docs: simplify examples Signed-off-by: Vincenzo Scamporlino --- .../building-plugins/migrating.md | 39 +++++++------------ 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/docs/frontend-system/building-plugins/migrating.md b/docs/frontend-system/building-plugins/migrating.md index 2fb64e0eab..e894e6b03d 100644 --- a/docs/frontend-system/building-plugins/migrating.md +++ b/docs/frontend-system/building-plugins/migrating.md @@ -1,6 +1,6 @@ --- id: migrating -title: Migrating an existing Frontend Plugin to the New Frontend System +title: Migrating Plugins sidebar_label: Migration Guide # prettier-ignore description: How to migrate an existing frontend plugin to the new frontend system @@ -17,10 +17,10 @@ Pages that were previously created using the `createRoutableExtension` extension For example, given the following page: ```ts -export const HomepageRoot = homePlugin.provide( +export const FooPage = fooPlugin.provide( createRoutableExtension({ - name: 'HomepageRoot', - component: () => import('./components').then(m => m.HomepageRoot), + name: 'FooPage', + component: () => import('./components').then(m => m.FooPage), mountPoint: rootRouteRef, }), ); @@ -29,36 +29,23 @@ export const HomepageRoot = homePlugin.provide( it can be migrated as the following: ```tsx -const homePage = createPageExtension({ - defaultPath: '/home', +import { createPageExtension } from '@backstage/frontend-plugin-api'; +import { + compatWrapper, + convertLegacyRouteRef, +} from '@backstage/core-compat-api'; + +const fooPage = createPageExtension({ + defaultPath: '/foo', // you can reuse the existing routeRef // by wrapping into the convertLegacyRouteRef. routeRef: convertLegacyRouteRef(rootRouteRef), // these inputs usually match the props required by the component. - inputs: { - props: createExtensionInput( - { - children: coreExtensionData.reactElement.optional(), - title: titleExtensionDataRef.optional(), - }, - - { - singleton: true, - optional: true, - }, - ), - }, loader: ({ inputs }) => import('./components/').then(m => // The compatWrapper utility allows you to use the existing // legacy frontend utilities used internally by the components. - compatWrapper( - , - ), + compatWrapper(), ), }); ``` From a7ecd1aa7b3105c70aef06e1b2dce2f11c8314d1 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 23 Jan 2024 07:28:50 +0100 Subject: [PATCH 026/204] docs: components Signed-off-by: Vincenzo Scamporlino --- docs/frontend-system/building-plugins/migrating.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/building-plugins/migrating.md b/docs/frontend-system/building-plugins/migrating.md index e894e6b03d..aafa7b8986 100644 --- a/docs/frontend-system/building-plugins/migrating.md +++ b/docs/frontend-system/building-plugins/migrating.md @@ -52,7 +52,7 @@ const fooPage = createPageExtension({ ## Components -TODO +The equivalent utility to replace components created with `createComponentExtension` is `createExtension` from `@backstage/frontend-plugin-api`. However, we recommend searching for an appropriate extension first, considering using `createExtension` as the last option for more complex scenarios. ## APIs From 969515dbabc1e962ab71b32c6e74754a7d511cfe Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 23 Jan 2024 09:43:05 +0100 Subject: [PATCH 027/204] docs: highlight convertLegacyRouteRefs Signed-off-by: Vincenzo Scamporlino --- docs/frontend-system/building-plugins/migrating.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/frontend-system/building-plugins/migrating.md b/docs/frontend-system/building-plugins/migrating.md index aafa7b8986..1cb1325421 100644 --- a/docs/frontend-system/building-plugins/migrating.md +++ b/docs/frontend-system/building-plugins/migrating.md @@ -174,12 +174,15 @@ The new `createPlugin` function doesn't accept apis anymore as apis are now exte // bind all the extensions to the plugin /* highlight-next-line */ extensions: [homePage, exampleWorkApi], - routes: { + // convert old route refs to the new system + /* highlight-next-line */ + routes: convertLegacyRouteRefs({ ... - }, - externalRoutes: { + }), + /* highlight-next-line */ + externalRoutes: convertLegacyRouteRefs({ ... - }, + }), }); ``` From 0f800e60780f200c0ec570723fc1dd1452753a0e Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Wed, 14 Feb 2024 09:49:41 +0100 Subject: [PATCH 028/204] docs: suggest alpha export Signed-off-by: Vincenzo Scamporlino --- .../building-plugins/migrating.md | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/docs/frontend-system/building-plugins/migrating.md b/docs/frontend-system/building-plugins/migrating.md index 1cb1325421..d1f92ef454 100644 --- a/docs/frontend-system/building-plugins/migrating.md +++ b/docs/frontend-system/building-plugins/migrating.md @@ -166,7 +166,7 @@ In the legacy frontend system a plugin was defined in its own `plugin.ts` file a In order to migrate the actual definition of the plugin you need to recreate the plugin using the new `createPlugin` utility exported by `@backstage/frontend-plugin-api`. The new `createPlugin` function doesn't accept apis anymore as apis are now extensions. -```ts title="my-plugin/src/index.ts" +```ts title="my-plugin/src/alpha.ts" import { createPlugin } from '@backstage/frontend-plugin-api'; export default createPlugin({ @@ -186,4 +186,25 @@ The new `createPlugin` function doesn't accept apis anymore as apis are now exte }); ``` -The code above binds all the extensions to the plugin. _Important_: Make sure to export the plugin as default export of your package in `src/index.ts`. +The code above binds all the extensions to the plugin. _Important_: Make sure to export the plugin as default export of your package as a separate entrypoint, preferably `/alpha`, as suggested by the code snippet above. Make sure `src/alpha.ts` is exported in your `package.json`: + +```ts title="my-plugin/package.json" + "exports": { + ".": "./src/index.ts", + /* highlight-add-next-line */ + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + /* highlight-add-start */ + "alpha": [ + "src/alpha.ts" + ], + /* highlight-add-end */ + "package.json": [ + "package.json" + ] + } + }, +``` From d0cad4716954178806616185ac0be430d6686e01 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Wed, 14 Feb 2024 09:52:09 +0100 Subject: [PATCH 029/204] docs: remove plugin id suggestion Signed-off-by: Vincenzo Scamporlino --- docs/frontend-system/building-plugins/migrating.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/frontend-system/building-plugins/migrating.md b/docs/frontend-system/building-plugins/migrating.md index d1f92ef454..504276379b 100644 --- a/docs/frontend-system/building-plugins/migrating.md +++ b/docs/frontend-system/building-plugins/migrating.md @@ -82,8 +82,6 @@ export const workApiRef = createApiRef({ }); ``` -In this example, the plugin ID already follows the [Frontend System Naming Patterns](../architecture/naming-patterns). If it doesn't, you may want to consider renaming that ID at this point. Don't worry, this won't hurt consumers in the old frontend system since the ID is mostly used for debugging purposes there. In the new system, it's much more important and appears in app-config files and similar. - Note at the top of the file that it uses the updated import from `@backstage/frontend-plugin-api` that we migrated in the previous section, instead of the old `@backstage/core-plugin-api`. ### Plugin package changes From 63c246ab62deb2af797f45057ccf09bf21776983 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Wed, 14 Feb 2024 12:04:49 +0100 Subject: [PATCH 030/204] docs: migrate plugin as first step Signed-off-by: Vincenzo Scamporlino --- .../building-plugins/migrating.md | 173 ++++++++++-------- 1 file changed, 95 insertions(+), 78 deletions(-) diff --git a/docs/frontend-system/building-plugins/migrating.md b/docs/frontend-system/building-plugins/migrating.md index 504276379b..6f51357c76 100644 --- a/docs/frontend-system/building-plugins/migrating.md +++ b/docs/frontend-system/building-plugins/migrating.md @@ -10,7 +10,72 @@ This guide allows you to migrate a frontend plugin and its own components, route The main concept is that routes, components, apis are now extensions. You can use the appropriate extension creators to migrate all of them to extensions. -## Pages +## Migrating the plugin + +In the legacy frontend system a plugin was defined in its own `plugin.ts` file as following: + +```ts title="my-plugin/src/plugin.ts" + import { createPlugin } from '@backstage/core-plugin-api'; + + export const myPlugin = createPlugin({ + id: 'my-plugin', + apis: [], + routes: { + ... + }, + externalRoutes: { + ... + }, + }); +``` + +In order to migrate the actual definition of the plugin you need to recreate the plugin using the new `createPlugin` utility exported by `@backstage/frontend-plugin-api`. +The new `createPlugin` function doesn't accept apis anymore as apis are now extensions. + +```ts title="my-plugin/src/alpha.ts" + import { createPlugin } from '@backstage/frontend-plugin-api'; + + export default createPlugin({ + id: 'my-plugin', + // bind all the extensions to the plugin + /* highlight-next-line */ + extensions: [], + // convert old route refs to the new system + /* highlight-next-line */ + routes: convertLegacyRouteRefs({ + ... + }), + /* highlight-next-line */ + externalRoutes: convertLegacyRouteRefs({ + ... + }), + }); +``` + +The code above binds all the extensions to the plugin. _Important_: Make sure to export the plugin as default export of your package as a separate entrypoint, preferably `/alpha`, as suggested by the code snippet above. Make sure `src/alpha.ts` is exported in your `package.json`: + +```ts title="my-plugin/package.json" + "exports": { + ".": "./src/index.ts", + /* highlight-add-next-line */ + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + /* highlight-add-start */ + "alpha": [ + "src/alpha.ts" + ], + /* highlight-add-end */ + "package.json": [ + "package.json" + ] + } + }, +``` + +## Migrating Pages Pages that were previously created using the `createRoutableExtension` extension function can be migrated to the new Frontend System using the `createPageExtension` extension creator, exported by `@backstage/frontend-plugin-api`. @@ -50,11 +115,27 @@ const fooPage = createPageExtension({ }); ``` -## Components +then add the `fooPage` extension to the plugin: + +```ts title="my-plugin/src/alpha.ts" + import { createPlugin } from '@backstage/frontend-plugin-api'; + + export default createPlugin({ + id: 'my-plugin', + // bind all the extensions to the plugin + /* highlight-remove-next-line */ + extensions: [], + /* highlight-add-next-line */ + extensions: [fooPage], + ... + }); +``` + +## Migrating Components The equivalent utility to replace components created with `createComponentExtension` is `createExtension` from `@backstage/frontend-plugin-api`. However, we recommend searching for an appropriate extension first, considering using `createExtension` as the last option for more complex scenarios. -## APIs +## Migrating APIs There are a few things to keep in mind in regards to utility APIs. @@ -84,16 +165,10 @@ export const workApiRef = createApiRef({ Note at the top of the file that it uses the updated import from `@backstage/frontend-plugin-api` that we migrated in the previous section, instead of the old `@backstage/core-plugin-api`. -### Plugin package changes - -Now let's turn to the main plugin package where the plugin itself is exported. You will probably already have a `createPlugin` call in here. Before we changed the `core-plugin-api` imports it'll have looked somewhat similar to the following: +Now let's migrate the implementation of the api. Before we changed the `core-plugin-api` imports the api would have looked somewhat similar to the following: ```tsx title="in @internal/plugin-example, NOTE THIS IS LEGACY CODE" -import { - storageApiRef, - createPlugin, - createApiFactory, -} from '@backstage/core-plugin-api'; +import { storageApiRef, createApiFactory } from '@backstage/core-plugin-api'; import { workApiRef } from '@internal/plugin-example-react'; import { WorkImpl } from './WorkImpl'; @@ -102,27 +177,18 @@ const exampleWorkApi = createApiFactory({ deps: { storageApi: storageApiRef }, factory: ({ storageApi }) => new WorkImpl({ storageApi }), }); - -/** @public */ -export const catalogPlugin = createPlugin({ - id: 'example', - apis: [exampleWorkApi], -}); ``` The major changes we'll make are -- Optionally change the old imports to the new package as per the top section of this guide +- Change the old `@backstage/core-plugin-api` imports to the new `@backstage/frontend-plugin-api` package as per the top section of this guide - Wrap the existing API factory in a `createApiExtension` -- Change to the new version of `createPlugin` which exports this extension -- Change the plugin export to be the default instead The end result, after simplifying imports and cleaning up a bit, might look like this: ```tsx title="in @internal/plugin-example" import { storageApiRef, - createPlugin, createApiFactory, createApiExtension, } from '@backstage/frontend-plugin-api'; @@ -138,31 +204,7 @@ const exampleWorkApi = createApiExtension({ }); ``` -### Further work - -Since utility APIs are now complete extensions, you may want to take a bigger look at how they used to be used, and what the new frontend system offers. You may for example consider [adding configurability or inputs](../utility-apis/02-creating.md) to your API, if that makes sense for your current application. - -## Plugin - -In the legacy frontend system a plugin was defined in its own `plugin.ts` file as following: - -```ts title="my-plugin/src/plugin.ts" - import { createPlugin } from '@backstage/core-plugin-api'; - - export const myPlugin = createPlugin({ - id: 'my-plugin', - apis: [exampleWorkApi], - routes: { - ... - }, - externalRoutes: { - ... - }, - }); -``` - -In order to migrate the actual definition of the plugin you need to recreate the plugin using the new `createPlugin` utility exported by `@backstage/frontend-plugin-api`. -The new `createPlugin` function doesn't accept apis anymore as apis are now extensions. +Finally, let's add the `exampleWorkApi` extension to the plugin: ```ts title="my-plugin/src/alpha.ts" import { createPlugin } from '@backstage/frontend-plugin-api'; @@ -170,39 +212,14 @@ The new `createPlugin` function doesn't accept apis anymore as apis are now exte export default createPlugin({ id: 'my-plugin', // bind all the extensions to the plugin - /* highlight-next-line */ - extensions: [homePage, exampleWorkApi], - // convert old route refs to the new system - /* highlight-next-line */ - routes: convertLegacyRouteRefs({ - ... - }), - /* highlight-next-line */ - externalRoutes: convertLegacyRouteRefs({ - ... - }), + /* highlight-remove-next-line */ + extensions: [fooPage], + /* highlight-add-next-line */ + extensions: [exampleWorkApi, fooPage], + ... }); ``` -The code above binds all the extensions to the plugin. _Important_: Make sure to export the plugin as default export of your package as a separate entrypoint, preferably `/alpha`, as suggested by the code snippet above. Make sure `src/alpha.ts` is exported in your `package.json`: +### Further work -```ts title="my-plugin/package.json" - "exports": { - ".": "./src/index.ts", - /* highlight-add-next-line */ - "./alpha": "./src/alpha.ts", - "./package.json": "./package.json" - }, - "typesVersions": { - "*": { - /* highlight-add-start */ - "alpha": [ - "src/alpha.ts" - ], - /* highlight-add-end */ - "package.json": [ - "package.json" - ] - } - }, -``` +Since utility APIs are now complete extensions, you may want to take a bigger look at how they used to be used, and what the new frontend system offers. You may for example consider [adding configurability or inputs](../utility-apis/02-creating.md) to your API, if that makes sense for your current application. From 5f7f4ec8904bd275c2313cd9e02fb86c92746c29 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Wed, 14 Feb 2024 12:19:42 +0100 Subject: [PATCH 031/204] docs: remove createExtension suggestion Signed-off-by: Vincenzo Scamporlino --- docs/frontend-system/building-plugins/migrating.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/building-plugins/migrating.md b/docs/frontend-system/building-plugins/migrating.md index 6f51357c76..16f465ece7 100644 --- a/docs/frontend-system/building-plugins/migrating.md +++ b/docs/frontend-system/building-plugins/migrating.md @@ -133,7 +133,7 @@ then add the `fooPage` extension to the plugin: ## Migrating Components -The equivalent utility to replace components created with `createComponentExtension` is `createExtension` from `@backstage/frontend-plugin-api`. However, we recommend searching for an appropriate extension first, considering using `createExtension` as the last option for more complex scenarios. +The equivalent utility to replace components created with `createComponentExtension` is `createExtension` from `@backstage/frontend-plugin-api`. However, we recommend searching for a more appropriate extension creator first. ## Migrating APIs From 8c40a0f0aa5bf7a797ce4e5e8bcb740c6ef0369d Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Wed, 14 Feb 2024 14:50:35 -0500 Subject: [PATCH 032/204] move Jamie Klassen to emeritus Signed-off-by: Jamie Klassen --- OWNERS.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/OWNERS.md b/OWNERS.md index 1e92e68c40..e9e14aaf83 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -63,10 +63,9 @@ Team: @backstage/kubernetes-maintainers Scope: The Kubernetes plugin and the base it provides for other plugins to build upon. -| Name | Organization | Team | GitHub | Discord | -| -------------- | ------------ | ---- | ---------------------------------------------- | ----------------- | -| Matthew Clarke | Spotify | | [mclarke47](http://github.com/mclarke47) | mclarke#0725 | -| Jamie Klassen | VMware | | [jamieklassen](http://github.com/jamieklassen) | jamieklassen#3047 | +| Name | Organization | Team | GitHub | Discord | +| -------------- | ------------ | ---- | ---------------------------------------- | ------------ | +| Matthew Clarke | Spotify | | [mclarke47](http://github.com/mclarke47) | mclarke#0725 | ### Permission Framework @@ -151,7 +150,6 @@ Scope: The Scaffolder frontend and backend plugins, and related tooling. | Deepankumar Loganathan | | [deepan10](https://github.com/deepan10) | `deepan10` | | Himanshu Mishra | Harness.io | [OrkoHunter](https://github.com/OrkoHunter) | `OrkoHunter#1520` | | Irma Solakovic | Roadie.io | [Irma12](https://github.com/Irma12) | `Irma#7629` | -| Jamie Klassen | VMware | [jamieklassen](https://github.com/jamieklassen) | `jamieklassen#3047` | | Jorge Lainfiesta | Roadie.io | [jorgelainfiesta](https://github.com/jorgelainfiesta) | `jorgel#8733` | | Jussi Hallila | Roadie.io | [Xantier](https://github.com/Xantier) | `Xantier#0086` | | Mark Avery | Cvent | [webark](https://github.com/webark) | `webark#8471` | @@ -174,6 +172,7 @@ Scope: The Scaffolder frontend and backend plugins, and related tooling. ## Emeritus Project Area Maintainers -| Maintainer | Organization | GitHub | Discord | -| ---------- | -------------- | ----------------------------------- | -------- | -| Paul Cowan | frontendrescue | [dagda1](https://github.com/dagda1) | `dagda1` | +| Maintainer | Organization | GitHub | Discord | +| ------------- | -------------- | ----------------------------------------------- | ------------------- | +| Paul Cowan | frontendrescue | [dagda1](https://github.com/dagda1) | `dagda1` | +| Jamie Klassen | VMware | [jamieklassen](https://github.com/jamieklassen) | `jamieklassen#3047` | From 38af71a3a08ac45720544b68461242a653751b19 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Feb 2024 00:30:26 +0000 Subject: [PATCH 033/204] fix(deps): update dependency google-auth-library to v9 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .changeset/renovate-8f23b96.md | 7 ++ .../package.json | 2 +- .../package.json | 2 +- plugins/auth-backend/package.json | 2 +- yarn.lock | 74 +------------------ 5 files changed, 13 insertions(+), 74 deletions(-) create mode 100644 .changeset/renovate-8f23b96.md diff --git a/.changeset/renovate-8f23b96.md b/.changeset/renovate-8f23b96.md new file mode 100644 index 0000000000..b1dee297e2 --- /dev/null +++ b/.changeset/renovate-8f23b96.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-auth-backend-module-gcp-iap-provider': patch +'@backstage/plugin-auth-backend-module-google-provider': patch +'@backstage/plugin-auth-backend': patch +--- + +Updated dependency `google-auth-library` to `^9.0.0`. diff --git a/plugins/auth-backend-module-gcp-iap-provider/package.json b/plugins/auth-backend-module-gcp-iap-provider/package.json index 675048e55e..219f16bc66 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/package.json +++ b/plugins/auth-backend-module-gcp-iap-provider/package.json @@ -36,7 +36,7 @@ "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@backstage/types": "workspace:^", - "google-auth-library": "^8.0.0" + "google-auth-library": "^9.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/auth-backend-module-google-provider/package.json b/plugins/auth-backend-module-google-provider/package.json index cd1900037f..4954c64054 100644 --- a/plugins/auth-backend-module-google-provider/package.json +++ b/plugins/auth-backend-module-google-provider/package.json @@ -34,7 +34,7 @@ "dependencies": { "@backstage/backend-plugin-api": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", - "google-auth-library": "^8.0.0", + "google-auth-library": "^9.0.0", "passport-google-oauth20": "^2.0.0" }, "devDependencies": { diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 223f8758a7..3ec8c64fec 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -64,7 +64,7 @@ "express-promise-router": "^4.1.0", "express-session": "^1.17.1", "fs-extra": "^11.2.0", - "google-auth-library": "^8.0.0", + "google-auth-library": "^9.0.0", "jose": "^4.6.0", "knex": "^3.0.0", "lodash": "^4.17.21", diff --git a/yarn.lock b/yarn.lock index 6280775f8b..b68bd27df6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4724,7 +4724,7 @@ __metadata: "@backstage/plugin-auth-node": "workspace:^" "@backstage/types": "workspace:^" express: ^4.18.2 - google-auth-library: ^8.0.0 + google-auth-library: ^9.0.0 languageName: unknown linkType: soft @@ -4770,7 +4770,7 @@ __metadata: "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" "@types/passport-google-oauth20": ^2.0.3 - google-auth-library: ^8.0.0 + google-auth-library: ^9.0.0 passport-google-oauth20: ^2.0.0 supertest: ^6.1.3 languageName: unknown @@ -4962,7 +4962,7 @@ __metadata: express-promise-router: ^4.1.0 express-session: ^1.17.1 fs-extra: ^11.2.0 - google-auth-library: ^8.0.0 + google-auth-library: ^9.0.0 jose: ^4.6.0 knex: ^3.0.0 lodash: ^4.17.21 @@ -28056,13 +28056,6 @@ __metadata: languageName: node linkType: hard -"fast-text-encoding@npm:^1.0.0": - version: 1.0.3 - resolution: "fast-text-encoding@npm:1.0.3" - checksum: 3e51365896f06d0dcab128092d095a0037d274deec419fecbd2388bc236d7b387610e0c72f920c6126e00c885ab096fbfaa3645712f5b98f721bef6b064916a8 - languageName: node - linkType: hard - "fast-url-parser@npm:1.1.3, fast-url-parser@npm:^1.1.3": version: 1.1.3 resolution: "fast-url-parser@npm:1.1.3" @@ -28915,18 +28908,6 @@ __metadata: languageName: node linkType: hard -"gaxios@npm:^5.0.0, gaxios@npm:^5.0.1": - version: 5.0.1 - resolution: "gaxios@npm:5.0.1" - dependencies: - extend: ^3.0.2 - https-proxy-agent: ^5.0.0 - is-stream: ^2.0.0 - node-fetch: ^2.6.7 - checksum: 65464122a5be72084d07947536d18a0dcebd115b28cbcd19da8a98763a67c8c8fde995bf2f358251397c939df9d2ff84f54628a94f634a98a16c7b7553cdb4a5 - languageName: node - linkType: hard - "gaxios@npm:^6.0.0, gaxios@npm:^6.0.2": version: 6.1.1 resolution: "gaxios@npm:6.1.1" @@ -28939,16 +28920,6 @@ __metadata: languageName: node linkType: hard -"gcp-metadata@npm:^5.3.0": - version: 5.3.0 - resolution: "gcp-metadata@npm:5.3.0" - dependencies: - gaxios: ^5.0.0 - json-bigint: ^1.0.0 - checksum: 891ea0b902a17f33d7bae753830d23962b63af94ed071092c30496e7d26f8128ba9af43c3d38474bea29cb32a884b4bcb5720ce8b9de4a7e1108475d3d7ae219 - languageName: node - linkType: hard - "gcp-metadata@npm:^6.0.0": version: 6.1.0 resolution: "gcp-metadata@npm:6.1.0" @@ -29333,23 +29304,6 @@ __metadata: languageName: node linkType: hard -"google-auth-library@npm:^8.0.0": - version: 8.9.0 - resolution: "google-auth-library@npm:8.9.0" - dependencies: - arrify: ^2.0.0 - base64-js: ^1.3.0 - ecdsa-sig-formatter: ^1.0.11 - fast-text-encoding: ^1.0.0 - gaxios: ^5.0.0 - gcp-metadata: ^5.3.0 - gtoken: ^6.1.0 - jws: ^4.0.0 - lru-cache: ^6.0.0 - checksum: 8e0bc5f1e91804523786413bf4358e4c5ad94b1e873c725ddd03d0f1c242e2b38e26352c0f375334fbc1d94110f761b304aa0429de49b4a27ebc3875a5b56644 - languageName: node - linkType: hard - "google-auth-library@npm:^9.0.0": version: 9.2.0 resolution: "google-auth-library@npm:9.2.0" @@ -29383,17 +29337,6 @@ __metadata: languageName: node linkType: hard -"google-p12-pem@npm:^4.0.0": - version: 4.0.0 - resolution: "google-p12-pem@npm:4.0.0" - dependencies: - node-forge: ^1.3.1 - bin: - gp12-pem: build/src/bin/gp12-pem.js - checksum: f41a88d339e9fe633dc915bc0f3335c0196fa318f994dcd5dfaa0f3f7aa2d99f6122e2c80bd0f4bb22f2b61ff645b7cc782a74e12ceaf6c9ad9e08cdeb4d615e - languageName: node - linkType: hard - "google-protobuf@npm:^3.15.8, google-protobuf@npm:^3.19.1": version: 3.20.1 resolution: "google-protobuf@npm:3.20.1" @@ -29655,17 +29598,6 @@ __metadata: languageName: node linkType: hard -"gtoken@npm:^6.1.0": - version: 6.1.1 - resolution: "gtoken@npm:6.1.1" - dependencies: - gaxios: ^5.0.1 - google-p12-pem: ^4.0.0 - jws: ^4.0.0 - checksum: f063ed3f418f5a9c33fbe599b09f56a55b18f6c8d2913f11cc2fc5025d53b96fd6ab1b4deb2c7631167d6b89d0939e4ea77f94767fcf338862ffda8911250980 - languageName: node - linkType: hard - "gtoken@npm:^7.0.0": version: 7.0.1 resolution: "gtoken@npm:7.0.1" From b66e306c723bee29f281a6a58543043369af251a Mon Sep 17 00:00:00 2001 From: swnia <119884634+swnia@users.noreply.github.com> Date: Thu, 15 Feb 2024 09:47:27 +0100 Subject: [PATCH 034/204] Update resource type list As the kubernetes plugin searches for the type `kubernetes-cluster` and not `cluster` the documentation should provide examples that are recognised in the ecosystem. Signed-off-by: swnia <119884634+swnia@users.noreply.github.com> --- docs/features/software-catalog/descriptor-format.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index cc2b3d1c7b..054b727e78 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -1119,7 +1119,7 @@ Some common values for this field could be: - `database` - `s3-bucket` -- `cluster` +- `kubernetes-cluster` ### `spec.system` [optional] From 79f237398aac7624bc7248c8725e123bf8e4e742 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 15 Feb 2024 10:54:38 +0100 Subject: [PATCH 035/204] chore: remove immer dependency Signed-off-by: blam --- plugins/scaffolder-react/package.json | 1 - plugins/scaffolder/package.json | 1 - yarn.lock | 4 +--- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/plugins/scaffolder-react/package.json b/plugins/scaffolder-react/package.json index 0263efd4f4..94b6b044dc 100644 --- a/plugins/scaffolder-react/package.json +++ b/plugins/scaffolder-react/package.json @@ -68,7 +68,6 @@ "classnames": "^2.2.6", "flatted": "3.2.9", "humanize-duration": "^3.25.1", - "immer": "^9.0.1", "json-schema": "^0.4.0", "json-schema-library": "^7.3.9", "lodash": "^4.17.21", diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index c93de86cc9..3acc3270aa 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -78,7 +78,6 @@ "event-source-polyfill": "^1.0.31", "git-url-parse": "^14.0.0", "humanize-duration": "^3.25.1", - "immer": "^9.0.1", "json-schema": "^0.4.0", "json-schema-library": "^7.3.9", "jszip": "^3.10.1", diff --git a/yarn.lock b/yarn.lock index 6280775f8b..154afc7c66 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8725,7 +8725,6 @@ __metadata: classnames: ^2.2.6 flatted: 3.2.9 humanize-duration: ^3.25.1 - immer: ^9.0.1 json-schema: ^0.4.0 json-schema-library: ^7.3.9 lodash: ^4.17.21 @@ -8790,7 +8789,6 @@ __metadata: event-source-polyfill: ^1.0.31 git-url-parse: ^14.0.0 humanize-duration: ^3.25.1 - immer: ^9.0.1 json-schema: ^0.4.0 json-schema-library: ^7.3.9 jszip: ^3.10.1 @@ -30474,7 +30472,7 @@ __metadata: languageName: node linkType: hard -"immer@npm:^9.0.1, immer@npm:^9.0.6, immer@npm:^9.0.7": +"immer@npm:^9.0.6, immer@npm:^9.0.7": version: 9.0.21 resolution: "immer@npm:9.0.21" checksum: 70e3c274165995352f6936695f0ef4723c52c92c92dd0e9afdfe008175af39fa28e76aafb3a2ca9d57d1fb8f796efc4dd1e1cc36f18d33fa5b74f3dfb0375432 From 3dff4b0b80c98a59b973d2928ab16776cb328218 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 15 Feb 2024 10:56:13 +0100 Subject: [PATCH 036/204] chore: deps Signed-off-by: blam --- .changeset/tidy-cycles-obey.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/tidy-cycles-obey.md diff --git a/.changeset/tidy-cycles-obey.md b/.changeset/tidy-cycles-obey.md new file mode 100644 index 0000000000..4a002269e1 --- /dev/null +++ b/.changeset/tidy-cycles-obey.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-react': patch +'@backstage/plugin-scaffolder': patch +--- + +Remove unused deps From 3d5c6680f6b83fa8c9bc17496dc741dce97b83fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20W=C3=BCrbach?= Date: Thu, 15 Feb 2024 12:23:10 +0100 Subject: [PATCH 037/204] feat(scaffolder-backend-module-github): support oidc customization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johannes Würbach --- .changeset/funny-meals-tease.md | 5 +++ .../api-report.md | 12 +++++++ .../src/actions/github.test.ts | 35 +++++++++++++++++++ .../src/actions/github.ts | 7 ++++ .../src/actions/githubRepoCreate.test.ts | 35 +++++++++++++++++++ .../src/actions/githubRepoCreate.ts | 7 ++++ .../src/actions/helpers.ts | 18 ++++++++++ .../src/actions/inputProperties.ts | 23 ++++++++++++ 8 files changed, 142 insertions(+) create mode 100644 .changeset/funny-meals-tease.md diff --git a/.changeset/funny-meals-tease.md b/.changeset/funny-meals-tease.md new file mode 100644 index 0000000000..3dce739952 --- /dev/null +++ b/.changeset/funny-meals-tease.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': minor +--- + +support oidc customization diff --git a/plugins/scaffolder-backend-module-github/api-report.md b/plugins/scaffolder-backend-module-github/api-report.md index a588840585..de03894a5a 100644 --- a/plugins/scaffolder-backend-module-github/api-report.md +++ b/plugins/scaffolder-backend-module-github/api-report.md @@ -205,6 +205,12 @@ export function createGithubRepoCreateAction(options: { [key: string]: string; } | undefined; + oidcCustomization?: + | { + useDefault: boolean; + includeClaimKeys?: string[] | undefined; + } + | undefined; requireCommitSigning?: boolean | undefined; }, JsonObject @@ -352,6 +358,12 @@ export function createPublishGithubAction(options: { [key: string]: string; } | undefined; + oidcCustomization?: + | { + useDefault: boolean; + includeClaimKeys?: string[] | undefined; + } + | undefined; requiredCommitSigning?: boolean | undefined; }, JsonObject diff --git a/plugins/scaffolder-backend-module-github/src/actions/github.test.ts b/plugins/scaffolder-backend-module-github/src/actions/github.test.ts index a2a8e11e15..75d9b74a8b 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/github.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/github.test.ts @@ -77,6 +77,7 @@ const mockOctokit = { getRepoPublicKey: jest.fn(), }, }, + request: jest.fn(), }; jest.mock('octokit', () => ({ Octokit: class { @@ -930,6 +931,40 @@ describe('publish:github', () => { }); }); + it('should configure oidc customizations 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, + oidcCustomization: { + useDefault: false, + includeClaimKeys: ['foo', 'bar'], + }, + }, + }); + + expect(mockOctokit.request).toHaveBeenCalledWith( + 'PUT /repos/{owner}/{repo}/actions/oidc/customization/sub', + { + include_claim_keys: ['foo', 'bar'], + owner: 'owner', + repo: 'repo', + use_default: false, + }, + ); + }); + it('should call output with the remoteUrl and the repoContentsUrl', async () => { mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, diff --git a/plugins/scaffolder-backend-module-github/src/actions/github.ts b/plugins/scaffolder-backend-module-github/src/actions/github.ts index 91d7a34aea..f32b65bc9a 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/github.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/github.ts @@ -109,6 +109,10 @@ export function createPublishGithubAction(options: { topics?: string[]; repoVariables?: { [key: string]: string }; secrets?: { [key: string]: string }; + oidcCustomization?: { + useDefault: boolean; + includeClaimKeys?: string[]; + }; requiredCommitSigning?: boolean; }>({ id: 'publish:github', @@ -156,6 +160,7 @@ export function createPublishGithubAction(options: { topics: inputProps.topics, repoVariables: inputProps.repoVariables, secrets: inputProps.secrets, + oidcCustomization: inputProps.oidcCustomization, requiredCommitSigning: inputProps.requiredCommitSigning, }, }, @@ -203,6 +208,7 @@ export function createPublishGithubAction(options: { topics, repoVariables, secrets, + oidcCustomization, token: providedToken, requiredCommitSigning = false, } = ctx.input; @@ -243,6 +249,7 @@ export function createPublishGithubAction(options: { topics, repoVariables, secrets, + oidcCustomization, ctx.logger, ); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.test.ts index 4cb007275e..c72a969e96 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.test.ts @@ -58,6 +58,7 @@ const mockOctokit = { getRepoPublicKey: jest.fn(), }, }, + request: jest.fn(), }; jest.mock('octokit', () => ({ Octokit: class { @@ -652,6 +653,40 @@ describe('github:repo:create', () => { }); }); + it('should configure oidc customizations 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, + oidcCustomization: { + useDefault: false, + includeClaimKeys: ['foo', 'bar'], + }, + }, + }); + + expect(mockOctokit.request).toHaveBeenCalledWith( + 'PUT /repos/{owner}/{repo}/actions/oidc/customization/sub', + { + include_claim_keys: ['foo', 'bar'], + owner: 'owner', + repo: 'repo', + use_default: false, + }, + ); + }); + it('should call output with the remoteUrl', async () => { mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts index 735919794f..52bf9be9c0 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts @@ -95,6 +95,10 @@ export function createGithubRepoCreateAction(options: { topics?: string[]; repoVariables?: { [key: string]: string }; secrets?: { [key: string]: string }; + oidcCustomization?: { + useDefault: boolean; + includeClaimKeys?: string[]; + }; requireCommitSigning?: boolean; }>({ id: 'github:repo:create', @@ -133,6 +137,7 @@ export function createGithubRepoCreateAction(options: { topics: inputProps.topics, repoVariables: inputProps.repoVariables, secrets: inputProps.secrets, + oidcCustomization: inputProps.oidcCustomization, requiredCommitSigning: inputProps.requiredCommitSigning, }, }, @@ -165,6 +170,7 @@ export function createGithubRepoCreateAction(options: { topics, repoVariables, secrets, + oidcCustomization, token: providedToken, } = ctx.input; @@ -204,6 +210,7 @@ export function createGithubRepoCreateAction(options: { topics, repoVariables, secrets, + oidcCustomization, ctx.logger, ); diff --git a/plugins/scaffolder-backend-module-github/src/actions/helpers.ts b/plugins/scaffolder-backend-module-github/src/actions/helpers.ts index 4d95a6494f..ec282c74d9 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/helpers.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/helpers.ts @@ -137,6 +137,12 @@ export async function createGithubRepoWithCollaboratorsAndTopics( topics: string[] | undefined, repoVariables: { [key: string]: string } | undefined, secrets: { [key: string]: string } | undefined, + oidcCustomization: + | { + useDefault: boolean; + includeClaimKeys?: string[]; + } + | undefined, logger: Logger, ) { // eslint-disable-next-line testing-library/no-await-sync-queries @@ -304,6 +310,18 @@ export async function createGithubRepoWithCollaboratorsAndTopics( } } + if (oidcCustomization) { + await client.request( + 'PUT /repos/{owner}/{repo}/actions/oidc/customization/sub', + { + owner, + repo, + use_default: oidcCustomization.useDefault, + include_claim_keys: oidcCustomization.includeClaimKeys, + }, + ); + } + return newRepo; } diff --git a/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts b/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts index 96e1f3b6e1..585526fe94 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts @@ -277,6 +277,28 @@ const secrets = { type: 'object', }; +const oidcCustomization = { + title: 'Repository OIDC customization template', + description: `OIDC customization template attached to the repository.`, + type: 'object', + additionalProperties: false, + properties: { + useDefault: { + title: 'Use Default', + type: 'boolean', + description: `Whether to use the default OIDC template or not.`, + }, + includeClaimKeys: { + title: 'Include claim keys', + type: 'array', + items: { + type: 'string', + }, + description: `Array of unique strings. Each claim key can only contain alphanumeric characters and underscores.`, + }, + }, +}; + export { access }; export { allowMergeCommit }; export { allowRebaseMerge }; @@ -313,3 +335,4 @@ export { topics }; export { requiredCommitSigning }; export { repoVariables }; export { secrets }; +export { oidcCustomization }; From 43b4e3500f8de50a80cfb189956f3a5688d0c337 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 15 Feb 2024 14:05:46 +0100 Subject: [PATCH 038/204] beps/0003: service and rollout updates + move to implementable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: Carl-Erik Bergström Co-authored-by: blam Co-authored-by: Camila Belo Signed-off-by: Patrik Oldsberg --- .../README.md | 80 ++++++++++++------- 1 file changed, 52 insertions(+), 28 deletions(-) diff --git a/beps/0003-auth-architecture-evolution/README.md b/beps/0003-auth-architecture-evolution/README.md index 713184be8c..4c11f1fc2a 100644 --- a/beps/0003-auth-architecture-evolution/README.md +++ b/beps/0003-auth-architecture-evolution/README.md @@ -1,6 +1,6 @@ --- title: Auth Architecture Evolution -status: provisional +status: implementable authors: - '@Rugvip' owners: @@ -60,6 +60,7 @@ The following goals are the primary focus of this BEP: - No advanced rate limiting or other protection against DDoS attacks. If this is a concern then adopters should still use other external technologies to protect access to their Backstage instance. - As part of the immediate work we will only add as much support for service-to-service auth as is needed for a stable API, and not necessarily make it very capable from the start. +- We will not aim to provide an abstraction that makes it possible to switch out the `Authorization` header for service-to-service communication. ## Proposal @@ -126,11 +127,13 @@ export interface AuthService { type: TType, ): credentials is BackstageCredentials; - getOwnCredentials(): Promise>; + getOwnServiceCredentials(): Promise< + BackstageCredentials + >; - // TODO: should the caller provide the target plugin ID? if not - probably skip the object form - issueServiceToken(options: { - forward: BackstageCredentials; + getPluginRequestToken(options: { + onBehalfOf: BackstageCredentials; + targetPluginId: string; }): Promise<{ token: string }>; } ``` @@ -257,7 +260,6 @@ The new `HttpAuthService` interface is defined as follows: ```ts type BackstageHttpAccessToPrincipalTypesMapping = { user: BackstageUserPrincipal; - 'user-cookie': BackstageUserPrincipal; service: BackstageServicePrincipal; unauthenticated: BackstageNonePrincipal; unknown: unknown; @@ -270,17 +272,16 @@ export interface HttpAuthService { | keyof BackstageHttpAccessToPrincipalTypesMapping = 'unknown', >( req: Request, - options?: { allow: Array }, + options?: { + allow?: Array; + allowedAuthMethods?: Array<'token' | 'cookie'>; + }, ): Promise< BackstageCredentials >; - requestHeaders(options: { - forward: BackstageCredentials; - }): Promise>; - // The cookie issued by this method must be consumable by the `credentials` method, which in turn - // should create a credentials object that can be passed to the `issueServiceToken` method. + // should create a credentials object that can be passed to the `getPluginRequestToken` method. // The issued token must then in turn be a valid token for a user principal with full access. issueUserCookie(res: Response): Promise; } @@ -304,6 +305,25 @@ router.get('/read-data', (req, res) => { }); ``` +#### Issue a service token for a backend-to-backend request + +NOTE: This is not what we want this kind of request to look like in practice. The goal is to forward credential objects as far as possible, keeping the `auth.getPluginRequestToken(...)` in API client code rather than plugin app code. + +```ts +const { token } = await auth.getPluginRequestToken({ + onBehalfOf: await auth.getOwnServiceCredentials(), + targetPluginId: 'example', +}); + +const baseUrl = await discovery.getBaseUrl('example'); +const res = await fetch(`${baseUrl}/some-resource`, { + headers: { + // A utility may be provided for this in the future if needed, this is currently fairly rare + Authorization: `Bearer ${token}`, + }, +}); +``` + #### Forward the user credentials from an incoming requests to upstream plugin backend ```ts @@ -315,8 +335,9 @@ class CatalogIntegration { }, ) { return catalogClient.getEntityByRef(req.params.entityRef, { - token: await auth.issueServiceToken({ - forward: options.credentials, + token: await auth.getPluginRequestToken({ + onBehalfOf: options.credentials, + targetPluginId: 'catalog', }), }); } @@ -402,27 +423,30 @@ backend: dangerouslyDisableServiceAuth: true ``` -The exact impact that this has is that it disables the check in the `HttpRouterService` implementation, effectively applying the `unauthenticated` access level to all routes. Furthermore, it will also change `AuthService` so that the `issueServiceToken()` method will now issue an empty token for a `'none'` principal, rather than throwing. +The exact impact that this has is that it disables the check in the `HttpRouterService` implementation, effectively applying the `unauthenticated` access level to all routes. Furthermore, it will also change `AuthService` so that the `getPluginRequestToken()` method will now issue an empty token for a `'none'` principal, rather than throwing. ## Release Plan The existing `IdentityService` and `TokenManagerService` will be deprecated and instead implemented in terms of the new `AuthService`. -The release plan for the `HttpAuthService` is TBD, but is likely to be shipped as a no-op for plugins using the old backend system. The goal is for all plugins using the new backend system to have endpoint security be opt-out, which will be a breaking change. +The new `AuthService` and `HttpAuthService` will need backwards compatible implementations for the old backend system. The plan is to not apply any access restrictions for the old Backend system, only implementing that in the new system. The backwards compatibility helpers will use the provided `identity` and `tokenManager` services if available, and plugins should provide fallbacks in the same way as they currently do. If these are not provided, the `identity` client will fall back to `DefaultIdentityClient`, and `tokenManager` will fall back `ServicerTokenManager.noop()`. -### Implementation Tasks +The backwards compatibility helpers will have the following behavior for each individual service call: -- [ ] Implement `AuthService` -- [ ] Implement `HttpAuthService` - leave cookie auth as unimplemented for now -- [ ] Add `addAuthPolicy()` for `HttpRouterService`, using `HttpAuthService` -- [ ] Implement a compatibility wrapper in `backend-common` that accepts `AuthService`, `HttpAuthService`, `IdentityService`, and `TokenManagerService` (all optional), and returns implementations for `AuthService` and `HttpAuthService`, such hat existing plugins can use a single `createRouter` implementation for both the old and new backend systems. -- [ ] Implement `UserInfoService` in `@backstage/auth-node` - for now it will just extract the ownership entity refs from the token stored in the credentials -- [ ] Implement cookie auth in `HttpAuthService` - just put the user token in the cookie for now -- [ ] Deprecate `IdentityService` and `TokenManagerService`, switch to using default factories that depend on the `AuthService` and `HttpAuthService`. Stop supplying implementations for these in `backend-defaults` and `backend-test-utils` -- [ ] Migrate plugins: - - [ ] Permission backend - - [ ] TechDocs backend - - [ ] App backend +- `auth.authenticate(token)`: If the decoded token has the `backstage` audience, authenticate the token for a user principal using `identity.getIdentity(...)`, otherwise authenticate it using `tokenManager.authenticate(...)` and return a service principal with the subject `external:backstage-plugin`. If a no-op token manager is used then anything but a user token will be treated as a valid service token, which is consistent with existing behavior. +- `auth.getOwnServiceCredentials()`: Use original implementation. +- `auth.isPrincipal()`: Use original implementation. +- `auth.getPluginRequestToken(options)`: Same behavior as the original implementation, using the `tokenManager` to issue service tokens, with the exception that a `none` principal will translate to an empty token rather than an error in order to properly forward calls with a no-op token manager. +- `httpAuth.credentials(...)`: Use original implementation. +- `httpAuth.issueUserCookie(...)`: This is a no-op as we do not need to support cookie auth in the legacy adapter. + +With this compatibility layer in place all plugins will be refactored to always use the new `AuthService` and `HttpAuthService` internally. The old deprecated services are only accepted at the public API boundaries, i.e. `createRouter` and similar. All plugin code beyond that point uses the new services. + +We do not roll out support for the new auth services for the old backend system, the full implementation is only supported in the new backend system. In particular this means that the new default protection in the `HttpRouterService` only apply to the new backend system. + +Users of the old backend system may already have their own protection set up, which we need to take into account, ensuring that we do not break these existing implementations. + +Several API clients will be updated to support passing `BackstageCredentials` instead of a token, although it is not a requirement to update all clients. In particular we hold of on migration isomorphic clients, leaving them to keep consuming tokens where possible. Adding support for credentials to be passed to these clients is a separate future improvement. ## Dependencies From 468f51100cdac1446682064b6cc78acc39778757 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 15 Feb 2024 16:03:09 +0100 Subject: [PATCH 039/204] Update beps/0003-auth-architecture-evolution/README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Patrik Oldsberg --- beps/0003-auth-architecture-evolution/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beps/0003-auth-architecture-evolution/README.md b/beps/0003-auth-architecture-evolution/README.md index 4c11f1fc2a..92e27e06e6 100644 --- a/beps/0003-auth-architecture-evolution/README.md +++ b/beps/0003-auth-architecture-evolution/README.md @@ -429,7 +429,7 @@ The exact impact that this has is that it disables the check in the `HttpRouterS The existing `IdentityService` and `TokenManagerService` will be deprecated and instead implemented in terms of the new `AuthService`. -The new `AuthService` and `HttpAuthService` will need backwards compatible implementations for the old backend system. The plan is to not apply any access restrictions for the old Backend system, only implementing that in the new system. The backwards compatibility helpers will use the provided `identity` and `tokenManager` services if available, and plugins should provide fallbacks in the same way as they currently do. If these are not provided, the `identity` client will fall back to `DefaultIdentityClient`, and `tokenManager` will fall back `ServicerTokenManager.noop()`. +The new `AuthService` and `HttpAuthService` will need backwards compatible implementations for the old backend system. The plan is to not apply any access restrictions for the old Backend system, only implementing that in the new system. The backwards compatibility helpers will use the provided `identity` and `tokenManager` services if available, and plugins should provide fallbacks in the same way as they currently do. If these are not provided, the `identity` client will fall back to `DefaultIdentityClient`, and `tokenManager` will fall back to `ServicerTokenManager.noop()`. The backwards compatibility helpers will have the following behavior for each individual service call: From 213de53ef2d0a48896d530f03ab745dfc0178103 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Feb 2024 15:26:18 +0000 Subject: [PATCH 040/204] chore(deps): update dependency webpack to v5.90.2 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- storybook/yarn.lock | 6 +++--- yarn.lock | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/storybook/yarn.lock b/storybook/yarn.lock index 42453e2080..e59970f4f7 100644 --- a/storybook/yarn.lock +++ b/storybook/yarn.lock @@ -11756,8 +11756,8 @@ __metadata: linkType: hard "webpack@npm:^5.73.0": - version: 5.90.1 - resolution: "webpack@npm:5.90.1" + version: 5.90.2 + resolution: "webpack@npm:5.90.2" dependencies: "@types/eslint-scope": ^3.7.3 "@types/estree": ^1.0.5 @@ -11788,7 +11788,7 @@ __metadata: optional: true bin: webpack: bin/webpack.js - checksum: a7be844d5720a0c6282fec012e6fa34b1137dff953c5d48bf2ef066a6c27c1dbc92a9b9effc05ee61c9fe269499266db9782073f2d82a589d3c5c966ffc56584 + checksum: 4af55f314c3610e0b17afd01960c4cf9d8d90c87a58ef7d7dc7a0a019f48e2bba4eff63b02d9da56fc61df5b04af0d66b3fa4ec82937a780d37a64db84c643fe languageName: node linkType: hard diff --git a/yarn.lock b/yarn.lock index 6280775f8b..926e693d07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -45676,8 +45676,8 @@ __metadata: linkType: hard "webpack@npm:^5, webpack@npm:^5.70.0": - version: 5.90.1 - resolution: "webpack@npm:5.90.1" + version: 5.90.2 + resolution: "webpack@npm:5.90.2" dependencies: "@types/eslint-scope": ^3.7.3 "@types/estree": ^1.0.5 @@ -45708,7 +45708,7 @@ __metadata: optional: true bin: webpack: bin/webpack.js - checksum: a7be844d5720a0c6282fec012e6fa34b1137dff953c5d48bf2ef066a6c27c1dbc92a9b9effc05ee61c9fe269499266db9782073f2d82a589d3c5c966ffc56584 + checksum: 4af55f314c3610e0b17afd01960c4cf9d8d90c87a58ef7d7dc7a0a019f48e2bba4eff63b02d9da56fc61df5b04af0d66b3fa4ec82937a780d37a64db84c643fe languageName: node linkType: hard From cf59dc40b9c21dc22ceffba0083a0bc80539cac1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 15 Feb 2024 17:17:16 +0100 Subject: [PATCH 041/204] Update beps/0003-auth-architecture-evolution/README.md Co-authored-by: Phil Kuang Signed-off-by: Patrik Oldsberg --- beps/0003-auth-architecture-evolution/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beps/0003-auth-architecture-evolution/README.md b/beps/0003-auth-architecture-evolution/README.md index 92e27e06e6..dc475fa14d 100644 --- a/beps/0003-auth-architecture-evolution/README.md +++ b/beps/0003-auth-architecture-evolution/README.md @@ -446,7 +446,7 @@ We do not roll out support for the new auth services for the old backend system, Users of the old backend system may already have their own protection set up, which we need to take into account, ensuring that we do not break these existing implementations. -Several API clients will be updated to support passing `BackstageCredentials` instead of a token, although it is not a requirement to update all clients. In particular we hold of on migration isomorphic clients, leaving them to keep consuming tokens where possible. Adding support for credentials to be passed to these clients is a separate future improvement. +Several API clients will be updated to support passing `BackstageCredentials` instead of a token, although it is not a requirement to update all clients. In particular we will hold off on migrating isomorphic clients, leaving them to keep consuming tokens where possible. Adding support for credentials to be passed to these clients is a separate future improvement. ## Dependencies From bf3da1642c1309428a957571ba5cd0aef3c38a61 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Feb 2024 16:12:27 +0000 Subject: [PATCH 042/204] fix(deps): update dependency typescript-json-schema to ^0.63.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .changeset/renovate-4b0ba1e.md | 5 +++++ packages/config-loader/package.json | 2 +- yarn.lock | 10 +++++----- 3 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 .changeset/renovate-4b0ba1e.md diff --git a/.changeset/renovate-4b0ba1e.md b/.changeset/renovate-4b0ba1e.md new file mode 100644 index 0000000000..54dcd58187 --- /dev/null +++ b/.changeset/renovate-4b0ba1e.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +Updated dependency `typescript-json-schema` to `^0.63.0`. diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index a9e80a4672..f5ed8e4d85 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -47,7 +47,7 @@ "lodash": "^4.17.21", "minimist": "^1.2.5", "node-fetch": "^2.6.7", - "typescript-json-schema": "^0.62.0", + "typescript-json-schema": "^0.63.0", "yaml": "^2.0.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index d735732949..3bff269402 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3775,7 +3775,7 @@ __metadata: minimist: ^1.2.5 msw: ^1.0.0 node-fetch: ^2.6.7 - typescript-json-schema: ^0.62.0 + typescript-json-schema: ^0.63.0 yaml: ^2.0.0 zen-observable: ^0.10.0 languageName: unknown @@ -44336,9 +44336,9 @@ __metadata: languageName: node linkType: hard -"typescript-json-schema@npm:^0.62.0": - version: 0.62.0 - resolution: "typescript-json-schema@npm:0.62.0" +"typescript-json-schema@npm:^0.63.0": + version: 0.63.0 + resolution: "typescript-json-schema@npm:0.63.0" dependencies: "@types/json-schema": ^7.0.9 "@types/node": ^16.9.2 @@ -44350,7 +44350,7 @@ __metadata: yargs: ^17.1.1 bin: typescript-json-schema: bin/typescript-json-schema - checksum: 0b0ba8f86456e7bd0b2d35d3c74a6cb72b168e779352398956d4f237eff011ec2a30b0ab1b3932bd5ceab99e1ea7bc6cc7158e49d49f24daeaf11771c27c7771 + checksum: 619ab7aece08e140ba9542c6378c335751dbff3994a23343d0af67786a0c1e682d532a436c1674ddb10bca3f34972ecac7ba529b66d0e9b3e00ca81defb3aa77 languageName: node linkType: hard From 86fa9cce6e8b161db894f848bfcc70cb9c44364c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Feb 2024 16:56:13 +0000 Subject: [PATCH 043/204] chore(deps): update github/codeql-action action to v3.24.3 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/scorecard.yml | 2 +- .github/workflows/sync_snyk-monitor.yml | 2 +- .github/workflows/verify_codeql.yml | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 4155f08e03..621776d69b 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -66,6 +66,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: 'Upload to code-scanning' - uses: github/codeql-action/upload-sarif@e675ced7a7522a761fc9c8eb26682c8b27c42b2b # v3.24.1 + uses: github/codeql-action/upload-sarif@379614612a29c9e28f31f39a59013eb8012a51f0 # v3.24.3 with: sarif_file: results.sarif diff --git a/.github/workflows/sync_snyk-monitor.yml b/.github/workflows/sync_snyk-monitor.yml index 080bd3cc73..7ee4e72d38 100644 --- a/.github/workflows/sync_snyk-monitor.yml +++ b/.github/workflows/sync_snyk-monitor.yml @@ -58,6 +58,6 @@ jobs: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} NODE_OPTIONS: --max-old-space-size=7168 - name: Upload Snyk report - uses: github/codeql-action/upload-sarif@e675ced7a7522a761fc9c8eb26682c8b27c42b2b # v3.24.1 + uses: github/codeql-action/upload-sarif@379614612a29c9e28f31f39a59013eb8012a51f0 # v3.24.3 with: sarif_file: snyk.sarif diff --git a/.github/workflows/verify_codeql.yml b/.github/workflows/verify_codeql.yml index 48195726e1..7c7ef286db 100644 --- a/.github/workflows/verify_codeql.yml +++ b/.github/workflows/verify_codeql.yml @@ -55,7 +55,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@e675ced7a7522a761fc9c8eb26682c8b27c42b2b # v3.24.1 + uses: github/codeql-action/init@379614612a29c9e28f31f39a59013eb8012a51f0 # v3.24.3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -66,7 +66,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@e675ced7a7522a761fc9c8eb26682c8b27c42b2b # v3.24.1 + uses: github/codeql-action/autobuild@379614612a29c9e28f31f39a59013eb8012a51f0 # v3.24.3 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -80,4 +80,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@e675ced7a7522a761fc9c8eb26682c8b27c42b2b # v3.24.1 + uses: github/codeql-action/analyze@379614612a29c9e28f31f39a59013eb8012a51f0 # v3.24.3 From 534786aa217935228bf26a63b69281be3ccf9e21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ragnar=20Halld=C3=B3rsson?= Date: Thu, 15 Feb 2024 21:38:00 +0100 Subject: [PATCH 044/204] fix(catalog-backend-module-azure): Add branch to Code Search query when provided MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Ragnar Halldórsson --- .changeset/real-keys-juggle.md | 5 ++ .../src/lib/azure.test.ts | 60 ++++++++++++++++++ .../src/lib/azure.ts | 39 ++++++++---- .../AzureDevOpsDiscoveryProcessor.test.ts | 63 +++++++++++++++++++ .../AzureDevOpsDiscoveryProcessor.ts | 20 +++++- .../providers/AzureDevOpsEntityProvider.ts | 1 + 6 files changed, 175 insertions(+), 13 deletions(-) create mode 100644 .changeset/real-keys-juggle.md diff --git a/.changeset/real-keys-juggle.md b/.changeset/real-keys-juggle.md new file mode 100644 index 0000000000..cb323853de --- /dev/null +++ b/.changeset/real-keys-juggle.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-azure': minor +--- + +Fixed issue where specifying a branch for discovery did not work diff --git a/plugins/catalog-backend-module-azure/src/lib/azure.test.ts b/plugins/catalog-backend-module-azure/src/lib/azure.test.ts index 493f0dda51..ce06fd6221 100644 --- a/plugins/catalog-backend-module-azure/src/lib/azure.test.ts +++ b/plugins/catalog-backend-module-azure/src/lib/azure.test.ts @@ -214,6 +214,66 @@ describe('azure', () => { ).resolves.toEqual(response.results); }); + it('searches in specific branch if parameter is set', async () => { + const response: CodeSearchResponse = { + count: 1, + results: [ + { + fileName: 'catalog-info.yaml', + path: '/catalog-info.yaml', + project: { + name: '*', + }, + repository: { + name: 'backstage', + }, + }, + ], + }; + + server.use( + rest.post( + `https://almsearch.dev.azure.com/shopify/_apis/search/codesearchresults`, + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); + expect(req.body).toEqual({ + searchText: + 'path:/catalog-info.yaml repo:backstage proj:engineering', + $orderBy: [ + { + field: 'path', + sortOrder: 'ASC', + }, + ], + $skip: 0, + $top: 1000, + filters: { + Branch: ['development'], + }, + }); + return res(ctx.json(response)); + }, + ), + ); + + const { credentialsProvider, azureConfig } = createFixture( + 'dev.azure.com', + 'ABC', + ); + + await expect( + codeSearch( + credentialsProvider, + azureConfig, + 'shopify', + 'engineering', + 'backstage', + '/catalog-info.yaml', + 'development', + ), + ).resolves.toEqual(response.results); + }); + it('can search using onpremise api', async () => { const response: CodeSearchResponse = { count: 1, diff --git a/plugins/catalog-backend-module-azure/src/lib/azure.ts b/plugins/catalog-backend-module-azure/src/lib/azure.ts index c8ff8dd739..3bdff90110 100644 --- a/plugins/catalog-backend-module-azure/src/lib/azure.ts +++ b/plugins/catalog-backend-module-azure/src/lib/azure.ts @@ -37,6 +37,16 @@ export interface CodeSearchResultItem { branch?: string; } +interface CodeSearchRequest { + searchText: string; + $orderBy: Array<{ field: string; sortOrder: string }>; + $skip: number; + $top: number; + filters?: { + Branch: string[]; + }; +} + const isCloud = (host: string) => host === 'dev.azure.com'; const PAGE_SIZE = 1000; @@ -48,6 +58,7 @@ export async function codeSearch( project: string, repo: string, path: string, + branch: string, ): Promise { const searchBaseUrl = isCloud(azureConfig.host) ? 'https://almsearch.dev.azure.com' @@ -62,23 +73,29 @@ export async function codeSearch( url: `https://${azureConfig.host}/${org}`, }); + const searchRequestBody: CodeSearchRequest = { + searchText: `path:${path} repo:${repo || '*'} proj:${project || '*'}`, + $orderBy: [ + { + field: 'path', + sortOrder: 'ASC', + }, + ], + $skip: items.length, + $top: PAGE_SIZE, + }; + + if (branch) { + searchRequestBody.filters = { Branch: [branch] }; + } + const response = await fetch(searchUrl, { headers: { ...credentials?.headers, 'Content-Type': 'application/json', }, method: 'POST', - body: JSON.stringify({ - searchText: `path:${path} repo:${repo || '*'} proj:${project || '*'}`, - $orderBy: [ - { - field: 'path', - sortOrder: 'ASC', - }, - ], - $skip: items.length, - $top: PAGE_SIZE, - }), + body: JSON.stringify(searchRequestBody), }); if (response.status !== 200) { diff --git a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts index 36dd920621..0817465d83 100644 --- a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts @@ -35,6 +35,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { project: 'my-proj', repo: '', catalogPath: '/catalog-info.yaml', + branch: '', }); expect( @@ -47,6 +48,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { project: 'engineering', repo: 'backstage', catalogPath: '/catalog.yaml', + branch: '', }); expect( @@ -59,6 +61,20 @@ describe('AzureDevOpsDiscoveryProcessor', () => { project: 'engineering', repo: 'backstage', catalogPath: '/src/*/catalog.yaml', + branch: '', + }); + + expect( + parseUrl( + 'https://azuredevops.mycompany.com/spotify/engineering/_git/backstage?path=/src/*/catalog.yaml&version=GBdevelopment', + ), + ).toEqual({ + baseUrl: 'https://azuredevops.mycompany.com', + org: 'spotify', + project: 'engineering', + repo: 'backstage', + catalogPath: '/src/*/catalog.yaml', + branch: 'development', }); }); @@ -164,6 +180,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { 'engineering', '', '/catalog-info.yaml', + '', ); expect(emitter).toHaveBeenCalledTimes(2); expect(emitter).toHaveBeenCalledWith({ @@ -214,6 +231,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { 'engineering', 'backstage', '/catalog-info.yaml', + '', ); expect(emitter).toHaveBeenCalledTimes(1); expect(emitter).toHaveBeenCalledWith({ @@ -227,6 +245,49 @@ describe('AzureDevOpsDiscoveryProcessor', () => { }); }); + it('output locations with branch if specified in target', async () => { + const location: LocationSpec = { + type: 'azure-discovery', + target: + 'https://dev.azure.com/shopify/engineering/_git/backstage?path=/catalog-info.yaml&version=GBdevelopment', + }; + mockCodeSearch.mockResolvedValueOnce([ + { + fileName: 'catalog-info.yaml', + path: '/catalog-info.yaml', + repository: { + name: 'backstage', + }, + project: { + name: '*', + }, + }, + ]); + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(mockCodeSearch).toHaveBeenCalledWith( + expect.anything(), + { host: 'dev.azure.com' }, + 'shopify', + 'engineering', + 'backstage', + '/catalog-info.yaml', + 'development', + ); + expect(emitter).toHaveBeenCalledTimes(1); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://dev.azure.com/shopify/engineering/_git/backstage?path=/catalog-info.yaml&version=GBdevelopment', + presence: 'optional', + }, + }); + }); + it('output single locations with different file name from code search', async () => { const location: LocationSpec = { type: 'azure-discovery', @@ -256,6 +317,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { 'engineering', '', '/src/*/catalog.yaml', + '', ); expect(emitter).toHaveBeenCalledTimes(1); expect(emitter).toHaveBeenCalledWith({ @@ -286,6 +348,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { 'engineering', 'backstage', '/catalog-info.yaml', + '', ); expect(emitter).not.toHaveBeenCalled(); }); diff --git a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts index d9638e6cc7..ddb3c5f59f 100644 --- a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts @@ -92,7 +92,7 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { ); } - const { baseUrl, org, project, repo, catalogPath } = parseUrl( + const { baseUrl, org, project, repo, catalogPath, branch } = parseUrl( location.target, ); this.logger.info( @@ -106,6 +106,7 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { project, repo, catalogPath, + branch, ); this.logger.debug( @@ -113,10 +114,16 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { ); for (const file of files) { + let target = `${baseUrl}/${org}/${project}/_git/${file.repository.name}?path=${file.path}`; + + if (branch) { + target += `&version=GB${branch}`; + } + emit( processingResult.location({ type: 'url', - target: `${baseUrl}/${org}/${project}/_git/${file.repository.name}?path=${file.path}`, + target, // Not all locations may actually exist, since the user defined them as a wildcard pattern. // Thus, we emit them as optional and let the downstream processor find them while not outputting // an error if it couldn't. @@ -138,11 +145,18 @@ export function parseUrl(urlString: string): { project: string; repo: string; catalogPath: string; + branch: string; } { const url = new URL(urlString); const path = url.pathname.slice(1).split('/'); const catalogPath = url.searchParams.get('path') || '/catalog-info.yaml'; + let branch = url.searchParams.get('version') || ''; + + if (branch.startsWith('GB')) { + // DevOps prefixes branch names with 'GB' in URLs + branch = branch.slice(2); + } if (path.length === 2 && path[0].length && path[1].length) { return { @@ -151,6 +165,7 @@ export function parseUrl(urlString: string): { project: decodeURIComponent(path[1]), repo: '', catalogPath, + branch, }; } else if ( path.length === 4 && @@ -165,6 +180,7 @@ export function parseUrl(urlString: string): { project: decodeURIComponent(path[1]), repo: decodeURIComponent(path[3]), catalogPath, + branch, }; } diff --git a/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts b/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts index f56369a0d1..44e618ffcd 100644 --- a/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts +++ b/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts @@ -158,6 +158,7 @@ export class AzureDevOpsEntityProvider implements EntityProvider { this.config.project, this.config.repository, this.config.path, + this.config.branch || '', ); logger.info(`Discovered ${files.length} catalog files`); From 984d5084d116c823da4b1c079ac0f0c6e1e17594 Mon Sep 17 00:00:00 2001 From: armandocomellas1 Date: Thu, 15 Feb 2024 15:00:38 -0600 Subject: [PATCH 045/204] Add header for calls towards Google Cloud Projects Signed-off-by: armandocomellas1 --- .changeset/dull-bottles-learn.md | 5 +++++ plugins/gcp-projects/src/api/GcpClient.ts | 4 ++++ plugins/gcp-projects/src/plugin.test.ts | 4 ++++ 3 files changed, 13 insertions(+) create mode 100644 .changeset/dull-bottles-learn.md diff --git a/.changeset/dull-bottles-learn.md b/.changeset/dull-bottles-learn.md new file mode 100644 index 0000000000..e279f9e0ad --- /dev/null +++ b/.changeset/dull-bottles-learn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-gcp-projects': patch +--- + +Add a `x-google-api-client` header for calls towards Google Cloud Projects diff --git a/plugins/gcp-projects/src/api/GcpClient.ts b/plugins/gcp-projects/src/api/GcpClient.ts index 10dcb5a4c8..e9375bcfa6 100644 --- a/plugins/gcp-projects/src/api/GcpClient.ts +++ b/plugins/gcp-projects/src/api/GcpClient.ts @@ -17,6 +17,7 @@ import { GcpApi } from './GcpApi'; import { Operation, Project } from './types'; import { OAuthApi } from '@backstage/core-plugin-api'; +import packageinfo from '../../package.json'; const BASE_URL = 'https://content-cloudresourcemanager.googleapis.com/v1/projects'; @@ -30,6 +31,7 @@ export class GcpClient implements GcpApi { headers: { Accept: '*/*', Authorization: `Bearer ${await this.getToken()}`, + 'X-Goog-Api-Client': `backstage/cloudbuild/${packageinfo.version}`, }, }); @@ -48,6 +50,7 @@ export class GcpClient implements GcpApi { const response = await fetch(url, { headers: { Authorization: `Bearer ${await this.getToken()}`, + 'X-Goog-Api-Client': `backstage/cloudbuild/${packageinfo.version}`, }, }); @@ -74,6 +77,7 @@ export class GcpClient implements GcpApi { headers: { Accept: '*/*', Authorization: `Bearer ${await this.getToken()}`, + 'X-Goog-Api-Client': `backstage/cloudbuild/${packageinfo.version}`, }, body: JSON.stringify(newProject), }); diff --git a/plugins/gcp-projects/src/plugin.test.ts b/plugins/gcp-projects/src/plugin.test.ts index 7b644d1400..8b53d0f0f0 100644 --- a/plugins/gcp-projects/src/plugin.test.ts +++ b/plugins/gcp-projects/src/plugin.test.ts @@ -15,9 +15,13 @@ */ import { gcpProjectsPlugin } from './plugin'; +import * as container from '@backstage/plugin-gcp-projects'; describe('gcp-projects', () => { it('should export plugin', () => { expect(gcpProjectsPlugin).toBeDefined(); }); + it('should exist the GcpClient class', () => { + expect(container.GcpClient).toBeDefined(); + }); }); From d99351873b8cb13c6fe8b275edb30fa523debfa2 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 15 Feb 2024 22:15:25 +0100 Subject: [PATCH 046/204] docs: missing leading number Signed-off-by: Vincenzo Scamporlino --- .../building-plugins/{migrating.md => 05-migrating.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/frontend-system/building-plugins/{migrating.md => 05-migrating.md} (100%) diff --git a/docs/frontend-system/building-plugins/migrating.md b/docs/frontend-system/building-plugins/05-migrating.md similarity index 100% rename from docs/frontend-system/building-plugins/migrating.md rename to docs/frontend-system/building-plugins/05-migrating.md From 4fcb4667f07bdba361e0118d15229276a194ef08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ragnar=20Halld=C3=B3rsson?= Date: Thu, 15 Feb 2024 22:15:41 +0100 Subject: [PATCH 047/204] chore(catalog-backend-module-azure): Update discovery documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Ragnar Halldórsson --- docs/integrations/azure/discovery.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/integrations/azure/discovery.md b/docs/integrations/azure/discovery.md index 742fbb6076..ad454c0ed1 100644 --- a/docs/integrations/azure/discovery.md +++ b/docs/integrations/azure/discovery.md @@ -160,11 +160,14 @@ catalog: # Or use a custom file format and location - type: azure-discovery target: https://dev.azure.com/myorg/myproject/_git/*?path=/src/*/catalog-info.yaml + # And optionally provide a specific branch name using the version parameter + - type: azure-discovery + target: https://dev.azure.com/myorg/myproject/_git/*?path=/catalog-info.yaml&version=GBdevelopment ``` Note the `azure-discovery` type, as this is not a regular `url` processor. -When using a custom pattern, the target is composed of five parts: +When using a custom pattern, the target is composed of six parts: - The base instance URL, `https://dev.azure.com` in this case - The organization name which is required, `myorg` in this case @@ -175,3 +178,4 @@ When using a custom pattern, the target is composed of five parts: - The path within each repository to find the catalog YAML file. This will usually be `/catalog-info.yaml`, `/src/*/catalog-info.yaml` or a similar variation for catalog files stored in the root directory of each repository. +- The repository branch to scan which is optional, `development` in this case. The `GB` prefix is mandatory, as this is how Azure DevOps identifies the version as a branch. If omitted, the repo's default branch will be scanned. From d185dcd12e2b7dfb2a372a363c3e8695a9a944bd Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 15 Feb 2024 22:15:47 +0100 Subject: [PATCH 048/204] docs: fix migrating apis links Signed-off-by: Vincenzo Scamporlino --- docs/frontend-system/utility-apis/01-index.md | 2 +- docs/frontend-system/utility-apis/02-creating.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/frontend-system/utility-apis/01-index.md b/docs/frontend-system/utility-apis/01-index.md index 2d0f0a9698..37120a5152 100644 --- a/docs/frontend-system/utility-apis/01-index.md +++ b/docs/frontend-system/utility-apis/01-index.md @@ -40,4 +40,4 @@ These cases are all described in [the main article](./04-configuring.md). ## Migrating from the old frontend system -If you want to learn how to migrate your own utility APIs from the old frontend system to the new one, that's described in [a dedicated migration guide](./05-migrating.md). +If you want to learn how to migrate your own utility APIs from the old frontend system to the new one, that's described in the [Migrating APIs guide](../building-plugins/05-migrating.md#migrating-apis). diff --git a/docs/frontend-system/utility-apis/02-creating.md b/docs/frontend-system/utility-apis/02-creating.md index 8935b31b75..fcb7ff1a65 100644 --- a/docs/frontend-system/utility-apis/02-creating.md +++ b/docs/frontend-system/utility-apis/02-creating.md @@ -8,7 +8,7 @@ description: Creating new utility APIs in your plugins and app > **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.** -This section describes how to make a Utility API from scratch, or to add configurability and inputs to an existing one. If you are instead interested in migrating an existing Utility API from the old frontend system, check out [the migrating section](./05-migrating.md). +This section describes how to make a Utility API from scratch, or to add configurability and inputs to an existing one. If you are instead interested in migrating an existing Utility API from the old frontend system, check out the [Migrating APIs section](../building-plugins/05-migrating.md#migrating-apis). ## Creating the Utility API contract From 7446616db052d65474cb0fe3f7ef85c5d33307d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ragnar=20Halld=C3=B3rsson?= Date: Thu, 15 Feb 2024 22:29:20 +0100 Subject: [PATCH 049/204] chore(catalog-backend-module-azure): Update tests I missed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Ragnar Halldórsson --- plugins/catalog-backend-module-azure/src/lib/azure.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/catalog-backend-module-azure/src/lib/azure.test.ts b/plugins/catalog-backend-module-azure/src/lib/azure.test.ts index ce06fd6221..21fc77e7f7 100644 --- a/plugins/catalog-backend-module-azure/src/lib/azure.test.ts +++ b/plugins/catalog-backend-module-azure/src/lib/azure.test.ts @@ -89,6 +89,7 @@ describe('azure', () => { 'engineering', '', '/catalog-info.yaml', + '', ), ).resolves.toEqual([]); }); @@ -154,6 +155,7 @@ describe('azure', () => { 'engineering', '', '/catalog-info.yaml', + '', ), ).resolves.toEqual(response.results); }); @@ -210,6 +212,7 @@ describe('azure', () => { 'engineering', 'backstage', '/catalog-info.yaml', + '', ), ).resolves.toEqual(response.results); }); @@ -325,6 +328,7 @@ describe('azure', () => { 'engineering', '', '/catalog-info.yaml', + '', ), ).resolves.toEqual(response.results); }); @@ -384,6 +388,7 @@ describe('azure', () => { 'engineering', 'backstage', '/catalog-info.yaml', + '', ), ).resolves.toHaveLength(totalCount); }); From 603a9729960098772ce12be1c230d527dcfbfa66 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Feb 2024 21:38:34 +0000 Subject: [PATCH 050/204] fix(deps): update aws-sdk-js-v3 monorepo to v3.515.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 588 +++++++++++++++++++++++++++--------------------------- 1 file changed, 294 insertions(+), 294 deletions(-) diff --git a/yarn.lock b/yarn.lock index d735732949..c14cb29002 100644 --- a/yarn.lock +++ b/yarn.lock @@ -363,24 +363,24 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/client-cognito-identity@npm:3.514.0": - version: 3.514.0 - resolution: "@aws-sdk/client-cognito-identity@npm:3.514.0" +"@aws-sdk/client-cognito-identity@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/client-cognito-identity@npm:3.515.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.513.0 + "@aws-sdk/client-sts": 3.515.0 "@aws-sdk/core": 3.513.0 - "@aws-sdk/credential-provider-node": 3.514.0 - "@aws-sdk/middleware-host-header": 3.511.0 - "@aws-sdk/middleware-logger": 3.511.0 - "@aws-sdk/middleware-recursion-detection": 3.511.0 - "@aws-sdk/middleware-user-agent": 3.511.0 - "@aws-sdk/region-config-resolver": 3.511.0 - "@aws-sdk/types": 3.511.0 - "@aws-sdk/util-endpoints": 3.511.0 - "@aws-sdk/util-user-agent-browser": 3.511.0 - "@aws-sdk/util-user-agent-node": 3.511.0 + "@aws-sdk/credential-provider-node": 3.515.0 + "@aws-sdk/middleware-host-header": 3.515.0 + "@aws-sdk/middleware-logger": 3.515.0 + "@aws-sdk/middleware-recursion-detection": 3.515.0 + "@aws-sdk/middleware-user-agent": 3.515.0 + "@aws-sdk/region-config-resolver": 3.515.0 + "@aws-sdk/types": 3.515.0 + "@aws-sdk/util-endpoints": 3.515.0 + "@aws-sdk/util-user-agent-browser": 3.515.0 + "@aws-sdk/util-user-agent-node": 3.515.0 "@smithy/config-resolver": ^2.1.1 "@smithy/core": ^1.3.2 "@smithy/fetch-http-handler": ^2.4.1 @@ -407,28 +407,28 @@ __metadata: "@smithy/util-retry": ^2.1.1 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: 38629787a3dbbaf7edad47c7e76c3b5015b20c7e9e8cbb343f5050f3c7bd31920e22e45e1ccbf21fe9fe331dc8f307420e5da6b5c5f0d26e1ee77e6a5dddd86a + checksum: e254357719b355a7c6cdd718c3896aca9971ea9479887c221841ec2c6e91f15b880f6a5d1ebfc62af223b4d02a0d4a882aa8394b436f9b544658ee87a481e1ac languageName: node linkType: hard "@aws-sdk/client-eks@npm:^3.350.0": - version: 3.514.0 - resolution: "@aws-sdk/client-eks@npm:3.514.0" + version: 3.515.0 + resolution: "@aws-sdk/client-eks@npm:3.515.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.513.0 + "@aws-sdk/client-sts": 3.515.0 "@aws-sdk/core": 3.513.0 - "@aws-sdk/credential-provider-node": 3.514.0 - "@aws-sdk/middleware-host-header": 3.511.0 - "@aws-sdk/middleware-logger": 3.511.0 - "@aws-sdk/middleware-recursion-detection": 3.511.0 - "@aws-sdk/middleware-user-agent": 3.511.0 - "@aws-sdk/region-config-resolver": 3.511.0 - "@aws-sdk/types": 3.511.0 - "@aws-sdk/util-endpoints": 3.511.0 - "@aws-sdk/util-user-agent-browser": 3.511.0 - "@aws-sdk/util-user-agent-node": 3.511.0 + "@aws-sdk/credential-provider-node": 3.515.0 + "@aws-sdk/middleware-host-header": 3.515.0 + "@aws-sdk/middleware-logger": 3.515.0 + "@aws-sdk/middleware-recursion-detection": 3.515.0 + "@aws-sdk/middleware-user-agent": 3.515.0 + "@aws-sdk/region-config-resolver": 3.515.0 + "@aws-sdk/types": 3.515.0 + "@aws-sdk/util-endpoints": 3.515.0 + "@aws-sdk/util-user-agent-browser": 3.515.0 + "@aws-sdk/util-user-agent-node": 3.515.0 "@smithy/config-resolver": ^2.1.1 "@smithy/core": ^1.3.2 "@smithy/fetch-http-handler": ^2.4.1 @@ -456,29 +456,29 @@ __metadata: "@smithy/util-utf8": ^2.1.1 "@smithy/util-waiter": ^2.1.1 tslib: ^2.5.0 - uuid: ^8.3.2 - checksum: 83ba19700e9c00c2035239c9fca710177d67eb7ee568ddafe256b40ca4724d0a1de4426c3bc5b9c66ba365206351eaf931ec4988b8dd44e17a42f8afdf84b0e3 + uuid: ^9.0.1 + checksum: 38366b504f5cda083637801b8a89cb03fb05fca56e6266a18d4fa4772a5556a461efcb82c8ed9690d8ba4c49e2ec2bdd71c7d3f987d2e7a85506439de2199ff6 languageName: node linkType: hard "@aws-sdk/client-organizations@npm:^3.350.0": - version: 3.514.0 - resolution: "@aws-sdk/client-organizations@npm:3.514.0" + version: 3.515.0 + resolution: "@aws-sdk/client-organizations@npm:3.515.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.513.0 + "@aws-sdk/client-sts": 3.515.0 "@aws-sdk/core": 3.513.0 - "@aws-sdk/credential-provider-node": 3.514.0 - "@aws-sdk/middleware-host-header": 3.511.0 - "@aws-sdk/middleware-logger": 3.511.0 - "@aws-sdk/middleware-recursion-detection": 3.511.0 - "@aws-sdk/middleware-user-agent": 3.511.0 - "@aws-sdk/region-config-resolver": 3.511.0 - "@aws-sdk/types": 3.511.0 - "@aws-sdk/util-endpoints": 3.511.0 - "@aws-sdk/util-user-agent-browser": 3.511.0 - "@aws-sdk/util-user-agent-node": 3.511.0 + "@aws-sdk/credential-provider-node": 3.515.0 + "@aws-sdk/middleware-host-header": 3.515.0 + "@aws-sdk/middleware-logger": 3.515.0 + "@aws-sdk/middleware-recursion-detection": 3.515.0 + "@aws-sdk/middleware-user-agent": 3.515.0 + "@aws-sdk/region-config-resolver": 3.515.0 + "@aws-sdk/types": 3.515.0 + "@aws-sdk/util-endpoints": 3.515.0 + "@aws-sdk/util-user-agent-browser": 3.515.0 + "@aws-sdk/util-user-agent-node": 3.515.0 "@smithy/config-resolver": ^2.1.1 "@smithy/core": ^1.3.2 "@smithy/fetch-http-handler": ^2.4.1 @@ -505,37 +505,37 @@ __metadata: "@smithy/util-retry": ^2.1.1 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: bf8e4b074eb061b76307c116b0d328de48473d0b52c338b8d179f6e710fd7c387001ef1c3dde6278c70963711e55e6b13579407e17c78222ea51dec378b8eca4 + checksum: 4b947f1fc5ca196b007855cc5d75accb82825adfe4dea370d181b9605bfe827ddc0f6cb073d7ef7f768a523be4e393a2dc1e28a999d118bda050f354997e8f60 languageName: node linkType: hard "@aws-sdk/client-s3@npm:^3.350.0": - version: 3.514.0 - resolution: "@aws-sdk/client-s3@npm:3.514.0" + version: 3.515.0 + resolution: "@aws-sdk/client-s3@npm:3.515.0" dependencies: "@aws-crypto/sha1-browser": 3.0.0 "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.513.0 + "@aws-sdk/client-sts": 3.515.0 "@aws-sdk/core": 3.513.0 - "@aws-sdk/credential-provider-node": 3.514.0 - "@aws-sdk/middleware-bucket-endpoint": 3.511.0 - "@aws-sdk/middleware-expect-continue": 3.511.0 - "@aws-sdk/middleware-flexible-checksums": 3.511.0 - "@aws-sdk/middleware-host-header": 3.511.0 - "@aws-sdk/middleware-location-constraint": 3.511.0 - "@aws-sdk/middleware-logger": 3.511.0 - "@aws-sdk/middleware-recursion-detection": 3.511.0 - "@aws-sdk/middleware-sdk-s3": 3.511.0 - "@aws-sdk/middleware-signing": 3.511.0 - "@aws-sdk/middleware-ssec": 3.511.0 - "@aws-sdk/middleware-user-agent": 3.511.0 - "@aws-sdk/region-config-resolver": 3.511.0 - "@aws-sdk/signature-v4-multi-region": 3.511.0 - "@aws-sdk/types": 3.511.0 - "@aws-sdk/util-endpoints": 3.511.0 - "@aws-sdk/util-user-agent-browser": 3.511.0 - "@aws-sdk/util-user-agent-node": 3.511.0 + "@aws-sdk/credential-provider-node": 3.515.0 + "@aws-sdk/middleware-bucket-endpoint": 3.515.0 + "@aws-sdk/middleware-expect-continue": 3.515.0 + "@aws-sdk/middleware-flexible-checksums": 3.515.0 + "@aws-sdk/middleware-host-header": 3.515.0 + "@aws-sdk/middleware-location-constraint": 3.515.0 + "@aws-sdk/middleware-logger": 3.515.0 + "@aws-sdk/middleware-recursion-detection": 3.515.0 + "@aws-sdk/middleware-sdk-s3": 3.515.0 + "@aws-sdk/middleware-signing": 3.515.0 + "@aws-sdk/middleware-ssec": 3.515.0 + "@aws-sdk/middleware-user-agent": 3.515.0 + "@aws-sdk/region-config-resolver": 3.515.0 + "@aws-sdk/signature-v4-multi-region": 3.515.0 + "@aws-sdk/types": 3.515.0 + "@aws-sdk/util-endpoints": 3.515.0 + "@aws-sdk/util-user-agent-browser": 3.515.0 + "@aws-sdk/util-user-agent-node": 3.515.0 "@aws-sdk/xml-builder": 3.496.0 "@smithy/config-resolver": ^2.1.1 "@smithy/core": ^1.3.2 @@ -571,29 +571,29 @@ __metadata: "@smithy/util-waiter": ^2.1.1 fast-xml-parser: 4.2.5 tslib: ^2.5.0 - checksum: 2a5b1da2343825c9e44d764d63e0be800dee5a51b26804daa6a57de12c6d9514a49a3673804a303f45d3a66d657ef8518ed186ceb205738e1794a81c224dc2be + checksum: f61f91fb45500108520357e61a018975f4e4a49df378fa16ad0fc15b59560bdfcc11c6cb7d95ad1c4b6e0607e99788662e4013d8d529af75a36dffa5c8bfe5ca languageName: node linkType: hard "@aws-sdk/client-sqs@npm:^3.350.0": - version: 3.514.0 - resolution: "@aws-sdk/client-sqs@npm:3.514.0" + version: 3.515.0 + resolution: "@aws-sdk/client-sqs@npm:3.515.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.513.0 + "@aws-sdk/client-sts": 3.515.0 "@aws-sdk/core": 3.513.0 - "@aws-sdk/credential-provider-node": 3.514.0 - "@aws-sdk/middleware-host-header": 3.511.0 - "@aws-sdk/middleware-logger": 3.511.0 - "@aws-sdk/middleware-recursion-detection": 3.511.0 - "@aws-sdk/middleware-sdk-sqs": 3.511.0 - "@aws-sdk/middleware-user-agent": 3.511.0 - "@aws-sdk/region-config-resolver": 3.511.0 - "@aws-sdk/types": 3.511.0 - "@aws-sdk/util-endpoints": 3.511.0 - "@aws-sdk/util-user-agent-browser": 3.511.0 - "@aws-sdk/util-user-agent-node": 3.511.0 + "@aws-sdk/credential-provider-node": 3.515.0 + "@aws-sdk/middleware-host-header": 3.515.0 + "@aws-sdk/middleware-logger": 3.515.0 + "@aws-sdk/middleware-recursion-detection": 3.515.0 + "@aws-sdk/middleware-sdk-sqs": 3.515.0 + "@aws-sdk/middleware-user-agent": 3.515.0 + "@aws-sdk/region-config-resolver": 3.515.0 + "@aws-sdk/types": 3.515.0 + "@aws-sdk/util-endpoints": 3.515.0 + "@aws-sdk/util-user-agent-browser": 3.515.0 + "@aws-sdk/util-user-agent-node": 3.515.0 "@smithy/config-resolver": ^2.1.1 "@smithy/core": ^1.3.2 "@smithy/fetch-http-handler": ^2.4.1 @@ -621,27 +621,27 @@ __metadata: "@smithy/util-retry": ^2.1.1 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: b1ab3853441f3a2e9235e3a8594b9c2496c777331da12c82ade6ce267c7839c6fa60a7e9c1c23a7ad55fca357d7c1a47c1b3b6a1512312f65a02908537be7eb9 + checksum: e92bbea7c7453cee74898ffde1092437693eb19b3a623243913805229ab6b1ec7905735a2dd58476915d574beeca2675f00eb89b5de8c3f099becc68c9ade901 languageName: node linkType: hard -"@aws-sdk/client-sso-oidc@npm:3.513.0": - version: 3.513.0 - resolution: "@aws-sdk/client-sso-oidc@npm:3.513.0" +"@aws-sdk/client-sso-oidc@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/client-sso-oidc@npm:3.515.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.513.0 + "@aws-sdk/client-sts": 3.515.0 "@aws-sdk/core": 3.513.0 - "@aws-sdk/middleware-host-header": 3.511.0 - "@aws-sdk/middleware-logger": 3.511.0 - "@aws-sdk/middleware-recursion-detection": 3.511.0 - "@aws-sdk/middleware-user-agent": 3.511.0 - "@aws-sdk/region-config-resolver": 3.511.0 - "@aws-sdk/types": 3.511.0 - "@aws-sdk/util-endpoints": 3.511.0 - "@aws-sdk/util-user-agent-browser": 3.511.0 - "@aws-sdk/util-user-agent-node": 3.511.0 + "@aws-sdk/middleware-host-header": 3.515.0 + "@aws-sdk/middleware-logger": 3.515.0 + "@aws-sdk/middleware-recursion-detection": 3.515.0 + "@aws-sdk/middleware-user-agent": 3.515.0 + "@aws-sdk/region-config-resolver": 3.515.0 + "@aws-sdk/types": 3.515.0 + "@aws-sdk/util-endpoints": 3.515.0 + "@aws-sdk/util-user-agent-browser": 3.515.0 + "@aws-sdk/util-user-agent-node": 3.515.0 "@smithy/config-resolver": ^2.1.1 "@smithy/core": ^1.3.2 "@smithy/fetch-http-handler": ^2.4.1 @@ -669,27 +669,27 @@ __metadata: "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 peerDependencies: - "@aws-sdk/credential-provider-node": ^3.513.0 - checksum: 655dbe99fde0d9413eb0c8f2c91815931ecc27eb00d470773acf968b3ceebb007bb8cffebe1261a6dc7af1ebf0a8a2d5d00060d02bf3e3be09f97bf71a4ce284 + "@aws-sdk/credential-provider-node": ^3.515.0 + checksum: f220a9ba8542460b2aa91ad060302fb9e68bdf096ecca2ec1d6e525f4df1036b330cb85d20bac3e8399276c0d8d8d388b3f2191b58804919c68369afac0be37b languageName: node linkType: hard -"@aws-sdk/client-sso@npm:3.513.0": - version: 3.513.0 - resolution: "@aws-sdk/client-sso@npm:3.513.0" +"@aws-sdk/client-sso@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/client-sso@npm:3.515.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 "@aws-sdk/core": 3.513.0 - "@aws-sdk/middleware-host-header": 3.511.0 - "@aws-sdk/middleware-logger": 3.511.0 - "@aws-sdk/middleware-recursion-detection": 3.511.0 - "@aws-sdk/middleware-user-agent": 3.511.0 - "@aws-sdk/region-config-resolver": 3.511.0 - "@aws-sdk/types": 3.511.0 - "@aws-sdk/util-endpoints": 3.511.0 - "@aws-sdk/util-user-agent-browser": 3.511.0 - "@aws-sdk/util-user-agent-node": 3.511.0 + "@aws-sdk/middleware-host-header": 3.515.0 + "@aws-sdk/middleware-logger": 3.515.0 + "@aws-sdk/middleware-recursion-detection": 3.515.0 + "@aws-sdk/middleware-user-agent": 3.515.0 + "@aws-sdk/region-config-resolver": 3.515.0 + "@aws-sdk/types": 3.515.0 + "@aws-sdk/util-endpoints": 3.515.0 + "@aws-sdk/util-user-agent-browser": 3.515.0 + "@aws-sdk/util-user-agent-node": 3.515.0 "@smithy/config-resolver": ^2.1.1 "@smithy/core": ^1.3.2 "@smithy/fetch-http-handler": ^2.4.1 @@ -716,26 +716,26 @@ __metadata: "@smithy/util-retry": ^2.1.1 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: 1c5a0465de54cb03e6fe7b1d5543638184227c2019a0eeb4537a42b6da0a9ef698abca25bb967b9fcdd3258ac73dfe0c8e120d96d10994ab9131b28533d0fb1f + checksum: 12287dfa469fb2c6b5bedd3cbd37f7416f8234669b5ed0ff38cb1217d746ba6a5e6ff227b091a7751d1c20489a6b8bd93bcbed8f394cb4c51b6bebb9a9f79108 languageName: node linkType: hard -"@aws-sdk/client-sts@npm:3.513.0, @aws-sdk/client-sts@npm:^3.350.0": - version: 3.513.0 - resolution: "@aws-sdk/client-sts@npm:3.513.0" +"@aws-sdk/client-sts@npm:3.515.0, @aws-sdk/client-sts@npm:^3.350.0": + version: 3.515.0 + resolution: "@aws-sdk/client-sts@npm:3.515.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 "@aws-sdk/core": 3.513.0 - "@aws-sdk/middleware-host-header": 3.511.0 - "@aws-sdk/middleware-logger": 3.511.0 - "@aws-sdk/middleware-recursion-detection": 3.511.0 - "@aws-sdk/middleware-user-agent": 3.511.0 - "@aws-sdk/region-config-resolver": 3.511.0 - "@aws-sdk/types": 3.511.0 - "@aws-sdk/util-endpoints": 3.511.0 - "@aws-sdk/util-user-agent-browser": 3.511.0 - "@aws-sdk/util-user-agent-node": 3.511.0 + "@aws-sdk/middleware-host-header": 3.515.0 + "@aws-sdk/middleware-logger": 3.515.0 + "@aws-sdk/middleware-recursion-detection": 3.515.0 + "@aws-sdk/middleware-user-agent": 3.515.0 + "@aws-sdk/region-config-resolver": 3.515.0 + "@aws-sdk/types": 3.515.0 + "@aws-sdk/util-endpoints": 3.515.0 + "@aws-sdk/util-user-agent-browser": 3.515.0 + "@aws-sdk/util-user-agent-node": 3.515.0 "@smithy/config-resolver": ^2.1.1 "@smithy/core": ^1.3.2 "@smithy/fetch-http-handler": ^2.4.1 @@ -764,8 +764,8 @@ __metadata: fast-xml-parser: 4.2.5 tslib: ^2.5.0 peerDependencies: - "@aws-sdk/credential-provider-node": ^3.513.0 - checksum: 4e89fd8f4aef57a777aff20e1afd5c502595c03ba1e21ece5940ea1b2d422c41e8bde83071ba42b96a8bfa4520197375e7f5e98a808c24c96673fc95fa4cdb3f + "@aws-sdk/credential-provider-node": ^3.515.0 + checksum: 9af6a2484909e88a83c411551d55ad149c80a8f449c2e54c499769535243602a6283cd71f6a0cf975b295a321a74e90eb95f6659bba93bae3d12e2186e7545f4 languageName: node linkType: hard @@ -783,36 +783,36 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/credential-provider-cognito-identity@npm:3.514.0": - version: 3.514.0 - resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.514.0" +"@aws-sdk/credential-provider-cognito-identity@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.515.0" dependencies: - "@aws-sdk/client-cognito-identity": 3.514.0 - "@aws-sdk/types": 3.511.0 + "@aws-sdk/client-cognito-identity": 3.515.0 + "@aws-sdk/types": 3.515.0 "@smithy/property-provider": ^2.1.1 "@smithy/types": ^2.9.1 tslib: ^2.5.0 - checksum: 2a13f19f7f5161f388d6a63fd3b6d5e666ba5b39e40d9a3f935aded9dbec423e0d89f2da8920212e2b1e63dfab690187a45e00bdb3e5c109dc7a2a156de2d066 + checksum: cb8cb5fa19b3e10e0a816eddda0a56a412da6bc68eba4807698ed94ef6bf2f3de288d959ced099b399dfc29f08154c84f8c025816e94226e5fb21c605e710542 languageName: node linkType: hard -"@aws-sdk/credential-provider-env@npm:3.511.0": - version: 3.511.0 - resolution: "@aws-sdk/credential-provider-env@npm:3.511.0" +"@aws-sdk/credential-provider-env@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/credential-provider-env@npm:3.515.0" dependencies: - "@aws-sdk/types": 3.511.0 + "@aws-sdk/types": 3.515.0 "@smithy/property-provider": ^2.1.1 "@smithy/types": ^2.9.1 tslib: ^2.5.0 - checksum: c19f98e0a59324cb9cd59220c91ed41ee6ee507de9acdb233f1a280db14491883c24d711d6a496f03d15b9301496ab561dba8d78dfdac5477c43c9d0bb2fcced + checksum: 3573bc3f1aa89bc8eedb9eb39c8c1d501a68aec5eb059364a1091c8bf10dfda9cfbd78ee49d3ad25ec1012f765a4464363c4cd70997e94005fd21245871a4229 languageName: node linkType: hard -"@aws-sdk/credential-provider-http@npm:3.511.0": - version: 3.511.0 - resolution: "@aws-sdk/credential-provider-http@npm:3.511.0" +"@aws-sdk/credential-provider-http@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/credential-provider-http@npm:3.515.0" dependencies: - "@aws-sdk/types": 3.511.0 + "@aws-sdk/types": 3.515.0 "@smithy/fetch-http-handler": ^2.4.1 "@smithy/node-http-handler": ^2.3.1 "@smithy/property-provider": ^2.1.1 @@ -821,111 +821,111 @@ __metadata: "@smithy/types": ^2.9.1 "@smithy/util-stream": ^2.1.1 tslib: ^2.5.0 - checksum: d6dc32a2e1cab7ff6b603aa087b80ec854064a9f490210d8d50db2b68f4586b7cfc78de71d6ae1e21788e8e171c6ee2c4c1fa6876ce59bab018abfd9cab13b2a + checksum: d13943dc7a83c9c129dd03a8b337b7753c791441d65a894085e00146d807738b420b9127f570a32497665be4f6e1cc8eeefd7cb1b013a04789d874c8b23b829f languageName: node linkType: hard -"@aws-sdk/credential-provider-ini@npm:3.513.0": - version: 3.513.0 - resolution: "@aws-sdk/credential-provider-ini@npm:3.513.0" +"@aws-sdk/credential-provider-ini@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/credential-provider-ini@npm:3.515.0" dependencies: - "@aws-sdk/client-sts": 3.513.0 - "@aws-sdk/credential-provider-env": 3.511.0 - "@aws-sdk/credential-provider-process": 3.511.0 - "@aws-sdk/credential-provider-sso": 3.513.0 - "@aws-sdk/credential-provider-web-identity": 3.513.0 - "@aws-sdk/types": 3.511.0 + "@aws-sdk/client-sts": 3.515.0 + "@aws-sdk/credential-provider-env": 3.515.0 + "@aws-sdk/credential-provider-process": 3.515.0 + "@aws-sdk/credential-provider-sso": 3.515.0 + "@aws-sdk/credential-provider-web-identity": 3.515.0 + "@aws-sdk/types": 3.515.0 "@smithy/credential-provider-imds": ^2.2.1 "@smithy/property-provider": ^2.1.1 "@smithy/shared-ini-file-loader": ^2.3.1 "@smithy/types": ^2.9.1 tslib: ^2.5.0 - checksum: d4e9f0486f44ca7271f8e245b9db483cc32a5b7600dc4de586fcbacf050a3b67c083c1bb9c86408a91ff1c46fd1b6c6428f592ca8d7537293c09f2359946039a + checksum: c136d4257460be8331d7645854b7e3c91205a1eb12efd3dcbcd84501c787085292d1ccc4578c4776308d91748bde5af2c6eaa873b56aed83ab472a878a2d8883 languageName: node linkType: hard -"@aws-sdk/credential-provider-node@npm:3.514.0, @aws-sdk/credential-provider-node@npm:^3.350.0": - version: 3.514.0 - resolution: "@aws-sdk/credential-provider-node@npm:3.514.0" +"@aws-sdk/credential-provider-node@npm:3.515.0, @aws-sdk/credential-provider-node@npm:^3.350.0": + version: 3.515.0 + resolution: "@aws-sdk/credential-provider-node@npm:3.515.0" dependencies: - "@aws-sdk/credential-provider-env": 3.511.0 - "@aws-sdk/credential-provider-http": 3.511.0 - "@aws-sdk/credential-provider-ini": 3.513.0 - "@aws-sdk/credential-provider-process": 3.511.0 - "@aws-sdk/credential-provider-sso": 3.513.0 - "@aws-sdk/credential-provider-web-identity": 3.513.0 - "@aws-sdk/types": 3.511.0 + "@aws-sdk/credential-provider-env": 3.515.0 + "@aws-sdk/credential-provider-http": 3.515.0 + "@aws-sdk/credential-provider-ini": 3.515.0 + "@aws-sdk/credential-provider-process": 3.515.0 + "@aws-sdk/credential-provider-sso": 3.515.0 + "@aws-sdk/credential-provider-web-identity": 3.515.0 + "@aws-sdk/types": 3.515.0 "@smithy/credential-provider-imds": ^2.2.1 "@smithy/property-provider": ^2.1.1 "@smithy/shared-ini-file-loader": ^2.3.1 "@smithy/types": ^2.9.1 tslib: ^2.5.0 - checksum: ceeded87585fde1093dac1a84ffa0c0d1377f844508431b06877529de40625d2b67ad23269a7ac32e0cac3338a7051a23c51095eb22cea359b80d3ac72a34b81 + checksum: c51f267c61de0d82afe47d97cdf58971e3b8eec6e7364fe28d3867addd65e156358b96c39a335fad521e9a6925b349a967ae3eed1aa6e9cb4bcc1f0931e3ed50 languageName: node linkType: hard -"@aws-sdk/credential-provider-process@npm:3.511.0": - version: 3.511.0 - resolution: "@aws-sdk/credential-provider-process@npm:3.511.0" +"@aws-sdk/credential-provider-process@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/credential-provider-process@npm:3.515.0" dependencies: - "@aws-sdk/types": 3.511.0 + "@aws-sdk/types": 3.515.0 "@smithy/property-provider": ^2.1.1 "@smithy/shared-ini-file-loader": ^2.3.1 "@smithy/types": ^2.9.1 tslib: ^2.5.0 - checksum: ec3be8da713542d61c40d38fb83d067115d0e54e9847ebe7291ed17aa67bbae731e9b2e54cf833bdc75b1b171e663bbbb005308e69634dc4a88926887c78881b + checksum: 11159b4c9502218ec6cba9a46ddc120e53aec7f04507c14d4e99a186073cfd363af438c623705890a2e8f6cc792475ebd14c82766dad82dabf7f20d9708f7faf languageName: node linkType: hard -"@aws-sdk/credential-provider-sso@npm:3.513.0": - version: 3.513.0 - resolution: "@aws-sdk/credential-provider-sso@npm:3.513.0" +"@aws-sdk/credential-provider-sso@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/credential-provider-sso@npm:3.515.0" dependencies: - "@aws-sdk/client-sso": 3.513.0 - "@aws-sdk/token-providers": 3.513.0 - "@aws-sdk/types": 3.511.0 + "@aws-sdk/client-sso": 3.515.0 + "@aws-sdk/token-providers": 3.515.0 + "@aws-sdk/types": 3.515.0 "@smithy/property-provider": ^2.1.1 "@smithy/shared-ini-file-loader": ^2.3.1 "@smithy/types": ^2.9.1 tslib: ^2.5.0 - checksum: eafa07c7502136dbed7b4308d921fec87f381fbc2c27c1d4ba8d34d902e3a4ef6b95713a6fa7be2fe82fa68fdf06e5dbcd971b4d55c66c7b2d3c4a36a58fd2cc + checksum: fbe1eebc50e9bd3715bca4e6ee1a2922cf1ea383953730a6bd3b88b12f23a95cb3ff0edaf8578c81156ef65648eb0295f5c39ed6ae2c8e16b6ce9d4c46f207e9 languageName: node linkType: hard -"@aws-sdk/credential-provider-web-identity@npm:3.513.0": - version: 3.513.0 - resolution: "@aws-sdk/credential-provider-web-identity@npm:3.513.0" +"@aws-sdk/credential-provider-web-identity@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/credential-provider-web-identity@npm:3.515.0" dependencies: - "@aws-sdk/client-sts": 3.513.0 - "@aws-sdk/types": 3.511.0 + "@aws-sdk/client-sts": 3.515.0 + "@aws-sdk/types": 3.515.0 "@smithy/property-provider": ^2.1.1 "@smithy/types": ^2.9.1 tslib: ^2.5.0 - checksum: 2a885d53f6a3b4a3f461095af3be0e4c7f48b93a21619f97e2ac076f43cabcf703e6cb5f57f594d7288b63dc5dec12a2096ae6c93993f82fdfee5f3227307c63 + checksum: f0a7e9855f78849143c3139c613cc1531a88272f3ec039d23ac9366b94495a3e07212ade9541e56c93560ad9f58600376e9de38706a4cd97aaf5f400911361cd languageName: node linkType: hard "@aws-sdk/credential-providers@npm:^3.350.0": - version: 3.514.0 - resolution: "@aws-sdk/credential-providers@npm:3.514.0" + version: 3.515.0 + resolution: "@aws-sdk/credential-providers@npm:3.515.0" dependencies: - "@aws-sdk/client-cognito-identity": 3.514.0 - "@aws-sdk/client-sso": 3.513.0 - "@aws-sdk/client-sts": 3.513.0 - "@aws-sdk/credential-provider-cognito-identity": 3.514.0 - "@aws-sdk/credential-provider-env": 3.511.0 - "@aws-sdk/credential-provider-http": 3.511.0 - "@aws-sdk/credential-provider-ini": 3.513.0 - "@aws-sdk/credential-provider-node": 3.514.0 - "@aws-sdk/credential-provider-process": 3.511.0 - "@aws-sdk/credential-provider-sso": 3.513.0 - "@aws-sdk/credential-provider-web-identity": 3.513.0 - "@aws-sdk/types": 3.511.0 + "@aws-sdk/client-cognito-identity": 3.515.0 + "@aws-sdk/client-sso": 3.515.0 + "@aws-sdk/client-sts": 3.515.0 + "@aws-sdk/credential-provider-cognito-identity": 3.515.0 + "@aws-sdk/credential-provider-env": 3.515.0 + "@aws-sdk/credential-provider-http": 3.515.0 + "@aws-sdk/credential-provider-ini": 3.515.0 + "@aws-sdk/credential-provider-node": 3.515.0 + "@aws-sdk/credential-provider-process": 3.515.0 + "@aws-sdk/credential-provider-sso": 3.515.0 + "@aws-sdk/credential-provider-web-identity": 3.515.0 + "@aws-sdk/types": 3.515.0 "@smithy/credential-provider-imds": ^2.2.1 "@smithy/property-provider": ^2.1.1 "@smithy/types": ^2.9.1 tslib: ^2.5.0 - checksum: 87468c93d84406a3cebb1df81311b5fd16ee93331c353991657423da90c960b1e3d4d033274efb81090c814dfff83064eec693abd4e6fba878961832b033b8d7 + checksum: 8890e83fc19072c51cee8d50ee7168800b9775aa00bcb7a0feb836e60fbae0fb8ba55133d256719033cd7ccda0f5881350474ba3f4769016f01ce7de4ef7b6e5 languageName: node linkType: hard @@ -951,8 +951,8 @@ __metadata: linkType: hard "@aws-sdk/lib-storage@npm:^3.350.0": - version: 3.514.0 - resolution: "@aws-sdk/lib-storage@npm:3.514.0" + version: 3.515.0 + resolution: "@aws-sdk/lib-storage@npm:3.515.0" dependencies: "@smithy/abort-controller": ^2.1.1 "@smithy/middleware-endpoint": ^2.4.1 @@ -963,22 +963,22 @@ __metadata: tslib: ^2.5.0 peerDependencies: "@aws-sdk/client-s3": ^3.0.0 - checksum: 1a122a966bd2e7c627a1a69ef6d8c441ca6f439a503d95cc58cca3f0333bc3d2f2c9fd715b4a6abb009a868240c8a1e33607002b7f6abe89e1d5c26fc3823842 + checksum: b4d7b3783508ce3ef93150b2a249490c5af803d02d91ce81aebf0ec055870aae3260d4d66e9bf1d4234fb61d11cfaa54f2916c9c2572cf4cb6ee1b15993c41b5 languageName: node linkType: hard -"@aws-sdk/middleware-bucket-endpoint@npm:3.511.0": - version: 3.511.0 - resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.511.0" +"@aws-sdk/middleware-bucket-endpoint@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.515.0" dependencies: - "@aws-sdk/types": 3.511.0 + "@aws-sdk/types": 3.515.0 "@aws-sdk/util-arn-parser": 3.495.0 "@smithy/node-config-provider": ^2.2.1 "@smithy/protocol-http": ^3.1.1 "@smithy/types": ^2.9.1 "@smithy/util-config-provider": ^2.2.1 tslib: ^2.5.0 - checksum: 23272bf6b5a6a7e8ae97bf2ad896487b670a1db766a2704606eb1fd852a273d99fb468bf3f7f3dddc6df7cdf5812a2195f842f949add6bc272fb90ac80241217 + checksum: 8ecec09ac50c33a24178e73be6b4238e5047984eebf54c4786392e82d2b1cdd09807ee97104f603d22890a58657335843f9587c66e544c2ffbb9a25f3e3bf065 languageName: node linkType: hard @@ -995,85 +995,85 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-expect-continue@npm:3.511.0": - version: 3.511.0 - resolution: "@aws-sdk/middleware-expect-continue@npm:3.511.0" +"@aws-sdk/middleware-expect-continue@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/middleware-expect-continue@npm:3.515.0" dependencies: - "@aws-sdk/types": 3.511.0 + "@aws-sdk/types": 3.515.0 "@smithy/protocol-http": ^3.1.1 "@smithy/types": ^2.9.1 tslib: ^2.5.0 - checksum: be66cd2cecff5ba6bb9cc1a3368f2f488b28d415e9122bded2d1810a57014860d63a91ef64c9edab854fe67f339d75a1ee84c6d5f6de9e534ec4ea708871ea90 + checksum: c989e0e55f51c631f914b444a0e3a6a81c79c1ad8046346c16a4ccfbb77aeaf48ba948a6b8185fc36c00abf07951cc374c3673da6d23d7de6cd3ec8d5b4d840e languageName: node linkType: hard -"@aws-sdk/middleware-flexible-checksums@npm:3.511.0": - version: 3.511.0 - resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.511.0" +"@aws-sdk/middleware-flexible-checksums@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.515.0" dependencies: "@aws-crypto/crc32": 3.0.0 "@aws-crypto/crc32c": 3.0.0 - "@aws-sdk/types": 3.511.0 + "@aws-sdk/types": 3.515.0 "@smithy/is-array-buffer": ^2.1.1 "@smithy/protocol-http": ^3.1.1 "@smithy/types": ^2.9.1 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: 5d95e405b5584c5fcc4c88f5ed7f49a85c95263d13a0859e167eda43ba3171233f38280430e42cc40baa923d64f02af36e442e7f374600ac8153ddfd7ba96853 + checksum: 89f2df7a3dd40c174586aec1349e4e144825cffb252df843b115390702523dc6cfb745756a69a1ab1e77e44340fd55bed04be3856fe96d937af5702bcbcf869b languageName: node linkType: hard -"@aws-sdk/middleware-host-header@npm:3.511.0": - version: 3.511.0 - resolution: "@aws-sdk/middleware-host-header@npm:3.511.0" +"@aws-sdk/middleware-host-header@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/middleware-host-header@npm:3.515.0" dependencies: - "@aws-sdk/types": 3.511.0 + "@aws-sdk/types": 3.515.0 "@smithy/protocol-http": ^3.1.1 "@smithy/types": ^2.9.1 tslib: ^2.5.0 - checksum: 4cdbe0959d17c32928025e5f57b56ce35e6a94bf907be5ef00e1b564735528a585749b95f0c91d9281275275e9e77a82ad946b1daeb0a320ea767bb602cd0d61 + checksum: ff066cf47b0ba2c64bd70efdec795ac2da8bad7ba8dd44913c98f42b153ca6e753b13b6c1ef7075499590279a5cc49b5a60511dae4512dcdb11a62a0e67fa061 languageName: node linkType: hard -"@aws-sdk/middleware-location-constraint@npm:3.511.0": - version: 3.511.0 - resolution: "@aws-sdk/middleware-location-constraint@npm:3.511.0" +"@aws-sdk/middleware-location-constraint@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/middleware-location-constraint@npm:3.515.0" dependencies: - "@aws-sdk/types": 3.511.0 + "@aws-sdk/types": 3.515.0 "@smithy/types": ^2.9.1 tslib: ^2.5.0 - checksum: 2749c882354da4a37cf800d98f2ad14ba128d974c05875e4e6639d4eed731f36bd437bb622ab1d8c3ffc58af7c7b18035fd08e82044c9e5910765aae5cf0db09 + checksum: c3e7c7b51c276eba7ceef05c18416d2cca976c83ac9db227877090c33f676b50b0c90ceaf5d924cd9894a4c2f44c0a2d06ec93da753ac646fecb68d5facef570 languageName: node linkType: hard -"@aws-sdk/middleware-logger@npm:3.511.0": - version: 3.511.0 - resolution: "@aws-sdk/middleware-logger@npm:3.511.0" +"@aws-sdk/middleware-logger@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/middleware-logger@npm:3.515.0" dependencies: - "@aws-sdk/types": 3.511.0 + "@aws-sdk/types": 3.515.0 "@smithy/types": ^2.9.1 tslib: ^2.5.0 - checksum: bcbd5486d63a7ffe0ea4131cff646be728b2f1d862231c133fdc6f67a3f39aafa9c09c059e2cb2c6720d569a3c0d9926e0acb61ccd505acdc48e3b34e706f60b + checksum: 32d251e77f43593ffdd192a4d0628f33773e29c14a3001a4c6519553e94958edbd0fb8e6954a65d1180b0caa16cafe9fc9b362d1ab663db1d1eac84b15667645 languageName: node linkType: hard -"@aws-sdk/middleware-recursion-detection@npm:3.511.0": - version: 3.511.0 - resolution: "@aws-sdk/middleware-recursion-detection@npm:3.511.0" +"@aws-sdk/middleware-recursion-detection@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/middleware-recursion-detection@npm:3.515.0" dependencies: - "@aws-sdk/types": 3.511.0 + "@aws-sdk/types": 3.515.0 "@smithy/protocol-http": ^3.1.1 "@smithy/types": ^2.9.1 tslib: ^2.5.0 - checksum: a9e1d7924115f07986b726c2f2bd1705227bf5660c3ff9fc356866600f2336cc0ecea5cf53cca7c48a9856216363774cecf64fa9bbe954e71cd5605fcb6d57b3 + checksum: 23c4a1e4d7de86196acfcfbc84bea84c8c3211c4831fdc7c975a6388022037bd5baa4e5809dca188631f06726b51fcf85af358f9ef526e255dc494b368d0da0c languageName: node linkType: hard -"@aws-sdk/middleware-sdk-s3@npm:3.511.0": - version: 3.511.0 - resolution: "@aws-sdk/middleware-sdk-s3@npm:3.511.0" +"@aws-sdk/middleware-sdk-s3@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/middleware-sdk-s3@npm:3.515.0" dependencies: - "@aws-sdk/types": 3.511.0 + "@aws-sdk/types": 3.515.0 "@aws-sdk/util-arn-parser": 3.495.0 "@smithy/node-config-provider": ^2.2.1 "@smithy/protocol-http": ^3.1.1 @@ -1082,21 +1082,21 @@ __metadata: "@smithy/types": ^2.9.1 "@smithy/util-config-provider": ^2.2.1 tslib: ^2.5.0 - checksum: 1e9bad58136131aaaac326fa2d05d192ef762217a85e0bc831801b45ed15089ef763c2b5d57f28d0b2dc87d2aca02d894a3570c6e85dd2677d0a78e663fd4f15 + checksum: cb67334b30eee8fcf52637407cab2787353e463bdf5a99a33966f0e765f6f8fc687c4bba2b63ec7a4374b2ab544be807e1849bd910e6f163ff43d036f026e7e1 languageName: node linkType: hard -"@aws-sdk/middleware-sdk-sqs@npm:3.511.0": - version: 3.511.0 - resolution: "@aws-sdk/middleware-sdk-sqs@npm:3.511.0" +"@aws-sdk/middleware-sdk-sqs@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/middleware-sdk-sqs@npm:3.515.0" dependencies: - "@aws-sdk/types": 3.511.0 + "@aws-sdk/types": 3.515.0 "@smithy/smithy-client": ^2.3.1 "@smithy/types": ^2.9.1 "@smithy/util-hex-encoding": ^2.1.1 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: 2b8593c20a40beafc88c03bd75fcf8cecd00ad68ed42f26e64c61a2006a622085764b29b6d05191ea5417a18d3503c455008482faaa4873e4e5404a0ac71e84d + checksum: 67a7d9ed3e975a3fd83266f18ce2d94e3dc457d7001af7031e732b2d96135678dd3323ecae56ffe214f3dbe23d1df295a0cd0d21df7f6b9c98699e6a9b4704ca languageName: node linkType: hard @@ -1110,42 +1110,42 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-signing@npm:3.511.0": - version: 3.511.0 - resolution: "@aws-sdk/middleware-signing@npm:3.511.0" +"@aws-sdk/middleware-signing@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/middleware-signing@npm:3.515.0" dependencies: - "@aws-sdk/types": 3.511.0 + "@aws-sdk/types": 3.515.0 "@smithy/property-provider": ^2.1.1 "@smithy/protocol-http": ^3.1.1 "@smithy/signature-v4": ^2.1.1 "@smithy/types": ^2.9.1 "@smithy/util-middleware": ^2.1.1 tslib: ^2.5.0 - checksum: 383054601027f89065948aa19ed708f9dcc190651bbb61363c0eb2f705d75e62790d2715c4fca4cf7c7e3288f9c1c9b672556799eafa36629cc26d1d42944300 + checksum: 7ee85c70a81b85e455c4411caad79c7b41a0a0fd9696feead394a31ca360b096c74f7b2b6b5d449fcfa622659305618e24a49d15c45cab3afa54503459a9b24a languageName: node linkType: hard -"@aws-sdk/middleware-ssec@npm:3.511.0": - version: 3.511.0 - resolution: "@aws-sdk/middleware-ssec@npm:3.511.0" +"@aws-sdk/middleware-ssec@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/middleware-ssec@npm:3.515.0" dependencies: - "@aws-sdk/types": 3.511.0 + "@aws-sdk/types": 3.515.0 "@smithy/types": ^2.9.1 tslib: ^2.5.0 - checksum: ab21772d16006e9326bd2238ea8b6013f89b35fd3c243ec9212296fefc68ddc54b91f5cde449c1a1119089a002b4c8e2278597649d69744dc015be2964df3ad7 + checksum: 3a91ebaf128ff63665f9aa69b9b2d475ebb8e519da2e94a142bcea7f9b173be52a0476b814e90e88e0936db3371e3c1c090b4e44018f006e1cfc37d2631a36d8 languageName: node linkType: hard -"@aws-sdk/middleware-user-agent@npm:3.511.0": - version: 3.511.0 - resolution: "@aws-sdk/middleware-user-agent@npm:3.511.0" +"@aws-sdk/middleware-user-agent@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/middleware-user-agent@npm:3.515.0" dependencies: - "@aws-sdk/types": 3.511.0 - "@aws-sdk/util-endpoints": 3.511.0 + "@aws-sdk/types": 3.515.0 + "@aws-sdk/util-endpoints": 3.515.0 "@smithy/protocol-http": ^3.1.1 "@smithy/types": ^2.9.1 tslib: ^2.5.0 - checksum: 4d09ca5497e17a748f4034f5c1374cbdf8701fedbd5b3275042dccd734a4e91d52eed5e493f1526cda965ec784502efb4f25ca55874f7996d1bb442895316ef1 + checksum: fd601cb0367d42e38b71494c773d82bde8970f9aafbdbf18d7cadc25732ecc2b787f0cfef2110755d0cef73d6aa3ce2ca77dc5353854bedaf392a69019b39ac2 languageName: node linkType: hard @@ -1193,31 +1193,31 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/region-config-resolver@npm:3.511.0": - version: 3.511.0 - resolution: "@aws-sdk/region-config-resolver@npm:3.511.0" +"@aws-sdk/region-config-resolver@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/region-config-resolver@npm:3.515.0" dependencies: - "@aws-sdk/types": 3.511.0 + "@aws-sdk/types": 3.515.0 "@smithy/node-config-provider": ^2.2.1 "@smithy/types": ^2.9.1 "@smithy/util-config-provider": ^2.2.1 "@smithy/util-middleware": ^2.1.1 tslib: ^2.5.0 - checksum: a41e119d85c690e957bc00af7a6b822f2bb4c3522c3762799a40a5e0d081cbed6028607b8528b98705af4b7e5ed1a2cbc9e9d39df415597271fba7b1da7f08f5 + checksum: 0ed7fbd6390baebdf511b30877236fa8be8716e0162e2c9e0138c9b41ebda7d99a6f3d6cf66cb4af24761631c2c29102ecfe7a5f08894e1de3c98ca6a135fa74 languageName: node linkType: hard -"@aws-sdk/signature-v4-multi-region@npm:3.511.0": - version: 3.511.0 - resolution: "@aws-sdk/signature-v4-multi-region@npm:3.511.0" +"@aws-sdk/signature-v4-multi-region@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/signature-v4-multi-region@npm:3.515.0" dependencies: - "@aws-sdk/middleware-sdk-s3": 3.511.0 - "@aws-sdk/types": 3.511.0 + "@aws-sdk/middleware-sdk-s3": 3.515.0 + "@aws-sdk/types": 3.515.0 "@smithy/protocol-http": ^3.1.1 "@smithy/signature-v4": ^2.1.1 "@smithy/types": ^2.9.1 tslib: ^2.5.0 - checksum: f1a4d3a81310e2867ac973be8bae1a04654dbd543fb9341cd963a53e378b821e5ae189da072f5ab7c86efd00192d0b8da76f54d9f612c30ad20574485c250b0c + checksum: 1f780409af431b3ac91beee294cf4af74fff9f3dffc21ddd1c8a1782a01916da162ceec27c133a9e86452279bdf0005f9118809820d557a248613cd44f0f7ec3 languageName: node linkType: hard @@ -1237,17 +1237,17 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/token-providers@npm:3.513.0": - version: 3.513.0 - resolution: "@aws-sdk/token-providers@npm:3.513.0" +"@aws-sdk/token-providers@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/token-providers@npm:3.515.0" dependencies: - "@aws-sdk/client-sso-oidc": 3.513.0 - "@aws-sdk/types": 3.511.0 + "@aws-sdk/client-sso-oidc": 3.515.0 + "@aws-sdk/types": 3.515.0 "@smithy/property-provider": ^2.1.1 "@smithy/shared-ini-file-loader": ^2.3.1 "@smithy/types": ^2.9.1 tslib: ^2.5.0 - checksum: 6f7765e0778814f2d94b8a2489eefecc1b314813af6462cfc4f5527257ece7c1501360664500d519da0fa48312200f040f5a7560928620ff3882c65f52fabe18 + checksum: ab51c440da9772d0ee58948241b975705c171395cf1bad81a4ffd8f11f34106af54dcba930e360fb19489beedc1106ac14432bd27abdfe4b7536dc2113841027 languageName: node linkType: hard @@ -1261,13 +1261,13 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/types@npm:3.511.0, @aws-sdk/types@npm:^3.222.0, @aws-sdk/types@npm:^3.347.0": - version: 3.511.0 - resolution: "@aws-sdk/types@npm:3.511.0" +"@aws-sdk/types@npm:3.515.0, @aws-sdk/types@npm:^3.222.0, @aws-sdk/types@npm:^3.347.0": + version: 3.515.0 + resolution: "@aws-sdk/types@npm:3.515.0" dependencies: "@smithy/types": ^2.9.1 tslib: ^2.5.0 - checksum: a2082c64b4aabecde26010a48f8535a90c8d3e666e63f82773f8b07dadd7c1ff05d4639c60ddfb7d3533fe4ac55259e34a23995ca868c13e6d7718a1392a6eb8 + checksum: 0874f1814b58eae6e7115c3d08c2bc56e558e73d1ff8c5f833a73b4a0f76a42743c83c36a4b2759177e41b1feff065e85450f7bc235a087b94e67db12f87d298 languageName: node linkType: hard @@ -1301,15 +1301,15 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-endpoints@npm:3.511.0": - version: 3.511.0 - resolution: "@aws-sdk/util-endpoints@npm:3.511.0" +"@aws-sdk/util-endpoints@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/util-endpoints@npm:3.515.0" dependencies: - "@aws-sdk/types": 3.511.0 + "@aws-sdk/types": 3.515.0 "@smithy/types": ^2.9.1 "@smithy/util-endpoints": ^1.1.1 tslib: ^2.5.0 - checksum: dfd8fa98e6abeb96edc1239d1c49aef740bc781eb772a05deeb3385d576921121e3a5050f5372cce2bd422848ece728271aadb8887dce413100a289311873b3b + checksum: 1ab8fcd3054dc0366f10813a01130d05f4ba33f1488c1a168f44881cb24f3fbc2393111b7b0fd4dc06c852e4c9a5bbe8a82717b72229b0977cdba8a631ddeee1 languageName: node linkType: hard @@ -1361,23 +1361,23 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-user-agent-browser@npm:3.511.0": - version: 3.511.0 - resolution: "@aws-sdk/util-user-agent-browser@npm:3.511.0" +"@aws-sdk/util-user-agent-browser@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/util-user-agent-browser@npm:3.515.0" dependencies: - "@aws-sdk/types": 3.511.0 + "@aws-sdk/types": 3.515.0 "@smithy/types": ^2.9.1 bowser: ^2.11.0 tslib: ^2.5.0 - checksum: 714e5bc1faab9aee8c4a2ba02faa9c3d61621d2cc789fad460b613916adf12992c0ab7144368fdfecac10f3ec2b67f5d6da01465e827fe682e2024274c2b5721 + checksum: 40f518006cb7e76d06d83dcf05222b0b0ff47c10b63149cd5db2c0c1db79c8eff34bd582e89c748897bc11697b7b357bdca77d569f57ad0b2081c088752d601f languageName: node linkType: hard -"@aws-sdk/util-user-agent-node@npm:3.511.0": - version: 3.511.0 - resolution: "@aws-sdk/util-user-agent-node@npm:3.511.0" +"@aws-sdk/util-user-agent-node@npm:3.515.0": + version: 3.515.0 + resolution: "@aws-sdk/util-user-agent-node@npm:3.515.0" dependencies: - "@aws-sdk/types": 3.511.0 + "@aws-sdk/types": 3.515.0 "@smithy/node-config-provider": ^2.2.1 "@smithy/types": ^2.9.1 tslib: ^2.5.0 @@ -1386,7 +1386,7 @@ __metadata: peerDependenciesMeta: aws-crt: optional: true - checksum: 611a71d736d82c74f3585b0dfdd77c329047bd2815fb1782188df433015525e01247ed85ffca60d5a7c618081fd7cb935814d6854a6120071b44261b12a3e740 + checksum: 4e91d9cd5bbe4aa8321417ea1bd9caf3229416ee624b7f67b5206b284a539116692412ca41d60dcb5759b841fed7d9fb570915566d1f7e620578657f89548a23 languageName: node linkType: hard @@ -45069,7 +45069,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^9.0.0": +"uuid@npm:^9.0.0, uuid@npm:^9.0.1": version: 9.0.1 resolution: "uuid@npm:9.0.1" bin: From 7a3d7fcfbd101ebcc695a48d24e9d0428127c529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ragnar=20Halld=C3=B3rsson?= Date: Thu, 15 Feb 2024 22:39:19 +0100 Subject: [PATCH 051/204] chore(catalog-backend-module-azure): Change version bump to patch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Ragnar Halldórsson --- .changeset/real-keys-juggle.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/real-keys-juggle.md b/.changeset/real-keys-juggle.md index cb323853de..55bdbf4db3 100644 --- a/.changeset/real-keys-juggle.md +++ b/.changeset/real-keys-juggle.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-catalog-backend-module-azure': minor +'@backstage/plugin-catalog-backend-module-azure': patch --- Fixed issue where specifying a branch for discovery did not work From 85ec23ebad7d641a50949d970d44bd5a4d039f5a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Feb 2024 22:13:00 +0000 Subject: [PATCH 052/204] fix(deps): update dependency json-schema-to-ts to v3 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .changeset/renovate-0300bde.md | 5 +++++ packages/backend-openapi-utils/package.json | 2 +- yarn.lock | 10 +++++----- 3 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 .changeset/renovate-0300bde.md diff --git a/.changeset/renovate-0300bde.md b/.changeset/renovate-0300bde.md new file mode 100644 index 0000000000..71ed1df83c --- /dev/null +++ b/.changeset/renovate-0300bde.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-openapi-utils': patch +--- + +Updated dependency `json-schema-to-ts` to `^3.0.0`. diff --git a/packages/backend-openapi-utils/package.json b/packages/backend-openapi-utils/package.json index 2a8ff3bb30..c6b88dc0ad 100644 --- a/packages/backend-openapi-utils/package.json +++ b/packages/backend-openapi-utils/package.json @@ -44,7 +44,7 @@ "express": "^4.17.1", "express-openapi-validator": "^5.0.4", "express-promise-router": "^4.1.0", - "json-schema-to-ts": "^2.6.2", + "json-schema-to-ts": "^3.0.0", "lodash": "^4.17.21", "openapi-merge": "^1.3.2", "openapi3-ts": "^3.1.2" diff --git a/yarn.lock b/yarn.lock index c14cb29002..11c2eb74a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3418,7 +3418,7 @@ __metadata: express: ^4.17.1 express-openapi-validator: ^5.0.4 express-promise-router: ^4.1.0 - json-schema-to-ts: ^2.6.2 + json-schema-to-ts: ^3.0.0 lodash: ^4.17.21 openapi-merge: ^1.3.2 openapi3-ts: ^3.1.2 @@ -32722,14 +32722,14 @@ __metadata: languageName: node linkType: hard -"json-schema-to-ts@npm:^2.6.2": - version: 2.12.0 - resolution: "json-schema-to-ts@npm:2.12.0" +"json-schema-to-ts@npm:^3.0.0": + version: 3.0.0 + resolution: "json-schema-to-ts@npm:3.0.0" dependencies: "@babel/runtime": ^7.18.3 "@types/json-schema": ^7.0.9 ts-algebra: ^1.2.2 - checksum: 6dc4bc836591d888beb20e8bf45dfa3b75df4a331f18675bd2e39a2262e105e0dc86012031fff0be408499a07ae8bc30f5a879c24696189b2c30a34edd5fa72f + checksum: 4f33a0fee49cc058b16b4e4effd83cfb0c6000c6916990fda61b3622fa7967c9edbad59e34991eb869ebcd4989383d4c72f99a51a77163ac47f80da2c57d2aa8 languageName: node linkType: hard From b64ce5a485bbdfd48f0d130a0b3afbc41b6a0e9d Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Thu, 15 Feb 2024 18:50:07 -0600 Subject: [PATCH 053/204] Updated to use default import Signed-off-by: Andre Wanlin --- .changeset/famous-zoos-remain.md | 12 ++++++++++++ plugins/adr-backend/README.md | 4 +--- plugins/airbrake/README.md | 7 +++++-- plugins/azure-devops-backend/README.md | 3 +-- plugins/badges-backend/README.md | 4 ++-- plugins/devtools-backend/README.md | 3 +-- plugins/entity-feedback-backend/README.md | 10 ++++++---- plugins/lighthouse-backend/README.md | 7 +++++-- plugins/linguist-backend/README.md | 3 +-- 9 files changed, 34 insertions(+), 19 deletions(-) create mode 100644 .changeset/famous-zoos-remain.md diff --git a/.changeset/famous-zoos-remain.md b/.changeset/famous-zoos-remain.md new file mode 100644 index 0000000000..be4147c21e --- /dev/null +++ b/.changeset/famous-zoos-remain.md @@ -0,0 +1,12 @@ +--- +'@backstage/plugin-entity-feedback-backend': patch +'@backstage/plugin-azure-devops-backend': patch +'@backstage/plugin-lighthouse-backend': patch +'@backstage/plugin-devtools-backend': patch +'@backstage/plugin-linguist-backend': patch +'@backstage/plugin-badges-backend': patch +'@backstage/plugin-adr-backend': patch +'@backstage/plugin-airbrake': patch +--- + +Updated New Backend System instructions to use default import diff --git a/plugins/adr-backend/README.md b/plugins/adr-backend/README.md index ebf99b4ee1..73af5137ba 100644 --- a/plugins/adr-backend/README.md +++ b/plugins/adr-backend/README.md @@ -65,11 +65,9 @@ The ADR backend plugin has support for the [new backend system](https://backstag In your `packages/backend/src/index.ts` make the following changes: ```diff - -+ import { adrPlugin } from '@backstage/plugin-adr-backend'; const backend = createBackend(); -+ backend.add(adrPlugin()); ++ backend.add(import('@backstage/plugin-adr-backend')); // ... other feature additions diff --git a/plugins/airbrake/README.md b/plugins/airbrake/README.md index 603311f92c..2426a6eb8a 100644 --- a/plugins/airbrake/README.md +++ b/plugins/airbrake/README.md @@ -124,10 +124,13 @@ In your `packages/backend/src/index.ts` make the following changes: ```diff import { createBackend } from '@backstage/backend-defaults'; -+ import { airbrakePlugin } from '@backstage/plugin-airbrake-backend'; + const backend = createBackend(); + // ... other feature additions -+ backend.add(airbrakePlugin()); + ++ backend.add(import('@backstage/plugin-airbrake-backend')); + backend.start(); ``` diff --git a/plugins/azure-devops-backend/README.md b/plugins/azure-devops-backend/README.md index f69951f70d..7c722dbe26 100644 --- a/plugins/azure-devops-backend/README.md +++ b/plugins/azure-devops-backend/README.md @@ -84,13 +84,12 @@ In your `packages/backend/src/index.ts` make the following changes: ```diff import { createBackend } from '@backstage/backend-defaults'; -+ import { azureDevOpsPlugin } from '@backstage/plugin-azure-devops-backend'; const backend = createBackend(); // ... other feature additions -+ backend.add(azureDevOpsPlugin()); ++ backend.add(import('@backstage/plugin-azure-devops-backend')); backend.start(); ``` diff --git a/plugins/badges-backend/README.md b/plugins/badges-backend/README.md index 1737482080..4e35b30c13 100644 --- a/plugins/badges-backend/README.md +++ b/plugins/badges-backend/README.md @@ -84,12 +84,12 @@ In your `packages/backend/src/index.ts` make the following changes: ```diff import { createBackend } from '@backstage/backend-defaults'; -+ import { badgesPlugin } from '@backstage/plugin-badges-backend'; + const backend = createBackend(); // ... other feature additions -+ backend.add(badgesPlugin()); ++ backend.add(import('@backstage/plugin-badges-backend')); backend.start(); ``` diff --git a/plugins/devtools-backend/README.md b/plugins/devtools-backend/README.md index c9226a92ab..4439c8cd9f 100644 --- a/plugins/devtools-backend/README.md +++ b/plugins/devtools-backend/README.md @@ -57,13 +57,12 @@ In your `packages/backend/src/index.ts` make the following changes: ```diff import { createBackend } from '@backstage/backend-defaults'; -+ import { devtoolsPlugin } from '@backstage/plugin-devtools-backend'; const backend = createBackend(); // ... other feature additions -+ backend.add(devtoolsPlugin()); ++ backend.add(import('@backstage/plugin-devtools-backend')); backend.start(); ``` diff --git a/plugins/entity-feedback-backend/README.md b/plugins/entity-feedback-backend/README.md index a3aae695e2..cef580a580 100644 --- a/plugins/entity-feedback-backend/README.md +++ b/plugins/entity-feedback-backend/README.md @@ -59,11 +59,13 @@ The Entity Feedback backend plugin has support for the [new backend system](http In your `packages/backend/src/index.ts` make the following changes: ```diff -+ import { entityFeedbackPlugin } from '@backstage/plugin-entity-feedback-backend'; + import { createBackend } from '@backstage/backend-defaults'; -const backend = createBackend(); -+ backend.add(entityFeedbackPlugin()); -// ... other feature additions + const backend = createBackend(); + + // ... other feature additions + ++ backend.add(import(@backstage/plugin-entity-feedback-backend)); backend.start(); ``` diff --git a/plugins/lighthouse-backend/README.md b/plugins/lighthouse-backend/README.md index 06455d90b4..df6c2415d9 100644 --- a/plugins/lighthouse-backend/README.md +++ b/plugins/lighthouse-backend/README.md @@ -72,10 +72,13 @@ In your `packages/backend/src/index.ts` make the following changes: ```diff import { createBackend } from '@backstage/backend-defaults'; -+ import { lighthousePlugin } from '@backstage/plugin-lighthouse-backend'; + const backend = createBackend(); + // ... other feature additions -+ backend.add(lighthousePlugin()); + ++ backend.add(import('@backstage/plugin-lighthouse-backend')); + backend.start(); ``` diff --git a/plugins/linguist-backend/README.md b/plugins/linguist-backend/README.md index 3bc441c238..8939bbe303 100644 --- a/plugins/linguist-backend/README.md +++ b/plugins/linguist-backend/README.md @@ -64,13 +64,12 @@ In your `packages/backend/src/index.ts` make the following changes: ```diff import { createBackend } from '@backstage/backend-defaults'; -+ import { linguistPlugin } from '@backstage/plugin-linguist-backend'; const backend = createBackend(); // ... other feature additions -+ backend.add(linguistPlugin()); ++ backend.add(import('@backstage/plugin-linguist-backend')); backend.start(); ``` From f3a63d07d6c67fccb9c5390f86acc87e71ca87f4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 01:15:49 +0000 Subject: [PATCH 054/204] chore(deps): update dependency swr to v2.2.5 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index c14cb29002..cb68ad962b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -43294,14 +43294,14 @@ __metadata: linkType: hard "swr@npm:^2.0.0": - version: 2.2.4 - resolution: "swr@npm:2.2.4" + version: 2.2.5 + resolution: "swr@npm:2.2.5" dependencies: client-only: ^0.0.1 use-sync-external-store: ^1.2.0 peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 - checksum: d1398f89fd68af0e0cb6100a5769b1e17c0dbe8a778898643ad28456acda64add43344754c7d919e3f2ca82854adf86ff7d465aba25a2d555bcb669e457138f8 + checksum: c6e6a5bd254951b22e5fd0930a95c7f79b5d0657f803c41ba1542cd6376623fb70b1895049d54ddde26da63b91951ae9d62a06772f82be28c1014d421e5b7aa9 languageName: node linkType: hard From 9873c44c2493d70a3d970adad0d904fdf0959196 Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Thu, 15 Feb 2024 14:39:44 +0200 Subject: [PATCH 055/204] feat: define signal type for notifications Signed-off-by: Heikki Hellgren --- .changeset/honest-pets-judge.md | 7 ++ .../src/service/router.ts | 69 +++++++++---------- plugins/notifications-common/api-report.md | 15 ++++ plugins/notifications-common/src/types.ts | 15 ++++ plugins/notifications/api-report.md | 5 ++ .../notifications/src/api/NotificationsApi.ts | 2 + .../src/api/NotificationsClient.test.ts | 14 ++++ .../src/api/NotificationsClient.ts | 4 ++ .../NotificationsSideBarItem.tsx | 53 +++++++------- .../src/hooks/useWebNotifications.ts | 11 ++- 10 files changed, 130 insertions(+), 65 deletions(-) create mode 100644 .changeset/honest-pets-judge.md diff --git a/.changeset/honest-pets-judge.md b/.changeset/honest-pets-judge.md new file mode 100644 index 0000000000..468fc1b6d5 --- /dev/null +++ b/.changeset/honest-pets-judge.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-notifications-backend': patch +'@backstage/plugin-notifications-common': patch +'@backstage/plugin-notifications': patch +--- + +Add support for signal type in notifications diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index b293ea4a1f..6c745e158e 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -42,7 +42,9 @@ import { AuthenticationError, InputError } from '@backstage/errors'; import { DiscoveryService, LoggerService } from '@backstage/backend-plugin-api'; import { SignalService } from '@backstage/plugin-signals-node'; import { + NewNotificationSignal, Notification, + NotificationReadSignal, NotificationType, } from '@backstage/plugin-notifications-common'; @@ -206,6 +208,21 @@ export async function createRouter( res.send(notifications); }); + router.get('/:id', async (req, res) => { + const user = await getUser(req); + const opts: NotificationGetOptions = { + user: user, + limit: 1, + ids: [req.params.id], + }; + const notifications = await store.getNotifications(opts); + if (notifications.length !== 1) { + res.status(404).send({ error: 'Not found' }); + return; + } + res.send(notifications[0]); + }); + router.get('/status', async (req, res) => { const user = await getUser(req); const status = await store.getStatus({ user, type: 'undone' }); @@ -214,38 +231,18 @@ export async function createRouter( router.post('/update', async (req, res) => { const user = await getUser(req); - const { ids, done, read, saved } = req.body; + const { ids, read, saved } = req.body; if (!ids || !Array.isArray(ids)) { throw new InputError(); } - if (done === true) { - await store.markDone({ user, ids }); - if (signalService) { - await signalService.publish({ - recipients: [user], - message: { action: 'done', notification_ids: ids }, - channel: 'notifications', - }); - } - } else if (done === false) { - await store.markUndone({ user, ids }); - if (signalService) { - await signalService.publish({ - recipients: [user], - message: { action: 'undone', notification_ids: ids }, - channel: 'notifications', - }); - } - } - if (read === true) { await store.markRead({ user, ids }); if (signalService) { - await signalService.publish({ + await signalService.publish({ recipients: [user], - message: { action: 'mark_read', notification_ids: ids }, + message: { action: 'notification_read', notification_ids: ids }, channel: 'notifications', }); } @@ -253,9 +250,9 @@ export async function createRouter( await store.markUnread({ user: user, ids }); if (signalService) { - await signalService.publish({ + await signalService.publish({ recipients: [user], - message: { action: 'mark_unread', notification_ids: ids }, + message: { action: 'notification_unread', notification_ids: ids }, channel: 'notifications', }); } @@ -285,7 +282,7 @@ export async function createRouter( throw new AuthenticationError(); } - const { title, link, description, scope } = payload; + const { title, link, scope } = payload; if (!recipients || !title || !origin || !link) { logger.error(`Invalid notification request received`); @@ -344,17 +341,17 @@ export async function createRouter( processorSendNotification(ret); notifications.push(ret); - } - if (signalService) { - await signalService.publish({ - recipients: entityRef === null ? null : uniqueUsers, - message: { - action: 'new_notification', - notification: { title, description, link }, - }, - channel: 'notifications', - }); + if (signalService) { + await signalService.publish({ + recipients: user, + message: { + action: 'new_notification', + notification_id: ret.id, + }, + channel: 'notifications', + }); + } } res.json(notifications); diff --git a/plugins/notifications-common/api-report.md b/plugins/notifications-common/api-report.md index d2dc1afb9d..6f40c76e58 100644 --- a/plugins/notifications-common/api-report.md +++ b/plugins/notifications-common/api-report.md @@ -3,6 +3,12 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +// @public (undocumented) +export type NewNotificationSignal = { + action: 'new_notification'; + notification_id: string; +}; + // @public (undocumented) type Notification_2 = { id: string; @@ -28,9 +34,18 @@ export type NotificationPayload = { icon?: string; }; +// @public (undocumented) +export type NotificationReadSignal = { + action: 'notification_read' | 'notification_unread'; + notification_ids: string[]; +}; + // @public (undocumented) export type NotificationSeverity = 'critical' | 'high' | 'normal' | 'low'; +// @public (undocumented) +export type NotificationSignal = NewNotificationSignal | NotificationReadSignal; + // @public (undocumented) export type NotificationStatus = { unread: number; diff --git a/plugins/notifications-common/src/types.ts b/plugins/notifications-common/src/types.ts index 1e7469244f..4d71004265 100644 --- a/plugins/notifications-common/src/types.ts +++ b/plugins/notifications-common/src/types.ts @@ -51,3 +51,18 @@ export type NotificationStatus = { unread: number; read: number; }; + +/** @public */ +export type NewNotificationSignal = { + action: 'new_notification'; + notification_id: string; +}; + +/** @public */ +export type NotificationReadSignal = { + action: 'notification_read' | 'notification_unread'; + notification_ids: string[]; +}; + +/** @public */ +export type NotificationSignal = NewNotificationSignal | NotificationReadSignal; diff --git a/plugins/notifications/api-report.md b/plugins/notifications/api-report.md index e2071dad14..0f5d7a0049 100644 --- a/plugins/notifications/api-report.md +++ b/plugins/notifications/api-report.md @@ -26,6 +26,8 @@ export type GetNotificationsOptions = { // @public (undocumented) export interface NotificationsApi { + // (undocumented) + getNotification(id: string): Promise; // (undocumented) getNotifications( options?: GetNotificationsOptions, @@ -45,6 +47,8 @@ export const notificationsApiRef: ApiRef; export class NotificationsClient implements NotificationsApi { constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi }); // (undocumented) + getNotification(id: string): Promise; + // (undocumented) getNotifications( options?: GetNotificationsOptions, ): Promise; @@ -128,6 +132,7 @@ export function useWebNotifications(): { sendWebNotification: (options: { title: string; description: string; + link?: string; }) => Notification | null; }; diff --git a/plugins/notifications/src/api/NotificationsApi.ts b/plugins/notifications/src/api/NotificationsApi.ts index 775dc6b244..266c8a1ee5 100644 --- a/plugins/notifications/src/api/NotificationsApi.ts +++ b/plugins/notifications/src/api/NotificationsApi.ts @@ -45,6 +45,8 @@ export type UpdateNotificationsOptions = { export interface NotificationsApi { getNotifications(options?: GetNotificationsOptions): Promise; + getNotification(id: string): Promise; + getStatus(): Promise; updateNotifications( diff --git a/plugins/notifications/src/api/NotificationsClient.test.ts b/plugins/notifications/src/api/NotificationsClient.test.ts index a7a2d1e5a7..1312b99cf4 100644 --- a/plugins/notifications/src/api/NotificationsClient.test.ts +++ b/plugins/notifications/src/api/NotificationsClient.test.ts @@ -74,6 +74,20 @@ describe('NotificationsClient', () => { expect(response).toEqual(expectedResp); }); + it('should fetch single notification', async () => { + server.use( + rest.get(`${mockBaseUrl}/:id`, (req, res, ctx) => { + expect(req.params.id).toBe('acdaa8ca-262b-43c1-b74b-de06e5f3b3c7'); + return res(ctx.json(testNotification)); + }), + ); + + const response = await client.getNotification( + 'acdaa8ca-262b-43c1-b74b-de06e5f3b3c7', + ); + expect(response).toEqual(testNotification); + }); + it('should fetch status from correct endpoint', async () => { server.use( rest.get(`${mockBaseUrl}/status`, (_, res, ctx) => diff --git a/plugins/notifications/src/api/NotificationsClient.ts b/plugins/notifications/src/api/NotificationsClient.ts index fe7a90d3e8..9ef7b287ab 100644 --- a/plugins/notifications/src/api/NotificationsClient.ts +++ b/plugins/notifications/src/api/NotificationsClient.ts @@ -60,6 +60,10 @@ export class NotificationsClient implements NotificationsApi { return await this.request(urlSegment); } + async getNotification(id: string): Promise { + return await this.request(`${id}`); + } + async getStatus(): Promise { return await this.request('status'); } diff --git a/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx b/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx index 90fba009a3..2163c6660f 100644 --- a/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx +++ b/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx @@ -17,12 +17,13 @@ import React, { useEffect } from 'react'; import { useNotificationsApi } from '../../hooks'; import { SidebarItem } from '@backstage/core-components'; import NotificationsIcon from '@material-ui/icons/Notifications'; -import { useRouteRef } from '@backstage/core-plugin-api'; +import { useApi, useRouteRef } from '@backstage/core-plugin-api'; import { rootRouteRef } from '../../routes'; import { useSignal } from '@backstage/plugin-signals-react'; +import { NotificationSignal } from '@backstage/plugin-notifications-common'; import { useWebNotifications } from '../../hooks/useWebNotifications'; import { useTitleCounter } from '../../hooks/useTitleCounter'; -import { JsonObject } from '@backstage/types'; +import { notificationsApiRef } from '../../api'; /** @public */ export const NotificationsSidebarItem = (props?: { @@ -35,11 +36,11 @@ export const NotificationsSidebarItem = (props?: { const { loading, error, value, retry } = useNotificationsApi(api => api.getStatus(), ); + const notificationsApi = useApi(notificationsApiRef); const [unreadCount, setUnreadCount] = React.useState(0); const notificationsRoute = useRouteRef(rootRouteRef); - // TODO: Add signal type support to `useSignal` to make it a bit easier to use // TODO: Do we want to add long polling in case signals are not available - const { lastSignal } = useSignal('notifications'); + const { lastSignal } = useSignal('notifications'); const { sendWebNotification } = useWebNotifications(); const [refresh, setRefresh] = React.useState(false); const { setNotificationCount } = useTitleCounter(); @@ -52,39 +53,35 @@ export const NotificationsSidebarItem = (props?: { }, [refresh, retry]); useEffect(() => { - const handleWebNotification = (signal: JsonObject) => { - if (!webNotificationsEnabled || !('notification' in signal)) { + const handleWebNotification = (signal: NotificationSignal) => { + if (!webNotificationsEnabled || signal.action !== 'new_notification') { return; } - const notificationData = signal.notification as JsonObject; - - if ( - !notificationData || - !('title' in notificationData) || - !('description' in notificationData) || - !('title' in notificationData) - ) { - return; - } - const notification = sendWebNotification({ - title: notificationData.title as string, - description: notificationData.description as string, - }); - if (notification) { - notification.onclick = event => { - event.preventDefault(); - notification.close(); - window.open(notificationData.link as string, '_blank'); - }; - } + notificationsApi + .getNotification(signal.notification_id) + .then(notification => { + if (!notification) { + return; + } + sendWebNotification({ + title: notification.payload.title, + description: notification.payload.description ?? '', + link: notification.payload.link, + }); + }); }; if (lastSignal && lastSignal.action) { handleWebNotification(lastSignal); setRefresh(true); } - }, [lastSignal, sendWebNotification, webNotificationsEnabled]); + }, [ + lastSignal, + sendWebNotification, + webNotificationsEnabled, + notificationsApi, + ]); useEffect(() => { if (!loading && !error && value) { diff --git a/plugins/notifications/src/hooks/useWebNotifications.ts b/plugins/notifications/src/hooks/useWebNotifications.ts index 7e34172a97..bf3b5269f5 100644 --- a/plugins/notifications/src/hooks/useWebNotifications.ts +++ b/plugins/notifications/src/hooks/useWebNotifications.ts @@ -37,7 +37,7 @@ export function useWebNotifications() { }); const sendWebNotification = useCallback( - (options: { title: string; description: string }) => { + (options: { title: string; description: string; link?: string }) => { if (webNotificationPermission !== 'granted') { return null; } @@ -45,6 +45,15 @@ export function useWebNotifications() { const notification = new Notification(options.title, { body: options.description, }); + + notification.onclick = event => { + event.preventDefault(); + notification.close(); + if (options.link) { + window.open(options.link, '_blank'); + } + }; + return notification; }, [webNotificationPermission], From 526f00a9bee4337eb77a880a8f3b54692bc4805d Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 14 Feb 2024 12:56:46 +0100 Subject: [PATCH 056/204] docs(org): create alpha readme file Signed-off-by: Camila Belo --- .changeset/nervous-lions-suffer.md | 5 + packages/app-next/app-config.yaml | 1 - plugins/org/OrgGroupProfileEntityCard.png | Bin 0 -> 25091 bytes plugins/org/OrgMembersListCard.png | Bin 0 -> 48346 bytes plugins/org/OrgOwnershipCard.png | Bin 0 -> 118385 bytes plugins/org/OrgUserProfileEntityCard.png | Bin 0 -> 16209 bytes plugins/org/README-alpha.md | 462 ++++++++++++++++++++++ plugins/org/README.md | 3 + 8 files changed, 470 insertions(+), 1 deletion(-) create mode 100644 .changeset/nervous-lions-suffer.md create mode 100644 plugins/org/OrgGroupProfileEntityCard.png create mode 100644 plugins/org/OrgMembersListCard.png create mode 100644 plugins/org/OrgOwnershipCard.png create mode 100644 plugins/org/OrgUserProfileEntityCard.png create mode 100644 plugins/org/README-alpha.md diff --git a/.changeset/nervous-lions-suffer.md b/.changeset/nervous-lions-suffer.md new file mode 100644 index 0000000000..b3a0400c26 --- /dev/null +++ b/.changeset/nervous-lions-suffer.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-org': patch +--- + +Document the new frontend system extensions for the org plugin. diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index 408b08acee..8bfd79be54 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -22,7 +22,6 @@ app: - entity-card:catalog-graph/relations: config: height: 300 - - entity-card:azure-devops/readme - entity-card:api-docs/has-apis - entity-card:api-docs/consumed-apis - entity-card:api-docs/provided-apis diff --git a/plugins/org/OrgGroupProfileEntityCard.png b/plugins/org/OrgGroupProfileEntityCard.png new file mode 100644 index 0000000000000000000000000000000000000000..c012b54d04bdceb776edcc8579e4654a9d410fa4 GIT binary patch literal 25091 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

yJPQ03ySfc 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 literal 0 HcmV?d00001 diff --git a/plugins/org/OrgMembersListCard.png b/plugins/org/OrgMembersListCard.png new file mode 100644 index 0000000000000000000000000000000000000000..2a4d2b073127401bf57f7d0b028972d5a6ebab11 GIT binary patch literal 48346 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 literal 0 HcmV?d00001 diff --git a/plugins/org/OrgOwnershipCard.png b/plugins/org/OrgOwnershipCard.png new file mode 100644 index 0000000000000000000000000000000000000000..f9358a65d1b2de5b9afb532a50e8f8785f5e8c5b GIT binary patch literal 118385 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�?^v)$kUsg>&jaiG-^+hZ|+RyyY)F~^8 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⪻@(g?`0K{%>k1FIQlj5!jT*F~L|nYN8Ds zM&@QiMPG>fDo|0n=7^=ax_H`;YblIpKeIwDeaoYBKi9H}H@{ii7m7j~uO2G(iA$zL zN}~g(4>0c~@fY`43eQ@%OpvJ-@QLR`Kjkx3IN2DhB*Hf*e{K~?s(J3ii~&!~Pi z=Qp$&hp0=x>a^ObSex67+VS}^7LZgm{%iDGK8QgMHEn}M^ z+7MlCw_R-(=1BE}6>W0lLs|Yu`of~O*UIFx=8F;eb}*oyxTZDS&t0&L+f@MYth9J0aYrDc2;|a?fB9ayT@q~X{$>tc!Mk&?MZ=W(E-O;@9o#`s{2K#q*2;F1!U@&NowVuNxU;8QB3wHpr~}@wB#l;f77M50@+cJgIqs`0 zDP3NSsn`cl6Mmycyy6VNY}nlFW`Es%*LBf<|32EK#TPi(Z)haS6%G`ak^FW1jZBv# z9)Ep>Ae`ba?ohETH?uQRIam;KY?Dc&(8_0p|daj)+T0UcL0o1nU6F^xD<^!jJ z>W|Z}eRxJ23-TZDe>rcP-eNtR!SvgNCY$b!*%e4pl*M%5c-c1_DcQlu`eovgyG^fS zPrBqwkl8eYwH_A^^MEA$(+`6QtZWXc%LrchUB0}A;+~XRwRdhMM2|x|Xsxu-mC3`y z+KmuW-A)x-ySSp=I8$*wYwu(n0#vda?p;|tEZ0{WXusB867D!qCoV8z!s{Cw^fS3W3xz(H1rNpU-674xyOT=AOjzT#vq^~}zk(j@x2l>d)~fyKXyT26xC-TMQ*LSXsY(O!c`@LCgoa^qUct! zoj+CRr;+B0Mki4+1r?HdL^(Qa9;0klv8$K#Zk*}lw0;llfE6EPq+t%yAcmvqXv^U%Ko{NP zVHA*tkVeJzLXt>r1=#OMJdG5`$qSZ-Kgb%Do&z?{1gevt>_KabS?)IJ@q>mT2W(37 zdxl`^yYPYW&7^klk@^On(zpxMGA7NhfEpz4H5ETpk@DirH_0%8@+}h4MR`}MxAJ_y zW#mBbfD+t#=VcM8%y%RwZsgg*E=C6z-5@dAPx(*uzVIq{vxIO31|y1aYfZ6X2NrNG zb4Q}K^&JiBc3E|`msi5WmnQVES7q+|gG~DuSZ@I89;3qz1g38oTqypa_-=bR9q`K& zm?Zk#BRaCOijVo*7nm!!?;9*SDT(^YwUBcxIcH;jxS!!%`nyV`Zju@q^qh=9W=h?@ zI@<5YvH~mr)YqwZfypr^25{0wdT>rm#&A50$3Lj1GTP$%U zaPC_f%5k6qWlR>{J?kiAl;yIdNejw~=eCisk3#FoBNRhCWUSW|0;e&(F^uL4qw?Q( zQT6Er^YTyY^~gR(R2?{3G^oZ$OdVvM^o7cp?!+Ym?_MHi-xWVHXJ6BMyA7ZLsYEN0 z8RIvK*Spssyx>Um_!8-{)nKqQq*k7!vte;5eMl}rTO3cYUfkl4tw8FsL-V5|0(pV5R)s7V{uS ze!KBSoNEHOag0RLG0~0B-cjFyw`z%LKe-U!%kvdFxr>tAUba%0-uXMluUgfz4zKh6 z=YIjPK<9Ch0lUsS90saMq8N)60_cewhH_;Y#=RB|LU|P$h89{{kBdd30`*?eZ62Bd zK{DE7(iWx;#SYdj$O}g5cKKVFTDEhgBT>|$hmnEv36leeyD@%iSd6?Q+EA5}aMf38 zjoS5fz^m}3A=rUIVqwwYJxe<-Hbr{w2%DrlhV$HI{XxjG8>4&Gzibo7 z7^Rt^pOtH?L8|yRhTfH{i>j>2^HlU-kbEWV!P+E>B71CaZ;Y}wP1?47NU4y*8#ceh zgoVbLkrUM}d@8`0Q_3_bvTKMCt?h@eO|Qd_mHd(WLSHY54qS}vg@XSuj+2oReedqv ztyI6F&PX!7T zP)#-z-bH4mV&$F>#W&O-tL5r zlX>SsY9`6M`Hk{zu!5Ij?!Hv!_#TqOLHV#7!4#fDn-u8R0`q)71bOgD+T;ZUPx6s#N6s45Q`%B-H&z7hT7Kpb zDNfOtt^5p#c`4P3bFo44xv1T!<4$%BGJ4^`$|Ii+J-2#n+-#0qn?$+SJx|=Kec(HN z9W2aN!MD2EfW$!xVOP?0pJmfbox?I^=qJZ!7m4EqrF>_kDwP_tp&FJqS9o1Jv2xX? z_c8Rtbj&@f$Z9gJce3o0YO8ms5-lZzykFa{=1WzfUCXzH1)XnuyS@6o1w*!|*-rF6 zV_m(s+4l=ZCkszI7+!W)K`9jAD~A>hC;wLU^??=}`2#Cl-h8f}C1g!%sv9EH`)Z;5 zR%>jKwyQ4@iy%M)T6n*RHCO$Hynig51FyjywZ7pumr|44qn-NavW{=~Bu@f6+7#!K zn(w+Du+y%rC&>B+@|`4Go_;QGhY64*2!Fh-ap0gFs{4NceL#Z01b$57))!HAA_^cg z{`PXhn)?qq`G?8T)E@XbeuR&}nV%SoXtq2m7oM7=dW1s`W8yb(YM3*U`Wx-QTeT;8 zluo|FuWIm(6O6t+r6drKhk-#QGoFjLUF+0i!DCX%yH-{a;4xk}xG>;v`Lg9QcjbM^ zSTWZJK_xIR;Z#xJRYhRk^?wdO85<49NI z+cs1vH1{>r4_w#m_@mA28Bb0}InCdpdGZ1Vxn`^!3R637(d)?g23Z`tN3OkCA&)jJ zw@Ut*^rDcMS+0&mDzkQ6Epi#fBT?eDM0A$vv43i-EDx4jICQU61oA^h2%v;){oD59NKaQhF)=cH;pHS?+z_jMmP!~Zlj0sq- zM9TR!4|ddEj%Vbr^P!@u zPlOauD$Ddsad2>c7+kPeUcV;i0{mpKyoCVoRh^VqCvK6trc6}p)K9wF#mJ5$oOF{G zx;mnhg(URX4yrJdpE>oDS?TgYoXE$#9e+uWR9f-tj5Yxjk@7%X2|~a9_VwZCXI~YL zK5UP$%PIj+IzfJ&WSF|Zr6ECSJWx@;-fc>`KCo`&CtwYGy49BO11IzmqDXWs_M27Y zwJw@e@fr2lDpL$OI}xOuY63n8$mvv}J~?W`#8$+mJdnNLUCrSG;(6b4HCwT)$57p~ z{#EO<;o9JW!7wO4S`wBm(_bUiedhSlAzA%x{y_L_c(QHjyyLc8!!OSHZ8++X!({H- zn^rJqk$8dZv_~AxIxIUCigv)>wY?tw?83DYD=x@LT?OJ0h;o|ic^kx8NcjPPdY+er zu1)lVdwmh9==mcurgNgrSPfWkCbo!1R@UQ5DPF^Ha>8ooG~bd^v6oIw8Qppv9ZmFU z3CblfUnt*rZ707;Fts?0j>yfm{7uL{^DSd?ErkgA;gczfpJ>P#*vh=I-C7#kb9!OL zg7d-w$}w-RAG#3WnFLSn%ww#OR$yqE`q^2^vpx`Ry-}{c-@hgt^U&SH&b!(TH4iiP zTI+C2xK-t&p6CXDQ4`l12AR1BsVoI8#{;b1ijCrh1N6_nIhmG=rSX^h$+x{IHql4x zxm6nNiC(pBEltlC$|Jm~-{d#HbUdTY$dboG;xFTr&WFU4ij|*AOm{z@91Z>GXd%nq zA&+GZj%q}dr3F7#FvoFCx`fD+9D;4|;6w9h3;c~Q6c*T=CbM?}V%&XrmTBXZ2-Im)az6Qy3-$n$10pyzU|Q?`{{ zuWj4|p64jD-c~Q>Dw57wDYI_W+54>aCEEovO<$dNm?Ybna*=I#qb^j${c$KaEnhrZ- z@RKyl^UtJJh$t7yS{mDPO4&cNR%k#e=SI1la{ub24Y-J_9#X6k2Nw#eGX4P)WV z?_C|PyzH*xSc!Mt+WY8{Lc-syBvIAL2DvR;q`Ya{P1K8jk&9YrA zZ9JjRRBzW%;UPf;q1n92wU{2AchZ>bWpX8M^Bpq9-#K68AOMG2AD z*eZ(ptIqJsc-0x)HNfla?jgSmVKc;(MLy7L@UXMw2M|=4-Jiu;>$_P^^@`^ zZ{FSrTQoq;S%;O>yxm%gc9iT~L|w><18ME`7aGgbMz6{fN9wQOm-Vlif0W!L`+Cm# z)t_~%&!I=e2Rn%~^~hMX9+@Ay1xZcD6?F0|pB`FD(u~zL#u8n0zi>P8v7r5qjPl~k zNJc;|WsQ}DlwuI#4gajiWj*+IfvMb{Q|Yv7BU|hf`AT`}w|Xm01x_}7Bb|JVEv&Jm zs-MaX2jeoF;UJt=)CisBN|pnsIoRZ%?i<4LztSNOI^`6Xtbfo8hI;+Vn2Fj!7DAfb zY-AqQMKkJ8JP_!I`DrRF}{=;QGX`(Uuk4)(#W&B`M*_dnHNIF>RMMV$a%3b2Ygime2tzFp?k_=ueY?nsY5GuunQ6>h?H>Xn?ESWL70+2Fm5y}u%%-)e zfReZe81hjs3OLt7ry5d%^w1QdK~v&q9de>l;f89pSlCx_wyVsve7iMYzv0?$^>Em@ zVIqvImxn*^z#Cbr33^(kbjE35GRb%@Pbd^@+7G3xhebmyQQnY)9!q79I^Q#1H9-Bv z`LE1z(I0LSTqeVVPPm_r2jEMbk(YZOV;dYg8x~0!?N|>A@3o}I+gRDZBq)VsPsSUz zsKf!HEXofh2*kmt2uY42GBBV_0>=|bS;}L?{Ctr_BQGzVN-xQa1-YK-RvzRLC$co) zgodcZ`X?MxpNY9kn=>4YOOJGRiF!qyJ=BWY@&r z0Lyd8}`v(r7}6yS{n^=K|19LOa)^pg-3H1L=EQO^%OkcY7G6Q_oK&NQcNQi*=8TX%an zLsrO-J!B7guybL;8Cq*oNsn--eer;F=8;~j1H{N@??RQMq(7WP;arN0ISt}Qw~JNY zR^g{g`kBi?toAs7jz$Qhkc~efl}`ZRiwU|gKs7g-ng>~RX^q^y=Lzg4Ds)9e+Gwtt zv%QIUlAEFSbgZIco2$g6$dkF)KgeMl3;rgjWTj13%g1GZ`6em1OzrUbD`~%B-F5N7 z&LiZ(&PByx>#~159tqP9iYlLYqJBBIncm0{{h(jTKID+(JhMzz%9kx44wE}<4jW~qd|U$1 zNqJqOrDP@6EBz2_WsaiV==>xX{6iUhNTf3_Qp^CNB(1U!WbHDi?-1i?=u$;9_N@0PrVeUFtGdTXZ;A|Xu;?F-lN3w@ zd6dCwobsR!7Rwb`maT$T)uDp~fI(-=WOkra|1n1`>P~)WUFcX{P&3Ts;gi7yVde5A zy0?70?N`X_n`D2R9x3odCy=Y_u^#<`TG}!OFex$et5DGkZ6-}CUGhVf$f@C2M#e;y zS}*Jv^NYQ`wv91gs=t{eM2K<$txBO@Bm?Pk9R>#{|R<2knb9a}p-S(?x?#e?@ zeV$-vpnF?g#htg`7S8Kb$kcE%KPdumMG?%t*1GNKnoSilr(V%nvZpzA2RkJ?~jN9{gOj!=kY3-YdfDJ(q?> z+hcFJ$y=V6p%uye<0om>Qb_0W;H4(xuX2}LQOosH+UV2(9El}!YpcrBtFk<6%&9ko zTW*$Xuk0hg@j6_4#d-vhaW6P=v!n^R{OPr_N&c*sF&u}mv zZyn$gis_=J6>}6cWn*q+(AYQTVAQ)8u;MNT|EW{TNx#yRU^Mg!v<`Zw?x@~Eoq8lR z^@F)O4Sz%__?hSaNW&6YDPO&MMc8TQ)nVz1rD4HPeDDOTOf=mrw!m& zArvHm8Cm3GuJqW}j9>Ldxhg-%nxYQc&h*1at%|JWg@_VVb))`~k#TuA?8?kiJX6`2Rh;(`0fpf;4z0!+S!_X4*a##E{2Uz=ZlabYu^}1Dc1E8rse!v7RzW%DO!$x7+D3)BPnVj@mC9A zkfeBDjw|<&7(k0Iid4AXxn(5L%x(^V?-8Et3BlMZ$Q3I)7LH`S3hEV z=1}RN>| zLisf|xxLiDTW>R?5j`>C&bgSdribm~p4hW-Rpdphl=Q}1m`Z+8t|nMeK5Afvd0DP` zuu<@{-^F;NZsd_KA9R_ha-{hq{nITrnOvoa>Wn4vqwpDXwsEd>WvkM!wz3?z)h!Lz(09XQTB$#f)OOnS}XwI}ACh{IgJ3G0r!!Y7MX zdJUF&1ikuAJoM|lN`=*mr!MOa zJ)nNeX+>iRktq6~^MR`@Z>{vVXs^l}U*~(qtMcrF(OIMQBDWU9@lWT;wfF9^@PK1> zl{do4wHL<)$479qZMnh?g}io3vCxlF)TVNx6T5+gjKg4jQm;)mUa*^hL;(#Sr4_%Lr9d1_YF9qu0B-OL`oSY`+<3zsVZ(;;aQK01 z!YX zD9V(?VjB5WnTzFRibgfX{1MH1a?F`CQ#8wdRM>i{R_6%oz3A#mhFG-Ads5!L5RTyz z*|vf|{HV~VylG@y9&VhPlpiIzkENS#To-<}VM93lfP-Z2+WW}6g@EHp=U~HJ>Q-BO z{W6Y^ zgJLcURWkMucr26Wx|S_nD0|E0g$uH`9B*QokiF$*!9Go~8OZ7E58ADq9EgS+<*k8| zMZ5Z^dcZ;anBzo`GEewbdB`9YHwRXyeybG#5w61P>#n&otiN+C9C_@Tu+uK`TOjC4 z8VACsUBd@-*7p;OLuensFQwWcFSAQ)ImKrTWETOn;%z&e_G(3rqxK;AQMRAX7r3L8 zCLKpO_Ecihs@d#DdGS;A3ei{#9E=V=No0z7^u}UE5oEJ58-Gqy%s3VkQ||?ihTb() zCDD8N)4O!!s0)&OH>u`ei3gmyRwG6wl_qYahISH+3A&&kHgSTm9A(grw;`9icr?M^ z=xU>_)Br})V?rfQi%R5CU+_e+)NyyXKnCUVWsAd(JFb*F_vQNDa&`_vM zC1qiQR;(%Uvn@*0@(ZyOe(*$}%04Dx~i{HMYtiD*c&3O^e3*ntkYhC~|jXbG!A_@DhX zh-C!qYgc~CN%zrL_B&&>qOeC(CfC5wJE7WF_PJf&yTweB{c4tj6>UiEa^vdjGmRH9 zhJRQsU%q@<*m0+wbZ_~6@xA36*WVp}BimLkx%QeS^PvoR-MHZxD|60FjRAL_qs)3+ zE1csUPF{=)q&&CL!U5XaVs5TAXJ@U@httRwJ(tw%`G#BSntqhaxd~}btdUMJg(jV= z!H1KTFd?Im6qHZ=NY_=EmUd;0g&a4S;dn7F6VUOZjrfR(iZU-%7xYIPn#+eCfAMWl z-d(<8)o|E#&lO<@*;_umoqWTS$(wUP^Z|E}u%jt0U-J{{mA6^|Uz3{e#lS&{y!k<<$?a?KT<+!_%Si-BE z1$$vUmNfnWA7UW+!?0vZvZ)aD!A|6}V<>bffMl$(k%}~@04h>eKOsk^qHidX%y#Xc z5P5DY*`tc68nP;<{N-bov-VP+;g#{ig~l5O84l75`#D{?DZ$+k{`0Pgw;bp#%E*yO zw^~6@jTDFs$#4{;9=YvM5Se3mN*h-qxf()tvnx96_NTB*e`qHOz<%fai{&BD#fyf* z#Ih;*DowVqj7-=`GLv zUT=tt#YGpdlWi;G;eiM37UpCBeM)&5$0s&O_exMF{z?J-=o9YZk&B}xxd>AOF&<~l zRArpDF*jMw8 z6!y!+PUbnWsOJ6JY9qgR@#28L3HgQIQGG??s9tQ^i2YSGZ`r&#Tzu(;vTbF3IQqat zDs#8oMUE3tIgZL4Gh5?IIJ9;gfLC3Jmq1KwMH z+k}YNzo^*+l4r51idjZp<|cmRp!Wr5^p6b0G1ZIU&NLVLQG$mBrO=whGylr+l$w^e zkxz&7&&9Tt@o?0G*2sI@<$_6^k|OgnU3gRi_6R3b9mo*55DEf7rOQW_I zOdMo}qC#%`CBA~+`1siYe@QgsR~7Q^RM|g`zfH2Yd_;asXl$dr9&r;kWg$g=3{vR9 z;8A^1DKh*~EejmfXjE4H5JpMe@|#pVo-~9UsYbswM7xdr`M9|Ly4%9~_43u(L2JV5 z6^rG#VlTa&*zJJfL_vl1x$Z}kU>}X-TFC31m1AVDyW$+SJgI6S zuRPWE3Y2@OMIk&&luPQdA33^#~bZoUSWk3 zbZAxxL_jZvRFdo&f$uG!l2!7N4f2Cvf0+nVdi{z0FfZUqN_KXjCz;~NVU(hsXOl9g zZSRReNzz>u$~YDD0y*tB`k6Oo<-!jm8Y(DT%QaWs9`3$#RJX0HUcDrQ`O?C$L%525 znmj*MT0W5D8agjsv&oU=;42JkgH|yxaJ;es!sC@mOA%H?Lio&{ilHi0rC7U^X$HAA zv0WTcJEjJ$np8fjSA}edxmrx^aXv^LBqZ8P1S9wLiFL#oOmcS|FE+?nHrL^P^$#S_ zkmE0wGd}3hKc#`icq~I(5;i3uC=of^Bu7t;+4#@|hGzVbRe9v8&^y1{i8hgy(tvyo zJ}<10*CXz<%c`&vTUdtqdcEzj$^XdH%pc{CvPoAw2CBMFKW(v1 z@+SI~9NBOz@4jne`1S9u4p*$ZQQM+wUsDzy z_|8A|l*cOSVc(LYUFuJAEVPpc&K2lX6f`6~4pyxFBwwWwTUb`e!=5|sylYswYWuKo zc&Ne=tL^;jp?%@-aM*kAwPDva_mj83G_gx>deBc%%`5Wj@4hpfb?(_Rcdw7lO#mt> zX;G=@Z{UpWTmfgE_&Sug$(y9w7GxAFPs4bvN>;j0cbLZ$Lsd%Z6~P*gj!0riVxu(Y zRBK$AK^gsEt~RwIA4U`oo^zoD*Ye^I_Hm9yUl4$SS2M^!g{pxfl#z=qrJGG{0L@yo z6uDJi8}Ok-K;7e0}~ z_Cf8VO01>PrxsKdisvJ`XTMcT6~&wQAdQ)fWB;U| zHYY&T|M5db`u>>Fm2tbXHvjCVhxZZ8(P5PHJoZ6Z$j4l7iaO>8rntMqq}01)%~2N> zx&lePS+2$CPdss{N1%g@lW`%Cf)zGZre=T;z0IM$Ph7a*`80IEfIWl31#gJWAAF z3$)##uy`fzQ&QZzkI% z9YOzTfB)+6tOq?z#|gym9vunq{N@+JC%^F(S)s?9n`S@ZhmtP5_yXIuB5zxn&D8bQ z6U;@Pdzp9OAm>4vyB)}YrAKoeYH!bGD}XWxaqfUW^2cwDIrKrdmYAh)TeZPHbagm! z6%C@KtOreU0uv(OJ^6>nSkSrn5uogKSd)Jt8^0?w#SGMz?nTQ1Q$-j&t*p+&o(S&^S6ZpS0iBB^W-#KN5N z^LhZoYEO?n6(`R`j6IYiCGM!7G$Q`zM*`C1R!wm{ss1=0G!#to?69nmNB>Ilg*$zf zW;hs^JBR2Fz(AMnf=sH>j3uWjt9=+EL^(q(@)Mw_-;R(}gs!4;cZbBixEhFqfcOqb zcB>x;2fd)9|9fFg!dMCYQ-uSY!(ef^yr! zB8?QIU$m3oP{42cL;W@UOO_3XZ@lTr8iv|DMeR=I)> zVT;!z(`cE*qOAj@heRf8DimGrc|>ymr5^^LE?Mo}q3u^P~ltbfRgmpx1fO zqq+7Db;yBvqeGSRhcS{|12}|DRjiSy*9d9h9N;;Ub|t#e1rT{Df{Ji%NUnAH0Eu5L zrTI&OVh>LGp;A(39u+1(tRHoI##JKr4_k6j)EEQ4Wa>2h5&}qk3rM_T6m}^9^P1>i z2#XdE>fhw5O=07PsW5h{Y#)`twVY^>)I#1UMZ1-fp(rpfJnnDz(?FaWm>YjtJ`X9Y z`mJ8*&*clR&<;$EvmTf99;??#G#LQTJEhJ$grb13zbonbESYSExk5Msad_{D=9nXGS_Xz36r zJvL@1+I-(rC;TuHz0pHu13un`iLLhW{nn(y z6xC^C_H2CE4R4*2?r1jJ>t%4F%G+75%j-3?r}okhuAH>W+gonJQNZ4WAtT$=CM+atW_ z;g1Rb_CH?^*Isi~;m;hD?z!uZaMn4$4g1KpnZx#3+qK1fFE~~B9Q?rA%+wa^M&iwIJ zVgEyR4u>AGa~v z1$(1;^gI#8S+=u)I0s2{Gy;UgR1`_(-IcP)haTvRpZb6w%GpYzDy#78pvf%qiQYt4 zyRainwJF-B3yD8EOgd$w|I}|}J3;H6KK)N>tpI+UAePB0_A&{|u~I%NfjQn;u8DSt z5A7*kJ}Uht#Q)z(4-C&be&2B2?Hj@#tMhk%ivk#p{ieUAKpZx5{mNhIH~axlMGr*L zkS-9aM@Yht$0aVm;^uJ2T_d_mzIx>%<&Fnb$^sH`V!`H%xNHp;kVi4_P=9Q%-p$v` zQ670TnDl^VmvYU8-g076PH_-9zOz-->Z_HYE>AgshAWRlBBhSy!ZTmC(<+@Z{pdYX zHMiE;|HOPiYM0NxCt@KE`;slfB{}w2E?c%#|L{XXcniw}wy^Y&o^;glDs|V$hVa^t zygzK-yeV8OlvBU@+3@w(yiMB<-)Fx$560E7oM*1O>hf@>>?c2D-~ILBPNjG7feSKH zSAjSL8Ycy)ZBssOF}^!ZJ(??x)0XFoP93S?~6OAUb4MsQ%Db? zn1mut0R=?TehHrbNxy?)y_+5hvL zbDp_#@B6-A+5I+z+5PU!ob#MA_0FC57ERlfH$$NY`YoH)#zhs%VIGKXx*%gj#G-@X zq>@2!$cr20pnrn~VnIF*&vlI(nhtdxOI`bpro;G{kOwNJE*WUmPl}eAXv(rHPY9NO=`Q%q00M zH52h`H9!2s^Q6UQD)g&ABRYI3C#5<~Chn?`y?2+U!n(Xq{2N!wy9(1GEgpeW5Cm3{Ze!Cp@|C@)SL^5PjGY;pkAn8PIo*|f<=e&WHT0d@ z-GU}h(8U*9;r438gM2mO!LAMp+mPY^mu~%G`s(r< z(<*$3hYN9qa%`=5bgjrgk$Csc9qGosd(vX;k}sRLpsf$5yb~^Mc`$ZY@d9(K^MvjF zRB^@aIn_sVm>XcP(cgNua(W2WM0189-TXK8%yDvIgaiJ=&6vvw#GK080F93Mj}R(k zk}*O=ros;f3pxZ$KjV6EO0)wFlz~|zMZLvVCt;k4-5JmH#09z82Ym#d=^A0LMm(Ml?8dVp`?w(%Sd;UE_qQr7)*i@g-FDcr zL-4iG33$a~Gq|?WKS#)l`>kA4THeTmx*kfYn9?In932eXw(n27zSWhMT|7N4J$JIv z@&ORmlf6v^E#tpJGf#w{Yh78 z&KKr))ej6Jy%3a_q|+@Q7qP~`+l16VN?bN`nu-2hjF$@<^HwEi9GX%yK zYIf1J0?&r8g%VF4v`>umF^}w!!FV;|gfVF(Uyay>9XGrh(J*8(A%B=nP? z*Q{RThvMxgd^O@xJoMS?ML9abM?QOPc4ZjuAHV#ol^ z^OH*O9{CjyJ(zZ4m;3_kl8+lz|K+XPsl_S0-no14av$W%{#W+T-WzLJAsAu{deKqyA{7sUozc(6(F6>!#Kae zjOxNP=`A6z0mP+#rcol36Fp+9XQYdxag**G?m2sEx$McD{OHy&B05D{WqWQ(EL#j7JN zlgCA$`_bB=E3`L_WQqe zcnIBGgz~=fA>&8kO%B8G&Q$D_<3mCQFiZd?13e#k^9$3WX=Bq@mak6#_T}5isR#3; zB6(I0FlK<>rhaDxy%q0z`6bw{p5=mO;1ba-~q8hR3@ufS77mT z64W({Q$bBng%^W(b~Bgbf}9u)N-#_NMcQ`>e7C^&b3$X;Uu90l5ZCRIK&BKcCanzK zuS?45q&0TUY|#YLnfW3#W4cMXBGkr88-4-x;)cARGJMkZF&kr8b4N`uGOEOOJkr4L zRFfvsjXW1t`3*k6Sw3Q!6JPn3xSf7DWQ%ev%#Y#$%!9b6yyH%J8~Mq2i1Sj1lzfzx%LVwIS%Y@4CI03RLX{GQvDf+L z&3n@B%?Hv_Jlwfx=>$_De6xHdUFQqN&ET3zch5{uz`R|6K*AR7h?^Qj5uNh-rLAU9 zIvWniUNtUIEMSdpSZQV^#sf&A@@85umM9Gar;z0;qhEurS}jJ;_QB60`k!4*S&xrm zd+!gns-nW%=xB$h=bgN_D=6qNzUwqU=ui0Jg)Z#mW^jB9OtmBt<^yW18eI&;w27bm zyxh#X)2tsB%i|IczA?xwj1-WXT{B)ChTsE2<0pqd2puTIw^g zN>p_cH2Gv%84^)3+>v0V>XcI&yR|YF+A_bO)sk|vMl3SD;B$QPA45yrQ(4ad-}`;| z6L0QOohw!e^G=Zn&A12uxJy1^{Dd@V%2fPvr8gQBO4`VGVe$TNH~eq9<>3_uyky>@ zbo0mmEzO=YuMmu68~GaO4DyfTYo!mY_;I@BzPnRbr+ej__LWy$bGCD)*>5tkhyJJb z>};;Rf=aHN0d7vz<+Goi4QAJSm4F^vpg)RbmU%8jX)3r(5F9a);Xmd|&R=9_8k54h zpvy7M{}xd{|ER~)WbmH_OB=^%PBlT(VFx4fK)UM?$zm$X^uU>N7jaWT2aaVP80T93 z665vbyvjSI+-+ARqvi)q=2CqAV~IMZw$b zE*hQ(U5OPKUiDPhHQ^5_y!z_okIXro3Mv)t(itxS#QvX#Akn5A2DL-5k!Hs;JUamk z!8k%dmF*PsYJCV6Vy!8bDSDwWJm{)eE04DapPiq8R+Pf4^8_nM~>}Qt)jhzZ@7@nh91Fk zT%#-F?NF63c6qYUX(nW*otm;Ki7WH%@TW-Ajz>@8oWO}~7xyIF7k<@8yO-jL!{?IL zxO>S~Zr&;H#qBG5aBudT{_1_E$%(nO(pTVRGBDPWS}nJL&FiXw3*FByWDOK0q*kz*9ZB-|mUf4~{F#CyMi3UfK%5NDA#5NwY@)3T>dOY;`s%e#dL>x&i0mg8(X^z0Y*fUEI z)O_uEw-@R{dl}I&U%PTC#&N@iIPIH!2|bN;n^nVUXe&ioo9x*ZEaQzs8vkPf>~UZW zqO@hGfIy3K92#wXD(@AG*CsYK;g4T&#Way=uB5se3ng+zI+a&k(#>BGOlJT>f8{^v z3P>9)u-3AXj{2!B#3>eR)4+7MVIZ#7@oFY~(R1?D@p$lr4~pPxpK%HJ+zV!|9 z5)GARmQ3LkOTh9tC=Ot*GtfItp;FPWaao!4i9xaFp3q}ib}q;n6hEPkgt2_$p&!wg z6`!gy2g7lW%w4Ed+)m^Eq~y? zbmUmP4T=s%y_v6eraiM>pQ^c2ZP&7Ymi@QiOUZ&+;W#d0klF&ovzkME=aSe8X1?Iu z!}GJBqlD zNK+^Oxsb8RWyny^`Wi2Y@c{|x1 z4mi)#Zh1OvfKNHwuRG)BAP+2D&d|5QbtJvwm>{qWfh#BCLX-=iY6lWmH;_MxE2#nH zTT@DPMLOFAt=3WKM)psRcT?M?KWG)|;-Ix%hJ2M@fQn>QG14!l;&3>Zzwk$8DWOdB zFTwv@34R}~{B;J2q1`fODUpU)H+x8twcdgx#)^6F0 zyc=|v+)(ZHhQ@Y`u%^UF*|BWO_4FR^<~!pztlyb-?>?B$Sv)Px!9DPfXQA7NTs=@} z)nriNo3V71zvKub9%8vilBGF#iR0BY>>z8g*rke67JLQs6Uv-jfQqEB{Sx<9Z*Or; zr^>EC9c8|W?L%HQ)CUoRvcFTdUEEh)CB8Pdl#CwIHiw|m+r{Nh`7wN1^Qnu@F>Sx_ zw5!tcJC|q8OYm-Q{ZzYQ*N#kovL-fdT%Y#r*=@VzbEZu1(am=4)uRvq<(TgcUYI<$ z7wsZmcX8=wcewuK**NG_A{n&d002M$NkleV2NUMKx_{HQ>xZ!gaCo2te!vSHx zGGd!K_s@`*URUPhiD}78IY(Oy5lr*Pz@S$?CTD?L&cdj&+sJrv(+q3JZ4ESx}9(%%0K z@__tKOJ?xD7YlJ(vB3fX?*Og(iLoHR1TlAzQJn|f_0a_<*wr)?SS}jga)A%+(fR_4 z3Xt`*CYbyN&9Vhv>>NizdAq2K)gUH-A($Bc_}cH|!v%OHB5z;e+gE4>J|FznJ1za~ zd)BAl{)?}WNRH7|-r7Q(9Ao2PJwIX+jSfERlj*2G>7ghYxjj1Iv!1}`dJ-OhgZK#2 z9e1ru+h$En=PjIu*TxmyTJ2`?uo!ZTTdTaPr-0|kc8QQ<%)#nG6RK6RljN8BX|S{) z55X$Rc#xQG)KmOw*CW0HJPA`OW2g8Db~%sP=v>YG6YP$SJ=)|LY!@352QJEw;NI(X zJGR?Le}3L#K^H6r?K6w&{@LI>a?zt;%n>jn3 zJAYvsiPt`M3S~PrPOF_njNUTlyG2*bvOUU$LxpIcI#oa0YwI1=dQMqTifMx5h86P~ zf0`4OCyycKEG{ICW%C?&0)4*A_W(BSr_{W^DoPkEYg6Pna$qzFwvBp^Kyc0a(7 zLWt83W{t;~lO%Hf<|!nze6&RHy*|K4kgVt8n2C|ddVEn>$p;_x?J4@l6#YQpGh&{w zBR6Ka_#TQMriV=*kw%Z_gPupz!To;j4NwtDZ`jG4Xvdsy<%{LsxE^}tHRS$PQcB65 zqjEXmwIg(__@Z>BBWd}~kEU&#C#Gc=PE8~6<8DfQZ%tjp{XB1$Nby(T#pbMDJ8Wg ztJj)p4~}bsD1~xbIK>s-nn7gZl`iVScFLhYU%jId5P}ZZlb`O zg0kExZP>6Y?cC9o7A>5T7R|y_-w2c_Yo%`AAbv2_T4XEkVMjdQPg&sk-gCaA7o?60 z@WQN@D+sIx6|uyW>5|_Yd@0*Q+WIIbuKo_C&z*GF8VR5>-$~{a>AhLVahfNtU3`jW zJBsyYp{F_RvBw@uZ~x3crZ4>Adu$i{lW%-$O8hIrUF#l6ANh|@6_h7`lDp(PckN7z z@J{kYSeP@ktN*@~gkz~V@AoLG{$IvD3Kf9f^s54HW4$l_m9p*9DsO!f@xfEnVTWtl5=p7MGu6-PSj>BI|7Ff6F8rzMVaFFkNSqytES zL9%I#@=-n?%~kmY?Y=$&Mb+Uz9bft)*vMDy8mrY12+QT0;A2-ZQM5u8BE`rQ{lm_RuCg_d@_ zG;9gmtXG1vUzYJKXfEQq&4$S~@yxrNyHVhs(a@i8FZt1QAMPW+@$N^{ZoJ~r#Hj+o z`v(#w(Kx6GQ;nFFlK&jbTcezB;NUY|Vwnbv^-5ic%$`xZIhb7v#$Pv3V+j|rs;wun z-tg7Ny}|Z7MwMlQH?*^iKcS%0EMrde42pjeF7d>~!1$h3E7D6o`0m8l7?$BM?!W%P zmp+%C|IRn3V@LQAu%E8Ld&z(N;Qi^wd+)^9-CwJ3wMSn|6xG%%v9kZGzI!EjilF`+ zyL2JzW5O~qNsz~XRn|TBZcV2DDZ;tJNcp@$w$E#>8eEgD1bxxxbDr6PF27~UfYOXO zk|I0Bk6B%eG#F!>xZf^dkf0gLnQ+H(%sMg*VO?PDN}0lz=_&G-6d*HAtNRf8rNp4- zN2Ncj%kr!<{MpIC>$IHSh7QGpo^3w(Lp`oSirkNi&J;t%FH2ow80u2mWoTZvRG5NqSGptt%5 z&&?q82GgSI0By_!Gh(9z*>4?Udod{XoJrwO!^@CEPWyNUcE-!@<|h!lM3JjOVz$1M zjXbYvz^`I7MqK5%II6+{KI-#wheBZ>E=Ju9iA+(Be5Y1%K_Z)#+ett93{qj4Ug$TR zt92)P?+-f{JEO&{hxl@1t{=K%bomwj*yS4ygK7Ror4WWZ=ZBN_+yy%vc#jQCXW`>N z3t#`qbnF7{qDPW97qcmOijxc;0t1fsH7NMeQ@{3h*dp>x(;%&%MrUBr9M`t4`e3l1Dc$c z`_XECMSFB7$NLd;aC~jT%DlcO{Pr8RL8m}xonPyls6@|WX91ln_t~IJo9N0n{ z!CPnbIbp@x)#;Ug`JOZ!zkSb`G9?|t?JFDbrjq^p_N618UL6?n*o-D=&-sB~k7&_Rs)`oMY#!A2F_n?&Y(b`a@l~>TeBtet#zpN&>zZD*G!3!7VFoMJA~Pw^h(q%dF*P6>|z&jID*n&xdo0(tga=1eacIhY5? zdH>W7qSzb_)?guD(UGF4*@J&}a>D$|Ja@pe9~1{L@>!m;2o^<}x!O*8raP8?G)Y#T zOk}y^c0|}K-vBqz=w44JQBUSC)Mj}mih7oztlE5K{Q(F+?z?+ix?u5VOr8%zj>O0K zc-y7da4F@!JH_SI8qTR7g%j1>vP`AZ+9UuKRT7M=daC4|Fa1^y6*5FOZ{3@ATz?=f z!k0VGxnQc5GgvvI<~*KI57bXJO+Q4>f`mmsU@W}+tK*@!=!hZ4YR|@;U7b@+-i%c= z*|Yv=yy{@QDtlU~eqW47w+yFhRphiNtp*EE;tU?4X^p`+;9Ae*S{K8lyrJ{rlZsr< zT8U}#jE;k~gfSd!^pFvTSZ4&0pbLKUGV|`F^<>&mo=BT4oU{YiK; z88idPuHuphfXksiZ9%R^07FUq`p_lRLsG-;7V0A8zZO)?BA5PLtm>ti15bH{)LNIXW3yV%1lJpUtQ< zWG180jEh`AK^PJ|{Z}W`55J z(*Q%RDfH#JQbsX8!SWqI#QDAXF>VtU;tT@^g&rK54R6-qyHyBB^Q$9u^id|+$ZvH4(H z`jn|@%58ABQJAu(+dG#!#d=1-=aAFEFfX&+)tfTtu1*W0Hkoa zy&C;p>S%{S8$EJN{yF!wkNvwCZ*fip#VAzNf=^~qNEX8&!g*|Ujr!4~sYWxpU6eu# z`Pebe!R8I>1uZv5d;N}~WE(um3g$C2)XBIFo^%zb#1k%jEjC8zGo>IXMyj-v6$|3H zyh9GVRu8Bco?Ke%$tneIRIy#W+vGx8o(;1L!-ow?{OjsEY!7;#I8I!YyTu={7|L>Y z@IONguJR&g(mFlUsz`!Jk2u#)XFT;=|DaspckSAbowogH!ThOsz;jAE3%8Qgam02A zrl;bS{^ygFPFAc(+qtglvwHhawF^~gL(23MkNUOA+uNgM2VFv?KN&CPsmJw2N%wks zgRPHQ^~v^e_ttvBE$gHcTz#lFaVH(Uq_fvTH} z6H1Y_U2bN{3Lv8#AYbiB^H;!X+){w`!kaE!yrJKec+DBW53ZWF&7SxBiWruu%GB`y zApHoIjT8A1Z!6&(ncrs}=ROt47i^NIl6u-|i26dm@dIL3EiD2laZ+);utIyWGwy!) zPP%Qf)P8Yr1FCa*VRl(Qs}I$xdp`mXL5^57IPKqkICbqhiqFzxu@nB+VWM0L@mL{+r=q{1{dPg$u$gv=_wMbIHOZ< z;A`|Yc#^V%S!vJuV`tx6*_6aVIuwWgPI$q}g(>|h@G628Db|10WI_FyG6(tME}Y2q zl$!wyI;N?^3Mc(mfB|XLs1f!h&jVdYY^VGr-d+@cw{A6d$#-Jx&QG)P z8pJ1}C~60mTJ6=F8qgHh8*G1LtV?U0<>ror`n%=~j#ZxiXGK~*db@gJLjc;#vzYEixd&-dIzXehn+DKhNqEZho$|yj^M*V zhpiUmf@`Sfs`{5b=U0~;-cH`C)FAorqfqV*gV^0{9Pyb{e`SQNkQosBu?J&(h z$oo!r;g8`_NmftlR7NZgzL@k|RfyrDFJcpPJ}1AiV@CK+`GG^Yg+)J~L8SDjtbC(4 z{Q_1oTW)ooiuE3xd#UioUY3M+a==~c>Sqh3KXs5%){?WFc8<3U&`j2$~R z4I7R{Ic{CyLy|wO;oyON>F#^)PTS7jnU=MA4Pv|g(VMcC_0=1^FgA8M_oCf7hcoU? zXtmc%!ETgOgWWTZO)EQxgg>^2MJDsHW1AO+pvyAYNMl?XP*gX_+)3M=E$}78(vEUN zNXJ~LdKrkdfK7Ym7k4b-1z~R3EVwluO<|6l?lc}*4uxvn7BaywI!U3w{1w^m{gngc z3&uIz@`WIoXjo~t`oX!8zYVZJq+9zFrywx$p+3tyq?*5&C*04p9$P<#B&`m@I)D6> zVQIve!Rf%3Bk9nIconfJDhye^Iyk4!XU?@cj%ig;h4hsB?LC4XGB0^bJ1Y81SNSq^w4GP&Hl{19VBylasMyIRT_(Nu-Xt2S@t>xEo{UR-U=roa7wb^ zOiPY>Y%wne@(c~U4<3+>j$G&$%;_#_N9iv}mC{1L_`*w}++mE+)Xz?1beo09XC8m@ zE9g}@q}as5l?tD6g2|tN+bCzc>|0W0YSbVRTLY1SJbgRqSN#<6`^m>*F^*6C;`SB3 ztK99IqQFN5fMkLGFx|F3FdeA9JK%HqOc_8;ya2t`AN9Y1pY`2&FfGKxoeSeNh%}Mg z2RJtsjy3Dm{9&E3V)5%jQGMmf!eBh)@-b7$1)al0KV=e8!{=2#+r+qeSm|ossi$|$ zVqCc!;}@>3 zbo^N3e)2QyYk#WWylF$)g0BG- ztZsJ=ExA0@1DfHeIXp*!49TmEJV%2KqSMWbHV5O{e8!X(#REgX>L<4c%E>pc44pSJ z9Xf#f$+sR!$LxM`*HanD6=L?iR(M|VzuXR4K=M%^RRaV<^zgUfi{e1(p(Uo}hs(0- zfDSIm(T47Y1Ix7h6F~jQF?D1cGrcDD|6*LGts+z#5wo&vN0^m{3bV@d*xQd+8_Y`y z5?;GbkUHjUaVdi05)WN19|RK4W169Qb^ss9MI8RM7^Wy4Ed6p&b(9%UOuyi{lU7ib zn-o{e^6P4ioh0ffL@b}hWk?3BMAVcK?H1u;vIDjRsU*XG&h*75cLD6)AiRcU`0$~& zOK!KW;LR(K;Xz3YRwpJ1S;W?mNN7?6DMMVqjt1_4cRds&)DA^lmYaOnpPQ!#{2^Ra z@HL3r@PH@ZxH5jsaI0_E#AB=eipP|8#67O_8o6-Y`vdUE`@^3NoU+8L8FM?iTwcuU z=Vr;AX3R0rDYP9Uz>}_^F{J$I#O&wYg>vrD7-`1c$@iJpy*u7kYO`i6W1=g68gX}R zMYWyUYs7H;!ezVU2l2r#+)w`4Pv`yQ+~I#<#r?K$qPot`zdN6Na|P=fX>Uq2U`{|Tnoh4cS5G{FMzcgk_f$=fEx5V1LR>;MJ}T ze4`>S!x0M~>@qSIU{zP7I+ydSQ>KXuT@+SQcAEOd54vEA%f4jF>}nlCz&j7q1``yp z{~mjlp?;7x2K6%?c5=C`P6~S7egKW3m@}ME9s#z8JPRQ$_&9kp#FON-H+uANeC-lf z>iCN1LF|-U$o_b?@{4);UpmgR3a8i#Sd`1<@>a0$0k7+ag{U9d$@)9sN#3?)U)qCv z$>+?Ql9tYyh=)vy^1DJwhhOBgB0X-pRDp|>`Nf?S$gwUtk#{E;_}m^X*uuxc%$Z7R zj3}m^Ja)rqGk>OhpvQlpKuiO#MKRd6j=4nF}~j7x5;e`UwS^a*3e` z=JDXTPA>A83S#Y4=ZMCUwHkp5RbWm={e-<_JIC(dn2ES`Wi-Czi3Rxqd{4szfDv4; zv@0gxTaqwQofd)?c{@3}iDVTu`$472wY2x6Dy@15UO~KLU)uSdt~7tijktAACJgF;4Od`m>=aiM+pq_Trzl(8}kbkV4 zD6Dx{q*V!|+m!}+xxxZif=q!Q?4DX)frfroo^=ey$Z#RelXKPRQXG|8=*r13$$V{M zHoBOK4U|RYxbI}cF8nh4Wy~An=x;OhLcdo7fN_C94?RT8h<{Pce9v*9sSTAM$ zwDZRlqi#(#C+9{mx-UL`5$gOwn9#>zM|n8zz2<_PuSB$9_-6g1hK+6qz!WQ4;>kn4 zTfA9vTvSB&`djhRf9&{U>ETsd(w5D8(!4p7aO=u=HxK((kD=e!;HV#~qNt~K)x~?htbSF^F)An?h<2Q{H6uF%KQAypDYX>I!~stjJQq7fG@AMU*4-t4?b% zPiDI4lVB}oIoU^AgD=WE^If8qUmvsDF;UxjENf~T<(qMhPg9Lh<2QKL)#Fz~W3?L! z6=k54KMF0xYG;MjSvLF;SQ_V}P5J%om` zxD^*!e@g}Dk(e_GAw46>W3Dcr+2FDNO26%F^W291SpC`^ zEwRM#gXdoSQ=YM-JeItzWk6zVe4fKSV@+ab031sF1F?D=HwEWje8rQut{i^6y7f#r zYGaU9X>gjXmsEqtp(7%OC_m?KAjIll3DY+Ni=3#qQv?X_wQny zO|Fx9RqHi}#Lo3AoW(fg3a*LeOg>o$-uo-Mgw-m|heFm7lR?i4nu&m0FE@MuYIJ1e zS3$QEqwK~>(mE$F-boQRPM$60Hgj$C%g?s{(}^gc{E4`E53$t;cmv{=om@ZC-;AZt z`j{4bh%=qEA_RTt5Zu!?aZEaNh`Z#tr|nq${N3BAr@m0?h zS6cR~e1$J@HIqd~as{cD?2+G?D{`}j8_l_-rkt6&yDjWa{2{oXe8PkY=`e0z>Gpo| z(`^5IZfNz)DW|FbFfJxmtb8DC#z%t|%$bvBPQnLA5ZKS`y|JtcJB_=V=iGc`K;cQzUaO!dKb zuym9&=+ZxQ_*rS#yiw`kemvy4X1V+fX$eT}1qQHHR`*|_$S2HY3b+wF&QKiAZc4aiT zb7eDU86*3F1-9w5q6u$x;dt8tjs_v#uWr4`xhONgBF>6RQ_ZyMIXex2VZtAcO)4YZ z$${}kwR*s$V7nh`WQz;DD-9)x7z#VlWt?6y1#-Gv%Z>!x^dJr2tbQ=zSb*C^HEq)MNdG z=?d?3hd8Kvd@hIiWd{k<092SED)FFf1HXIsfwX&fSK2UbTv~*k@`>YzOTuGTLlMgU zRhBnH$m1+(X-N!b1GF4X6Y?vQmJ`)jj*))DP<DAh=|AQX61jjB(~u>)N*e?RhkCh{Ga~KU+{)PtHN&|HJLg=l=kmr*ItW&8ZcZ9K z=l+pqf==YoVV;NlNjpbWM5R2g?n&o4#r?odIFR#u)gJ?Skkykepv(Y-rKfE@Ro+y7 zh?W=X!Lo?u^q_tcSt~#1Zu5_Rfx(&FZhWB=6x6wzLQJ>#7v2Va1a`#AN?ZW)C>(;I`W6ij< zaM|QEVG_^1Zi#&>he%rg0w2|gfP?=xm`kz0Yt0gUUr+-0i(}D$O)w5F9~tTg!Pa!Z z)1a9w1x_Q~&x>(%IXa1;t9^FDG6dTyF3RIhN7du&+fa`(v~JcBku1^&9GCSKyZ+83yW$n&>RnMbuC7 z&bYxid3hrDF#&^b@|d46M;a&ZL`DtR+$=1odeAZPR__BR541RSRCB!Sm;@l>n_-L2!MS(TkT4K_M1N8JsXn}(U?J)w zw_|NVZYOOoE!xpbEU)Mv)2fFs8*Hs@n{1W-RO5bLjAKgT)o+_})E#tyUEM~#dcmUb zK9XESe9K8OVuKcVvKV4s@eC`O1mT@pJBA9<*r?T2Jo6|I_*D$EP{+aTfe9**ekoHN zYzI3|c%uoYIPg-ND^dnK_`@}usgPiWcY2}U?BZbc#QGsmcn}S0Oo3bibnV3I=O5OC z*GH>g)We6V#*H10cO2vC@BN3<(WA#>J~boIt60gV|Dp@0SoBuBcOBsr2i_bet&lSa z4vN8T`o-FK^wAw@8y4lWu_#|MZz6^@bwuFie=QXHgPyP6X~=AeQ3@7vhOs+AVM#GW zOIeAwqlwptSPReIh}6akoa!#$8-49C-Z$HET4{cY@kx|&-Q`=Qdq;V))laP-^;CqS zYnfbkdFUE+7Vak>H!j{!zCRs1Qr(Z*lc^^aAANJ<27Ffli}Kks=cFZb=H}hv-uhYg zr{2mCm(L}|_~?s#U$C44xFgOTX`7Ton9a=gw)Jf>PuPw%CN@op9{V`50$*=uD6o+5?1vjoX5U;Tl?ttwB^y=Y3}05X%TK;8Hi5e{mDI&Hp-Y^ zIH;8Dr~WJl<4U_dtV|vqDj5Nxd^9uS_(i4GpCLwU8`)6N=cr#7<5s1rZHsX*XGJXi z3z}FKkm0V3;0`bdC6m?nQIKsHU(V zg9F4ozzctd4IP|@O~ULb2Bw zv}gxcP}v9X2red8J-ihUitS7D=1xrW&c;KYHb6@yO~LRa?AonDMhZI;mDz@0-pp9~ zYvOLfLz!qYeG0L+L*E4RSW($J#C_9xYG|MKy&~8P89K$MW}$CVtj{M?{b+izQmDUl zii@HS9Xb^6IG${`t{lQwJRi5)&I-U!N%Hv7qiOZ(RcR~kC!afOPMSC4>|T3b_9LC- zdm~)7|EY|x-q@&NVy88q6gY=-HxN7D*3@cmn=7kdL3OXQ%E^VMg*eCX+(b&=3Dz8C zbj;zv8DI1)A5)Xf*-!vbXs*;V6+|w?9peO@+w7bIQoxXil$C=DXrCzeBO`$`q{@-~ zh^-VEDC&vY7`P17EiC%ASZ|xj?P`lcg5d~#l791sTI6wi|D=c3h|TwmL(C+d>};m- zgNM<13O0=8tht%8dP2;=Jll&O))_u`blT5XA>u2Zd=;Wg3FWcVN`occxvt+dhYo2{ z++N;GPkj|%T3oWTJf&EgqwxG8zF=Y1-ZbwilhfRVe8ED8{*uJ z)bBc0E6B4^)F8B}$pI|y2Tgy1Uic-1WbvipIerOpU}wtGO9?GfIU54-?bw(8H2gw6Rj-Vae(1FR$aXLXDN}nR7yZ%ykUE&&f z-nueu*ihVdb|4-3Y5t05*M56H`KE1~?SAs<6DN19Okdo2|L6!R7;3LFF4#{<(wZ3a zcGY}7cR1afm5$i`%GS={BPPBIB!FUk9QdCALQS!cXX0(lIV6i~M8(M}(;aU@4SQz7 z=RVqW!-yhiMR-5?_%Xx4Y~TscPLM|h1tupwk+UX;Lo<{B@|yM0#Q%iyDzjp`k^4fk zPM-iDM!khOc z(+qOaskt2lv@LNq?U5htoBIF>)(;fhnFqH>*qMg|nNp^}QZYBH%#H>>JK&@Xv7eXhgXrK1`ipSCe6n8 z1r7|xL!O86O2Ihy0+jWb@?6_NueeO9HL@QKX|E0Ex}=WML;bF)L{$}SwP}{bmj=({ z+>7rc>|A~zoxN_nJ>)qK9}TkeG6B&aS)X2YY6(>!7duSHqEYRb%K<5#<3pN$nYX~1 zABBFJ>#xwyi*Xd*w~nnnSQx9;s1z#@2)XmIXdECU5U9czgGMATaDlgaG@(kaP?7Oe z0R83`D$&g;4)YGi(LD6H^rv(}CQT-nFf@p%7~~?^)L-pv7yh7Jwfdm~bd1vPtdzR> zC%2KX8s{u)0nA!Gf!M$s;W5YSi;%T@lp$hV%H-W65`8MhDBFEW*bItjLy=mWKEtJ0Nh3W=(7wkj5` zmE--O8@`FO;d6n*@>$GZqi}-B`JiWW^uJsG>US=;lyp4@(?`VqY_SxdI?>249k=fj=cB|m^&^4bT3`cQv`+A8@I zkl(&_3ofX)r?bzVl@{UlmEps9LERyQf2s$@wa|U@TKoFF$TcbJ&vi=Qb2NSOaqY#j zA9#1>dd#{N9?aL=k+nleC19^oKdz`5={};o?CF_L`J6QmY#abw(SvX2I@?LEk6*ss zgig30Jh}jnL1YS{LMTnD*5Af;NXDApuum+5>*YxnT7YlXQ`_ky>vbKpZ}njH;WU{s zD4k-(Aa;A6jUqq*^s3@af&8$zQBMSz9y^yaQ|UK*b}%(neL$e>pYnsY`PP+TBe5Xg zi4O)H@{jcuDqN8rS=s9;bz72w%az3Bt?vRgh5I5PE@@)5kxbVu;EeVZKm7EOc3vc~KC16|iQ|Pp6 zI7Rhn8l8-+c9FM&dXrIUOJ^4$@HRqnlTjdCQJID3dXODD^arYr7lvwV{YXEkh<+*# zNzb}Yl1jTaLIO~u180LObmWMj20nOtyNmRDaSDI5&XumvuVR}k6jr(AkK(33QSN+i zbCf&VS3>@U9)n@=?f1z+N!H(RFw>!+tS9-O1!W*&R1dG*$BrFgi*nw&!h5oP5(7r% zkbVn#JWsQJTTc$M9Pt^_&->svtY24q$g^SDvteKLz_?uSG^e_zj;eiP8q0w0 z+a<%gNl?Q8eNodkP1rBFdcz!!g}CwQKNFm57$WN#WMhtV5UF`ma)|0*xO^_rqLa!I zfHSE0VUXjOh+Kb`BOxqOF9BIU(hQs+jqX`yn5{w-NW?2?I%7vH+K-hPcXH-aDaEdO#nQ41lmt5a|OHCyuXNI)}* zYJ?vZ$^8g*g9Z)21D+$&@IzRXZ$BCjct$${j^rV8^tmxh$b%<2@Q|VV&e>1UUlR`mOaQs9q&~UMYgL% z*IU_<$-oy6Ct5u>{ec~`Prv9729d?#r}<~}u!%JPyB|!`PPEC9@8ImWOe8qqv7l)P{O^EnP3i~ zjXz?M9qFM%@xh=WqwO_JUHcEE{CuY{QX>Jzl(pHE>8iZep2)f@VIa$iQQ=*WeCY+A zJ|WcftG~jJ!-tRJ!y{YL#!Y+EY<$IY!OZcw`ExhO{?K=7qd!bXYo+LgHQ zjy>(8ir-FA)0Fb<;&$?Vq}3KVZDea>qFG0cd>ipoBKg?uov6)qXw!Ca?=Zy)O z4c*mOiR){6*bw}}H31KJ;%8$n%DFrH(;AK)Ig%cErFJYEGep>_=Rl>fGmAWZeWjlaox)0t3=Puc*+( zxy$I8dc5q1hSRLqgZIJnfF{qThIzl9%8=8@bqYy@7eF!!u#VeT%ieU)!`tBR ze%D|4I(z!~G-+JpUB8WTvkdzW9!{a089Oem*tkADv|(LZkHy*JZ5Elcrq4)|fom7G z@7;qju+1iLZ+BJ`cB8PHJP!-K-h_u?{{ZgIE@ckmtspDbKGGsSV)%%(bk01Z@4=#Y z?Zyo?jOuC8oVjV#aDU)<+wPs_V+|OYW%C!}0(oc)>#@g9q|G~bq@5^VIk-lT8kMF^ znVKe#AD<2##hV+t4%#Q`O9f})OP|lU^oq0uuNa(&df2pUXIi;_ZMyxQJD*fL&HE>+eY) z{nE{8H16~IuebIrbKm-%>23e|{q)GjUG~7_f4%KRrk#FV{FYCr{djP)Y=^;vp!e-R zhsC&0QZM<7ucv7fMx}rH?PrXU+gVWo8<;wIM*PJ7Aj%nw!KX@qp z?SC#$2l&CCm|TDDdFQ5Izh-I1b(fg;nrqWeUKHQ~&*%KilJw8N|C}^qLVbdx{F<%% z)9e5CJL#6?t4$qDBdC!S2gy4OGF89G?cAN5@X=qhny_PihAJ6MPo@SHz(;&}S^8^4$S z=8ONH4ju7(GcR7cEdBEv{vczzOT7B;{yg1!|GlY{VHscim+wjsuBk7``KrGEdfR(V z-Pi88IsM^3ezZk3?}z@<+uxr?4DtJ-Z^NtpUiFvnZo!f8(bxZ8nm?u3ENGzi;?wgV z{Oae^mwxcSHusYK)N?OL|MZ5p81g$mzB|3-9lw{Qp7r#r(m(yqThom36EjVTHQTqO z-~9W(O1Is)T%;#O%nu%|TD203@{Q^2>1Sh6J~Is(WN!|r`Nuh@niFcQZZo#C>AOnZ z;(E)pon+eqHQzL%?&{(Z_W7JXP;%~Q3Brd=9|)YzP;A4v^#J9g!J#9d$#3QuQ`xj^!%@;OD>&~ zKK`+17_Fzn>t6T0bjyt!)2n~=ob;O4Eb11%Zta2eS0B7H{qVbw)^H0JjZYu^>t|pg z-g=wk!LH-!6CeL^`t&F74VjbaqRXeIkNn@OEPvb0+tP2n`kU$Z|LBtRGuOFty` zbm&BS`m)cYi!Yy+{`w!T?gXpRUwZYI(@i(R>yuXqpDlY}l-6}!_KIK%o zEI4H)l^}|!T4l@;V!<*bIL*kxi^$}Z4yl?bGFJ7Wa{vYw^>a{CuU|z+95naEm6BxS zQg?T0SL+Dr%l^AEzXuH8IYR?~;&)$^7vkJieh53n+x8tYc}^I=borw6;nzPm`~l&yzvt1N*^g(OKNsn!43-h;i_e*n7vl8!fk(EHQ4d9Z zOd3Bbef536*mfaKA(r>ou;~8BxBRT_=Ch5wo$4yP%kfK>lkn@pv!1ymef3X%J}<;K z?mC$6#|HooW0#*{{bGt1!Q$k-VPX*=TuE$ebTzl65pp)Ki#n^)skhTZtoI7c$DrU><8X;MO- zQC5``8E;IGGv3WSd5=+CbFe5*+x!zy3SGSOD}f*T-M6I=zxub@F2w1|g>&YoZ@u@! z>1h{S)F;n|;Fl~e)<5@0@58R&sSJ3psIrYYjz0LSZ*03L?8&*SFS;x*#EHFP!Qyl_ z-X?M?VH|!%_~5HvpZ@R_uWc(neDIJoV&o{}U3$@_=_~L2)4UMhh#m3!*RR7E_8mAb z`oHn+52d9`mbKOOlO`Uw`BlydqV*=$4sbQldn45uqn%{N z_GeJ^JDVpE!BLxTY+|YURy7aH69|K^GxZ5YQpQpIrXDU z`mvq7XsT7k6`fU4X^DynilYwVgGVc|p?Je}kEBN)+?gIb5f`iOO7v@Jv3ihZ*Hmdn z3VdQzT^tT($VE>3*v~uT@W(EbF-Ytd1H{UktMJ|~KUj)J&?QHMoG(R42PR*!Sba*! zV-768&n)h3#;p{#fJcq#`uS(H(yPA z4;-?FyKvFj>9hamMQJ2*pPH7;@Sn!^V3GOm&)t&l#>a4u96O$_ zx^P~4-z%>$AFf<7E&bL@E=vFKr8`VhS>~_ZzAjyN>zdMnb#h`@?MIJ2W&Ss?zv0TaJ;?bEx&Y0dc*rS8IOa-ZF#?N=cz#q z*GEbY2JJn*roa68wydbE9Enifg3?M+K&&r84mb1zFzTf7Y8cUbz!ufHi>^R73igV+@=CvFP=(|5m_KKH$EnVw;| z9pk*Y3)7!sr+pOOJi^8N^Pl<*jJqEiUx!s&Hl+`L{qxGU#1CQN*RFlf#cdwH^6clQ z&;HMKxi7Q`w;}w>Gkw>uWWDstpGp7sD_&Lm$8?# zyMfXBny|TnQyk8{ngjK)npwcXrxfOta@(E@TT*0M=iAY?0v#i~^-nNqk8Hag0P<{q z?Kqa>8dH|FJ?AhaTv?Iwsg(3OKLeS7e(qIzJ&FSv{9m)0`a(|jy=$Kw6k zdv_jzKPS_rmrfPf;%7V!`FyT>JY9AfZvpji^UYh3pdbA6@W{ja(?|dAK4lSCJQ7#o zUS)33@t?w{{&i*gkI${N`r*aOEZhq6zV~0A&Yn3EU&$Php7zWcY2^bu%)jTqaDJMM zuVylQ^-F8g``>jFULX1x`q;qq@>ide-u2$gjQ4xL|CIEF&puGILx!w=@*nO?U;e@> z!}4&|vuCAWc*O$C-+jl9^qFfP$UJTeaUQIsQ(BD+fPeVgyRAKs;XHhBKfd3ISYgFI zxcx3NY^74Ro>q=%wwI2gQ#_%=OfrH?S2tV zon_dxV_$WZ2gaIByV8QGW7F%Og)3)-b7uNa!r!@PZF=u3t}xnj&!1&)FFA4qSKBBX zcNcyh3v`8V;~wm`AxxjYczSyI6+W;JA3u>^`lnw{8}LO+g|FSPGHu?~mA>#tFEG-} zp1L6Y!Kt? z#DM$q9(rh7+PHBqCgyRrDCeY`gQ>xx(f_hLu6IwGqZN8T5rrpZ`bzTt&0#Vuq1 zkugf}KJ87#Z7i!dtWP8G^~yhgqP>--f-jA^x_xJGRX_hpPYX9gAJzP>0EplVZr>-X*@pIwg>lcQ~qh#NoU{!_3pd2 zBSordCOPfgeK>vhf7ZG?Mrjd-fhnn>OsvS~fhoFI{`>==9$AU24MP#tlVv zxWpxwW0yR_H?HF^Fz6%Toj7qaeg4xArq}-Z(lmX>h%{opa;#?ccwCO|Er9_V40zAcsNof2IDij!a#p zIQ(#QHBYJMv0WLYO)=?ASA1z8nsn=r7Beq7S!k6fO1MLKcg)JwThsd0Sd`D7h(-B? zbk<;u8JWvS6SACHzzW=Av~vL!=(OYF?0?$2P#AQc$8iN^ahWD9{>V2BV-(5gjQK|& zirBJ|Cy#%c!x&RWu`vZwOc6dGjDNtEf36HTv#U6CQe1i!6Gt27bAbnK%ib>VJpA$% zOVZUB&d0sltXjvt?=#;`i+}eM`1{B7-#6ikmcF3)Wyrokzdf~UG2LU{-86l5A>+#q zeb%ipgz;fYhD}@cqz5F#X`ptZh3w6w2LKx%k_8+fL@BhN`^r5fZnZ5wb@S$x^RH|1x5%uIr zl@~1s%x2t{qVQ*LxGJ50-pn+d51PWi?OXSxG0*uM{QYe@=a)ZWFuLt^>9Og~`i<2l30os)x3w8*W;S31V0N z2ol?zMi^UvV7mAVtsY5jjo8-=^~YGTlCgA+f8FqDjq(H2$bw>i{JT!G!+lN zD(u8VuH#>Fwf&v)PiF*h9VBc;=WzWAN8b{_?}qS9oLnSTyz+rWx3eDWuqF;M;#^ z<+SmF#=d5OvixV~48$b!5a*DJPM-7KkIFw9@N-beTi(ou4Cwf0dM)KRnz`|&>Q)MI~s z@C@dEoA(*}|I7`kicUWMo(+jWuQo3mI zXYuzLTZmiTg&xNl*lfUzwWq}m41KUa^VBm|mw}@U`QBdOpxs7~ABtUje1$jpgQ14W zCG{LV^}kSLo|97j6TeQ8oMOt?(-od6*V%d$$~lfKb=7qq;_WMIaQn))R;5)x-ky#f zjYSy>Z%D@R$NtHB-~+EnntO~q!^G)_e%=|!b%U;^{ZazX@&sM1`Hc>el>{*#VJ<_y zLJOeu2`^Q))&<6DMoLRXN=py(C7ueAmuO@&L39&iCIGT8Jp8?5<)*X~Z_k)CdU%?O z`?Npx+b>85am&VyD>tVft=yPyeQ0A^y)jNmDxdUFKf+sI{&e>JmT=_war}&YTMOL@ zxKdO7z{M#ON2OO>={xY`-vYmc`#N@^mz_Pqvd_PGLAv42wN}*UT{1r~DShR3-l760 z%CbvC*jb9NO)7lno=5F|LFP>ayX<;r>!-hYufy7uiHv7oI2-rx;Ub{fKfwE+JJzK8 zuxrfl#hV^U?|k_smVV9EixU4m#=ZI*H}1h39LA@4v&Z2f z*+J&Eb@7_+My68JVDvFtue?^_24DLS0`fL72GJFZ-`WTj$5TBOqz8V(zue{$Yw#CN z=S8W9>+f0aZ7D(#-~Qlz=>!&1XAK;brjMJDxTDTnRa%1V1mgvl_^(mSuEFQmtDQmz z%%3_vz3rD?8|Zq(H@SS{<{RonAbmxi(FqhQZuhijbH^K)&pSA3w%wD%$ zi=XyReDRhargy&dm4<&K9(v^;?-~Eb-M8YFpu5tYKfV`laKMH}qkXagC7z`Gqj)%X zEgq=d6g%a7TMpku!`*0{zU4hmgYE05$8uL1x=%&><@0TxTe&mNAp4QS9OPrvCu!aD zt$xbc&1IZVag$Uw)a7Q;JO~0=zLSoWCa??)!F=wfAJmN_6QM_N|CJN05M#oU;Rgh= z80oh!SAWDWt5-}(9|Q56WfQQ8-P(D@6;spq|92f0$ki{fKl~v! zq#(nyS!Fb(x%0-PKYYh!uuuisqy3dD_oi=r-EZZpxOgrD_D`I+!2ILeVlKXTa{8I8 z{5g$Jf9j#M{3h&To00+P`fom(Uj5o7mcQuIN$Cq;d12a#UHcnv*phC=1;+AQH{s)Q zoOkFK|GRbBQ;zy99raW$HJl#&++KBEbLNjpZ~o(_np)|!4BCsg0sYsf?#Bne4i!CH zs78Ou{>jsdPYLzD@Wv>fR>MkftGxM>Yrt`3e-%YIk1PA0cWZnl;=^mUr44I#rCIoB z-&{OIJ9HTO3hK|-4}peKc|;0N;bs2_)G0^(ycky{!XHa&B2tWQQA`jvX=tE!DgyL) zqglyKEh~g-Vku;CUEB~L6;)^c*n!dkM{LQ!t8ZFsUW}9JJ9n?a3Gq^sd;u21@4>xN{Q2v} zm(I@?ufKh5RW+IF2`y^RDHh|~uxoA2DQwQ5abP&#!;h0h)AvgkOiui37uMnRj9QF; z@@w~`Nw~n_z2Q1pES@nI3zGBG|M|J|(?Pr{>O)_=EB(#C{>WZ&NIRU2+#XBMpjeEe z{>ft2Ai^nbgQq%#ntmIVBuY`C-_YbKfmnFPq*Kn)cu)C}qp>L8jjsY?QI31cxhU@x zn)TWVcP8j|A<|35oKA^v7xz*{&#|XenTzX~tz<>OU(c9^H^)=5;SDlZ=H(VJvVpMuI%6j&A*T;IhbLE5W z#4_#LZ+rz~cx-yzHP6pu`25*(6aU`%l2@jKxG?yuuYDnXEwMm@)e&;gly*`Z?0wsLO*@0ZhLm17uK>H+X0=oJrOXX3*{P`bO%k z*vRvhhMTq=!W(c#qzliVfHuJMRF_T5I&QmdOS=8`o#~lRpPHU=rQf=8*`?LF=$2cs zn1>5qH|CJZPQ%XmZ~f-dtfx$T<2qbCe+`Rq6)TuefBLkY=ncw*O8a(rb{tgo; zO}hK`E$N+ay*d5K2QJI6Uz{{)czWr}=G!03c|Gj>E?nEp4JHzW(d$(zm|63X9c2Y2S{+xZwl0yB4zbw4_`& zkF?5AS!?F%90LPw{RtV#J6(EIj!KubcylnPh7rd#c+TYyb5Xu-OIp7UZ(qTyn&;wm zk1S^n0P7S_ZT&rN`*|_00>e{FmW%HwlCIwE;X#%t=oVQKk*e$QT+jkzRL@bjTxH9Wi)`GxjTs+`EyBWXqOiS+Zmm%aV?Iv)=cuwZ7T2_xb;Ij&zRf{hNem9+a{Pgq& z{GET~foW`<+f9NKr%g#ez)^;8|NKvIBd?y^ojawci+$I^5q59hS|r29!0^c)!l{^LiP2;ZWD)#zOAx@1iXwrumk zow-Bs)1T7=+Mv(+4nDbz6LFyE@-pzn&s8`VHPaWF>xy$u+2B9@t-I5AZkd}dJ$*V} zKs_iOjnB?2jKUM_mG3?!9R=J6|NaIWWN0h(8Ki;uZxeZoV%Is%1rkZS88XK9WU#=I zDi2crd|~$5(8WTrXYyyOC_bB3re`*3jE@ln(EC;p^5H*t?dM}c{;W)h2Pyt>-{QiB ztJ0DstJ8k?MdJ|cEAPNfr5T!jZRY=HNtb?c&#MMrB@0m{+qPGIYZz9&twFRgXDIU{ zY;ByTK{WT2^RF2=5x?@^zm=X@{9H3DZNe6q=W*6|tFUbCYHPo(@|Le!nXbZ%pP%@x zD{Mml!MD64{rrKuTag-$E8hw~zV~*Ea!bW^58a#IdHT7wcl-jpl=;)UekHOReCx?) z)zV7vc*8&c#+UGNmv7Gyz6sffKl8nRP2aoiru4>B&Po>_cXFDAv%3{W4ev<*;~l@7 zjymv=^k-lEyP^-HrJ$?W*uy>z20q{m=lvY(&)#{x|6+Eg_LbWl`=xcp=%4D2z=_84 zg(p_@hVu;Kf&_gP+ddk98Hc^Tixw}$$Ag|vQ}^B9CgmORpr$etY6ZH)4M?2+V34sG zZ0NfxM2fy8((ZA9qHhi=^aaN{(3$VaW&e%aaHSY#T8`wHFUb%JGT!;A*g3KR6bJe$ z-xNYlDc5)?Huwka6Z`3838ed=DbSz;?l1Ly|)nSTJIH5d)E^Lg%8KB7s<91lDTecAIHm8`)RF##VwVz50d zbm2i^9c);?e5-G8K;PTP7nG59iF{bHb`ImSZfi%Ng<6HsOr%;m_y%2Ha^|1iLlaSmcIiX{i+Gnpp^=_umaJUB}!n+Xd;Y6ih(l*Olm zp#bce&Xh8ZP_Sl0NKd_J1g<|I(U6n-Jn2k;(^tX@p+z*yXQKvw8{Rm+`M#&@kB#c^ zX_M2%#~+sd@cd(N!y95Js=Vid*QAep9g}gh-z>JvC7=8rCSmZC*{O0A_83)7dHRcQ z-)0jxn!PWM8@%-RL(hSE9Cxbz`I_1So+Z&)A6wr>|t*e%zW09m%|I_^;VKAS2%AkRXwPF z+CpnjK2UI=q2Dru|Gf1UkogU~wJXYjFIXS!JuC21?0!?mq^a17#FKM&VBoPlTvl%% zDRcV;y^dW(wz*jbr27+C{yhmzAC>7nkw>0+OBQF=gC}G4s*PzKm@IO83Ilz#c$q&; zm*i_VZcM8;u18Ih29q(o^f(t2@N4h8Jw4f2MK?jXJdFLu@4o62O16ir=pxXD@7(zF z^!C%v#n!Wf(g)6eLn`;Diap8neOZui@6es4h8ZS4RFK5Va8 zy&C;?BK{_&4xD4pUs~7?{9$}&8iJ3i4Mf;!FCw|-TK_;hurDzJ`pqsTwMfzj49?|a zk&ls%*VXzh_+PdO9O%SIf*fZAl1yKT=VA!Z7_)p_;b||`*yti$2<^xMv)l@2D3(JZ zx`x|VA-QjylW~UAuodOhlYQapPMpQfnb-pl@V@2Yf{PDKe3_Hsjysm&+ZNkVKGQ@O#v7hXSAS)8WI8zc)T!xP-@C}}dw=|=v(k6J z`KaAOIa%cWZq2Gq=?CAMo4)_8IoMh=G#!Uk{kOdJu=K_^A8Y~+o6(uhyYS$2?GK-H zf#o%nJMe2cdJx0OF%Jvi`T{>b$n@yW>b-IqpFh9dzK6kcx%a{Sc_`n3Ly=jJ z({Tz3Zvz}(XzwOGz^q%l#a}oLTab-l+DI7K2C#9BbHK2Wz!|+gf{*P){2|Sxl8gG) zj%bJcSGBV$-^4(^#Cdz9FCfR-rK<_`nT~yQ<+62Y-vh?tU6;|Al%xOJ33jj=3udba z58N+#13Vc=QT%^66{{HG@UoCrB|RqM#F9!bH>!{mjzYG5W|vu38-_qa(u=6XpwqAd zoWzGHCJaY3<&=S)j6otz4ibb6!g~t%LFpf{M|@Pgr1+t)-iWOec=-dB_yi`Z{JZ;! z=hOE;^d{pt7sn4;Xy|pZm0ekn?2*0#UVG=0-Y^39wd)>Cw|(Yvn=DTnJ0iXIxP#Nr z@0y2PO5d4o!ai|L#@}?(A!+?4{>0?rdh8cxaDOxl34A#0$;GSFMaLdwv~NFq22O*R zk2ogO?BJM0^QL?-wpaYad(So+c9?tc(Hl;nCywnf94}L`8ZiU=vwDQ6H`dfihfhtH zo;1x!?t5x^x*<+q>E5^{-Tvs}bmwDB(r4a#CQgm%GMclG**`6RX+2I_QMtU_R(=&p z6)(REZxW!@2963TavOX>qqIZTmm8e%QLnV0L4IT0ZWsociY5g{Yg(|!Op;xYqbA3d z1GcBad+9fP01L9}D5Z;x=P{kxX-D&BO_Z3o>`eJX8v<#y2zWXx17`y_L z@|V(-@e|U1n3Rvjdj|?TN7NT8gx`ILo>AP;bnoTywN_RQQt}MOF7HcjZTd(?OW1^`lY4SLbpH$K1#F|4ifuHVox^ah_$2$v=3oDh(~Og=?|=K(ccIz_rOjKm zr+aV5duRAty!eFlM;|=KXih$DDkkH+w^xBLsdkO)NJEC<;TQ42m}qknJ$2uzt&PV~ zk4GLosbY%QgbCGyIeEr(`aPvydd`+Hr`yNdll(MdJJ@hu zRue*62Is|e#Spwi4iJtLjgfnODZ@2-262*6iLg~fP7E#EAmjMMB!|xngydsBgFQnK zGVK>eA|AM)?HbXMUU$?%_ILWMgDaY_Yw4_B7w>hD!OurP`I}e2*S6 zJau)BOgH~}A%5OR>qNL1lk)e!>BKZ^`s8%PA(L=3$381e#6R$cV=bSTCzq_X0hwDY z?B^)uM1icFz?uIEquhMzT#pr_6Q@tKzc-w85LVB9Qc7B1ReCusd#U<4Z9R@_?7~Z- zV{tl+4StYi;tWPy_06*O_zT}#9aQyTbhNGfGv20rq)k5Y^*^u{fszw0LQEy6@2?>5+LW)5_IV`GaVWfK)W?R+>T;Cseu>w9Emzz>9tQXLeW( z(W_x!z;>oQF!!g7Y?|M~iBazjw9WO^d>N2Cav zFf;To@)*v%{^o5rn-HFZUIw;*PN*+G=c0^$8ZVKuyz}tK(_@}~VQE^0BN-L0xZw4+ zAAVm<7>DRaiOgYG&E*G-bkk~E|GB^}k#y^^kNQNcfb;JS$De}Lz`@pzNWpxrX1%bc z`Za4iR*j)3yHW0ZHKCWL|JQBU-o#b^`=59`J^Jj^X(hHI4RrAPE5>K6{(3*)SnVo2$+srpf;9prD1%p*NicHMFkeWqoRl_g+MZrqzCN9P)?|D+b833WyQX6m7QZ$P z8bE>*qrEW6Kj8e2<3x}G5u}#qqFr{@38dYJNlO7#5f_tZ7{8dTKvt5S-Z_I2U>i<>@4g;=Am z8V?8NB4rg|MnYtM8l|9+LwXMZGoD!BB)^9DJfSQ_`JB?(KD1Ik@d(lbreDr^E}e6P z|J3@ASDcqV-8CxRHFq&q$hV|Jri@D;eQUL{b}LrNTZJnxKOYUQw4kDT0$W4=?G~O+ zqOvKE>A=w*{9jP)Js-!@N-Q`HrC);;@ISe@{<7!|k35&gca5|g?l3%6^FtQXa9+6r zSN*x#%|H{sf6x5%ZhRS&;bU(*Dcw3}DsC<>rY+bjdCH6_>D*cSSswL$^VYejnx5r4 z^T?@bGIX{J-~ZK9X~EJpX)Ru0kmZFB5XFK@DnBP^L9W6BNxW3ClL75ks(@9XodHNFT$? zkpF>C#54RpUNAfYCzaed`vFW4SEXa89hSI#gA;XzM;9+l*WPif#hVvZTgI0N)v;g@ z&A|4IE8p`+nWY)|8TNtShZikd1x{*tw)NFF`~+J)PD|s$M$(SQagDR4&%m!IlQ9WC z%-C09JIP;Qe|K4L%s3JsB)aOd_ZiJa$DEkHfBP>i-n@vfH`wG`fJy&ayfE51VkG+T zm=}+9j1&3X8@lHB53KN1A;^_qh0tt{U?+_JNjkdF2^#3w8rXEZ;O4#9PD!LW>#*aL_x{obdydkZ`q*vjt zdzRdD@ z@bu<4AA&vCLyUjz+O28hhV5w*zP>s8;g{0MIDV1aHox=3OVanh_jG#pnKd?rnt8;y zbj5p*$U5iEU2S{GE9*Q?NXW)dt00&m-u&6p5q=iwvHxnmGI zX=Z@jKDI}b>t%IgATSqR_>iu0fWXV^-U}*#(2s(wz^OJXuu{JIP&pw??FaA{(GLMN zRv5`A66-`(j&NpnDyUUHGV_!cXZMVLv(Z1rSb7hXFo{7ZaM{ z@n=`1$>XY@o0+e6Ct9u+(2vylA9v2je((-_^y6U5K5y3k_^^jRjEJ7_yLZe>k3U=L ztL*2hyjJE*9>srt{{sB1y8)*>Jdi&7d#9lsgVPCzPP9MKl<=Rfdpy0g79S;GeZ#rj z6E~rA1WvlyoHk%P4$#?6){)^$gQU0iQbLq7+Oa{jPd>5p=hf2R@<*duV3xNFhhe1^ zUF8==13lMqotxUnP#8td|W9wgADvCbF%t^GpO`sW#Z2CKg7Wp}kks@Edt3mnP z>ZItq64I-E=noD1#A>hw|HDr{p8oPXU&F_F-eV7=XC6K)@vj*keQrVe!!Li%R*9N< zRk|pkkcE5bfHFCGY*)HGK6oSY5^`_$)jhHbtN2*hBj<1F$|GV^OU< zeaN@Y4&~yj+0kqHP$CSlOS?{rvCU&HE#XfY| zU&5#7t-%q5n2gINWS)Qie)d<=DbCZ(K7ZBhGyyL{Qr}nb<;;Voonhyla|_Fd{-Wx? z;vu~Cm(SZ)7uH>&%1LSnB?L*rdU8@up3MJ9my$Zi4NK?Y<4Byl)sMWevBpdLD(|HcO9(Vq1~7O8psK`K?nr2 zaXs4&%VMsn_QY~9TjlW-a)Z%WOa6#Es4~P~`h$KDo^b}X$L=-0Wzn(yyAJGR90ku$ zvu3@XZi=S|!^3EmAC+Ul@VKx7yyyiU(dZ;rS6@5PZ^kwQ3x!J@ z(aTl#G|0G5jC(x!F&|F4H(`&NJa{_3mN^SAI&RwrOxVBl#dV3tC3a!8nSt>q7xz5T zp{dRWSv*i<5bUnvAOnQ*!D%;&ETE##jjuE;YYL?JObHVvykcehW|8eJaKIWp(z^^I-X&4* znH))X6?)0eHn)=z{JD-oUruYUUjv`T^=e-)2Dah<(JyaG56qjJKKVQEO{d^Q6n;=g z;d#7>`Gb3Jv#(TcN4wv3=tY084ysF^isJ*PVfB3C7=I^%xkGSV0k>~HvkZThw+9>YtXzT$TQ&M$8}8E@l1>1bEHrxq_t zfBwy{q_>@M4t_yACfgytIZGC$Z)3ZS^wK{5b>YR(VK}~V9VX@5@he9DvSipn8tan~ zZIEVyv0lo88fGeM(iE}poS9?->&|Hq048R+qWO^!J zvk~G)js@v_(ypGvaDK^iB38S{jTw#=@2%K#xe;e0_wA=_o<72D zE1U3OR12kJY8gzF_bJnqWiCwr+VJHraRqw)tCC! ze@~nI_i6KMPO*`P{hQCVTxg8vTXT(8F0-FF#;|__7Tp}_HiJ(_`#m%dQo+w`mqUab zkncfAYWy(!q&OSXB9iq4au( z4rZLEN<2>=q3s*-4p@n2VLTm-ZPaD?%}CbQvptHml8>0R8|9SH`Mq5f_-tR`3i~LR z^}T4)_H^4dzuD)$TS(8*B}|&ygnC*v9MXk@j7oSWyy?0<)=F{fGkU@x%=8gqK7%E3$xl;`uCAHUQJH;b zdU4JA^y2DjZ<-MxpOwv^PD>@e$i)t9SwQ)*LpJfoWSbLj9%W~rlrJZ3Rt^daz8tM& zNv23@(Ch_?C*GWVa^l@vDCLuOZ=_{!D`H3Ovk-u~jSB>c`bs@BGCeYI3Et_;5cc^Pj{H2g;QWL1JV3(dA~aF@1tpg&yKG~ebPwZV7hne_-y?9Ywg(vi89>Nt z=H;TBEr!W7p29WUVrgS`1nrP4we9WOc3@xfM*AbZ1&~v(UkEG}a|ooFebscWIe zua$B04ai#T)plmrlS_>HEagz2sl`Q<#us)B#-c;#1Z#L?dZ?a&2{n`vURQhS9?-E>_S%zjF|NuH4C=H(at^i+Ju7 zMK+_}$b+yXDtNOYRsf~E;qm53=i5z7l`6x7zM;~SHTe~VuZVNSj5lvixG@M|<+lTa z!C9~?n==C zG8$x6#OcjWt_C3KWGDyXMsDS*{42Zl%Rx>m)t&JRM1yWaendTaunsZJa-n@+R$6coYF~yt_OY4>W_0?`py0PMo)oxC{ ztG-)tamB0jhy2hEic|SY_ZQOcU4^E;GR?l0SMDIE-uDJBE&C+DOpog5uq}CZ$0Z+T*w&!K{wb15Ewf_tA&Cfq;%9u)ANMET$| z$n)~RPq`g<`E2e%SB4+7l26C5eaOCymwM2Tz)Pz`hT%g&-COW-C${`$hq2uth&gy1 z!JHl$P62-Btnw{}i!4z)HHL}|?ZXr0wN%RqeWkXtPa|!HiuoZN~wUREpWjcjNB~s$cS_1`tlIDt;{mg|4H@BE@TSsO_L$28sImAgcQEnFf z;KSa`OZt+#%!s(`RF;wBk{zfF!MZ%zlLsjIH^|I-#zT+ZDe!Fi6&dik%#^(u zVpOo33m9aoH7R(-(}Iy7dTDLdQGp~s8K8(i0dgx5C*>je?nJxHF~tMCSyX+kS( zJNk^TNV5n=B*qKXf+z^sJs3C$Fs_w!;$Edt)twvAOv^0cmEK!0x1CVfOz*BS#lAw7QS{C9 zF#Pf`p|cZT58ja0V{7F9AKU{ryr2RF4eJ9QDk>%us$h*}&3lLqC?Jo;ARn@L4StNp zh7-TxG5$)v@Y3S~V&E<8bSZHKKJzn9x?q$(rtN>=B|ndqw6C-y21!gn$-$%jIME)7 zttQ%H!c)U?ke^h}$RMwkIQz*J=J6!#vpbOaJ{x3uMVIx2`PJMf{27D4CWxdLY341F zaVx0gIKB2{i%0dC)0l&p+`QQ5J|LZy73X~OrsBI9AQNTE_gX6&dL`@F{6U>oa_|5b zHd%?lWcO?-26Q?2$Pj+zA3t#c;Zr($&8cW%DIwd>)nw%svJ>oK zfGi;_Ls(bvx`CiGTtRQ7PH#RlF~N^`k&RUFqZ}rOVknPEylBHu=o)0c0tFZy5t1vY zjNPCw>>rC0VTR(NXdPef#IcMm_Zr$Iw9nqd)r>ZEhd2^!EjdXKzA& zG>7%ljaBj;Joxw(2yEvpze2mT@Vf&gv%k=442o?GfiSi{2FEk46gwi~m{Igt-75`O z8$iLeGR}VDE1%9U5))UG@45{Y7sq)22&*y&BR0Wt!;oFHz)}rIfvkQ z-oe#w+g`E{_VNz3(>*A=EEdJFJeW@h9=eE(wBvzbnk(Qo76Z}`AT&sP&=1BK%_$7$ zxzc05h*2?u=si_ktyreUVmZT8ke@M#lq439ITPZ_YDaJFmULDg+EdwCmS>-qZ>>r? zjd5jperj1!PnPAhH9N{OM0_;PBp-ssNN!ts*}hvM4wqkNp?uF*d$xR6g(!LI{K_h@ zx&4xtc$M4uf$=o)PQZSlc982Y?#XI5lr>Oc?@h+d3%ICdX77Z1qQLc5lxH9AA1ftr z5$vY}#QjO!<>ZNY$hSr(2GUBe;m9M!K@{eA^+Yza43CQ*eq2#b{rAs1T4X5@nD@CIKqG!XpMf(Znkl1P*8$&2xjSv4B#LB8|?nohI`!(@`Ko zgz-$wh%!kSK`1%ch4myAUVKg1loRqe5oZvzm0v@|rCAR|Tadsq30yn+hW+*nMHlyx z>7csK|E`nU-R6IBNX>`PMP@ATKK-gfwJ9(nK0BRwq}*Jt(fV; zXrn4Bk|PKQ7D7fF`NRhaBl~bBLNb?%h-X$SyDj)BAH3o;0g-Y%a%6DRJ}J-kaZzh0 zzMhHgC8J{D3#;Dz=na2?ZWY+y86<)t{azW=mW>PXnJ)Sx{9$~^Q2)1bzz>9U7uoIC z>Vn^MUB37&MdAo1$1XrL@!3BjE(nOiR-8INCJ{u04Z-C=tjr4+NiF@rxPG-R?QmvM zhIrJIVT49~F{-IVE{JrxklK~>BSv9&z;GOah?hJ05vHnLk`WZSvgFCE(DT(^+#2II zifE6dhPPa`U|p7a9tC>>f+JbqSAY!H?5!(m>rpfmeQh&WZTrpRZ`MkLpQggNB4lHr_u zckrbwJb^7yP@ez*KmbWZK~!V%%{^w;sZmBY8Fp6g)grVs@swu)g9y*mNWDSl28LW) z#B%$6wV^Di3Q|BTA1*bBq}e0;8uUg?xvrzICStf+}QbYXAi0hQQFZ; zzVR0Jh_jSg#+7fd;IsB2LO~D}QTHnlHC#>L?j_{ZrY)ubwpl($aw;rr4K3dKiEka0 z-_F0vZ^dn0$Qx(0QCI;ptZfF&h!z5PL>{-8aBuerd@)!1yVXVwVx`OeL2!weIA0-a zU%p-GHyJV*3F_$)ihSm&kBLtDYkik1g81iSP3-wqx(QK#_G?}fuXKxi>6Uyt+{}c= z9%U%(uZ;z}kWacQs9jZQk)yk*deg>0Z?Kk?R z03f@qJ=EJuF6%E=gkubK)s(KbUHLO#VaVe@7qw;bR@G4qSS)V_ zzqHTpIo|ImSn?%e825L_X(l5u@#agFd?+aUKh5T9w3iPWSzZqAW*^mue9}m813I4i zy?rM?ol%EYrGs547hh6;2G|M1+t6#<-#rhfTrQNYYoB5+3o4`^o z=x76tFg~}2CfYA=@P~rNH%kY7=@fGt1c|Qp%N^xp`&jbuC(zPBtfxHRlDd#C(5xRG zfw*b?ptO#s#Ylij@_NYf#cz;8NL-h1?D+!zP?S+2qk3?x3H1bsjXq~Zs=#~e070EF zbL>fEKquqwNUsJnMlH!47Un5C3c<>DT3*qVeB^LktE=`qS*rtYu8<%uIg!$4{I011$`;U(6|WKQ&`_IEQcyTb4hSOIXx>> zzB71zX67D|{^rX*nc0NO=N|74ta5YZbmWK;HUYQ3D~5(;YRg(V$>|2Y@*C_`vO!;E zL~J+nWe2&@EAq;Y@OoK`c}5fUlheWrHuA|+^ zwwG|iJ&F*M@O`-GMO{TO^N{A^k>%k+sQ!`rYcOUB&>tw5R+Q86x{A^LSj-fi57|Z` ze>j#F)a>9ri#81!?1cI?TV;~HvCi$ZloEcj6?e)+Ju=wda4N>icsAYe{_-VhypF1J zl1^^7{9K8^V?u1=Hhb*?F2QAWy7X;r}w3oe+d>MiuRt+4F zJj)LV;K7t38vJahl;I8s8flRjgA!iu5Rz%=i-8Zl2`!yL+FhmlZ{4oS=zF_I*ftaP zVQyWJD+Osm&r=a9ms{UO-HBdltz4;1sU*FnMK@58?YIom-xpZ&?TvWTwpKbcW-3qV zzCiKx=*#d4`4#x_vk6_D_T|ovn|Op;i!lKe+#kGj$?xsb!uJJM<8oi9`#^(oSN+A? zja_lE2?YJdAqHa@CwKI?(rTr6t&8Fvi|7xMd5aDjrwy7WDSHv9Dg*oazQ?ivSoj_{#(e&T1m)qM$oUp z!Z=-|2x5#Ezp>HJ63~Fci{9#3NQy&-uiO*jPLDLrA*6e}9OdaKKoKq$jbU6 zgFS=GVUe)r6;-aGS|Uo&$_Gt#c4P5EM-@j$);g3XmW}OGKpu8+xn} zkcuxQvULDqP z3**HHk2N@-8c-Tv?i@B`2TpC+f)86YjFbB}m;ex(Nr)E>5_3stH%VM*?!6cdH=3^ajaGpZdV>lFGDhK!VcM zTUvB`3%b4T=N@fdZNJ%A_LebykgJ;@NpoL~_R?Eg;zie6x^vWMJMD%?Ao9zd463jG zLg|*Ac4^^X6{vd8UDKyjxqD)av3|kk0~%X-RhOl~F{W1ZLuZsjWu`QvD9KEPdR(*@;bKaX@4=KALI^q2v7`gTSX%OtZ&6Kcf{yh?eCIga zdxsBB>(*|;%bmRUQoq}tZT2McAj^b>kc_8BtA1QTVEC{TLfnF|Bl1<+AO@RcngXO% z&QOw#F;)G(6tmEN%YpzD5(Zi)FmX{B^F+uu6bpw)3<`9nmn$YcHcJiq+B}H0M+6|) z^)$=MC%?%-w&b}TJ7bTx@ADpkEhl{GG6&kjzh*F;G6b7MUJk3$cJwv-qukcyU?<9A zb`wH0bMw@|TeAUz%MU83U=}Q84*BtiA%i>8 zx{X`Y<}LAp39}?u_M0ss#8^c@ScbZM%MpqRPQ*C?5C&d;mJ|(2P7r!Z86w;R*wnRq zB6in^usu7oRT?k(#KfDEZmoLno?W}9E~{?Wl=WOSof1}iKI=_PoZg2}b8uD3x2Ne= zEB(^7JMzUNJCxpA5Tit|qJ2#Adx_d*JJ?DXw zy@%T#c~7zi4jCgxa(_4fhT74B!*H~n-@~N%mTwZ9i_JlW&<+FQJGzp z!^=%h=a>B4zRYf?ukO9vQ}4fW+jB|SLHSJA+F?khTv=G=Yem`gl`Hi08`9z_`MFA` zd%Ww5@gm$xJJ`QjVB3R_s0E}op)|Q9j6iZz<`o@S=DX&KGO`GO6khhR9;)eC#b0n) z)XN>^usI7jVUPU50_c~IN=p^vL)s8*8RfQ>Ex7+SgL$r{eV~-lWU_epxug%@IBCUD z`l)KhkzexD4<^6jWwJkEfG6W<3f4+wQdNb*Slu&Wy(ITpZ(QogfH^42zG`#vKu|Pv zv2n0LNiFw!bJg4TdIMthdzZnyUr@-m!kc&`gfB=v8iYOQ-LA4IMjv~kCC1qJgljDv zLz#*vv5lu=ogJz(McHW?GK4Hk+m#tKox;#~!spfw4eyldvw~KKOwhkkZuwXBo1u+X zCf;;ye=x8SjWDK~H)s0Qh8&?3G{@O!6A_JOnE_ z<2pOi`VDwe$IG2!bJ)Tj*&Du=*g7Opv=7ne_8o`}5MeymgJ-ZVX}X{LT9>}3Db?wo z^6x&Mmqkt;jO|<_u=jT)C*IiW-EP%;_w@`MN7-By7vznGL1{s0SGuo|eCm^Zg_2EP z#e0LKWe}aGUioXY{VClWY-cGKUB-%Lyrt{U)oTwWn3y+A%A6PiQzG+A+$KI;r9G1e6BC^4XIo|e?=LGFL!Rh zqraPBM+b0-r{-AMZJO(C)1Qv3CucN9ZUoazO4($FzbKPO8QD`eRzgONz{favv=JR- zdij*j4#-XskWZ)RbhUJu@AY8jnHbX*l|!|RhJ00}5$j?$N{FB`9qP%FO~mcs3OVf) z1U^4SySgl)rAT&K`AIN1j9yHF$g?P$G9%WcgMnoXIzi?DB|nPL(%DYfpUb0i0dgQK z_kfGpVCb>R1cH9GcwvvsGg5yVh18Hz=rR8xt@<^)yewW_toLYtwA0j8{86#4-~8c< z)lT>#5%-hx<<4#P(;2-MPsiDwU=QnRKqebf2|z||lt5%hn{tah3v@r_hpUY5Uqof? z4faoqxUsJjH}8=hBW?A26ei!paK^XVSU%|oBcjF>{)$sOLmxTT<_ak3!naEczq?Rf zLh}0pcUMP$=w)A1Mjo;vL+QT2rmQ_p(>Cf;P+G9nO26#W9{y&!xu;+4r-Y;MfM;it z+Q?1i+H6Sk{*hT55RPwU&HWn z<(PLkqsunY;%ty%n@J)=9K`!rtqGgABkFOv265};1dR7*FNfPfAuat>Dy)nPr34(G zWEzD;Vi!Op24RT!WuI6LuX+O1DK%>MEExUg9j>y&_@zX`YK9e=h}t}qVnD0*W#m9u znmMK)1n^s?ceMK#cCa1n0OVF8nr;2LI8Dawh79Ew{3FL;l^pxYyH{^YJGOCtLpuCX zpCt-InRcY*kcM+WdadAo?UiSBWJBe#DPt|@%h@3oP6VDtznoT$z?S{Hbx3JI|M<&&Sr$jXT<0V zXnB6|$&L{j!uY%x##bQ=CYc*;rpW0Ll{|w{?UkRxU*uxch#=e!<&Q#SJc*=OkT?+7 z=;N}8iNd=~OOvL+6W1z$M*g&H*yH{~iXv3MCag#kPxpWG2%jaYDur43lA|2YNX6u}v6?&-voZVH-B#1rwuh zT*NN4hzyU58&l~dUU`ISHyK9(NJx(4W;-*Ye-YY+U_K03mEDZVzB9r-xD4Tb@8MYa z#>AWZy*c?VL#dB-%5<*V7HRU9`K2u3g=?mT-%}`gf|SFFc`InhSNRgvo2@9*>T>O1 zX6k1ox?oL;?AR4_KDGyEEz@OtWp2AP+uHc9F8sQ+A#KDlh%Gvl3;!>A&Fv75tol*h z3*a|vo4Pe%#za@Hmexcy3t;lw81 zmg6G4-h6=LM4Uk?(UEig+Ki6F5g1%1OJ^1v4@=`gP)Q%dC`;r<5oA`(Aa%Vl2MBT= z|434;h*EpW?+Flun#zt48di$VPgZk!ms8j$mNwAoqr%8$WvV9U&L;be8s<1;{WGRL z-Pz8l$9UrDwH%ySn?x6jI3lpaY6H;K(J+Jnf@Qth12Z!8OGDmXjN+-?3X-4d#IJhd z0UHx0k522ck9;E*#H@|v3Jn9JU2CpjLQGiXhkqOo9E3-X6`@C52z$aHNk&9mg*Ntn zUj}wEPEXQ4+H7{p#f8yD)H@vzbdvSWc-bb!v2u7kJ1h2W@t)d$MD1s9D#^! zD}1?AH$Pft?VwiQEI08{e#o;{LP)%&0cjOFz5S9vC_3UzioAoM`nJ-(|{V zcVsvK`KllFeQNK~&nlC4F9t(d6Ob5uo?=ofyAo&=#0SF17_UeAiS}_dauim)`O7np z8RYT4wP13A2_br4iG!WZUnCqPd_T$wOZ!3^>7cOm16QW4{~8_>d)y=8{$j-%(vX%P z4cZt>e3SzWJ55kUSoQZhzgbuCbK>YHqq*7AQ?8h+e`Xl%!wf@-i6_1L-yyCDoK$n7 zO|2~g%*Fp&pcs>5STCQ|c9XE=qJtu^JbrItxmmxJeI!%)2pNnM8I=UbD^vI&FEWcL z`9rU;Du+#&sW{rJ=GBrbdV@hmyT^3#2*iD`mwYo;$-Nx%Ru)IMQXmjME|Ct&T)))H zeEg?DWDEz$FHRBss3&f;vHMdP=5sFk2fPCm8ojH(Qc*JVHslz@C3`4jD_><$?Xzuy ziKiv-TyO66X6Mh7Z_$Ss2NcE(2`!>Ro4188?Fj<0Ht1!RKV70qt+fEgyPvEKs@2nH zhTM{YJZ;TjD);&q7iA*H?nt^Uu{@o)Xs$O0duDj zM-w(Gu$i43caMxf^o_rr8@N)-z+Y8$Ci#|a)yth#{kS^G$uQS{J)bEbX3%%NS+s@{ zBzhOa2C7*9Amq5Dkf*UvGaX82R@lgRP@3N1Sn1}dlr1Kc-`J4$`Eb5Jt zoR9US{%feHKaZ1(xmwBPJ&icWQg0=M?@e)+nr@U5B{Gl&9+5Exw5%%PJ zJ>>&uMiv5*CdxjpicD^e;xUNp)^1H(_2o{Zaen>65mr|9W#_b0XT2PDUk#M{FP-jY zMBULG)i3g@Nw`xL*6wk1fG6WoBAR1Sr3GQQ1_&b z@woFOUwTP^P@wAwWdK_Hr$Z^9E6b4wz{ZxefpKs4kjNmI<%)p1|B!!Q7OmbYI|}=>s9=_YTrQU~XSl0^A!g*5&pT?bzF?G594b z_4Y-in!@> z4OFWjxlEfm<_G1v9mY_u`l7E*yclVV$p%R}Us&^8bYu6hwhU z*(K~&D)NoP^Zi6Nk?dz7635Yx;rA8(L&k46eUd4*#0}GmT^+}HSn`z=@M>|Lw@s*H$m1&Kk?I8IXl<%pYY2o)2Hsw=J z9&@4-F+FC)sC*{*w(WYkb5FIt`g}G#M1!v;az1$2S1qyPJ&Yf?v{o`%9rnuPaNu=>Ap<)*Z8W8+J$!IrJg{4TC4zbL50gT^0XG-t?mhg^L@Gn@xX ztzv}`-4tYp#KS!n#Y|Kva@14Apw)ZDFk1CfP&(TEBs-$^yx#`B_zkk23OSabR{GQM z)QJE;IiIXUW(30oTGh*}5})fyOb{+R9OWe~{c8O1$CyssdwC`~wykX65;xVNzD*I^ z76er*@nL*c5(c_eq(sa(!lKCMlK?}ku;XdQ(*txJ;K?}lip4~XmBm7JV6XRZOuqSn z9Ztf1+ldMqm8hgMfz1nP=)!L{*ds4PIE;g?CP=1{`Q}LXkmcvJSF%>rt*ueMS?Y~j z^kxDsw4}<1X(%7(w+11AH!ZV=PmhL|K&Fvz6~^s1`=s|=a9q0NH3z4$BmB5U%D#2( z;&k;bv(w5ou{u*KtZDZ0$VHYSZ7sN^dO60j3L2GH`1(pZ%7sSEa=TYcC?(+u4N^o~ z`v;ThDU=maaSQ53ue3pVXQwaS_>=VGdu}%iWDnuZ zpZHu4tJdOt?YT=9rLW)ev$SZ%GRyy~cU+zhnlL$i;47c+LEl$IEBpGwD}VL{wv(qm zrF^CP0@u`c^cwjAI)o9BQaG`Nl+r zlWt%6u5UZ>dy`88SB!|BwjmxuCC=^*c1jC>J?5{hp+=0j5wINE+F|w}O+PUhS&Y5z zU*|XZHfQjA59d3~CyMTte#b|;CI@3S_2S;kuU)HN^aW1x59Yo1iX+m67wnhD;A9R4 z%D(N+W$9o4^$DApI}&({guRHn^>w_b%+R2&zzzzwNe~P|Da3F0+Tz_FI~9t43^6rW zluz?Nvx}lbmE=~M>oKPnHOj##{!mMm5X9?~&bkbe9(&T1^!%cA>7|t$ZSHA%_fa{t zmrj(wOZ!VHR7*JqSWm|2DBX*_-I%zBKSDnI!A%Hst@K!Xi1x@+ea4N)y%!VmEgQDS zJ)ZPlK0;OmL70O1w6`}i49>CQAVJ8+x04fYHbM)*C1&7!ra+mQC}{T_4De)p5Kf%n zmo10J$~Pw980eyi4J)nKRaPYFt#UFQb?22&NrA|)oER4Q=5@#{C+a5IS|v0{y47)jtd5`zGxZSPMF^PP}-w-Z%|r9*IQb2?I8K( z?Gm=5kG>jp&YJW(6MeW}X1#)tFR)+yniJB(7gwa$AAfSX=HA=0_Oh+t_~?V_rbiyM zykYpY@wjQz(*?(zkUoFK2hw}~`Kq+uP7ukguV^Iqon~KZlWu36kS*0$zVWLwCg42nr0t$>jTtrxA*c5dzcqTB zx2q%Io5+tn_tiavlTA#{ZErYxjq|e1&R=4Hi?KJ?7YPx#QcHU0K?L?`%%o`?^O=t3 zQps@+;K9j!$agu;Py8^v?Dv^ZpPCLja8&y8zdVuV%w0v5>4f7ar{8|(q3P3~IyHUx zzuk=!vam{S{;+JjM6o?yp%F7nD}!X5jK->*QRH8tV^R+PIo^d;zg}}f$9uC+?8_KwK7j+U@cym{h)eZ;l)Qk2$xSo4HJ1e=Y+&8BgipDHF3jG}Fxv<586C zoJFJ26Y=PcL?FN}qYxS?R*#4#BF|laW`J zBa?f?Y>`2NrTxUoa72wn4bxXIGtS1}mpgmNAfjwXWxQ0Iod%?- zbUm&7)KW(ukq7U$f2L#`smjH=s;4tykBIw3^nHOnMC>($+txSn;bZGdcqniPb07`=wsJ@BYo-j|1=#k<$$zc+4CVvcBQoRrB&&XXP(aW^x#uZq$jX4{>isp zk&Zv~@N~!A*;zfC6ROji_U=Ln5nsEs@Ps}l^j%-cySL+I8>cRQXlf`PW_SeRmaRR% z{I|F5>5u$r)5bbG>X$9B&_P ziSpH6h_z|};Hfrt?}c2q!|$;+7XYH(zfFU&XAv~mS|ZEqL3Tv40Cj#*8z-{#VI{*O z%`)H$yidybp(#IYWlP$+soHiXsRm($x3efB5;jL6bcKGUzb%jpIp$YHk*v`KgPiDTG-X2+t`VUW$p|CCMfAeESW3<@ zizq25di5s3Tw&yD$OjK6BbT3bM7m|p;`H2#Y9j8+2Bim{T9Uqa!|b#M6F1o~8Xqru z--XAf6Q)hVzTCm-@x?Et|8>hFY3Zu{$PebeLVy9}PC*TZDcRsN^ef8(F@#%O@K-o!CI_;cc-go(oB^y9ndr}tlSOgj3&acSx54e6^`1)u%YigfJs$?3fp zABoBB;PmeEW~NI|IVgSP>O0exZPf&q{$@M7l{+}Im?6Gcj>b|jqIlZnBl*1#J= zGG6Jm`yK2o;hcrf8o|WQv4L;WqxnYr=_`~3(YH$rFDQCJ;?=d9P&}^|VPeyif0*;`STTaX^t(9@5F?bojt>_}2 zf3la{tdP?17mck%o8;(W8QFHOuFH4J_TW1qc z@{9eU2Upl?`P%NS5QDx0{;{KnrQiL%BhqolPOx7X=FVN6zV*!~)6%6IO!WZ=j!FO5 zpS>pi-QPWwe((2=OvfKTA+3CIbNbrXo=D446jx9`nQxBham75lW) z_f40){^0aYY>Ro{9~_mYPwz~NpI@JD_}O#mm$xj6`hgYj?OWe@SUUThscFjOk!k+? zwduz{T99Txh3w%&%gxswOItR_1QZp1${G8m zx1jt79W*A*o%>Sy>Ax>XkI!CVK0D*w1Jbehb>L^$Jd>_?->h`_tZ}e^WBS(BbJAnK zUY=$hH#uGQo}+Ag)7vhanJ&C^TKeQi?!f6Kxc5dq*`gQy83_3`*R$nS4%e^Fx#bo0 zWg?<^n4@c#&>#gUo9fF=iJNHr62a52$4wZ8mpixE5r}MdVX{lDn!DrKju5haw2L^c zkYkaN6nW3N*$qrNn3;tBU7AyY{z*frcXBiBE`mMv$>}U}6vzNBmSvDJ%5!K>XNVd^ z4u3^tUIm?2PjL?U2tJd-<8^wMQQae}D#6eAND3=}#{`7Atj~*q1sd zUH9N~>5}6PNN+p)@LX@NJL%x`iFclq9+*VwRCi-9a=_6_D_8sX{mz|0aOAJn{)@@FA%vqMUL+AWuYtx-`m)Y6CP>`S0 zReu$P*HK5g9Ynq6dT0HyJ@A5{_T$OmBM<9d5asoDK)v`QUXZoSwEi(({Efo<0X%Vq zdl*Z6j+bqWdW<*pSX*PfE;IA8&4qp|Og}G_yQ|R5SGqS?)s6SHJxSB9lCMltzx}oA z%X^&f{egYL{?LbVT)2Q%6UaBn#$ol+fRWFiz5z2o>kHzn zUJ7XSSEP55PmL9SSd;mg*XmDtTjkZ|k$=>Pkv1vkmtw&QnP>;Q`nRX~*)QS8JplBy zj}t}NB!`&@)x9@LLI2_(h8O1AJ<;WOduM-Ro!fT|2zQ|U)5n5n&%BRvY&B@|*iVs$ zJZ{tDIO{tG5a(D2k%Y)LI`6m0Bdz|xD%gl2pC*hh~~pZwG*X(RS5f8h&{rUeVu zr9c1hF=^j@aqopuV>^eX*Bm=8UHLbsra5y~r!V~DBWc0>?)0I*I5xfhjfdtkeE2U< zNWb&$!_u9%y^y}}KWC@aFKtPm_|(bijI$45F|D{WkLXPA`=g`NL5GY<-~QLR>89%! zrSmV^KfU>FGg*;Fcix2ur~md>C#2aAtxRA1{Oq)L_15(9zd0q%JaU5Nj~h249ed)$ z^x==4m>!0IzWRm7(l&hE@}pOtoJNcqmR7IYobJ10Y1)pjO+Wk0+H~Jt9*1=IvH6j0 zgK0qXxghHV|2bEn&4i>6I%@}PAFKzq2NIEvvM8tca{<&*{E?P4U^afrWgLF?@fqR} zA31tR>KZ>1?+oDH%YTw#yhsC54mk5artDr%nQX88BYpfCJ(-fW0P_33(3Qs4@h$kV-C!TZxW98Ey&h7b#xmCXCTugr2p&mnppq$o9YS7EB zykd#Ql7G|j=O{NMU!^b*u7tG(#Go_<`@|VmU^|Mj$5R+_DwAad{Tol2ktU7qNSFWf z4R~P@Pva1C_uM7v8y~zR{qA|Q(r15o4_Pf-|LDT>8`nRYe*VCGqq%d=qO=vA@b}I;GTrphf;4U4 zu5|t}`=?KQ|GsqRV_1R1s>fZAFE!df#On3kn4oiV%&-|N)K`A@Zj0aX=;Cz1#4(s$ zPfp*tWiCGSGC2L=#mA&C{OqxG!~F}4<{p%TKObIx{!!^;-?#&^_eqoS(&LB!*Int+ zXYj<1Fn`IK^o^N&y3^(7&P?}W)%~UipEVj@)Q+sBpbFGt}@cd0S7t!VBW*xKBZ3to{f*zjp!#zixavY!g>5Y()nI27`$78q z$+3(e2>J|=r$T4ko<-Cb+i^GZm3}242ftEJna=k1=7#~ymn~U0BKo?p@_N8nSuA;K z@rHe`J^DC2oNh}G&!1;xcg%e({r-8cPbbYdBHj1ooEqx^6DOzRfoB+wZ5Ag?pP9~| zbzEA#VO@IoY3Jvd=FM4+s{v!h(;FfswaaH7gYrG471Ro^{D6Iy-{6y;g5SEe@KEpF zT%9%S=>a6SM@Are<`2Ubp$VNs@PV%lc)4?PJ4VMh>aPcJyZmv7w1POwH)Y?^#_=LnuHC|5fKc^KBk;zPE`8&)G;u;l zdf)qhh1G7{uR(tAJujqx`ImFjWxsQH`uo4qbIk&?Z~3xK>7V}jtaSI? z%hQ*?^r#U${@AK?=9w2IZXJ4IDSpvL;CCs$`<=PzhMzCA^gWp1Zr%EFy6p0q>6bSz zGTPUkJ2idgQxBzk?posd@8-Str1aho9Fgut{AD}{F>J-J&sSY}zs2vyq-G2A|N2RO3G$-AB{j(5a-j^SquKd{DD1Sbx;HRbO`=&dwO1xqbeIOq_>}b7$)uG(y>^P=k}I6AH`2+ zCWx1p)3x_LlP-MCfoVTXxEC&4O(a&%pSP!7{9!{x!^846ld~A7qAXbPLb~Xf zJ0FbiK@cY_64nRZPm;Vdop{2;bkDuZ(~9LAZKBA0PI`a-^F`^M*zQ69D%^kna+HG( z-v_JL{*ZRhJ$@e}zI*L9(=i0w71d4Ie}4H3+%sW_O$QLU;f96jEpMHc4xK(W%{pcx zP6XMO?zt29bt)nw{{QSJ3(~pgpOyAIU{reU*|lcE-FLiT?a=%PIZr>mCLKD%x1$|& z@YvLemH4ID26Nyv-g`}2ditr==@0(o7&~2uwrs}2kZoHDipAJdPyA?X)uHOja71ae z0-|>&fr+}alE3s?@24l3U(uR&&0F8|ffJH}i3g-`Mw}FOO zWdeq>m0aCMnA|xk?5y)q@0mv(n8xE2j-TH5RGK<&6kfbq<%0o)#It5vhNBNhD;0tS69M@u`Lb!ng1IMf%|#zf=+rxp?3! zXAe229+4fymiCruaVTHuR~J_8q3R6HmaFu@SUPezPD&a)#P*SI+1BFaPK_;6SEki| z+d=X415zDJtQF{LA`!llI||ncRa1)zW({vQ0p1W|@XPmZQBfe{m0-&$vaQ&WmVLUn zHCN~dNU}!|U%O_T?dhg& z_s1X{8Hg2lgb5QzrYTcKre!bS_+G={5^`58_i{`G&7zo$uf^9VStj#U(3xL>nljaY z5%`;8@AYXha~(&|t0F zft3h&Clp&2Ej7qyRMj7oH6Xm*()WJiPqbIJ7TZ>aVU^q`I0~5?XFQx=W8I7yLd6{2MqjA!Qm8+0HvSXOpwGMXjMba)l8LJ(K#bZbLF?ZdV zMAic7I7m}fFmXV73H!_$e*1IR+tv(bTWJuX{`JKN7r16YR(%D`7{l~zhsg(B;7Q}d zYk?rrlEMiF-Ye8NosbqGmW7I55Siu&uLpKe+LO;22b!!Bo1vE!%r2igJhTAp5LwcmL~Laog~)9kg;QH+A1(8hw!RBxvRrz5ON(xIVO3YV z=9K=vz+FpvWpd?b!3$nnqY2+}K}ij6uZ4ws3|{`^Y`LhuewZ0{>Af_c~h;K$`7L zf&GeEqekP@GAmEzrxExCm|;Eaw zo_-|GjHE?HzhKgWY|cc=*kedf3}8S}I5A~t0nND_6-b7Bt4zcRtPcQQi zIHR*X5fabf`34JmCk+~rI78d3JrUVck@I<{;!_gK1!(pQ0+Y)bW>9jdWRwl~|J*Y# zz4nOx(_wh&GKa8d*0jlKCiZFH|MUyU8kC+|x;mXYeX4!#wY2|?nfs-uusy^(N}&Md zP}Rt)>3Ma#R*)&nMvP5&=@Zvt-Fb(IJ18efg7Qcbc{nx)cUOR_C2 z%h-ScgTV%a0|dCl8eWyE|9`FZ@3YUj=T7h4_o~X;yXxJw_u6aP|K8`Eb@x5@+^4Yn z&7X|tPWpA1?MgrK?Ki}MEjz@M^UJWB$bJ4p25k11#n(g0L7nRPPqsj1w-|RvGZe{n zOzjnM5eM6KU=PlxEJk6=&62uPqvlq=UB3P5eOvLx-tG9LVJ+UR)2GGlFKzM6qfGpb zl<%ld*cA4Cw459EtF1W^m8)@W$<2}yRsGQ}>n!7_OP!uqV%f!;2@-Enc_+TO`L0iX z6t|UqG}Ax-@Ga@~{r92&g-Z^&>vx(q&ugW;+J3?I^Bx22wiUGaYR{4~uI?Z0m+_8j zPZG=4&70G9JaJ_WcFNC$V(XsgDg?vCiem+Bq3V7B06+jqL_t(RVV(29@%wq&o(*no zbd!=O5qA~7=HrHSKNmwXY^v8idP{Y4NI^R0p9-txpZLLMysqLxJm=9=aLuNS|}vu5=}C5BV6LszRN+?|L%5;03#JK91{I z*nIJe_TV;I{905<}1CFzquui*Ha0pRsEuzkQ#;?C&4?IOD;(z{)yA9h19yo|E zbbcl6!F}8p^41a)@j-vh3!jx1u#@}H{)5sq%f6mrjX32Xa2>zzDf@Ed1s85j2l+&i zgSh@8`Fu9U``Xt%$3MJRaG=su!f}j~ zj@>!eCf_J>t%`ciMIOYgFZ|m2plP@7F5IVyhFB&AbMrZKmXxoqE`9p`N7H+5z1Ot=@RrY|9Z1st$@jb{?cTN}aYygOW?--9o7u1(j$*Big(Q z(Pix;3n$Xmd(Tam;ftvGn0D2{*R04mS`9e^e^4ztql1X?A4K`?o5Sn=aS8%w8dTi2 zKA`B@1=YBWO8yicUAfwxwz2_VG@-++H$-0R&akFDgQuMjCI?>aXG2tV+gG0V-=XIe*I-vrd{|&$amfP3B#CtVl4X3TR)zzzThG} z;`F>c2dG$m)ZE#y;dGSDV*6}7h+z_HLd*Cv$r$K9pUWfeZku*khS52ExpT+1ZT1Vb zE@$GL3M-RPlL>j(TKp^Hv^8YsEY>yGq{UzCL>dqEIQ2GhQy*c_R;#dStcLHBE>&`k zBd0ARVWSatQM`J*Ma*E>6ILjTKVD{7?3|2t^P_5RtZ$zF;;+7#9@u|4{o1d-Fn#mO zE=qfFD+uq2e(_5#NN@S28`Eb#^LTpi-*VUd6cYC@q;t2=r8oWK=cV1dH)2P8UHXw9 zy(Zm=d&%GWwmT6Y^8okdao}k(_(CP0)M9Nud4t#)HI~I~JlLn|8{c$8y5^c4cx2#O zB<4HQ|MdE6)BE20P=n2$BRvQ!MAFQS!l6p&6rqgay@#!#cN#q4+4>E z(5^q}gg)t5>%(!T*?`3RDNhFfQyzt1?%cj>W10g#EYP$p+{_`zZuDb*4IsqCrrB{V zzRsd7-`je7!+>0$k@FpekKf+$Fz~>`#=yGX)!04eXz-m0i@*Exy1fS9Idnos@$MvN zYr&JeL`21RLxZpOHU+}PJ>woH-X%uBoIIM-?#S79K zUvXpFwBBK--k;zJCcpQdPiGqs{O7PvEc@R%v;Wx!` zD*{d=zwn>_F1_iyzb5_mPrWShmI~g7{@?H{@ZW#W?GOon@@-9zNAGS$f9uabnSSz> zH>6+tu@~F((!YfJ-rxP{{dlJNeQu^>%5gQrHU!o<|9|qKFQy-Q>Av*p7hRgZ<1K%N zry4X5^LOdu)okL$77%E6o)v$KPbD%;xiE%mvEyJJI&Xw&tkmp+@iSgKs3F{nM0tjj#upVFpTTPk}i*>gJX<+Mo zztY-0yfC5jif)mke1o(5(7f?cgFpAi52QEz^mXZH-*BxZxb(pOKl{rE((nJ5J5hHp z2OgeJZ~BD~rZ>LvdOTbFn<tfKSJ9?VUe?=Zt?b z{o#MT!>sq=430(ij-?}yZ*iT-c{^h@YQCg2rUOBe+zvrR!zIWc&!8(HW;k0~k zdA#FKznH$~2lu5{eD|g42fy>r(=o*3vk`hh%q?Cb3g-p{4jfp4vjs>=Ezo!xithPF z8Vq5sQC)1qu8h+z3&=>hO57r{n3O_~Kwqs*Df=}%a;x;9% z8<+P;8H+Drr*_L$CqCI*3SD*h@ZnPnxZm*5p+jl!-sTth#u;VM+(|fzcZSrT#}6c2 z_KN?IUhutFaMT*bI5RFskPWmR@sh8bb@P;K!U}3{TiBkkH1iDw1u|*;6UZ0wYcX7a zTQ{ir<;M@D;7hcj#XQCVX$L_g(9~c@aS~VIW_Un~ZZvMv zb}4#V^Iu4n1`77rs*SI(FNGpru`K?WPoKwCcyh)`d@=8MS{U}ron}8RO@l zZS7^m&-@hj<-bMIZ#Wn6vHwgcTBnLd?UEkHmpTv5AF=D9xy< zyGuHLU$E!jri;J(A(TZ6E>N6ZQ|}LZ;2rF z#QJIckj{F1(SB8W5>MaIHH+bU{^y2$-0s{)1-5YNUemw*m6xR-f8Dj|HQ)CRoS#=E zei`%8N9K_~arf8lGQX#N+MUC-ck5O>@B4|Pc6~N{DG&qJ@omlQH+OqbFl@;tzOeZf z+&@29pvXdE1F)98$gVi*Ha^8sX`g=>xA+=IoNvMnZJC`MFE76B^-7th46aA@SsaC3 z@!K@?F+hvAxYj$)h=(~(tY1Dk=MZk|8NBvl95meV$MHY7`&9biUw`k!$8I}5Z|&c| zAD`B2Ni*IVN5~vCOOWmWyt8v&|iA8PJ=dq)ni`-beXhb}M zTYj>gQ1pJB8cbf+@Gcw+@BnU0Q4wtUo@n5)fM=jScAQUa@NqZ-5eT>~lWfZvsl>qK zNzM}S4nK|ra5V`OP~B-BAG^$`M#=>)ll8;u5+goNLzXVV2#Mc2U717q4t?iAL&beG*Tat9lKtg%_~kO z?jcTU%#KDSI}k1-(zS#0WPF_9eQds3G)`%0XOfiic|4)d+dRo*(qbjAukdPo)+yIh zz%)M9Sr0Q!e9}>e+VDz6>6;O>aZc7RX_B>iwdWGNPnp8a4)&L^U2*3w=$TCDM|{%H*Is{zWt)#X<~G?g$>+;o%~pKj zueA>B&Ivf2L78zfUZ7D_CyLYLH3_27B;OJnR)l_IQS;**yu0P@4^XyuxtLhJ+qHLX z0B(TVPOzUaR^f+TWG_>CMZ>*=qQb>jzAH4}$EnQ0TYyWA-RZ zyAAb)WfCDtizsG09C*X|_Bu}9MQt<&@i2Lp;|_wy6%-Luw>1t3=wPIla2W{AO-B6D zu!{xjnr|J5q+Uu~8fA>15D2g$@y$T8o6%Z)Ax=)jn<+jTT!ozq-hbAeS8K<+<}wbs z9l(d{g1Z;+!y-BAGG4Vcj;!yIVx3vUXsyC?*5`1G5FW#XZ@%;6OD~|Rk`c!R$8x5Y zQL!;7?R98_bKKdl3`yZz#qbSplgvSSyY7N4|VU6ocpodlbH%|Wi&!g;lcN9Wc- zS)NODX{CHFIIeO`z2_G8>RgH{np|((;NdR~^Ai|*x(?1;_WsYs{oG}Ni9wI7$E(>v zUB@-grLfC3Q{g_o;%?5ZZq08!aocpke2eBto1B0<;%u9nOf8`D1^uVuiDB|*-B3iA z6`r{VOT*JFwAK^523-&V(u|9HGCXdluXvB%RAsfbk{q}ll&X;)V4ZhsD7T@JMgvUqn&JWj#02^Yi zatf0jFTOJ4WSlb>MO7cWxWlQ*+spxB(vBTlG~jV2a`F4zI7KRYH>P*F{!YtH;uT`l z_uvdjRLvqMCydBNrs9*DJS0V<&)V6vjH=TW-M;&Q1Paowcvs9tWt^b2XT^?&sFNA; zc^C>>B3Y4)LLRO6-gbZb_}vGbFUa`IlC0}&A-`bBv~X~9w8=1Bkc+r1x!q0{NsCwF5vFA#S5U}?}e zDQQgB<0m#$vw*QVU#{ANQ((~Ux5*~6P>hyYI9G!_&Lg8n3mPw|Gxd{ zQ=g3c&OuMp0B zkHfr&9eOZ}Gz2+|r>wI#)@k&Da|&O2#Vx|?@S6e$kEWw~472pRIMKxn%8Zk7|MoLt zHZV)7Sef|vA|tF0vZGIlI2eY$-5`{Op5!x)Zkp;V_C*1I=jA?`fzc;VUZnikib)S> zOjxGiTA};CVtjRqHfCIf37%ifv3UR-baD`F}^Z4Tgu? z*mpl19%J!%c-zb2BXMCcr~}WFq+<$9(1+P76F@g@%H@3b#c$(4hkjl;&v%$@=7IAO zMLw-rI8%R@c0`Dq6`N)P$aajIKytcyaIv4%x<$cbTk_?&$(HR*#R~hTXM$PO<}>VY zjQZ|WDxGPmErmi!ZSL4k;#cydy2rDRY7O*Jxwi`u@n2*ahO5lU{v=d7%S}g(c9J-S z=d9u^?J>5pO&@BDa<1*mo%3sP-t}MZ92WO1VzehMaR(45kMc0K^8*&F?u|vw-GK<> zay{EvA|Ud;P?*w^%-k@-J{%WDa9(SR+4?6gDy{fh=U$o#eeiY4{UK=y+&w9!i z3P6LOO4!yc`*^PPo!Qooto8Vnhv9i#yhiC-3QfepYb>w5?VW#q0zf$)-;nFmr)!jX zd=F?In<`!+J}N{JT%;`O$!#+5C%y@(1*Y(9#esM=4z5LCm#*39d*ID{xd-!$_;jXY zn>~uBw8FY+;l;IG4%r}jlw-Mu^etXqixqg#=DFdD7aA6a>+2Q0#+N7Y<)4wWxIb@k z9`gWjA!Xa%w}z6ATk{7+_HD(Zhu7fQlSlxWg-Sl_-G00OM?&b=P12T#Q$FrV_W=;~Y}zWF zX0|Ur{l&ua@psh2o^bG{r&XoA+-j$k2NR1OTqWhnz&bcoAFNJ#MokSbT+m#MT8%kg zRoJIOo()8dtd{3ZJrHF~w9j#SG@N%Au=ptx^KG)y&2NG*?;&sB0bt#S((t7tzS> z?2_Z1I&NFx{e=F$A;3_qGP)=VTZ*MiWlwyySCDp4HqRRNIJ(#{Wmo!YPfPQ$3qy9o z?@-!r9kWH7eJtwt*6d!j-e^(vaddXx7iVc)eBJ)Vc^TA_UG)rWi!Zu)(?&eeVl94a zVLmOKSTLM5pQ5t46sk`F8yd+_?D%R2FrUkC65$yetIOP4A{_x}jkn^_@t@CauJ8AT z(@bbV&aC3aIK1}YPk6|t%=r_`O2=VGmj%1F(0(K1c4etcU4Iqs3AdUJj4}B|An%OF zNp_W881*Up;BmlP%W20GT_1;yC$1&zQ)qU%p)?qd0%Jpi*a+ajslkKVkalG%m_MtO zFLcD>;%3t@q-n4t6R^{`i6TDdTWbLS>?P*Q^v_-Km(w2N`^LfUluClIHA zv-5pDYBNsjk*mmx{Mn#jD<}u#`tVC$5ufQ9w)C5Ukwv$Qmg$%42|c>TN;sssKcPj)5 zuTD{eAoT`l@vd*6KP)iuaXX01-Ua!)Apyq8@TZ4pH(^cq@p;YhNrB@tgIW+aWkqjP zHBI4rR6RV{|j)#7bY#o3Iv!Jif=%_7>aTGzN2Q!OJ21rNkc2d?YzsH^SU zHXtGACmiK3caC#1VSk=K$mhk6*bE$E;d7yOrZOLLnLp!pUT5^ziBIQ%F5lXU?=(?c zGw@wbX*uIv{z`_~!gPdVJ9lF~n;o|1*l3Q4A?Ir7X?~VU8RE45G%V?7D&*f?DHy_kIN#r#=rgiiKDG0R1Tc(vwx;F=31CMLYL z!T?XL%ulIDhT%=S6vH1sQL@sti?+qpwNHaoWC>L@pkYjP8^0w(4#rLrW>}ZJMAfzn zq6E|#PW>9facO)uJinI2IGJvC*B7rMjeRI*y9|oO8MF&+XT_^pjeZt)2@`+^Qv7x0 zy;~9ZJhrdBO}su|uz`789f_u`A6p;fQy&yYn_Nz9QZ4{oo9$y;zgFwsaOdNij*GH? zqR3war5&vgj*!th^|*7>*&)FN#<+4ZVYa}JH#lxfuaXQL;8-f983x4ML}}BnL1%Wr5rtr0JXA8i zSKN-4wP^TZ;>}(0)mS*BFv&F}eZPOqBQ|TW%TDd==NOI8uBa$jiqn{lNz6tTMF7GH86oLVhv zmTmgA3qgud?O^7?$%C5n=0RE;7KFW)H9C$A|AC0vv zMb1cLu-b7me`S9eK$k!3wtSU#ZOx!VlhUQqIgM1)D#}na^<8=z!yM{biIv5j*8`7P zAJIxt1=#1WIW`?pwWZT-i@y?7@ovE{cdkWZe(1>I_;T2CcxOW7c1e9F66e}AajxJw zj{TfRlhe5p4Qo5!5)^C4xyRUGh|&d{VFo1pI>Qqj7g8=j87jD0t{*l-o*5szj$yTv*B{($BE?nnLBE)Dtt+;qN}+a( z3?Fq2-Xu&;N}9CX!``AuPOLIM@(fH;AHGeD$vWPInI*@|d5}*VpF?Lt43+Z6*L=AP zGP4@x_=$tb+r{17>Ke|AF8%o?ng#%2+NG)~iYh=e1_4qp))VVPx?IMt5qe};3H@w4 zWhgn_$plyyi@%TeR_``B68&@ol&e2ORwB_xU4Uj3@j;$%g5}YV?GwQI|JC zo?NJqDVi3qd)ItGAi^>0^5VQ;@ezpIabh@#+g9cg>nw`i3=P=gpjk9{Z?#^1?b^X* ze68&kjbllQI;L|wFju=vbqtE1^&>v@i$6fa{{SD8V#hS@G3BQ-8mH{#Hs1K;1VM{H z!Emn2e(?)xYbh=$eKS_}=S3!c*~jk=D4tA1#{9E?SQ#;YrZPc)U)$u!|B}m zbx6qZQ{}jAh0i9pc^oc2Q4b^Gr;34e)-l|9j#-7iCfr;Vi>UcOXmDIXxLmD?bC%A9 z**K2ZQzjq|@bQ_>_SIwdd7k2N6TP%+>TwS2#ToV(murRnh>vDy&g}5ik0IJE;yljb z#o9ca5!Xp%nVF+Est%)4OqM9K5`uHba$VIlFyI0=(@iKDz}Sq zXo4c*90xRn1G~ugr>^+CxLV+7>jwRBB6Eft__D8i`WuS*N3O=rUmG(Vlb}lx z%j1o1&M1rcnTj8#JRV7V$Su8&d^F8=oB@0u!k)oky}@wF!B=$y<~4EKN?JWPD7P4n zz#gVMq+clLSdn_>XDnKi1Fi<+8p2xoT{_IZWL7EB7GJd6U)XF_JMyhQHkQzD$aM6DR;JOP+XDei$B-Rq z>DnRw0PXl^0>&aaeQL|LB?>_)qa>zF*~cQjx5mG&!JEGA_~p*Scpmvzo;)~SVT^>0 z0pn?n$kr%0%Ft-T2G=$gFhI<;xei#9#~^o5`Nsx60n+u&*P$=PeA~iW|kK*%VnX_pl(;5Jr4GVgp0*`5^m}hPKJ-^&<@Pw zeCT%ZivOOPL5pu3u>fc%AD41XJ^bZE!wi`f@FnE-+2zIispvI)krzJ*#oJak&vj*` zGd-PgGVTc_1Ga;~K*0bq#c*?SB(OYaKXCIpFlJ-NK^3@0VTW_=pcezTbzqucl14Gi zVv*#7QteWvsLPCOkeVGXM(9T3u5OD5jbYCv_jqWah|82Up`u_~RRI+8#+RI^840`O z`LZ&u4G}yOZPON>v3UJyEj@U%-|ULUXkgOkfs_Y9Q21@zFXRvy!|CD#j?JgX8v2}P z9HhRhz@2f$BA|{cZHvGlqwo zYvE)jXrI_Qm40KnsFg0Gz6m>Rvnb^WtUY(cJmw}Gm2EqAhxAKs6rDxvQ`@k4YP8yG zTWh?ZHd@P6>%`XUrM0-Q5Db5Qs>6)L+bc=6LzQFgiEn5;S!rb}`9cl1I`R~O+LKV} zEQW60xG|mFyk#+}13EhO@Lb8fhL_sbJ-|F)@DDFK9<61|1q!k5%H`A2&*!l6UUIi- zW@#)+({5_??WZ_Du3WU;ulO`0xi?p!g*wMp7F%vPK0gk)b8Dw?kkz2)^?8 z0v+r3Ya<1@o^_AE+LUCJyyD3{t`La(Ic=YsxxdrD@a zLIo212`A?mJ9@H!3#zN6Q1Z=JXL6m(ajc9twg57FS@#4<5?7{ftqMkOyhawI-YDw#seI+!(F#V;(}j{?a?v->>TfnAn2 z!;m*{=(JJEG9@eH5zGRQW-0sPq=cOeKozgelljnmiH2(n1usm*BWRyz^%wFHt3qy3 zvjYyO6;$nFNj}+e)W?kH_?kFFkA!cMIB%UdOC}NT>N9TU4=)*4Mx`KFjk?&Rxv)mxgADpC}u6_1#UpkYS|$}lR+>L1nC+>E2LaXP7v1;(J+ z8AeZMoY}V>WN6vJ;fo^gRB6#sxRhIoHhr*^eihF!>Kdo@weK*}p*D^#HSqOomwp+} z>lXvz@y78mZl4D9iosxc?X>}Gg`=VQqFxL*UkmKa4ko^b!!FZ8McVCiCmpzNuNAMR z!%kX3$=9|BP8Ut^DM4>8so*W>#(2Tb<1-&T|MS1^oUY>c#8AhL7W9fDyKdciWa|nR zoNyOGm`&>epPG5+ImC%!+hV)rI8hP*8L$}viECSY=x%)P(mYWfdUlN^H50j#&#z{h zf~+VOr@^%@!f)na$n1(1*Fg6N93j|zV&ph-GmPzz_dElWQOv?9Pk zkNL91!_X0Gq7b$!2Zu^N7+XeK4c!MQCKs`tY@p65pt)v;6(;E)Jo3x?oV4R>?*>DY zQ_9Wrkt$U?RAyibe8Y|$tJ^FJUA6F4a5vS|QSWpnU z%5f1-F2EAk5N-L+jX(WO@8edq_eS=TqiA1aMvDVYLuh;J*i`|pn_%)uuSMxxe6ac8k8-w+E z;Bts!p{rpuII%v)ufOo$3|Owhs^dH8r{#3wMm}~Y))+1Lv^~9{k&*pL=xY2bD)sy5 z&|?<3c8?)>7hmmRG#>}U1cEPnlh84ymfjufX_O&bYdCr)p`+|9sxRUrw)C%}b}z+6 zME9h2Y3rn?li#FN&LuyjJ@%NA{v=dBhS0IThUj(K_Ng-yyJ9^U+H_#2id@O6@@?Iv z80xx*m%^MQ|8&yNxpLDCAN@N1Vux2dy`=VhZu>>fMjve7ac!N089q8<7>Z9j^o+$W z@>YSE1dPu(qv8XOHhH|CJ_^kvaF3V%$~fG=eTM)YmM51!8WUDim*EVKwP`T?i{BD9 zDIcKg^SE$b$#?CV0Iw~jApF>+zTkKD&2OmDt;x$c7+*7yX4;-H_ zzrfK1@zYMctpl4!@_{FSv!|&5J#UFe%V#RtdTPu^_)7*G_g zL$z%UsR27Ci5F?)c$vDvj@CnKJcx^YY8kGDzAUx~3w#*$2kxw0MXs>?S12XWV-zNl%f{xMAqOg<~3;S6( zD7tY)=jGQJumYeA>yi~;^5EM92j3@~D$dAlA0WopH9*sp<^n?>{+la`@sGc#Sr%*J zMT08)tUf|spg}Ie@z~3@4d3ITkfRBkGRyc`7ar!b=OxS2cp)#osla|`91UJ>Q;&_e zu&^`Egq%C%{9Yal6LKry$&~Q(Lk_qG=9+I0UF*cv8Dn=yvgh_gu7?mMl_q}Y<#xHO zY#!(hGE$A^q{or`jiUM39VIkHU)P@6qbSFp=MV`w3+3&^9h5ws1jkXzvPjNbi+|Pp z(1PmY=;FQ2MpNr;QaT&2gI45UJ!%ioSRmTz?^4sx2WK3GleXnt^26HVcd1T<M;&+Dnc z$t}_-b+{f((e0Zp+8I`&2gHUNi;3~DM>%H9n$5rQg^MhQ8Ale1<~K8kIiHw6<7AvT zIiMbBpyZ9wK}obFg@MW*`2#Y+C>J^8?MvklUWH$lqy*uj8SP{?6nuL(W)ddJ_-%1X z5nK}@&BYg_od7bgs6<%Ck)49qN(>4<{bu|aKmL{KjM8^{{p6%^*b3tSxp;k`pb*xu ztifmsXVzcJgTckecbldcvSE+A8@hkY1t{+Ex*NWSraI&9lrUx7vR;ajPyZT6*_Z25 z77h8*W1N?LUh&SJ?f7_-7j@t5iFf;KL%=TfW)isCiH0L#@sVk~&Wl%t&|;!7j9~PIT>GEgQ&QAy`7E2iy{peFf@=tp~JbvzL7Ps1cRPl1{uzy)g zf#FC$I+smCM;e;E)6UW1uQ{VM7&ey3g9HeFu!ppT8po)(LkS$0k9H$(J;5~*6SVb( zYlbJ{g*m!4eq1}~CyLFVoquSPe4P!t@%fG|&+CmgP7>`-aOule}byMTI#dx&S>i1+*j^l@Cp2xy|C}e-ovhdZ-xc#`KJzk^S zF8M6hhQZ7S(DKWEhk`cUB41g`akK7x1jgb@UXQl$kiM>^5_NvGjKnzDfwr^o%J>Z* z@B%k)hae&$+urz4yotgMiDN?12|22;CO&%`XT|tl34_&+Vv)?P`PL zkbL3a9CybdJHk!!;M)#8;<8gk$Z5T596EicHKNuGLIP8AW#|QfgX{ysmq+PCagM%z&%#J3-F&cHSAM4E`*W^e%sG0%o_# zkdnlV%VCd&b;Vf##Iv9|*v@+vl97~Yz z;_Tgnj~(N+pp634+(z8C*lfoYlSOL%TreTml}C?VoG8e5nfO@5FV@B67u7y!yt@!8 zsu3w8^U1)oSv_u1u;bmW^F1D0R4qGwp2_26GRpupc`1patEeg-OUg{GXS`5R;UQrs z0USj}124lF0hf5U{A8b~SUodjI*tpQ8SqG1;H4|dw!~O*pxiExev@}4?d;ohVtmbg zdS(1D2R%0<3Nu6I;S}ymRMEKA7PX|@0%3gOb}0Bf*SVi&2R#pm{ZOHLb5fIy+#^Q# zku%XSkfjPe6OkNeb9mtn%u)-FEBk&15=J`~wF6L6-UA-}+DUCue(Rq12m%nE*aj%U zggqAZx8lY*K>aO#G;D4|zpp=TYP2rdZ&CQSPe(fOWm}xcfeU!<^@*Oczcq6s?Auzy z9QZK`S6X`|js7B{cGb=iZC0z*>9=!fA>bDtpC7Vq?f3Y%X~a6vPJgDFe~qI{wSKyM zwR!BqA4~7Ci}+8KAJw~P^ID8g`Yk%de>Q+P;5au2DvRx>9?KAYUNe_k=)sO(oRzrd zbS>bG0zR*FAL|R0JG^=>H{GuRt|xK)@!sy{S{!h|xbL95WwhMBjfdmvL~rtKzD333 z8=9p;8*NQsu@i{{f#>V;Jj3T-YhHQH$R)>t_;Nk><_dA>cAFJ2#aEPON6f|oYA&HJ zo|brA*rnZa8OPo5kMmlybM5#w-m+n@SbU10=*#iZEBj;JChF z0^oY#U7PuPRrxvXVZMk%>xq7eACD7K-DAyg=uV+H9xew#81zz9!64tq$=9q*F707_ z{Mv2)WB|&Vcbj&!?Ot+7x)ryU?d0=q_;13%`apyKrAYrpX{`_pY#=@;(;Po_oK=}| zGL9*WizP26goX#9Q5%he>^*!%Tk?jk3-#Z#8*^oHP=tM29w^}_+;)W6>G5RRk>|zR ziilB+!;VUhujob}M_9_cc?K6pruP=Fr2(;F!&a#o6w|5V2{n$}N~{4c&axr0HVwpxNNlNPjgfHZ0d|Z@Rc1oinj1`vy9T^d2wK|yUfgtD{y!U#VBM1Rc^b4 zog6YglA}?@V0FDHAhjvQAY$Is9dR`~=`QcEBYp+bn2A~#5MNCVCdQpZBcC#w@0Prb zw5iQ0^fb5a-sxLRv{~{osbe#!eXG9k#|xz7d1|smU2|dUfWxr?J1y3#_+~&aTAONj z>6m`oDROtFnU96Q)cVoW3#l#UG#rex1m?8Edt-kIXUs@-O|a zf17`FcJJxej(_ftIBCx=+|V%!|E;$8qD6d?LswIZ{)Xu_i#@&K z{&69__DL${S; z0*;lA(+nZSA?)q2L_5ZB#z~IP2|Ii|3@Rka1pb=upvDxc6j0r@JW3i-Y_jHZwmC!( z7990y5JgmjELher0m!*11X(B1Nzw(E^g5GdTqirit8x15X>_9PSP7Az2EflEs*qyD zG-v}Z+u%ns+O}BQ>C{cIv`q%u#v;V5H;c~r!44q+k7&)WcUb&T^8>@F-6w|NXcl`% z9=^rzNuKUe&a`;9%8AW0S(c_H2gPMilnI2d2V1-FWi zCu6^_5S`_?iF0o;Ar~GdFA>vM19XUdULTat4PQ5kKaZ2+;VuK>c6(Iuaavnp%Pnm> z%&|VyAxZ_-rC2n<<~wmOqBsc@Z>of22*HZ4-iA8w#9>2yx^BhmiJ5G|# z{DU7JTC_FKZbv6X^P}Jr*50PgiNrzaJVrweb)cMHR2))Se~PF)+-gj!E^a zJqeYL&ONiBJO!0@=%YLq<#QY@b$B}UBbet7ua~REPGw=x&SgB!VQo>#PO+fQXNB>R zkC!NHv%hT_%tl0_&g$|hYJvH=&4n8w+V{f1d|`=YJER(b1%hiLz8u!<%=)!WZNmj} z9xsQZYYfLlzMsEryl|4Q?HV^C$a0d04W5gAz^?D(34H}?_FP}M+O?OT%ou)*r(2}# z<}*&2jf($wMdL?jkkX>FntSWnN7$@@wmxY9u7(3(SG#v)#3l8iTgiz!gmV{Jr=ng(Jl$a@mz18d&($xu*7*N#=w zi{m!uQ4*J*mVb7VQ3&N~R3vXryx7SI6nt9;Ax0Z!DcT+uP8sn`Q=P*UE(2Dw$ApVT z_S_{CF>(uP9;oLM_R3J`6HiUCH;$xKgCm#90g{P}B)Oo}GqLEG7>%*+Mxr8ygvlD|gzpRferV9yE1ckhr0M4Yj2tP3ytO-gYu3f;!i{r#=9di*AjRTqZ>3QtLlSS*@ zjk3+ap`|#oUzcDwVwnMrvlxf*;k_lNmh{aJB&BcTgif}D4htm^4nF(MF0TDvqYgx& z?|i4W9*l!4Z`g5Hu`U=S7}RMU-^Ku+HYtPR>%;3yeVth{?m%&H4w_L6hlvIu$ka#A zVHmjnZpE75C`r{&+ryI`g9)O64W63f;5c{=mTP8-Ku>l>yI>abWa^>|HHvOOL)-WX~E3i~vA#Y%wv& zQJ#PaCPL4Hnf?Carb>CUZ;TM3GE0{_H@{LP)MT1X)lJSAJ|Vv2?crIN5Hxi(vKdsG z(XytV(lB{ZimGdZf&YH|NoYDMT|qgD$}%CRoJx7GzIY|=%B$T+OTFc+`z-W(U_zpu z{Co^^2c^qLTSf9}FNG@UFKu+~mu)8O_SBS!ueI~VizoW^24pEz@yMRqwmG1SXl6DQ z$xlKXt>vwao`0-wp7U@hkE64QQpez8Y}t7figv!o0o9y~acSUjn#aKd*3U7fgY~RK z*tvd=hDHuUtK0gE1K#XH%#L5$$K1PL_fI#4xS!)7FU#)Fqj6{6(d1K3a#MIaCKI=IInSolKDYTtOEww|3+% z>?(@I1)g@EyYYvuNDbC~S%EEWRpoRMC{EapSzjIK?-6rbQ0d93)$V2HVyn zdZ9;LO`isB{HE57gO_R>H)v719;rSMpYg&+p}W}Xl9O>8YPrB zH6dI0@&k5Utc4Q~au)gZR-Ezq?zC$^C*tAI`tibX*g^!qgiQE^cz4`g^NLI2eqSM? z3_srHI9#HM-z`~k5*P%DwaxkVz_hTnF62w{23`&f71o`1HYSA0>)8*S!I-7&o&9+1Mz)a(;QR)jOE-UO9{Y|-ShfVvC$_3N5JcjvRT z(yf=W>(;NgZi5T8VN^EMul6KV7)Vy_E>-{Oruo>_F+7ASPRXl1YudHnZQIU1)`jME z*iOIBp-PMyr;`71RAZ9du(tT5rN2qqrS9pd=X0ITd1L5OV_d%u*Uc!JJGDpAQg$x! zQjU`Y=RZ@Nad|$mM48=j@&K`K%wwEym$0@fra%@dUqf9A_|WN41Hj!NctfCrVh!@u9`K z;t@_6&(N3s;43aE=4&m!+dy-6Pd6$;o$Hc(YX{#GXfTcMG{0&glfl^Zv>6()q6#Sb zy~Vl)5>gpQGh*{i*)k4W3j7WN$5!Z5jx5rQ59?Npi&{_^@v*x9jDN{8gV?FLfo?@@ydGaHoHm|wox&rSZd);)IC?bQaOssx zK7Mbw`I2;Knob}gw^d<4!nje6F6vP=8Rrg{ZwW~ll#!QzXxd~FeEgG|K=NHA#L88Y z<6-Q+8?8xq4~@R;m_e{jSJZ{+Fw|&|RlabNMV*($LgEx|`gVCzE;FZ;wzGrT3SshS zk7HK+qU>YcH1Qs90Uu<-@+& zC{;{Z4ASzCO|-R&c`&k@lnJ@{33=JK`A1*3$ZRWKnqWt}aNa1!`J(eo4KTL7w5Z9; z4sj#CpoqtpiPP-QSlep8bKvb-PeE1E7R(e zC(}tJ-bzsVRiZ)P-8OlryD&Z$i<4^}#m|9*)H+Y|izS7K zK?yR$VFKJojoI@edDDHknMsSD$z~z3TZd zT=MbzgFka!TC-|CJ&lAMX8~B}t5XYr_1yFxE?oO(e7?Ad>o%{>cYUsP{+TUyu%MmK zbylGO*m12jd3vHQ`S!xPPTVGL{N`3w0vdKjDF*vG$1!%ELtT)q=qHIdbk1}+=bL{B z*iJN*%pUE=aUzqn+x|xfMX>>y0UEImzBc1y!{tl9bNFi9f;K%Gr(Gjp(EjnV0~NdX z9Mp1`LYvlFg^9YkAQjf)m6FXUHIA~kteX$|224Pv6Xq!vCP+k07;k;4DIzTNBdO;n<<-Ol+yZiv(G%RT_J-;Y>)xMl(3WzUq zwuPsW7q{P+KKI}w zY2gH(&y9ZsAya~y+eln+*3!GXR%B{8YHB&U#JS6FB#N2@ePW7^rdrtHnDlPU-{nD! z{iS*#-xvwG7Nf6G#3MUgY2_ldmDhF1xLmAF!RNb9ozY+fDJ6<`ob-F z6zh{Weh6xKY?@gdk-RGLL{QSsNjS9oNol{7Xz}G36z;|sm$!9T*ck?s9mKH^Y*^zR z3`p4Y3~<3H{9CsibouDC^=Z38DXm+(COzk}UFnrKUz+yqIoFOqJz9>`E--!m!p^|4 zd)dD93*Y$<(_h^B;dICS_ofpkPBg~?f|qZuwS0^OKfB=|%e9<4qOz?0m{YB>e9#I^ z`H!{OjVrf1@|mwnx4+ZQ{=4!am;0qB8uGbc`d!+Q?{?>L4|D`$x%G$nojqUYYFSph zk4i@CR_y`0#P!*=U$%J;DSrud!}_^&&4m}G?|kmfY2W#KmwfzQchm0l3tLvDzj)VU z>5e-eN(&2q4{+}eAl6B7#w=jq$rI=5VJA^8pD5JN%dOyBx0YGKH~Ag~+3|C4oM+;^ z;%|6e)7%B$uXX0h#^JT{)yLQ4&V@M7V+y^taux*0`E227*IpB}OQYR0&3w+z(XgO# zloS#3zU|dW>h#8gp)@%W^~rF3_VpEy&)iyfw{1+0Q+7xdS2j2tC))D~&cZ#F&Uz_e zb^SpVD3H=iEa zj&1yf1$+$839F?dP!X6F0zBUM=6Cn*P3eO3x7cyV<8NFN-u2&m>|Xc6^vHRa)nm5{ zR&7@VW`sa<{O(Q{ocFAm9l!f7J(7Oll~1NeZsN7~_}%k-)Oc**v88_bQNBL z>1;&4aH~^h*G!2`!$_*MHD1?QT~J(lcPi`5HwlZ=>C#NQds))iUvybP-$4Vhk7n4!$7PgIl;!;$tmEX} zx%KiHov$^LOqDltwt|bgwmiy;A4)Lw#kBhJH?%L>+`nd=d+H%iz~(yzdHaQ5S@~kf zW7^^0So%*L&bp}oU8!TyXt7AJAvwp~S%RrjN2}|p-a1arI_^jH+g9I;_XI1eV8ka0 zv&~bfnQ@XAMVCDku38L*8%vh{=06*g>ggf)lLVEuF~=Z9Kht7!<^r7@!IsfW4^fg# zuE&@RGy)rP)AblMIA6MVY&yVl=4`~<)(y@pi4DIVNqaJ$D!=lu>22!{%ZIMH9e9Z; z{A<_N6Vsy5vk{ozKf+w!&v#9YOIvXjaB^#ykYq3wrJW<#&=>7l{rI${7xNmf0l{Li z&$xZl9JQV} zwfq0^h`m*I!F}*5nztc)t<~au1Z5}k?6JoBAOC5zHSTn})iqnOdFn9a(Qoolx;em- z=SKZ$LR)K}|BzjI$6lCK=twD1f(d5M{J}ptTRr63!DZP*m0b<9@^4Ee zf8IMC)yjyFL}&Fo^d4B)vFZzzZj|o+?t8qJ-Ag0EgIa|_O+mlB8^Yd>7S{i8h0*)b z)0X$Im2}h_wrctzH83RP?j&ZSVaxEO__@Iy+~l4FldCAx`b3EVtO@^pCl`swV(JZK zU}}pugf%4#E05Xy!!(#Z^FsjuaBr-w?Ut(l>OFWv(3bf`Ewjz0}2BvwoqW$Whn(={H*8SxC5rZI)Lk(GS0q*@iv%vE82)Isc~@6$c7 zR;j?m_Ctq*sRgqCiLOa&(2n@XgD}~eEFWxY^2GiB3c58Twv|rXA7$v~$os%f6?L(* zh8-bWYX`eTXaTpoMZVD|nBJ$i<(lz^o2$dQnAOvl(c%fhH|?~1m6i>%m)^gD+g+S7 zv$+Hanx%z=DLfyohAA!?c353_%N)`@_Bt;)*!j}ndv3fuPX4qK`5oWg?ylu@{aeR( z`q$=T`<+j;;A$N1O&xK^9jC_gLgwDEuT~c&JUv51D|+j9U8E362c8u1gKb^I7`3J9 zzXtSk;>EI6r^kBA(Y;R8ju>tXHx!wwxP?%Wql|YiD|#ZFz-vx;(B;6vL?O*oWBVYw z+x9aR8Ve#|61_1n9$V4?GF;R+zus!RmvdLvwOs92{#+ib@{w+}g{I+CuBr4W1GcTt zbg7fv_?a!*O93!HIOL#~i*7~vQS(Qu80s(06l>gh_E_>sx_liVA+cZShvO-mN*5z| z1?vbk)^~B46#@4>6?jf7G4JS@yNU)2!VhMHIX`zfyq#bx_uvqm6{64D7Hh+yOZ(L<)f2Crhdw%R8Jds6D9pqZ(%;QgrQmdX%+YxH__Y#*0S?ZI>e| z!app!PZk-uD-|0}ZD>zt2Z!|~iRg}$=XPz-3EU@Z4;oX?YTF;YA{Dm$h zuI9GAA0;Oz4|2GjrOs?j~ti{eGZx{|1Lky?}7=G>5Zd1gF??$j{f@~Dt;IlSl zssb1c8h^Ax+XqLXyTG&oyHDBGj*}frW9s)mXCi%$={NaQID_ks0Avn(V*&!x`ZT1y^%u2pOhFkotoAIrN?{S%t9qa4cjc=01LZoM&i-o0_1Co~_bPztyh9fhz`$2E5}0+GPf+x$yFP3a4E^XW4?acPfz%p)LV0(}anYE<5gwu`9eazV+f`{J1{D z!~MtMxvDZLm~{4qV|`s8t1a88O1=o?{?WDihAuSrgz6Fn$^{6Ny$+@?-@ZeU zKGv$BC&IJA<2TGOKG{)XZ6Z?g3xanWb-RE3*nwVYLB!bpyjI2y7$lwOQj*Dp|K%lH z8RwX(^UBUj%3ClE0NrK2LEBYGN<>^P~0x; zlckYYZ!XnXfo(nN-GVqjc{Ox}!d2<5qe8Vuoj;r|(Gi=|aKG;zs{3uS`LDJI4Vhht zPus0HOw`crt3|ZoIaT!rYG$$Z=;mzL_G3a5(a;Z|XiDOhjfRyT`;@4GNA1id0_DOn zI9ON^G>}QwNIKvz$lmCa8RuQd+ z$vCSyo4(m*pMy7Lfkzwszpa*9Rf>83OgxE&SF4Gy_|>8tQ$frt90G>mgJF)s1|rg1 zPnz4Bw(G8J;a-qe^h3kYBBm1f!9SgyoSzLJ5y*#50cMZb-k%s3bhl7j$fNN6FhBZd z;4=KbHK%3yjCtyjch?SYZRWs6gdqKG9hu-R+~w4E0s(X0Yd|?-=Fu}Z&WnPKgC4|R zz>;?v|(Hm*yeUOCPcCR)LCrg=uQV~uoAoIvJHp<&;L32s$N4P=dX3t$!QF|G3 zP_8Z3-QzoW=J;=hPg~qy$JERoNkFHY+&I8njFjNA6(<`e(E5$vR#`t+{2`9v$qqAB znsn-lemRvMqa4e%Ci9@hfUQ{{RMw?pMhprXtM-GVT!Z8RMD(mL)sU(l*q3-o9;PYt z;jC(k&mJ&kM{}<=9QSJwy%d|pNAOQHG-rL_O8uA^_oPsh@E`_+spbmliTlD0-}*Sj zuB@rUTX$rxx>WHhH=fnNJ)E(tnj2p9%Vb_Hv9sgs-(l>sg0SidSQX=5}9BG)N;Q01YlP`r= z1#aBO;@_u&bNzdp@9XYXfF}aP=4M-x03V*GeF&)5mEdmw33fDp7WB6vkzaI3TAEkGS00dm4RU}s zv@r^HweDYjnlvSAB#kTFy{oox%B_u=dg^U+)Zgff0fw=j5;Z?35lcTh^r5E=1;R2u zn0)D1&rnSs(%T4d`j8nk)ihA;KKTR+v{tRgW?oO6u}GNml&XbNGtyr;^alayl^DIt zE3&o3`s>k3R}j?`l+~lpSt`e@Xxts={J+xpc((f>uqylz zopDYgyPzSgfvAE3A_gnd@0Uo5t@PR8o96Ep>CT;;W4o<3Zzi^k5|=MO@RhF%axR>$qlgvt$r)b zYw7ZogwPf>YSaMiE6uyPHx!M78+SFSF|e4FTJsYZwfWtd9- zHJUOXu=E368~MZSE{Xk zpf!h$76Oea2*l$>>85{YW>|2sG{S^V3;bJhj9To-uOpLI7|)O9oYAz(daT zpKGgNvm>f+S@Gmy)dCB4`BsiS=5zdpgE&x3!cn-Lh{SN^S#m2)Ui~m!lUj~o?J5*_ zKI{>6i&nm_%(wG4u(jGtZPut#0r*-3F4cG^A7VkA4Ex@z)#QMsRIbT^J6rmr~t;mdPLOO1}ogF%6_-uAnNOR zYZn96hz3(Jce}p{Vo0&c==IVV`#k1+rI$^)@)Jp)4dZ30BhHs>&(&0qPUq(%-ch31 z__z+vB`lpg7j}Jd!QXdGT1tB(&as8c*Q{a##^o<{q&6MU_nXtRfotFVXCGJ;x@I-_ zP1B5)bEe-5etMBcWK@Qd8nMCD|0~GE-)DUcIAGU>pY~ama)}a?no-Bl;bQ#-Z$ir`V?0l{2Su_n9 zRT{&?L))Rv&&Dl{w`ijq0=k)F}f48fZnE}|$#N0k! z?V5qxh=uMV#4+&h_i?xN329=N5Z9uaWx_%RD)#EZCY!guYzw{f*H@Vq_G{w}TXbM3 zKPlO+W$VJeFFMaSfMd@*Mme5AM{i`~V{e&aO+M?Xxd0%owmwJ zJMQUKv9+hLVv??iV0#*%P_NgHB-xT~V;eT$H_`>71^Mn9e%}@(s{szCcdpDSBVO3x zvDP&;ZKKA@8HW8l4+@4xRr4Ue;l@irCxSw9?wICtZ*Ul&LHaG4=ZB9B1VVtFA2CeJtTK zZbF1ie+}JmiG7yt_r8}`y0&*x+G`7etH$IvdkA#*@L?owFRtBNo|ecTtV|OK>XAjJ zmDKNzeBM6QQ5dXOcqP-h7?OHn;LLj6cy!dn!cs0KEhywfMOa^c~nZcMWHmG65Fxq-BLx&_NkCo zV_R9v?qaB&;{;?Sw6+0sBtH8fXIx7wV|aq3Jku!k=zqKh?l-^}s`Kp6yvg^xYhrRyxWxte z0D~vUdPH|eT4$6!wSMB{K2j|r5*skN5orSbF$=XVjJGPcc=T7?htaj~SKNp@B1K^L zT!|T+8o#{Kd$|C?nsqx!#i=*1Y5J8tQqqi4NY{_ra^X0<|`(#iOU!P9Zg0s+`oQ1#}AE$}vdr-M6jMusdJ zHYgO45EOuR8>y2GYVA_J${Y#Si{1P@er@pct?JyrY(=GDM9M6$-HFHWZT>rHPr^>u z&t~m-g`E~l%lncw;b7j`av`V}jkD}B0j_alL?BJl0)Wul+Sq)Qy3xlsqwfM+b;9CF za((3V6{Cl|u8GsV-~W>VWQ?C5%n{R@aYxQz4-N7|^a4ls1N*C!0ZT8$^p?}_rKn{( zhjlKT-(2!YRYNmywTT~k5hKYM(u9I+*;@$;fyan`Y1H8^L8JX`Lwq=ot;cENf~L{x z=;j-DTAIHE*laGef+=XxkQ*K!fB9iB9%c&HmX>##O( z-I8$c@{`b3;F=gL;?ntuGA0ieyUuaYr2@Mf)2Iql!Y!zYL~(kVX$*A5@zKoajX&Ol_+v@656rFws6xr z?55giGXxla<9Du1aXu+RH))KRI<$oqA)LaX{dzRFryMcYHr{KZEBZb?KMCx6&Zn%( z6z?+Ds0q`~omvyRXC}MsX~QvdSZL9Gwwc9samnk|SjK90a1i{tUhJ zS)k+Qns5NQ;tHc2QT-~t+Z*pUl(OajbaPAfp*5bdDmRh_9~Q_-dKyHY9{74Y^OhrJ zl7Nw90FK{Ke|=@8V)`*}uI3{EmK!5 zZE026+L1yqrm(1p`OgaA9k8ceLK_slrqcI<7!_%+gZJzgVIz#ZtK{7d&Bzu5*``Z2 zLBE=-0@*Y}a3s;Z(GPkPo^GYyfL^-8?ypXze%t?-C8JKj32pPm+{jE~wZc?>C2`i> zB<2&B&&ey7t5vST-R)}(MK|>K)cHBI)Jng^?luPWrB*SHTlQa_|LlKWug4T`X z>J_6{w{-vi>3+e#Mg*2Q@Y^9yJ1nYTR$#{gi%JlgnH&sB)7Ju4r)4|?becxVLj@DH zJ7~)UUB!a-+HmK?4ESl?r@b$9H*sMiMbc-zH-*RcL$qlLhv-V3-|hSklNd4!4v{bL z{J!Y&?;ATxw~7M)mU)y2!_ffEFSRkhq=lO+f3w5p5Bc<*;g;=<`(l98%u(|?k{qM# zO!*qe_)2uh)K8}}oNznKXi5NCdm388ubR2Su>is+yP|2!z*CEiKp~Y~orvOZWm{q zi%Ui2=uhZiauK{X><|%82`irJ7pfN|YyC<>xOShb9~$J|5=YjB1;MMF-Nm~re~?$x zIPw_rrU<4+7Lj7C9`3s~ZvgDPs&LpM!$thDYRX@TV0-*^Yg|xglFqYr@AJkwU~ew7 zogkwrW9e*dFYDOVs=%N^jcl{IaZh-^e{9@W%p05G1)7ZN6mrfAGVC{ki7QdnQ6me~ zD9KBOvDaS%u$wI$v)N#L(Yj(lL^wXG?A#J3W#P3kQ`7}@P2mwSk(Ehi6EjVrSHUoi zpnO>k>b?4%MT0Q#&r!yCH3xW{&k5Hdde#>Ei;@&Of*+AFIeI^Q@12Fg-^g2(sS?R+ zaY^}9f+&%2JR&`kAk=qA=V0}kM6z#e@|ptc)@Ggpppt9Jyu~AMW-(T#4t%4tOZZv3 zK>cHwo?YU8Pte9xQVoH;9Q=?xRMA16HEc{^gskUj1fbozEnggDAy`nbng$r64cs%z&G( z;a`5Bu+rgo(k(gKbNd*l5`R?kdi5d1{%UTZP!_yS@_BS7_&DxLhe$L>Ry8SElicBm z;ob||ZX}GJ<+1x1xZrjz_pM@4;cVVYD7FM<3L3L0f%vT7`yVMjU-^xZ9;-Fj)N^Ue za{ExszO<`yXTN{sm|^$fa!OTfEWD5Z?I*@JzEgpg$G*8sQ34nRjUkBUFtXM0wc4cExE4)5xE z8V|Sri|5mkzf*&5Tt*Rj0?j_=_&&W9x#8$Qspw%;oOVsh8!PqlUDZ?K7%9mIZJvi; z7qSLUpjTE#5D2e^0B6CLMKa?UaEN6^(bC7Ey|!{k%t^ai1r6Z*f@jmYI9)ZOwnSwgWqa6nEg$|{bs9sbM+NR?6wilYYyI`KNoJ0m!Uv>F2*2H;~E#- zH|wcQ{QV(din+V}2V3l)$x#M`*|=?Apte*Qm1B6gHs39v;0W48Q{JJ?&hX;18_6^%-Y*qw8XJLNeBo$X~Vc~vPY zEo9I{c^KDbUg+>^v~F2e9=DJWmg^w!@z(kiI~Ft>z|~A;{O9?Fdt{tx&(C z_vCML`n*$^k&s*@i1TVY)TI52fdH?iPf5im>3a>(#eo&rgk zMwTt1w3HSp8Xs3GNPt^C*YW6@9j)q55{B(<~~|x{Rh|Z z`)fW=Fe6q+D+Eb~Ez41Sqi4}uMrg0;kL*BHx&I`chG}P{M6weuf6$aaK!jVkrg88q zzY`2!^l^6=&x#-aVNr;-3Ghs95Q23K`kwD1l!5|7iNDec2(Z}{!LC8VkwlP6)4?d@ zu5JQxHd9oE1jYe3z}h zXI@ho(07iZpbm`nQxAPjS1NtcJexH_r=Q8g=8t(dk*QfhdB^2CPm(TOMTX&~jA+dZ z9llj1AK#FK%|rf-UFC+;meWp+96!8X&WVc%5}ZW0m%jQJ+=dV zrKAV}ZQy=`#KQ!VCjBl22|5TGJ?_1qC)%Kik4L83(q8g>LWplRsf8c3kbh+s8IZ?i z!>uUQLZGG_5Y=mtW(*IQeLGV>ZRz+k5J4QIXcM%M&o!{a$%R7#yTB$bBn$AfMvKO6 z_fhL+lCJMTZExSf_@_~eY}Iu7D$m?C<1xwsO^tq;)&Omh_MB~W)JoHC z31DtNLf4R`P3-V#5~EUSJoHjcPZK#edDzQi;O*5xHO=Te>dz`3@75BnCxc4Xv-dY& z1nu9cHp#CfnMIl|eGmV`k>G-h&mReSD&OeVZ~ZnktG88Cs0e6c;wEjfQ_6?~lNw4i z;yN;&8+XJ6kiAJAq5FF{{N%Tbo4HOOg*B>Zpx=g1(8o2YvTpQRO}FJvgHl5zc`>bq zN&R@kTFV{>=n4UZ0yj!);Kq{OUQ$B;$pI1DFN{ zxjt%u2i-fv5zW4>!N9twlOZ96)a`N+ zO#H!?Hwy5OU7hmuW4p$jGZ}*<5BTHF4>-t(Sn+tySAa}nc7uXV*z{QpQ5NC6@@7&@ z{T207wx71!+?t=?`te{_k)?K=|7sTn{d0Ov987vHPqG{m$aeF6zZ@D zW)ew1j%o0wK3Zy;uUN6&fHo5ZFwwTuiYgAM(gwoUm@;hv22;kNG+THOE+5lnN5Ok6 zL0Wc$@X}JUh#;etgK1r_^4poG=Fqf>Alie_*}i5GtVWP7{2Ej~tHsr%UZ{b<0mOVR z^858GE$F;&uA*~tQe|M$Mx~mptf~siKU^x^6-q*U^>nffjArv z-?Uy~57}xtO^fVQyhV_&}@DQE2${=lQ3^qs19>TN84j+=daB(=2)4WFFgpMIT`r`?`3<*h{mjz!Q zh@IE_eE(a;ef^=43a(eo@l2+Mw_b3jumLXh`Uy9lMQm^eIs^q09#k`>j+7vL zAv=#xdp^#-VaQ=A+k3Fqg!B~(?b_Ny1kpxHRe)tK)Jxa+N1obhXQ(J!w8DvJQ9E9SbK{lRpB1#0P^jBS zty~E?PT<)0Cea6~Uym;A?PFyxn`rJvh5_O~2zeV=Zu79n==F4}e8~W#(ybf?-n?M> z_a&vsG|RK(nJp~xJ>cWb^juI<@o4kIETDaD0Qc&pG1&^!J{U5v(%QXGigVT_kK$wv zdI<~j#IOiXP!Tpre@H#50P&vV_IPO>CRXe|%qRENs|-$EhR51HHZ#49^0>^hy>IAY zhYOmla|iAFMn~UBDhuoL&e|6{VAPUq*|H6)mJXW6;SH>2>iDS(rQR|g^!MR)t>o>{ z*D1{o22U?`CMiPvvU!%gm1(D1yaYp_@cY@&%m8-wlcN>Grb=={TVmvjoVG^>vfTS0 zG-*4@n`BQ|9h)vKQw`L9XYyH-`rMTtmzgeI{MS|H&ni`W-ut-C6$Gz*p=TkaWSlL4 z^GIsd@WnU$c7%M{TE5NZ*$y;$bFmWn4jcUPJ15+t5z7^{0pH#1g#oWkQVup82Hjie ziJKy6P2znAxJr|H>lQ!Pm8Cr#Wx~on+``LO@vH{yqI?)JS zH>${w1`(FPn}LOWI>2^zuzGV-`HK3+lc4%nCtI{$J9B8@YWFj;CXa=^Aqa?dYWj>K zXC5cpn3uKpYt#sZCY|r%GcRumg=m~odt#^f5YEGv+0IxM+qkOx;XFG1;G(@J&oQzh zD|J`Yhi`XV@!pv)$2waz^ER>-s+QLYBkqan6F5}Au2-lASMr(5yWx|7?}<&Qc8-bm zFz`ywevb*HJngs!Isx!3mj~6ZO+M~Ad=h!()6Q4OKMcIcWDu`4`AhjIiK; zBscF{=f+3Ap&oUGEP2t0HgjDZwuMsd?7_6Q#=X78mfCAP1BrbLU6ZFAXYsG3H^X^o zQDEiWmy-uyW<2cfy`%fB)5Iz)YEeJo+e?Y?`7Xxd#;VDQM_YreO#`A>)mb_D{4<$5vbRDJ1{VWKYp4XNv)Z657;A2;JE`LpzqKR>HCgbLjPbTm8 zTjc(HA`LywBZh!H27ZHSZhcT_Jdduv=@R^Sc|Yo?Cg-uWl{h_fHG_`N+Vbi17*HPJ@X*{G@9UbiFKb{)SgI?R-gr}7Dp;Y~;NjM9 zz8y7OW}LNj?@ca_jUq!vWo8bwQy7z%(5XS@k6; z;Z)NZF2sugIm~-}SQny#$jqY-cm# zX?c!$uHl`P_K)A-H)gJv?fl%UKd~G7JZ|UM8Ew;SotLlh2!|oTm!~}aCMJd!Ki4R0 zuIKuK#Coh*k>{}CPO}rF#ltu6j5(07Nc0>*Nytx$0q2_Ln_4Ggmdri?wmBd^vX3Iw z(%weO;7{NWb_ft(aqynw=D65*6IJ|EQXQ#TbMkB!7`zP#>qD~#Ep|Glqp){(-d(&j zXSi$#Ik(s%6~5J@Yw$}Mj;P%InR)(Tu$`0^fGWLODk(Xfh<%~cHTkHD=FGhu<}t1R zFoC66XGoz1L;og&rrC7o{-Q0Q5wGwz4WH7ak`jyl>Fb-;MX@1N8hxs#hHKG&Sbw#~ zlwElFN0;Jkw2BNVCHdI2!@^AzHP+79!35nL|`YD2q8GO4+fX~3xU*0R5syE)=s z>I&%0Med$JLIt+UHrP!dY2>WlQflaqCPP--+%Mt;-2tww>GCv8XG@wAJL zJrVwV`S{uH>N4Ns88f!PpFT>dHbkb(>pEwoQ%+?HQ?~C@b6Gp;-KPKszo#>W>@z%F@_d6R8%dq+twKMaA1JyQOtIya0hXqwL@dM{gz1P~CkIak8Y1bTGlq=FH!wo$FLp(=$fb9-pmPG_x z$6NlhdlFDsAYaJO$UT{sl+Qlp;7k)}5qO9b%*(Pe>MwZmgSi1OBe6GJ1ITW2_UFdpV@H6F5zwowVg&sD7~W)lv;RF{n~>UfD5vfq8gSJ zOgEd6HE-4V?*}JDx06@-Z)-9CzUEVI=lpmAl$pPHJ*n5@KKx1{QnIf*B*Qc4u@rB_ zwxGQH8&CS3sA%8)r_i-T!+X(P4^nC6f;8KiV0D%)xnbAa(1iS{B6k(R^)7l&4DQC# z637JKvm^bvNJR2KJif?8(6b+Y<8%Ao4=#TJk>c{=XERi7-s5{w5xb}t2pM&sIhmghxVgF4-w5WfT1F&4t~>}P)=e~M8mx4Xn$ zg_pi>DUZi<_{n>1t@(;`97M(jEb+3{KHcSVKK)e%NX;wX!9?23AyyZlPZ_xGOur`m zVpY<9MESmLfIH9BP_M`RHvjNSQaWBa{XQA~iK$F^kd3_UmCNVrU9Nnv{-jI33b@<$(5(b@=Kh#jxwFbd@DldJo6LlSxZuMH z&KEzIv7vi_jn7%G5>&JvWuU&zczmH4orO!G)rUAbq`F)Ik;;9I@wA=L98Xgf{FSdw z)npV9DM9|}>ryTVB%08R+Nu4i{fWE7X6xp*02gcHCmJd>TI%-R3etw2c<;4b9_$Z_ z$H&NP{|s0y=5r?_Hs9h3D!;#-x_SPyvB1n&$P8~W)0-an=INGxr?J#e_86w?>@9X> z*qgUgbAc4ybWV;wO#8K40wp@p7rju>@Rw)VZsB6T^S2}vuZ9l(!Xe(OMqcDtA1-yA z7J{2iiO3SmM|(e}EYSzD%)&^h5^DkDg#ei!u)4CwepgTmV=u<{dI_FCsO2$ps8r_< zTDsIxqP zS@?bmcAOuMQ98E>n!|+Oim-7}Vek7v#W_2IpnM(?%8s%%w-$Fl{yrAGpSxMKwL03i6w8m2HC_0 za&}`aiSdf+B^96-IRF~D21nCv=}jH*%V~iXKPqoy+sMMZ_w=pp%0Ng;x;aADseNz% z#F?T@HatslaTe2?TB^$zn{}gg!*{|mVzzG1Eb+CST;jo=^BoF2(t{OzUn0B$UyxgT zE6P?(HHJkjrqd?Nvl7Pj)COg}PTmwuWyp1@S#?()(CPTa#QF>dO|6a6Bre^cHG*$7 z^f%9wHF`XuDVXUD|6s$*w1Re4={Gi~{4C#bPv-n523%(Kw}>m$XS;Pi<|TiZj}uo& zRvxH`Bv{c6W%Vv1MeVVhDJSqu6*OM?8Vjsq>uYaLhc!;Qf(lb{A&{VwqFkQpIl;p>dWMmK7Krx?&uTZn2GlVTU+ZcIXcpvO&Ums z_0v`xeS%ekn;efzJ4=t0UoY`Sqxj#^qDglh#ZiycXpiKq-n4UAd~iuHZ_HM4r*7KG*t2X>7sRo zyYOOslZld5R)Z{>4L?tYy-P9Y9tI`s!y0hxXfE-&L_xHYdA{Zr*Ym&>?t60$%4}{tTcGsuA zH~02VYxea4R~+L(S>znh@CC5c0`Bx~tFOmO8o7U~gR(zxCn1gVH36gc1y=mH9AUJH z^-B&bQ02Yj`G*GE1?Cl-I1a1Zr}x7+W_-CD?+Y%f8FSvc>#TUy^TR0Q2P)V>lXLOz z1I2IEWtzfIGtQgJRu)Z`ID}lN`gBtnyE#l?+Y5tbDG56N1_X@%Iq8gOJy@Gv>==+h>`YmapzY=AXTNGS5GokbW!WFmK(TCHM#Lz=N3?oif%=m; zh>c~CQp+#%U14smswXS;_wz^#HJ*VpUc6{Dh}fIEzn}+XpnQk@L>L z>$?%`yX~W;6({8-l4Q_?wyQFLhpZdV#5B<5PIl&2dzB!}aVZb}n2WCBVasN24!Cu2kVrq{`Z4 zc>v{;b=GpJt&kC@*V3x{4uTjq6u#>{GdEa$43nrF{`-T{CQ@E z{86~eQ&9HwdOQnw&LmRqktjLKI-wub3tSnk07&D%X*%{Po^;;FI-3Xkp{G_?ZyDWO z`qM9|!%Q|YaZBn~-)k>C_NV)(&?I6t$&PW~LyLRMrER86R$cvGkK|VLU3>Marcb#0 z)!3INP0h^0fyy2A5qwS;)ghK1&NvayKH)X$Cuf~6OBiV3ZZS6diTF?^EYwP6-)Vmg z)|(+TozwW-2z<|r+bvu7-r)Y#Z)uN-+Ln(-e8Y4GqX~(g&A{pIuR1;Zvoc|(L#O5t z8B09SU;f46pGZhyHE}xq?9NxIKZz0@L;Jxb+&$(U4zbcN-T}=H;bXa)La9t?OHm+2 z^ut?M+|nONt3S=v7*wI|ZH|T)8HD7aUOVoy_lh8!jra(X{O!=L^Zeo!8SmopR+^Ho zTSaLUD;0vs%rD>7xz3QhBzh5eP_`!1B-5m&4mcFZ{zw6^nO0hL2EO{`)w;s@jbE_+ zsLH>x6bR;eF5bc%;Ox~>pMBCsz^mN$xK}QL^7gIjS6hEJ4i<=h)h2S^iufMm(bbuK3c6@T!m;SYv|+D z*l$8`C2jh-X8ZdR^GnK1oc}AsN55sL9l1MwVY*MYc4~KEupaX*{9goczTk9f%S`H( zTHl5H0u!`_6wj1=h7nd%8So(yWW!f;5}iognYP$a06Tarfu7#(=4I^Q4{}jfy}mg4 zp2MoDQ7)h4{O}y55o~r+crIxBUBgDE9kB&$-%z{f;`;f>AE9tD3Fvm>Ab?9%m14~f z$B!LDOm{(6>8r(C&!L*Zkg%OP*DUa1{R=-8oH4Io+9k*4pWAWsd>^_wMzd~BF8qJJHe@Z#9#qWGZ!{)!Xo2k^M%Owx_oJ0+EN6s=d7*E^@qI1lq3V zqh1mB13Q11_@{ZP!m^rimIwRMNbSp(F?i08|1o;@IIdO8IKqc3y{CEYDqZ5$9JXvp zL^GH_x_ElEps=vADb>pEA@gl}FXlZ|&}@BxSNkv@bxQzraJdcL`Jnh8+i5S%(r*=~ zeaJqV>rDiT*p}>=pwemUr@1&$YPa=k?^foA&NC71S%>IcuaQv3Hk$hK@^X)i6Ck1g z@jEIpJ~GichjO$mYdkVOWn|zC#Gah79L!-Pr0+qrhTn59@6M#g?9IMH*?k>yH>FW> zzZLw!??A5f0`nq;}>5uIxHCS$Vt=UCEzp3pL3 zWzg&x`1xWqjAwn`QjcN(gP5U zon);3$62KcMwGqt@W#z|DeO)a}ipvUUUS%dY$lxhTk9TZCQim2WL%O zTwT!t&)LNO8-p_5UV5Er3^6os<-|Y8n6$EWlk#u=pT^}%nRDuRPr)uN1&7lZOAGl^ zD2;=6|F_jv$rr7R;;@s5p*hBTy!$uS*Zp#Hb2k&zHlLfQ<07?nME{3&&WJkh7d26x z&4fX=>jJcNI`RUVc z#vbUotU$P%*Y4UlJAk>4XKD5QOJHayJa+rYarV2B(5^?-|8?NMefYaWx5@MKIXnxN zUlw22qIN44CFC9_oQR7Wt;qfpMf}rBI8$6gJ0<_BO9tscXb3v?wz6#N5aUlz$M|;1 IE&G@M2jiSEuK)l5 literal 0 HcmV?d00001 diff --git a/plugins/org/OrgUserProfileEntityCard.png b/plugins/org/OrgUserProfileEntityCard.png new file mode 100644 index 0000000000000000000000000000000000000000..d163243ef1a25ef2759ebde889009cfebc6ae6b3 GIT binary patch literal 16209 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 A-~a#s literal 0 HcmV?d00001 diff --git a/plugins/org/README-alpha.md b/plugins/org/README-alpha.md new file mode 100644 index 0000000000..a7e1a071a9 --- /dev/null +++ b/plugins/org/README-alpha.md @@ -0,0 +1,462 @@ +# Org Plugin + +> [!WARNING] +> This documentation is made for those using the experimental new Frontend system. +> If you are not using the new frontend system, please go [here](./README.md). + +This is a plugin that extends the Catalog entity page with some users and groups overview cards: + +- Group Profile Entity Card +- Member List Entity Card +- Ownership Entity Card +- User Profile Entity Card + +Here is a Catalog group page showing the group profile, members, and ownership cards: + +![Group Page example](./docs/group-page-example.png) + +And below is an example of how a user page looks with the user profile and ownership cards: + +![Group Page example](./docs/user-profile-example.png) + +## Table of Content + +- [Installation](#installation) +- [Packages](#packages) +- [Routes](#routes) +- [Extensions](#extensions) + - [My Groups Sidebar Item](#my-groups-sidebar-item) + - [Entity Group Profile Card](#entity-group-profile-card) + - [Entity Group Profile Card](#entity-members-list-card) + - [Entity Group Profile Card](#entity-members-list-card) + - [Entity Group Profile Card](#entity-user-profile-card) + +## Installation + +1. Install the `org` plugin in you Backstage app: + + ```bash + # From your Backstage root directory + yarn --cwd packages/app add @backstage/plugin-org + ``` + +2. Enable which entity cards and tabs you would like to see on the catalog entity page: + + ```yaml + # app-config.yaml + app: + experimental: + # Auto discovering all plugins extensions + packages: all + extensions: + # Enabling the org plugin cards + - entity-card:org/group-profile + - entity-card:org/members-list + - entity-card:org/ownership + - entity-card:org/user-profile + ``` + +3. Then start the app, navigate to an entity's page and see the cards and contents in there. + +## Packages + +The `org` plugin can be automatically discovered, and it is also possible to enable it only in certain [environments](https://backstage.io/docs/conf/writing/#configuration-files). See [this](https://backstage.io/docs/frontend-system/architecture/app/#feature-discovery) packages documentation for more details. + +## Routes + +The `org` plugin exposes a external route that can be used to configure route bindings. + +| Key | Type | Description | +| -------------- | -------------- | ---------------------------------- | +| `catalogIndex` | External route | A route ref to Catalog Index page. | + +As an example, here is an association between the external catalog index page and a regular route from another plugin: + +```yaml +# app-config.yaml +app: + routes: + bindings: + # example binding org and catalog index pages + org.catalogIndex: catalog.catalogIndex +``` + +Route binding is also possible through code. For more information, see [this](https://backstage.io/docs/frontend-system/architecture/routes#binding-external-route-references) documentation. + +## Extensions + +### My Groups Sidebar Item + +As the [NavItem](https://backstage.io/docs/reference/frontend-plugin-api.createnavitemextension) extension type does not support conditional rendering, this plugin does not provide navigation items, so to use the `MyGroupsSidebarItem` component, we recommend overriding the [App/Nav](https://backstage.io/docs/frontend-system/building-apps/built-in-extensions#app-nav) extension and adding the item statically. + +> [!IMPORTANT] +> As you can see in the example below, we are using the same attachment point, inputs and outputs as the default App/Nav extension to avoid side effects on the NavItem and NavLogo extensions. + +```tsx +// ... +import { MyGroupsSidebarItem } from '@backstage/plugin-org'; +import GroupIcon from '@material-ui/icons/People'; + +export default createExtensionOverrides({ + extensions: [ + createExtension({ + // These namespace and name are necessary so the system knows that this extension will override the default app nav extension + namespace: 'app', + name: 'nav', + // Keeping the same attachment point as in the default App/Nav extension + attachTo: { id: 'app/layout', input: 'nav' }, + // Keeping the same inputs as in the default App/Nav extension + inputs: { + items: createExtensionInput({ + target: createNavItemExtension.targetDataRef, + }), + logos: createExtensionInput( + { + elements: createNavLogoExtension.logoElementsDataRef, + }, + { + singleton: true, + optional: true, + }, + ), + }, + // Keeping the same output as in the default App/Nav extension + output: { + element: coreExtensionData.reactElement, + }, + factory({ inputs }) { + return { + element: ( + + {/* Code borrowed from the default extension implementation to render the logos and items inputs */} + + + {inputs.items.map((item, index) => ( + + ))} + {/* Here is where we actually modifies the default implementation by adding a static item to render a group of squad pages */} + }> + {/* The MyGroupsSidebarItem provides quick access to the group(s) the logged in user is a member of directly in the sidebar. */} + + + + ), + }; + }, + }), + ], +}); +``` + +### Entity Cards + +The `org` plugin provide some entity cards you can enable to customize the Software Catalog entity page. + +> [!IMPORTANT] +> The order in which cards are listed in the configuration file will determine the order in which they appear in overview cards and tab lists on entity pages. + +See a complete cards list below: + +#### Entity Group Profile Card + +This [entity card](https://github.com/backstage/backstage/blob/master/plugins/catalog-react/api-report-alpha.md) extension allows you to view, edit, or update groups metadata, such as team avatar, name, email, parent, and child groups. + +| Kind | Namespace | Name | Id | Example | +| ------------- | --------- | --------------- | ------------------------------- | ----------------------------------------------------------------------------------------- | +| `entity-card` | `org` | `group-profile` | `entity-card:org/group-profile` | Entity Group Profile Card | + +##### Disable + +This card is disabled by default when you install the `org` plugin, but to ensure the card will always be disabled or enabled regardless of the extension's default definition, add the following configuration: + +```yaml +# app-config.yaml +# example disabling the org group profile entity card extension +app: + extensions: + # this is the extension id and it follows the naming pattern bellow: + # /: + # use false as value for disabling the extension and true for enabling + - entity-card:org/group-profile: false + # or + # - entity-card:org/group-profile: + # - config: + # # set 'true' for enabling it again + # disabled: true +``` + +##### Config + +There is only one configuration available for this entity card extension, which is setting an entity filter that determines when the card should be displayed on the entity page. + +Here is an example showing the `group-profile` overview card only for entities of kind group: + +```yaml +# app-config.yaml +# example setting the extension to only show up for entities with kind "group" +app: + extensions: + # this is the extension id and it follows the naming pattern bellow: + # /: + - entity-card:org/group-profile: + config: + # The default value is "kind:group" + # For more information about entity cards filters, check out this pull request + # https://github.com/backstage/backstage/pull/21480 + filter: 'kind:group' +``` + +##### Override + +Use extension overrides for completely re-implementing the group-profile entity card extension: + +```tsx +import { createExtensionOverrides } from '@backstage/backstage-plugin-api'; +import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha'; + +export default createExtensionOverrides({ + extensions: [ + createEntityCardExtension({ + // These namespace and name necessary so the system knows that this extension will override the default 'group-profile' entity card extension provided by the 'org' plugin + namespace: 'org', + name: 'group-profile', + // By default, this card will show up only for groups + filter: 'kind:group' + // Returing a custom card component + loader: () => + import('./components').then(m => ), + }), + ], +}); +``` + +For more information about where to place extension overrides, see the official [documentation](https://backstage.io/docs/frontend-system/architecture/extension-overrides). + +#### Entity Members List Card + +An [entity card](https://github.com/backstage/backstage/blob/master/plugins/catalog-react/api-report-alpha.md) extension that displays the names and emails of group members. By clicking the member's name, you'll be directed to the user's catalog page, and the email opens your default email program. + +| Kind | Namespace | Name | Id | Example | +| ------------- | --------- | -------------- | ------------------------------ | ---------------------------------------------------------------------------------- | +| `entity-card` | `org` | `members-list` | `entity-card:org/members-list` | Entity Group Profile Card | + +##### Disable + +This card is disabled by default when you install the `org` plugin, but to ensure the card will always be disabled or enabled regardless of the extension's default definition, add the following configuration: + +```yaml +# app-config.yaml +# example disabling the org members list entity card extension +app: + extensions: + # this is the extension id and it follows the naming pattern bellow: + # /: + # use false as value for disabling the extension and true for enabling + - entity-card:org/members-list: false + # or + # - entity-card:org/members-list: + # - config: + # # set 'true' for enabling it again + # disabled: true +``` + +##### Config + +There is only one configuration available for this entity card extension, which is setting an entity filter that determines when the card should be displayed on the entity page. + +Here is an example showing the `members-list` overview card only for entities of kind group: + +```yaml +# app-config.yaml +# example setting the extension to only show up for entities with kind "group" +app: + extensions: + # this is the extension id and it follows the naming pattern bellow: + # /: + - entity-card:org/members-list: + config: + # The default value is "kind:group" + # For more information about entity cards filters, check out this pull request + # https://github.com/backstage/backstage/pull/21480 + filter: 'kind:group' +``` + +##### Override + +Use extension overrides for completely re-implementing the members-list entity card extension: + +```tsx +import { createExtensionOverrides } from '@backstage/backstage-plugin-api'; +import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha'; + +export default createExtensionOverrides({ + extensions: [ + createEntityCardExtension({ + // These namespace and name necessary so the system knows that this extension will override the default 'members-list' entity card extension provided by the 'org' plugin + namespace: 'org', + name: 'members-list', + // By default, this card will show up only for groups + filter: 'kind:group' + // Returing a custom card component + loader: () => + import('./components').then(m => ), + }), + ], +}); +``` + +For more information about where to place extension overrides, see the official [documentation](https://backstage.io/docs/frontend-system/architecture/extension-overrides). + +#### Entity Ownership Card + +An [entity card](https://github.com/backstage/backstage/blob/master/plugins/catalog-react/api-report-alpha.md) extension that displays direct or aggregated group or user ownership relationships. Each entity listed in the card links to its respective entity page in the catalog. + +| Kind | Namespace | Name | Id | Example | +| ------------- | --------- | ----------- | --------------------------- | -------------------------------------------------------------------------------- | +| `entity-card` | `org` | `ownership` | `entity-card:org/ownership` | Entity Group Profile Card | + +##### Disable + +This card is disabled by default when you install the `org` plugin, but to ensure the card will always be disabled or enabled regardless of the extension's default definition, add the following configuration: + +```yaml +# app-config.yaml +# example disabling the org members list entity card extension +app: + extensions: + # this is the extension id and it follows the naming pattern bellow: + # /: + # use false as value for disabling the extension and true for enabling + - entity-card:org/ownership: false + # or + # - entity-card:org/ownership: + # - config: + # # set 'true' for enabling it again + # disabled: true +``` + +##### Config + +There is only one configuration available for this entity card extension, which is setting an entity filter that determines when the card should be displayed on the entity page. + +Here is an example showing the `ownership` overview card only for entities of kind group or user: + +```yaml +# app-config.yaml +# example setting the extension to only show up for entities with kind "group" or "user" +app: + extensions: + # this is the extension id and it follows the naming pattern bellow: + # /: + - entity-card:org/ownership: + config: + # The default value is "kind:group,user" + # For more information about entity cards filters, check out this pull request + # https://github.com/backstage/backstage/pull/21480 + filter: 'kind:group,user' +``` + +##### Override + +Use extension overrides for completely re-implementing the ownership entity card extension: + +```tsx +import { createExtensionOverrides } from '@backstage/backstage-plugin-api'; +import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha'; + +export default createExtensionOverrides({ + extensions: [ + createEntityCardExtension({ + // These namespace and name necessary so the system knows that this extension will override the default 'ownership' entity card extension provided by the 'org' plugin + namespace: 'org', + name: 'ownership', + // By default, this card will show up only for groups or users + filter: 'kind:group,user' + // Returing a custom card component + loader: () => + import('./components').then(m => ), + }), + ], +}); +``` + +For more information about where to place extension overrides, see the official [documentation](https://backstage.io/docs/frontend-system/architecture/extension-overrides). + +#### Entity User Profile Card + +This [entity card](https://github.com/backstage/backstage/blob/master/plugins/catalog-react/api-report-alpha.md) extension allows you to view user metadata including avatar, name, email, and team. Clicking on the email link will open your default email program while clicking on the team link will direct you to the team page in the catalog plugin. + +| Kind | Namespace | Name | Id | Example | +| ------------- | --------- | -------------- | ------------------------------ | ---------------------------------------------------------------------------------------- | +| `entity-card` | `org` | `user-profile` | `entity-card:org/user-profile` | Entity Group Profile Card | + +##### Disable + +This card is disabled by default when you install the `org` plugin, but to ensure the card will always be disabled or enabled regardless of the extension's default definition, add the following configuration: + +```yaml +# app-config.yaml +# example disabling the org user profile entity card extension +app: + extensions: + # this is the extension id and it follows the naming pattern bellow: + # /: + # use false as value for disabling the extension and true for enabling + - entity-card:org/user-profile: false + # or + # - entity-card:org/user-profile: + # - config: + # # set 'true' for enabling it again + # disabled: true +``` + +##### Config + +There is only one configuration available for this entity card extension, which is setting an entity filter that determines when the card should be displayed on the entity page. + +Here is an example showing the `user-profile` overview card only for entities of kind group or user: + +```yaml +# app-config.yaml +# example setting the extension to only show up for entities with kind "user" +app: + extensions: + # this is the extension id and it follows the naming pattern bellow: + # /: + - entity-card:org/user-profile: + config: + # The default value is "kind:user" + # For more information about entity cards filters, check out this pull request + # https://github.com/backstage/backstage/pull/21480 + filter: 'kind:user' +``` + +##### Override + +Use extension overrides for completely re-implementing the user-profile entity card extension: + +```tsx +import { createExtensionOverrides } from '@backstage/backstage-plugin-api'; +import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha'; + +export default createExtensionOverrides({ + extensions: [ + createEntityCardExtension({ + // These namespace and name necessary so the system knows that this extension will override the default 'user-profile' entity card extension provided by the 'org' plugin + namespace: 'org', + name: 'user-profile', + // By default, this card will show up only for groups or users + filter: 'kind:user' + // Returing a custom card component + loader: () => + import('./components').then(m => ), + }), + ], +}); +``` + +For more information about where to place extension overrides, see the official [documentation](https://backstage.io/docs/frontend-system/architecture/extension-overrides). diff --git a/plugins/org/README.md b/plugins/org/README.md index 0c5452aa62..9cf165f06d 100644 --- a/plugins/org/README.md +++ b/plugins/org/README.md @@ -1,5 +1,8 @@ # Org Plugin for Backstage +> Disclaimer: +> If you are looking for documentation on the experimental new frontend system support, please go [here](./README-alpha.md). + ## Features - Show Group Page From 540d4820ac10eb5fbdbfb7f931a99a1928d3f689 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 14 Feb 2024 15:11:01 +0100 Subject: [PATCH 057/204] refactor(org): apply review suggestions Signed-off-by: Camila Belo --- plugins/org/README-alpha.md | 144 ++++++++++++++++++------------------ 1 file changed, 72 insertions(+), 72 deletions(-) diff --git a/plugins/org/README-alpha.md b/plugins/org/README-alpha.md index a7e1a071a9..35d4342070 100644 --- a/plugins/org/README-alpha.md +++ b/plugins/org/README-alpha.md @@ -25,11 +25,11 @@ And below is an example of how a user page looks with the user profile and owner - [Packages](#packages) - [Routes](#routes) - [Extensions](#extensions) - - [My Groups Sidebar Item](#my-groups-sidebar-item) - [Entity Group Profile Card](#entity-group-profile-card) - [Entity Group Profile Card](#entity-members-list-card) - [Entity Group Profile Card](#entity-members-list-card) - [Entity Group Profile Card](#entity-user-profile-card) + - [My Groups Sidebar Item](#my-groups-sidebar-item) ## Installation @@ -85,73 +85,6 @@ Route binding is also possible through code. For more information, see [this](ht ## Extensions -### My Groups Sidebar Item - -As the [NavItem](https://backstage.io/docs/reference/frontend-plugin-api.createnavitemextension) extension type does not support conditional rendering, this plugin does not provide navigation items, so to use the `MyGroupsSidebarItem` component, we recommend overriding the [App/Nav](https://backstage.io/docs/frontend-system/building-apps/built-in-extensions#app-nav) extension and adding the item statically. - -> [!IMPORTANT] -> As you can see in the example below, we are using the same attachment point, inputs and outputs as the default App/Nav extension to avoid side effects on the NavItem and NavLogo extensions. - -```tsx -// ... -import { MyGroupsSidebarItem } from '@backstage/plugin-org'; -import GroupIcon from '@material-ui/icons/People'; - -export default createExtensionOverrides({ - extensions: [ - createExtension({ - // These namespace and name are necessary so the system knows that this extension will override the default app nav extension - namespace: 'app', - name: 'nav', - // Keeping the same attachment point as in the default App/Nav extension - attachTo: { id: 'app/layout', input: 'nav' }, - // Keeping the same inputs as in the default App/Nav extension - inputs: { - items: createExtensionInput({ - target: createNavItemExtension.targetDataRef, - }), - logos: createExtensionInput( - { - elements: createNavLogoExtension.logoElementsDataRef, - }, - { - singleton: true, - optional: true, - }, - ), - }, - // Keeping the same output as in the default App/Nav extension - output: { - element: coreExtensionData.reactElement, - }, - factory({ inputs }) { - return { - element: ( - - {/* Code borrowed from the default extension implementation to render the logos and items inputs */} - - - {inputs.items.map((item, index) => ( - - ))} - {/* Here is where we actually modifies the default implementation by adding a static item to render a group of squad pages */} - }> - {/* The MyGroupsSidebarItem provides quick access to the group(s) the logged in user is a member of directly in the sidebar. */} - - - - ), - }; - }, - }), - ], -}); -``` - ### Entity Cards The `org` plugin provide some entity cards you can enable to customize the Software Catalog entity page. @@ -221,7 +154,7 @@ import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha export default createExtensionOverrides({ extensions: [ createEntityCardExtension({ - // These namespace and name necessary so the system knows that this extension will override the default 'group-profile' entity card extension provided by the 'org' plugin + // These namespace and name are necessary so the system knows that this extension will override the default 'group-profile' entity card extension provided by the 'org' plugin namespace: 'org', name: 'group-profile', // By default, this card will show up only for groups @@ -296,7 +229,7 @@ import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha export default createExtensionOverrides({ extensions: [ createEntityCardExtension({ - // These namespace and name necessary so the system knows that this extension will override the default 'members-list' entity card extension provided by the 'org' plugin + // These namespace and name are necessary so the system knows that this extension will override the default 'members-list' entity card extension provided by the 'org' plugin namespace: 'org', name: 'members-list', // By default, this card will show up only for groups @@ -371,7 +304,7 @@ import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha export default createExtensionOverrides({ extensions: [ createEntityCardExtension({ - // These namespace and name necessary so the system knows that this extension will override the default 'ownership' entity card extension provided by the 'org' plugin + // These namespace and name are necessary so the system knows that this extension will override the default 'ownership' entity card extension provided by the 'org' plugin namespace: 'org', name: 'ownership', // By default, this card will show up only for groups or users @@ -446,7 +379,7 @@ import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha export default createExtensionOverrides({ extensions: [ createEntityCardExtension({ - // These namespace and name necessary so the system knows that this extension will override the default 'user-profile' entity card extension provided by the 'org' plugin + // These namespace and name are necessary so the system knows that this extension will override the default 'user-profile' entity card extension provided by the 'org' plugin namespace: 'org', name: 'user-profile', // By default, this card will show up only for groups or users @@ -460,3 +393,70 @@ export default createExtensionOverrides({ ``` For more information about where to place extension overrides, see the official [documentation](https://backstage.io/docs/frontend-system/architecture/extension-overrides). + +### My Groups Sidebar Item + +As the [NavItem](https://backstage.io/docs/reference/frontend-plugin-api.createnavitemextension) extension type does not support conditional rendering, this plugin does not provide navigation items, so to use the `MyGroupsSidebarItem` component, we recommend overriding the [App/Nav](https://backstage.io/docs/frontend-system/building-apps/built-in-extensions#app-nav) extension and adding the item statically. + +> [!IMPORTANT] +> As you can see in the example below, we are using the same attachment point, inputs and outputs as the default App/Nav extension to avoid side effects on the NavItem and NavLogo extensions. + +```tsx +// ... +import { MyGroupsSidebarItem } from '@backstage/plugin-org'; +import GroupIcon from '@material-ui/icons/People'; + +export default createExtensionOverrides({ + extensions: [ + createExtension({ + // These namespace and name are necessary so the system knows that this extension will override the default app nav extension + namespace: 'app', + name: 'nav', + // Keeping the same attachment point as in the default App/Nav extension + attachTo: { id: 'app/layout', input: 'nav' }, + // Keeping the same inputs as in the default App/Nav extension + inputs: { + items: createExtensionInput({ + target: createNavItemExtension.targetDataRef, + }), + logos: createExtensionInput( + { + elements: createNavLogoExtension.logoElementsDataRef, + }, + { + singleton: true, + optional: true, + }, + ), + }, + // Keeping the same output as in the default App/Nav extension + output: { + element: coreExtensionData.reactElement, + }, + factory({ inputs }) { + return { + element: ( + + {/* Code borrowed from the default extension implementation to render the logos and items inputs */} + + + {inputs.items.map((item, index) => ( + + ))} + {/* Here is where we actually modifies the default implementation by adding a static item to render a group of squad pages */} + }> + {/* The MyGroupsSidebarItem provides quick access to the group(s) the logged in user is a member of directly in the sidebar. */} + + + + ), + }; + }, + }), + ], +}); +``` From 0b1e0b1028a971da4c8d2500420cfb6805b4c800 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 15 Feb 2024 13:43:23 +0100 Subject: [PATCH 058/204] refactor(org): remove individual cards screenshots Signed-off-by: Camila Belo --- plugins/org/OrgGroupProfileEntityCard.png | Bin 25091 -> 0 bytes plugins/org/OrgMembersListCard.png | Bin 48346 -> 0 bytes plugins/org/OrgOwnershipCard.png | Bin 118385 -> 0 bytes plugins/org/OrgUserProfileEntityCard.png | Bin 16209 -> 0 bytes plugins/org/README-alpha.md | 24 +++++++++++----------- 5 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 plugins/org/OrgGroupProfileEntityCard.png delete mode 100644 plugins/org/OrgMembersListCard.png delete mode 100644 plugins/org/OrgOwnershipCard.png delete mode 100644 plugins/org/OrgUserProfileEntityCard.png diff --git a/plugins/org/OrgGroupProfileEntityCard.png b/plugins/org/OrgGroupProfileEntityCard.png deleted file mode 100644 index c012b54d04bdceb776edcc8579e4654a9d410fa4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25091 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

yJPQ03ySfc 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 diff --git a/plugins/org/OrgMembersListCard.png b/plugins/org/OrgMembersListCard.png deleted file mode 100644 index 2a4d2b073127401bf57f7d0b028972d5a6ebab11..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48346 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 diff --git a/plugins/org/OrgOwnershipCard.png b/plugins/org/OrgOwnershipCard.png deleted file mode 100644 index f9358a65d1b2de5b9afb532a50e8f8785f5e8c5b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118385 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�?^v)$kUsg>&jaiG-^+hZ|+RyyY)F~^8 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⪻@(g?`0K{%>k1FIQlj5!jT*F~L|nYN8Ds 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 IE&G@M2jiSEuK)l5 diff --git a/plugins/org/OrgUserProfileEntityCard.png b/plugins/org/OrgUserProfileEntityCard.png deleted file mode 100644 index d163243ef1a25ef2759ebde889009cfebc6ae6b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16209 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 A-~a#s diff --git a/plugins/org/README-alpha.md b/plugins/org/README-alpha.md index 35d4342070..fb9fe9d181 100644 --- a/plugins/org/README-alpha.md +++ b/plugins/org/README-alpha.md @@ -98,9 +98,9 @@ See a complete cards list below: This [entity card](https://github.com/backstage/backstage/blob/master/plugins/catalog-react/api-report-alpha.md) extension allows you to view, edit, or update groups metadata, such as team avatar, name, email, parent, and child groups. -| Kind | Namespace | Name | Id | Example | -| ------------- | --------- | --------------- | ------------------------------- | ----------------------------------------------------------------------------------------- | -| `entity-card` | `org` | `group-profile` | `entity-card:org/group-profile` | Entity Group Profile Card | +| Kind | Namespace | Name | Id | +| ------------- | --------- | --------------- | ------------------------------- | +| `entity-card` | `org` | `group-profile` | `entity-card:org/group-profile` | ##### Disable @@ -173,9 +173,9 @@ For more information about where to place extension overrides, see the official An [entity card](https://github.com/backstage/backstage/blob/master/plugins/catalog-react/api-report-alpha.md) extension that displays the names and emails of group members. By clicking the member's name, you'll be directed to the user's catalog page, and the email opens your default email program. -| Kind | Namespace | Name | Id | Example | -| ------------- | --------- | -------------- | ------------------------------ | ---------------------------------------------------------------------------------- | -| `entity-card` | `org` | `members-list` | `entity-card:org/members-list` | Entity Group Profile Card | +| Kind | Namespace | Name | Id | +| ------------- | --------- | -------------- | ------------------------------ | +| `entity-card` | `org` | `members-list` | `entity-card:org/members-list` | ##### Disable @@ -248,9 +248,9 @@ For more information about where to place extension overrides, see the official An [entity card](https://github.com/backstage/backstage/blob/master/plugins/catalog-react/api-report-alpha.md) extension that displays direct or aggregated group or user ownership relationships. Each entity listed in the card links to its respective entity page in the catalog. -| Kind | Namespace | Name | Id | Example | -| ------------- | --------- | ----------- | --------------------------- | -------------------------------------------------------------------------------- | -| `entity-card` | `org` | `ownership` | `entity-card:org/ownership` | Entity Group Profile Card | +| Kind | Namespace | Name | Id | +| ------------- | --------- | ----------- | --------------------------- | +| `entity-card` | `org` | `ownership` | `entity-card:org/ownership` | ##### Disable @@ -323,9 +323,9 @@ For more information about where to place extension overrides, see the official This [entity card](https://github.com/backstage/backstage/blob/master/plugins/catalog-react/api-report-alpha.md) extension allows you to view user metadata including avatar, name, email, and team. Clicking on the email link will open your default email program while clicking on the team link will direct you to the team page in the catalog plugin. -| Kind | Namespace | Name | Id | Example | -| ------------- | --------- | -------------- | ------------------------------ | ---------------------------------------------------------------------------------------- | -| `entity-card` | `org` | `user-profile` | `entity-card:org/user-profile` | Entity Group Profile Card | +| Kind | Namespace | Name | Id | +| ------------- | --------- | -------------- | ------------------------------ | +| `entity-card` | `org` | `user-profile` | `entity-card:org/user-profile` | ##### Disable From 59fecda9aa4af6b0611b3cefb3743ed0fe3a1efd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 09:20:46 +0000 Subject: [PATCH 059/204] fix(deps): update dependency sass to v1.71.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- microsite/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/microsite/yarn.lock b/microsite/yarn.lock index 14292c36bf..6b522f9cf8 100644 --- a/microsite/yarn.lock +++ b/microsite/yarn.lock @@ -10392,15 +10392,15 @@ __metadata: linkType: hard "sass@npm:^1.57.1": - version: 1.70.0 - resolution: "sass@npm:1.70.0" + version: 1.71.0 + resolution: "sass@npm:1.71.0" dependencies: chokidar: ">=3.0.0 <4.0.0" immutable: ^4.0.0 source-map-js: ">=0.6.2 <2.0.0" bin: sass: sass.js - checksum: fd1b622cf9b7fa699a03ec634611997552ece45eb98ac365fef22f42bdcb8ed63b326b64173379c966830c8551ae801e44e4a00d2de16fdadda2dc8f35400bbb + checksum: 5ba6b4b994c7ae94286919d8be8a3692038c27175b4ad3a3ac43f51a91188590d3ddd3a0ef1022135503b9d70d0732efdbedfd56f3ee1a4651549f5b13f8942f languageName: node linkType: hard From 786c9c498f8722bd8d67e8857c5b2f9b5daaefb3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 09:29:25 +0000 Subject: [PATCH 060/204] fix(deps): update dependency luxon to v3 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .changeset/renovate-58582bb.md | 6 ++++++ plugins/linguist-backend/package.json | 2 +- plugins/linguist/package.json | 2 +- yarn.lock | 11 ++--------- 4 files changed, 10 insertions(+), 11 deletions(-) create mode 100644 .changeset/renovate-58582bb.md diff --git a/.changeset/renovate-58582bb.md b/.changeset/renovate-58582bb.md new file mode 100644 index 0000000000..b1a58017f4 --- /dev/null +++ b/.changeset/renovate-58582bb.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-linguist-backend': patch +'@backstage/plugin-linguist': patch +--- + +Updated dependency `luxon` to `^3.0.0`. diff --git a/plugins/linguist-backend/package.json b/plugins/linguist-backend/package.json index 2cc278ba44..3ff3c36f6e 100644 --- a/plugins/linguist-backend/package.json +++ b/plugins/linguist-backend/package.json @@ -45,7 +45,7 @@ "fs-extra": "^11.0.0", "knex": "^3.0.0", "linguist-js": "^2.5.3", - "luxon": "^2.0.2", + "luxon": "^3.0.0", "node-fetch": "^2.6.7", "uuid": "^8.3.2", "winston": "^3.2.1", diff --git a/plugins/linguist/package.json b/plugins/linguist/package.json index 70885f0cbf..5b60c5217d 100644 --- a/plugins/linguist/package.json +++ b/plugins/linguist/package.json @@ -53,7 +53,7 @@ "@material-ui/core": "^4.9.13", "@material-ui/lab": "4.0.0-alpha.61", "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", - "luxon": "^2.0.2", + "luxon": "^3.0.0", "react-use": "^17.2.4", "slugify": "^1.6.4" }, diff --git a/yarn.lock b/yarn.lock index 4b092ff735..174087abea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7603,7 +7603,7 @@ __metadata: js-yaml: ^4.1.0 knex: ^3.0.0 linguist-js: ^2.5.3 - luxon: ^2.0.2 + luxon: ^3.0.0 node-fetch: ^2.6.7 supertest: ^6.2.4 uuid: ^8.3.2 @@ -7640,7 +7640,7 @@ __metadata: "@testing-library/jest-dom": ^6.0.0 "@testing-library/react": ^14.0.0 "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 - luxon: ^2.0.2 + luxon: ^3.0.0 react-use: ^17.2.4 slugify: ^1.6.4 peerDependencies: @@ -34145,13 +34145,6 @@ __metadata: languageName: node linkType: hard -"luxon@npm:^2.0.2": - version: 2.5.2 - resolution: "luxon@npm:2.5.2" - checksum: d8b671ffd2ff0b438af862ac11082a81b3aedd9f8b6a4ca636f944224f8dd381125cd4d274ca0a8aedcaf2cfca0fc2ca96fe4cc1b16a28b7af701afb95c0bff8 - languageName: node - linkType: hard - "luxon@npm:^3.0.0, luxon@npm:^3.3.0, luxon@npm:^3.4.3, luxon@npm:~3.4.0": version: 3.4.4 resolution: "luxon@npm:3.4.4" From ec160939f7a8ea6ec9a912109555a987bb2dc318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 16 Feb 2024 11:07:31 +0100 Subject: [PATCH 061/204] add cmd limiter to the cli MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/nasty-suns-speak.md | 5 +++ packages/repo-tools/src/commands/util.ts | 54 +++++++++++++++--------- 2 files changed, 38 insertions(+), 21 deletions(-) create mode 100644 .changeset/nasty-suns-speak.md diff --git a/.changeset/nasty-suns-speak.md b/.changeset/nasty-suns-speak.md new file mode 100644 index 0000000000..2c5fff25a1 --- /dev/null +++ b/.changeset/nasty-suns-speak.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': patch +--- + +Add an internal limiter on concurrency when launching processes diff --git a/packages/repo-tools/src/commands/util.ts b/packages/repo-tools/src/commands/util.ts index 933945bc5c..e28aae6267 100644 --- a/packages/repo-tools/src/commands/util.ts +++ b/packages/repo-tools/src/commands/util.ts @@ -13,29 +13,41 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { execFile } from 'child_process'; +import os from 'os'; +import pLimit from 'p-limit'; + +// Some commands launch full node processes doing heavy work, which at high +// concurrency levels risk exhausting system resources. Placing the limiter here +// at the root level ensures that the concurrency boundary applies globally, not +// just per-runner. +const limiter = pLimit(os.cpus().length); export function createBinRunner(cwd: string, path: string) { return async (...command: string[]) => - new Promise((resolve, reject) => { - execFile( - 'node', - [path, ...command], - { - cwd, - shell: true, - timeout: 60000, - maxBuffer: 1024 * 1024, - }, - (err, stdout, stderr) => { - if (err) { - reject(new Error(`${err.message}\n${stderr}`)); - } else if (stderr) { - reject(new Error(`Command printed error output: ${stderr}`)); - } else { - resolve(stdout); - } - }, - ); - }); + limiter( + () => + new Promise((resolve, reject) => { + execFile( + 'node', + [path, ...command], + { + cwd, + shell: true, + timeout: 60000, + maxBuffer: 1024 * 1024, + }, + (err, stdout, stderr) => { + if (err) { + reject(new Error(`${err.message}\n${stderr}`)); + } else if (stderr) { + reject(new Error(`Command printed error output: ${stderr}`)); + } else { + resolve(stdout); + } + }, + ); + }), + ); } From 7cac636c9235226d80b143001140171b123bb08f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 10:11:37 +0000 Subject: [PATCH 062/204] fix(deps): update dependency swagger-ui-react to v5.11.7 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4b092ff735..214e70fc91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -43237,8 +43237,8 @@ __metadata: linkType: hard "swagger-ui-react@npm:^5.0.0": - version: 5.11.6 - resolution: "swagger-ui-react@npm:5.11.6" + version: 5.11.7 + resolution: "swagger-ui-react@npm:5.11.7" dependencies: "@babel/runtime-corejs3": ^7.23.9 "@braintree/sanitize-url": =7.0.0 @@ -43277,7 +43277,7 @@ __metadata: peerDependencies: react: ">=16.8.0 <19" react-dom: ">=16.8.0 <19" - checksum: 4556acd2fca69d5b0f7fd1592467818428611f7fce37aeea002c71604c3fdf5314425a99180f08ad257676eec80123cd9ac31e2576102ac9f9fb5f420cb5bbf4 + checksum: b4589abe7d4cdc97fd05f8f8f8b40daee33bb4604faa1f9c513b3ef4765fc407fef37c704dbc56c5493fa5430472e8cb30921a4d4ba544372266d06ef4d1b1e6 languageName: node linkType: hard From 211ec252125b3e7f19f7d20d110f2881e932594d Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Thu, 14 Dec 2023 19:03:53 +0100 Subject: [PATCH 063/204] New plugin to publish a git repository on gitea Signed-off-by: cmoulliard --- .../new/factories/scaffolderModule.test.ts | 4 +- .../.eslintrc.js | 1 + .../CHANGELOG.md | 7 + .../scaffolder-backend-module-gitea/README.md | 5 + .../api-report.md | 20 ++ .../catalog-info.yaml | 9 + .../package.json | 41 ++++ .../src/actions/gitea.examples.ts | 158 ++++++++++++ .../src/actions/gitea.test.ts | 49 ++++ .../src/actions/gitea.ts | 232 ++++++++++++++++++ .../src/actions/index.ts | 16 ++ .../src/index.ts | 23 ++ yarn.lock | 16 ++ 13 files changed, 579 insertions(+), 2 deletions(-) create mode 100644 plugins/scaffolder-backend-module-gitea/.eslintrc.js create mode 100644 plugins/scaffolder-backend-module-gitea/CHANGELOG.md create mode 100644 plugins/scaffolder-backend-module-gitea/README.md create mode 100644 plugins/scaffolder-backend-module-gitea/api-report.md create mode 100644 plugins/scaffolder-backend-module-gitea/catalog-info.yaml create mode 100644 plugins/scaffolder-backend-module-gitea/package.json create mode 100644 plugins/scaffolder-backend-module-gitea/src/actions/gitea.examples.ts create mode 100644 plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts create mode 100644 plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts create mode 100644 plugins/scaffolder-backend-module-gitea/src/actions/index.ts create mode 100644 plugins/scaffolder-backend-module-gitea/src/index.ts diff --git a/packages/cli/src/lib/new/factories/scaffolderModule.test.ts b/packages/cli/src/lib/new/factories/scaffolderModule.test.ts index b43946228b..767ae0fc44 100644 --- a/packages/cli/src/lib/new/factories/scaffolderModule.test.ts +++ b/packages/cli/src/lib/new/factories/scaffolderModule.test.ts @@ -77,8 +77,8 @@ describe('scaffolderModule factory', () => { 'templating package.json.hbs', 'templating index.ts.hbs', 'copying index.ts', - 'copying example.test.ts', - 'copying example.ts', + 'copying gitea.test.ts', + 'copying gitea.ts', 'copying index.ts', 'Installing:', `moving plugins${sep}scaffolder-backend-module-test`, diff --git a/plugins/scaffolder-backend-module-gitea/.eslintrc.js b/plugins/scaffolder-backend-module-gitea/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitea/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/scaffolder-backend-module-gitea/CHANGELOG.md b/plugins/scaffolder-backend-module-gitea/CHANGELOG.md new file mode 100644 index 0000000000..a4f1f20179 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitea/CHANGELOG.md @@ -0,0 +1,7 @@ +# @backstage/plugin-scaffolder-backend-module-gitea + +## 0.1.0-next.0 + +### Minor Changes + +- TODO: Add Gitea Scaffolder Plugin diff --git a/plugins/scaffolder-backend-module-gitea/README.md b/plugins/scaffolder-backend-module-gitea/README.md new file mode 100644 index 0000000000..84b8ab6ea5 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitea/README.md @@ -0,0 +1,5 @@ +# backstage-plugin-scaffolder-backend-module-gitea + +The gitea module for [@backstage/plugin-scaffolder-backend](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend). + +_This plugin was created through the Backstage CLI_ diff --git a/plugins/scaffolder-backend-module-gitea/api-report.md b/plugins/scaffolder-backend-module-gitea/api-report.md new file mode 100644 index 0000000000..51e2f3bd17 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitea/api-report.md @@ -0,0 +1,20 @@ +## API Report File for "backstage-plugin-scaffolder-backend-module-gitea" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { JsonObject } from '@backstage/types'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; + +// @public +export function createAcmeExampleAction(): TemplateAction< + { + myParameter: string; + }, + JsonObject +>; + +// Warnings were encountered during analysis: +// +// src/index.d.ts:2:25 - (tsdoc-characters-after-block-tag) The token "@backstage" looks like a TSDoc tag but contains an invalid character "/"; if it is not a tag, use a backslash to escape the "@" +``` diff --git a/plugins/scaffolder-backend-module-gitea/catalog-info.yaml b/plugins/scaffolder-backend-module-gitea/catalog-info.yaml new file mode 100644 index 0000000000..20511015cd --- /dev/null +++ b/plugins/scaffolder-backend-module-gitea/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-scaffolder-backend-module-gitea + title: 'backstage-plugin-scaffolder-backend-module-gitea' +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: maintainers diff --git a/plugins/scaffolder-backend-module-gitea/package.json b/plugins/scaffolder-backend-module-gitea/package.json new file mode 100644 index 0000000000..9140c294f8 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitea/package.json @@ -0,0 +1,41 @@ +{ + "name": "backstage-plugin-scaffolder-backend-module-gitea", + "description": "The gitea module for @backstage/plugin-scaffolder-backend", + "version": "0.1.0-next.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/config": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/integration": "workspace:^", + "@backstage/plugin-scaffolder-backend": "workspace:^", + "@backstage/plugin-scaffolder-node": "workspace:^", + "node-fetch": "^2.6.7", + "yaml": "^2.0.0" + }, + "devDependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/cli": "workspace:^" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.examples.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.examples.ts new file mode 100644 index 0000000000..64d73d498f --- /dev/null +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.examples.ts @@ -0,0 +1,158 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: + 'Initializes a Gitea repository of contents in workspace and publish it to Gitea with default configuration.', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:gitea', + name: 'Publish to Gitea', + input: { + repoUrl: 'gitea.com?repo=repo&owner=owner', + }, + }, + ], + }), + }, + { + description: 'Initializes a Gitea repository with a description.', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:gitea', + name: 'Publish to Gitea', + input: { + repoUrl: 'gitea.com?repo=repo&owner=owner', + description: 'Initialize a gitea repository', + }, + }, + ], + }), + }, + { + description: + 'Initializes a Gitea repository with a default Branch, if not set defaults to master', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:gitea', + name: 'Publish to Gitea', + input: { + repoUrl: 'gitea.com?repo=repo&owner=owner', + defaultBranch: 'staging', + }, + }, + ], + }), + }, + { + description: + 'Initializes a Gitea repository with an initial commit message, if not set defaults to initial commit', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:gitea', + name: 'Publish to Gitea', + input: { + repoUrl: 'gitea.com?repo=repo&owner=owner', + gitCommitMessage: 'Initial Commit Message', + }, + }, + ], + }), + }, + { + description: + 'Initializes a Gitea repository with a repo Author Name, if not set defaults to Scaffolder', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:gitea', + name: 'Publish to Gitea', + input: { + repoUrl: 'gitea.com?repo=repo&owner=owner', + gitAuthorName: 'John Doe', + }, + }, + ], + }), + }, + { + description: 'Initializes a Gitea repository with a repo Author Email', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:gitea', + name: 'Publish to Gitea', + input: { + repoUrl: 'gitea.com?repo=repo&owner=owner', + gitAuthorEmail: 'johndoe@email.com', + }, + }, + ], + }), + }, + { + description: + 'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:gitea', + name: 'Publish to Gitea', + input: { + repoUrl: 'gitea.com?repo=repo&owner=owner', + sourcePath: 'repository/', + }, + }, + ], + }), + }, + { + description: 'Initializes a Gitea repository with all properties being set', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:gitea', + name: 'Publish to Gitea', + input: { + repoUrl: 'gitea.com?repo=repo&owner=owner', + description: 'Initialize a gitea repository', + defaultBranch: 'staging', + gitCommitMessage: 'Initial Commit Message', + gitAuthorName: 'John Doe', + gitAuthorEmail: 'johndoe@email.com', + sourcePath: 'repository/', + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts new file mode 100644 index 0000000000..bd40b68603 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { PassThrough } from 'stream'; +import { createAcmeExampleAction } from './gitea'; +import { getVoidLogger } from '@backstage/backend-common'; + +describe('acme:example', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should call action', async () => { + const action = createAcmeExampleAction(); + + const logger = getVoidLogger(); + jest.spyOn(logger, 'info'); + + await action.handler({ + input: { + myParameter: 'test', + }, + workspacePath: '/tmp', + logger, + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory() { + // Usage of mock-fs is recommended for testing of filesystem operations + throw new Error('Not implemented'); + }, + }); + + expect(logger.info).toHaveBeenCalledWith( + 'Running example template with parameters: test', + ); + }); +}); diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts new file mode 100644 index 0000000000..a740b4938e --- /dev/null +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts @@ -0,0 +1,232 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputError } from '@backstage/errors'; +import { Config } from '@backstage/config'; +import { + getGiteaRequestOptions, + GiteaIntegrationConfig, + ScmIntegrationRegistry, +} from '@backstage/integration'; +import { + createTemplateAction, + getRepoSourceDirectory, + initRepoAndPush, + parseRepoUrl, +} from '@backstage/plugin-scaffolder-node'; +import { examples } from './gitea.examples'; +import fetch, { RequestInit, Response } from 'node-fetch'; +import crypto from 'crypto'; + +const createGiteaProject = async ( + config: GiteaIntegrationConfig, + options: { + projectName: string; + parent: string; + owner?: string; + description: string; + }, +): Promise => { + const { projectName, parent, owner, description } = options; + + const fetchOptions: RequestInit = { + method: 'PUT', + body: JSON.stringify({ + parent, + description, + owners: owner ? [owner] : [], + create_empty_commit: false, + }), + headers: { + ...getGiteaRequestOptions(config).headers, + 'Content-Type': 'application/json', + }, + }; + + // TODO + const response: Response = await fetch( + `${config.baseUrl}/a/projects/${encodeURIComponent(projectName)}`, + fetchOptions, + ); + if (response.status !== 201) { + throw new Error( + `Unable to create repository, ${response.status} ${ + response.statusText + }, ${await response.text()}`, + ); + } +}; + +const generateCommitMessage = ( + config: Config, + commitSubject?: string, +): string => { + const changeId = crypto.randomBytes(20).toString('hex'); + const msg = `${ + config.getOptionalString('scaffolder.defaultCommitMessage') || commitSubject + }\n\nChange-Id: I${changeId}`; + return msg; +}; + +/** + * Creates a new action that initializes a git repository using the content of the workspace. + * and publishes it to a Gitea instance. + * @public + */ +export function createPublishGiteaAction(options: { + integrations: ScmIntegrationRegistry; + config: Config; +}) { + const { integrations, config } = options; + + return createTemplateAction<{ + repoUrl: string; + description: string; + defaultBranch?: string; + gitCommitMessage?: string; + gitAuthorName?: string; + gitAuthorEmail?: string; + sourcePath?: string; + }>({ + id: 'publish:gitea', + description: + 'Initializes a git repository using the content of the workspace, and publishes it to Gitea.', + examples, + schema: { + input: { + type: 'object', + required: ['repoUrl'], + properties: { + repoUrl: { + title: 'Repository Location', + type: 'string', + }, + description: { + title: 'Repository Description', + type: 'string', + }, + defaultBranch: { + title: 'Default Branch', + type: 'string', + description: `Sets the default branch on the repository. The default value is 'master'`, + }, + 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', + type: 'string', + description: `Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository.`, + }, + }, + }, + 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', + }, + commitHash: { + title: 'The git commit hash of the initial commit', + type: 'string', + }, + }, + }, + }, + async handler(ctx) { + const { + repoUrl, + description, + defaultBranch = 'main', + gitAuthorName, + gitAuthorEmail, + gitCommitMessage = 'initial commit', + sourcePath, + } = ctx.input; + + const { repo, host, owner, workspace } = parseRepoUrl( + repoUrl, + integrations, + ); + + const integrationConfig = integrations.gitea.byHost(host); + + if (!integrationConfig) { + throw new InputError( + `No matching integration configuration for host ${host}, please check your integrations config`, + ); + } + + if (!workspace) { + throw new InputError( + `Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing workspace`, + ); + } + + await createGiteaProject(integrationConfig.config, { + description, + owner: owner, + projectName: repo, + parent: workspace, + }); + const auth = { + username: integrationConfig.config.username!, + password: integrationConfig.config.password!, + }; + const gitAuthorInfo = { + name: gitAuthorName + ? gitAuthorName + : config.getOptionalString('scaffolder.defaultAuthor.name'), + email: gitAuthorEmail + ? gitAuthorEmail + : config.getOptionalString('scaffolder.defaultAuthor.email'), + }; + // TODO + const remoteUrl = `${integrationConfig.config.baseUrl}/a/${repo}`; + const commitResult = await initRepoAndPush({ + dir: getRepoSourceDirectory(ctx.workspacePath, sourcePath), + remoteUrl, + auth, + defaultBranch, + logger: ctx.logger, + commitMessage: generateCommitMessage(config, gitCommitMessage), + gitAuthorInfo, + }); + + const repoContentsUrl = `${integrationConfig.config.baseUrl}/${repo}/+/refs/heads/${defaultBranch}`; + ctx.output('remoteUrl', remoteUrl); + ctx.output('commitHash', commitResult?.commitHash); + ctx.output('repoContentsUrl', repoContentsUrl); + }, + }); +} diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/index.ts b/plugins/scaffolder-backend-module-gitea/src/actions/index.ts new file mode 100644 index 0000000000..ccf7f23fab --- /dev/null +++ b/plugins/scaffolder-backend-module-gitea/src/actions/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './gitea'; diff --git a/plugins/scaffolder-backend-module-gitea/src/index.ts b/plugins/scaffolder-backend-module-gitea/src/index.ts new file mode 100644 index 0000000000..026da0735a --- /dev/null +++ b/plugins/scaffolder-backend-module-gitea/src/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A module for the scaffolder backend that lets you interact with gitea + * + * @packageDocumentation + */ + +export * from './actions'; diff --git a/yarn.lock b/yarn.lock index 3394871837..065b070197 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22051,6 +22051,22 @@ __metadata: languageName: node linkType: hard +"backstage-plugin-scaffolder-backend-module-gitea@workspace:plugins/scaffolder-backend-module-gitea": + version: 0.0.0-use.local + resolution: "backstage-plugin-scaffolder-backend-module-gitea@workspace:plugins/scaffolder-backend-module-gitea" + dependencies: + "@backstage/backend-common": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/integration": "workspace:^" + "@backstage/plugin-scaffolder-backend": "workspace:^" + "@backstage/plugin-scaffolder-node": "workspace:^" + node-fetch: ^2.6.7 + yaml: ^2.0.0 + languageName: unknown + linkType: soft + "badge-maker@npm:^3.3.0": version: 3.3.1 resolution: "badge-maker@npm:3.3.1" From f6426a32f90d505ca9836eacb1b1dd3b051986e5 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Fri, 15 Dec 2023 15:23:26 +0100 Subject: [PATCH 064/204] WIP. Create action to publish on gitea server Signed-off-by: cmoulliard --- .../src/actions/gitea.examples.ts | 6 +- .../src/actions/gitea.test.ts | 111 ++++++++++++++---- .../src/actions/gitea.ts | 38 +++--- plugins/scaffolder-node/src/actions/util.ts | 4 + 4 files changed, 117 insertions(+), 42 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.examples.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.examples.ts index 64d73d498f..748a869205 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.examples.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.examples.ts @@ -20,7 +20,7 @@ import yaml from 'yaml'; export const examples: TemplateExample[] = [ { description: - 'Initializes a Gitea repository of contents in workspace and publish it to Gitea with default configuration.', + 'Initializes a Gitea repository using the content of the workspace and publish it to Gitea with default configuration.', example: yaml.stringify({ steps: [ { @@ -52,7 +52,7 @@ export const examples: TemplateExample[] = [ }, { description: - 'Initializes a Gitea repository with a default Branch, if not set defaults to master', + 'Initializes a Gitea repository with a default Branch, if not set defaults to main', example: yaml.stringify({ steps: [ { @@ -61,7 +61,7 @@ export const examples: TemplateExample[] = [ name: 'Publish to Gitea', input: { repoUrl: 'gitea.com?repo=repo&owner=owner', - defaultBranch: 'staging', + defaultBranch: 'main', }, }, ], diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts index bd40b68603..52a29e2bbc 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts @@ -14,36 +14,103 @@ * limitations under the License. */ import { PassThrough } from 'stream'; -import { createAcmeExampleAction } from './gitea'; +/* import {rest} from 'msw';*/ +import { setupServer } from 'msw'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { ScmIntegrations } from '@backstage/integration'; +import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; +import { createPublishGiteaAction } from './gitea'; +import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; -describe('acme:example', () => { - afterEach(() => { +jest.mock('@backstage/plugin-scaffolder-node', () => { + return { + ...jest.requireActual('@backstage/plugin-scaffolder-node'), + initRepoAndPush: jest.fn().mockResolvedValue({ + commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6', + }), + commitAndPushRepo: jest.fn().mockResolvedValue({ + commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6', + }), + }; +}); + +describe('publish:gitea', () => { + const config = new ConfigReader({ + integrations: { + gitea: [ + { + host: 'gitea.com', + username: 'gitea_user', + password: 'gitea_password', + }, + ], + }, + }); + + const description = 'for the lols'; + const integrations = ScmIntegrations.fromConfig(config); + const action = createPublishGiteaAction({ integrations, config }); + const mockContext = { + input: { + repoUrl: 'gitea.com?repo=repo', + description, + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + const mockGitClient = { + createRepository: jest.fn(), + }; + + const server = setupServer(); + setupRequestMockHandlers(server); + + beforeEach(() => { jest.resetAllMocks(); }); - it('should call action', async () => { - const action = createAcmeExampleAction(); + it('should throw an error when the repoUrl is not well formed', async () => { + await expect( + action.handler({ + ...mockContext, + input: { repoUrl: 'gitea.com?owner=o', description }, + }), + ).rejects.toThrow(/missing repo/); + }); - const logger = getVoidLogger(); - jest.spyOn(logger, 'info'); + it('should throw if there is no integration config provided for missing.com host', async () => { + await expect( + action.handler({ + ...mockContext, + input: { repoUrl: 'missing.com?repo=repo', description }, + }), + ).rejects.toThrow(/No matching integration configuration/); + }); - await action.handler({ - input: { - myParameter: 'test', - }, - workspacePath: '/tmp', - logger, - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory() { - // Usage of mock-fs is recommended for testing of filesystem operations - throw new Error('Not implemented'); - }, + it('should throw if there is no repositoryId returned', async () => { + mockGitClient.createRepository.mockImplementation(() => ({ + remoteUrl: 'https://gitea.com', + id: null, + })); + + await action.handler(mockContext); + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://gitea.com', + defaultBranch: 'main', + auth: { username: 'gitea_user', password: 'gitea_password' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: {}, }); + }); - expect(logger.info).toHaveBeenCalledWith( - 'Running example template with parameters: test', - ); + afterEach(() => { + jest.resetAllMocks(); }); }); diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts index a740b4938e..18e15fc191 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts @@ -35,20 +35,19 @@ const createGiteaProject = async ( config: GiteaIntegrationConfig, options: { projectName: string; - parent: string; + // parent: string; owner?: string; description: string; }, ): Promise => { - const { projectName, parent, owner, description } = options; + const { projectName, owner, description } = options; const fetchOptions: RequestInit = { - method: 'PUT', + method: 'POST', body: JSON.stringify({ - parent, + name: projectName, description, - owners: owner ? [owner] : [], - create_empty_commit: false, + owner, }), headers: { ...getGiteaRequestOptions(config).headers, @@ -57,8 +56,10 @@ const createGiteaProject = async ( }; // TODO + // Create a repository in Gitea + // API request: https://gitea.com/api/swagger#/user/createCurrentUserRepo const response: Response = await fetch( - `${config.baseUrl}/a/projects/${encodeURIComponent(projectName)}`, + `${config.baseUrl}/api/v1/user/repos`, fetchOptions, ); if (response.status !== 201) { @@ -174,31 +175,34 @@ export function createPublishGiteaAction(options: { sourcePath, } = ctx.input; - const { repo, host, owner, workspace } = parseRepoUrl( - repoUrl, - integrations, - ); + const { owner, repo, host } = parseRepoUrl(repoUrl, integrations); const integrationConfig = integrations.gitea.byHost(host); - if (!integrationConfig) { throw new InputError( `No matching integration configuration for host ${host}, please check your integrations config`, ); } - if (!workspace) { - throw new InputError( - `Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing workspace`, + if ( + !integrationConfig.config.username || + !integrationConfig.config.password + ) { + throw new Error( + 'Credentials for Gitea integration required for this action.', ); } + // TODO + // Check if the integration config includes a baseUrl + await createGiteaProject(integrationConfig.config, { description, owner: owner, projectName: repo, - parent: workspace, + // parent: workspace, }); + const auth = { username: integrationConfig.config.username!, password: integrationConfig.config.password!, @@ -212,7 +216,7 @@ export function createPublishGiteaAction(options: { : config.getOptionalString('scaffolder.defaultAuthor.email'), }; // TODO - const remoteUrl = `${integrationConfig.config.baseUrl}/a/${repo}`; + const remoteUrl = `${integrationConfig.config.baseUrl}/api/v1/user/${repo}`; const commitResult = await initRepoAndPush({ dir: getRepoSourceDirectory(ctx.workspacePath, sourcePath), remoteUrl, diff --git a/plugins/scaffolder-node/src/actions/util.ts b/plugins/scaffolder-node/src/actions/util.ts index 327cfa9c2f..7f7121ef16 100644 --- a/plugins/scaffolder-node/src/actions/util.ts +++ b/plugins/scaffolder-node/src/actions/util.ts @@ -92,6 +92,10 @@ export const parseRepoUrl = ( } break; } + case 'gitea': { + checkRequiredParams(parsed, 'repo'); + break; + } case 'gerrit': { checkRequiredParams(parsed, 'repo'); break; From 49d4e7617ffab6789ac65d88e8de05073275b1a6 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Fri, 15 Dec 2023 15:56:44 +0100 Subject: [PATCH 065/204] Import mising package to create the setupServer. Update yarn.locl and package.json Signed-off-by: cmoulliard --- plugins/scaffolder-backend-module-gitea/package.json | 4 +++- .../scaffolder-backend-module-gitea/src/actions/gitea.test.ts | 3 +-- yarn.lock | 2 ++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitea/package.json b/plugins/scaffolder-backend-module-gitea/package.json index 9140c294f8..239146d842 100644 --- a/plugins/scaffolder-backend-module-gitea/package.json +++ b/plugins/scaffolder-backend-module-gitea/package.json @@ -33,7 +33,9 @@ }, "devDependencies": { "@backstage/backend-common": "workspace:^", - "@backstage/cli": "workspace:^" + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts index 52a29e2bbc..ae80f3bdb7 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts @@ -14,14 +14,13 @@ * limitations under the License. */ import { PassThrough } from 'stream'; -/* import {rest} from 'msw';*/ -import { setupServer } from 'msw'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; import { createPublishGiteaAction } from './gitea'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; +import { setupServer } from 'msw/node'; jest.mock('@backstage/plugin-scaffolder-node', () => { return { diff --git a/yarn.lock b/yarn.lock index 065b070197..3153025cca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22056,12 +22056,14 @@ __metadata: resolution: "backstage-plugin-scaffolder-backend-module-gitea@workspace:plugins/scaffolder-backend-module-gitea" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-backend": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + msw: ^1.0.0 node-fetch: ^2.6.7 yaml: ^2.0.0 languageName: unknown From bb0b6568afc6e4d5025a35857976eafd04aaa937 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Fri, 15 Dec 2023 16:48:30 +0100 Subject: [PATCH 066/204] Registering new publish gitea action within createBuiltinActions.ts Signed-off-by: cmoulliard --- .../catalog-info.yaml | 2 +- .../package.json | 2 +- .../actions/builtin/createBuiltinActions.ts | 2 + yarn.lock | 37 ++++++++++--------- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitea/catalog-info.yaml b/plugins/scaffolder-backend-module-gitea/catalog-info.yaml index 20511015cd..adfcc754fb 100644 --- a/plugins/scaffolder-backend-module-gitea/catalog-info.yaml +++ b/plugins/scaffolder-backend-module-gitea/catalog-info.yaml @@ -2,7 +2,7 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: name: backstage-plugin-scaffolder-backend-module-gitea - title: 'backstage-plugin-scaffolder-backend-module-gitea' + title: '@backstage/plugin-scaffolder-backend-module-gitea' spec: lifecycle: experimental type: backstage-backend-plugin-module diff --git a/plugins/scaffolder-backend-module-gitea/package.json b/plugins/scaffolder-backend-module-gitea/package.json index 239146d842..93dd86e92b 100644 --- a/plugins/scaffolder-backend-module-gitea/package.json +++ b/plugins/scaffolder-backend-module-gitea/package.json @@ -1,5 +1,5 @@ { - "name": "backstage-plugin-scaffolder-backend-module-gitea", + "name": "@backstage/plugin-scaffolder-backend-module-gitea", "description": "The gitea module for @backstage/plugin-scaffolder-backend", "version": "0.1.0-next.0", "main": "src/index.ts", diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index f9c15597a8..51b2a18a77 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -75,6 +75,8 @@ import { createPublishGerritReviewAction, } from '@backstage/plugin-scaffolder-backend-module-gerrit'; +import { createPublishGiteaAction } from '@backstage/plugin-scaffolder-backend-module-gitea'; + import { createPublishGitlabAction, createGitlabRepoPushAction, diff --git a/yarn.lock b/yarn.lock index 3153025cca..64bc6fc64e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8492,6 +8492,24 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-scaffolder-backend-module-gitea@workspace:^, @backstage/plugin-scaffolder-backend-module-gitea@workspace:plugins/scaffolder-backend-module-gitea": + version: 0.0.0-use.local + resolution: "@backstage/plugin-scaffolder-backend-module-gitea@workspace:plugins/scaffolder-backend-module-gitea" + dependencies: + "@backstage/backend-common": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/integration": "workspace:^" + "@backstage/plugin-scaffolder-backend": "workspace:^" + "@backstage/plugin-scaffolder-node": "workspace:^" + msw: ^1.0.0 + node-fetch: ^2.6.7 + yaml: ^2.0.0 + languageName: unknown + linkType: soft + "@backstage/plugin-scaffolder-backend-module-github@workspace:^, @backstage/plugin-scaffolder-backend-module-github@workspace:plugins/scaffolder-backend-module-github": version: 0.0.0-use.local resolution: "@backstage/plugin-scaffolder-backend-module-github@workspace:plugins/scaffolder-backend-module-github" @@ -8616,6 +8634,7 @@ __metadata: "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "workspace:^" "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "workspace:^" "@backstage/plugin-scaffolder-backend-module-gerrit": "workspace:^" + "@backstage/plugin-scaffolder-backend-module-gitea": "workspace:^" "@backstage/plugin-scaffolder-backend-module-github": "workspace:^" "@backstage/plugin-scaffolder-backend-module-gitlab": "workspace:^" "@backstage/plugin-scaffolder-common": "workspace:^" @@ -22051,24 +22070,6 @@ __metadata: languageName: node linkType: hard -"backstage-plugin-scaffolder-backend-module-gitea@workspace:plugins/scaffolder-backend-module-gitea": - version: 0.0.0-use.local - resolution: "backstage-plugin-scaffolder-backend-module-gitea@workspace:plugins/scaffolder-backend-module-gitea" - dependencies: - "@backstage/backend-common": "workspace:^" - "@backstage/backend-test-utils": "workspace:^" - "@backstage/cli": "workspace:^" - "@backstage/config": "workspace:^" - "@backstage/errors": "workspace:^" - "@backstage/integration": "workspace:^" - "@backstage/plugin-scaffolder-backend": "workspace:^" - "@backstage/plugin-scaffolder-node": "workspace:^" - msw: ^1.0.0 - node-fetch: ^2.6.7 - yaml: ^2.0.0 - languageName: unknown - linkType: soft - "badge-maker@npm:^3.3.0": version: 3.3.1 resolution: "badge-maker@npm:3.3.1" From b2953bd1085c85eb84240560cddd383a9c284a07 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Fri, 15 Dec 2023 17:02:14 +0100 Subject: [PATCH 067/204] Update package.json to add gitea module Signed-off-by: cmoulliard --- plugins/scaffolder-backend/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 9026761a7c..858591749c 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -64,6 +64,7 @@ "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "workspace:^", "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "workspace:^", "@backstage/plugin-scaffolder-backend-module-gerrit": "workspace:^", + "@backstage/plugin-scaffolder-backend-module-gitea": "workspace:^", "@backstage/plugin-scaffolder-backend-module-github": "workspace:^", "@backstage/plugin-scaffolder-backend-module-gitlab": "workspace:^", "@backstage/plugin-scaffolder-common": "workspace:^", From f1645f3e0a4649866775658f37306b102b9396c4 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Fri, 15 Dec 2023 18:19:39 +0100 Subject: [PATCH 068/204] Register the missing gitea action to publish Signed-off-by: cmoulliard --- .../scaffolder/actions/builtin/createBuiltinActions.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index 51b2a18a77..2ee0a7644f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -75,14 +75,14 @@ import { createPublishGerritReviewAction, } from '@backstage/plugin-scaffolder-backend-module-gerrit'; -import { createPublishGiteaAction } from '@backstage/plugin-scaffolder-backend-module-gitea'; - import { createPublishGitlabAction, createGitlabRepoPushAction, createPublishGitlabMergeRequestAction, } from '@backstage/plugin-scaffolder-backend-module-gitlab'; +import { createPublishGiteaAction } from '@backstage/plugin-scaffolder-backend-module-gitea'; + /** * The options passed to {@link createBuiltinActions} * @public @@ -160,6 +160,10 @@ export const createBuiltinActions = ( integrations, config, }), + createPublishGiteaAction({ + integrations, + config, + }), createPublishGithubAction({ integrations, config, From f8afc5c059abf9ee3ce02e9ece15526c1fb09dff Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Tue, 19 Dec 2023 14:58:08 +0100 Subject: [PATCH 069/204] Align default branch name. master -> main Signed-off-by: cmoulliard --- plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts index 18e15fc191..011be04fc1 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts @@ -122,7 +122,7 @@ export function createPublishGiteaAction(options: { defaultBranch: { title: 'Default Branch', type: 'string', - description: `Sets the default branch on the repository. The default value is 'master'`, + description: `Sets the default branch on the repository. The default value is 'main'`, }, gitCommitMessage: { title: 'Git Commit Message', From a37b65c79ee3343282d3dd666141b28cb7bd0dfd Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Tue, 19 Dec 2023 15:00:47 +0100 Subject: [PATCH 070/204] Reverted name cgange from gitea.tst to example.ts, example.test.ts Signed-off-by: cmoulliard --- packages/cli/src/lib/new/factories/scaffolderModule.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/lib/new/factories/scaffolderModule.test.ts b/packages/cli/src/lib/new/factories/scaffolderModule.test.ts index 767ae0fc44..b43946228b 100644 --- a/packages/cli/src/lib/new/factories/scaffolderModule.test.ts +++ b/packages/cli/src/lib/new/factories/scaffolderModule.test.ts @@ -77,8 +77,8 @@ describe('scaffolderModule factory', () => { 'templating package.json.hbs', 'templating index.ts.hbs', 'copying index.ts', - 'copying gitea.test.ts', - 'copying gitea.ts', + 'copying example.test.ts', + 'copying example.ts', 'copying index.ts', 'Installing:', `moving plugins${sep}scaffolder-backend-module-test`, From 4d2c01bfbff613825ace8449559529a78e56fa1e Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Tue, 19 Dec 2023 15:02:25 +0100 Subject: [PATCH 071/204] Changing the version from 0.1.0-next.0 to 0.0.0 Signed-off-by: cmoulliard --- plugins/scaffolder-backend-module-gitea/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-gitea/package.json b/plugins/scaffolder-backend-module-gitea/package.json index 93dd86e92b..c9e9a4e07d 100644 --- a/plugins/scaffolder-backend-module-gitea/package.json +++ b/plugins/scaffolder-backend-module-gitea/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gitea", "description": "The gitea module for @backstage/plugin-scaffolder-backend", - "version": "0.1.0-next.0", + "version": "0.0.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", From 827d88fce12dab230882a009f5104843b194ad7d Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Tue, 19 Dec 2023 15:03:42 +0100 Subject: [PATCH 072/204] Removing the CHANGELOG.md file as the releasing flow will create it Signed-off-by: cmoulliard --- plugins/scaffolder-backend-module-gitea/CHANGELOG.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 plugins/scaffolder-backend-module-gitea/CHANGELOG.md diff --git a/plugins/scaffolder-backend-module-gitea/CHANGELOG.md b/plugins/scaffolder-backend-module-gitea/CHANGELOG.md deleted file mode 100644 index a4f1f20179..0000000000 --- a/plugins/scaffolder-backend-module-gitea/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# @backstage/plugin-scaffolder-backend-module-gitea - -## 0.1.0-next.0 - -### Minor Changes - -- TODO: Add Gitea Scaffolder Plugin From 11d697be7709e43d46417e7a8afe55c2bf3adc40 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Tue, 19 Dec 2023 15:12:41 +0100 Subject: [PATCH 073/204] Updating the api-report.md file Signed-off-by: cmoulliard --- .../api-report.md | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitea/api-report.md b/plugins/scaffolder-backend-module-gitea/api-report.md index 51e2f3bd17..f83ae73fb7 100644 --- a/plugins/scaffolder-backend-module-gitea/api-report.md +++ b/plugins/scaffolder-backend-module-gitea/api-report.md @@ -1,20 +1,27 @@ -## API Report File for "backstage-plugin-scaffolder-backend-module-gitea" +## API Report File for "@backstage/plugin-scaffolder-backend-module-gitea" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { Config } from '@backstage/config'; import { JsonObject } from '@backstage/types'; +import { ScmIntegrationRegistry } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; // @public -export function createAcmeExampleAction(): TemplateAction< +export function createPublishGiteaAction(options: { + integrations: ScmIntegrationRegistry; + config: Config; +}): TemplateAction< { - myParameter: string; + repoUrl: string; + description: string; + defaultBranch?: string | undefined; + gitCommitMessage?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; + sourcePath?: string | undefined; }, JsonObject >; - -// Warnings were encountered during analysis: -// -// src/index.d.ts:2:25 - (tsdoc-characters-after-block-tag) The token "@backstage" looks like a TSDoc tag but contains an invalid character "/"; if it is not a tag, use a backslash to escape the "@" ``` From 90836c82e4f0cdecd3a210c02bfe91e8e9793380 Mon Sep 17 00:00:00 2001 From: Charles Moulliard Date: Tue, 19 Dec 2023 15:23:37 +0100 Subject: [PATCH 074/204] Update plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Charles Moulliard Signed-off-by: cmoulliard --- plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts index 011be04fc1..7b6547b30d 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts @@ -169,8 +169,8 @@ export function createPublishGiteaAction(options: { repoUrl, description, defaultBranch = 'main', - gitAuthorName, - gitAuthorEmail, + gitAuthorName = config.getOptionalString('scaffolder.defaultAuthor.name'), + gitAuthorEmail = config.getOptionalString('scaffolder.defaultAuthor.email'), gitCommitMessage = 'initial commit', sourcePath, } = ctx.input; From 3e20f53e49e2c26d10042d3f04ce6c3be3cea131 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Wed, 20 Dec 2023 09:36:09 +0100 Subject: [PATCH 075/204] Rename basic to Basic Signed-off-by: cmoulliard --- packages/integration/src/gitea/core.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integration/src/gitea/core.ts b/packages/integration/src/gitea/core.ts index 95caa24c06..6d11a4ffde 100644 --- a/packages/integration/src/gitea/core.ts +++ b/packages/integration/src/gitea/core.ts @@ -120,7 +120,7 @@ export function getGiteaRequestOptions(config: GiteaIntegrationConfig): { } if (username) { - headers.Authorization = `basic ${Buffer.from( + headers.Authorization = `Basic ${Buffer.from( `${username}:${password}`, ).toString('base64')}`; } else { From 07bd2fbeb43259acd8c8b64f2601b264b47d3164 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Wed, 20 Dec 2023 09:40:08 +0100 Subject: [PATCH 076/204] Rename token to Token Signed-off-by: cmoulliard --- packages/integration/src/gitea/core.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integration/src/gitea/core.ts b/packages/integration/src/gitea/core.ts index 6d11a4ffde..f188613d2a 100644 --- a/packages/integration/src/gitea/core.ts +++ b/packages/integration/src/gitea/core.ts @@ -124,7 +124,7 @@ export function getGiteaRequestOptions(config: GiteaIntegrationConfig): { `${username}:${password}`, ).toString('base64')}`; } else { - headers.Authorization = `token ${password}`; + headers.Authorization = `Token ${password}`; } return { From 51fec95f3340c20d772a73add9d28e748ef08a96 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Wed, 20 Dec 2023 10:52:28 +0100 Subject: [PATCH 077/204] Add test to valid the baseURL Signed-off-by: cmoulliard --- packages/integration/src/gitea/config.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/integration/src/gitea/config.ts b/packages/integration/src/gitea/config.ts index 28070d570d..ae1fe5ee87 100644 --- a/packages/integration/src/gitea/config.ts +++ b/packages/integration/src/gitea/config.ts @@ -16,7 +16,7 @@ import { Config } from '@backstage/config'; import { trimEnd } from 'lodash'; -import { isValidHost } from '../helpers'; +import { isValidHost, isValidUrl } from '../helpers'; /** * The configuration for a single Gitea integration. @@ -62,6 +62,10 @@ export function readGiteaConfig(config: Config): GiteaIntegrationConfig { throw new Error( `Invalid Gitea integration config, '${host}' is not a valid host`, ); + } else if (baseUrl && !isValidUrl(baseUrl)) { + throw new Error( + `Invalid Gitea integration config, '${baseUrl}' is not a valid baseUrl`, + ); } if (baseUrl) { From 1c91c52a096f95a0479eefbbbc98d0be46cca52d Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Wed, 20 Dec 2023 10:53:00 +0100 Subject: [PATCH 078/204] Updating the test case to create a git repo Signed-off-by: cmoulliard --- .../src/actions/gitea.test.ts | 46 +++++++++++++------ .../src/actions/gitea.ts | 1 - 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts index ae80f3bdb7..6461726dcf 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts @@ -14,12 +14,13 @@ * limitations under the License. */ import { PassThrough } from 'stream'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; import { createPublishGiteaAction } from './gitea'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; +import { rest } from 'msw'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { setupServer } from 'msw/node'; jest.mock('@backstage/plugin-scaffolder-node', () => { @@ -28,9 +29,6 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { initRepoAndPush: jest.fn().mockResolvedValue({ commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6', }), - commitAndPushRepo: jest.fn().mockResolvedValue({ - commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6', - }), }; }); @@ -40,6 +38,7 @@ describe('publish:gitea', () => { gitea: [ { host: 'gitea.com', + baseUrl: 'https://gitea.com', username: 'gitea_user', password: 'gitea_password', }, @@ -52,7 +51,7 @@ describe('publish:gitea', () => { const action = createPublishGiteaAction({ integrations, config }); const mockContext = { input: { - repoUrl: 'gitea.com?repo=repo', + repoUrl: 'gitea.com?repo=repo&owner=owner', description, }, workspacePath: 'lol', @@ -62,10 +61,6 @@ describe('publish:gitea', () => { createTemporaryDirectory: jest.fn(), }; - const mockGitClient = { - createRepository: jest.fn(), - }; - const server = setupServer(); setupRequestMockHandlers(server); @@ -92,15 +87,36 @@ describe('publish:gitea', () => { }); it('should throw if there is no repositoryId returned', async () => { - mockGitClient.createRepository.mockImplementation(() => ({ - remoteUrl: 'https://gitea.com', - id: null, - })); + server.use( + rest.put('https://gitea.com/api/v1/user/repo', (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe( + 'Basic Z2l0ZWFfdXNlcjpnaXRlYV9wYXNzd29yZA==', + ); + expect(req.body).toEqual({ + create_empty_commit: false, + owners: ['owner'], + description, + parent: 'workspace', + }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }), + ); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'gitea.com?owner=owner&repo=repo', + }, + }); - await action.handler(mockContext); expect(initRepoAndPush).toHaveBeenCalledWith({ dir: mockContext.workspacePath, - remoteUrl: 'https://gitea.com', + remoteUrl: 'https://gitea.com/api/v1/user/repo', defaultBranch: 'main', auth: { username: 'gitea_user', password: 'gitea_password' }, logger: mockContext.logger, diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts index 7b6547b30d..0c7a5113ee 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts @@ -47,7 +47,6 @@ const createGiteaProject = async ( body: JSON.stringify({ name: projectName, description, - owner, }), headers: { ...getGiteaRequestOptions(config).headers, From 331c4c242ac438cb0edfcd5d9c62b2b83447071e Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Wed, 20 Dec 2023 11:49:55 +0100 Subject: [PATCH 079/204] Try catch the method calling fetch Signed-off-by: cmoulliard --- .../src/actions/gitea.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts index 0c7a5113ee..03f3ce27e2 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts @@ -57,10 +57,12 @@ const createGiteaProject = async ( // TODO // Create a repository in Gitea // API request: https://gitea.com/api/swagger#/user/createCurrentUserRepo - const response: Response = await fetch( - `${config.baseUrl}/api/v1/user/repos`, - fetchOptions, - ); + let response: Response; + try { + response = await fetch(`${config.baseUrl}/api/v1/user/repos`, fetchOptions); + } catch (e) { + throw new Error(`Unable to create repository, ${e}`); + } if (response.status !== 201) { throw new Error( `Unable to create repository, ${response.status} ${ @@ -168,8 +170,12 @@ export function createPublishGiteaAction(options: { repoUrl, description, defaultBranch = 'main', - gitAuthorName = config.getOptionalString('scaffolder.defaultAuthor.name'), - gitAuthorEmail = config.getOptionalString('scaffolder.defaultAuthor.email'), + gitAuthorName = config.getOptionalString( + 'scaffolder.defaultAuthor.name', + ), + gitAuthorEmail = config.getOptionalString( + 'scaffolder.defaultAuthor.email', + ), gitCommitMessage = 'initial commit', sourcePath, } = ctx.input; From 38ebfd198044d433b27531084e181913f4371fd8 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Wed, 20 Dec 2023 12:00:48 +0100 Subject: [PATCH 080/204] Remove non needed declarations Signed-off-by: cmoulliard --- .../scaffolder-backend-module-gitea/src/actions/gitea.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts index 03f3ce27e2..a68f92592e 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts @@ -170,12 +170,8 @@ export function createPublishGiteaAction(options: { repoUrl, description, defaultBranch = 'main', - gitAuthorName = config.getOptionalString( - 'scaffolder.defaultAuthor.name', - ), - gitAuthorEmail = config.getOptionalString( - 'scaffolder.defaultAuthor.email', - ), + gitAuthorName, + gitAuthorEmail, gitCommitMessage = 'initial commit', sourcePath, } = ctx.input; From 31e0e51bb99fb05077a65c6692767670d7971761 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Wed, 20 Dec 2023 12:05:20 +0100 Subject: [PATCH 081/204] Fix url typo error: ../repo => ../repos Signed-off-by: cmoulliard --- .../scaffolder-backend-module-gitea/src/actions/gitea.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts index 6461726dcf..6290b2b33d 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts @@ -88,7 +88,7 @@ describe('publish:gitea', () => { it('should throw if there is no repositoryId returned', async () => { server.use( - rest.put('https://gitea.com/api/v1/user/repo', (req, res, ctx) => { + rest.post('https://gitea.com/api/v1/user/repos', (req, res, ctx) => { expect(req.headers.get('Authorization')).toBe( 'Basic Z2l0ZWFfdXNlcjpnaXRlYV9wYXNzd29yZA==', ); From 3bff96fc389c8983a66603983a9a60e4462d87f2 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Wed, 20 Dec 2023 14:25:30 +0100 Subject: [PATCH 082/204] Fixing matching condition Signed-off-by: cmoulliard --- .../src/actions/gitea.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts index 6290b2b33d..4be82a0e12 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts @@ -38,7 +38,6 @@ describe('publish:gitea', () => { gitea: [ { host: 'gitea.com', - baseUrl: 'https://gitea.com', username: 'gitea_user', password: 'gitea_password', }, @@ -93,10 +92,8 @@ describe('publish:gitea', () => { 'Basic Z2l0ZWFfdXNlcjpnaXRlYV9wYXNzd29yZA==', ); expect(req.body).toEqual({ - create_empty_commit: false, - owners: ['owner'], + name: 'repo', description, - parent: 'workspace', }); return res( ctx.status(201), @@ -120,8 +117,11 @@ describe('publish:gitea', () => { defaultBranch: 'main', auth: { username: 'gitea_user', password: 'gitea_password' }, logger: mockContext.logger, - commitMessage: 'initial commit', - gitAuthorInfo: {}, + commitMessage: expect.stringContaining('initial commit\n\nChange-Id:'), + gitAuthorInfo: { + email: undefined, + name: undefined, + }, }); }); From 3a40f3c5dfc2bbc746efbd4106c0481f184fb302 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Wed, 20 Dec 2023 16:59:50 +0100 Subject: [PATCH 083/204] Removing owner as non needed Signed-off-by: cmoulliard --- .../src/actions/gitea.test.ts | 3 ++- .../scaffolder-backend-module-gitea/src/actions/gitea.ts | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts index 4be82a0e12..ee0457cac0 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts @@ -88,6 +88,7 @@ describe('publish:gitea', () => { it('should throw if there is no repositoryId returned', async () => { server.use( rest.post('https://gitea.com/api/v1/user/repos', (req, res, ctx) => { + // Basic auth must match the user and password defined part of the config expect(req.headers.get('Authorization')).toBe( 'Basic Z2l0ZWFfdXNlcjpnaXRlYV9wYXNzd29yZA==', ); @@ -107,7 +108,7 @@ describe('publish:gitea', () => { ...mockContext, input: { ...mockContext.input, - repoUrl: 'gitea.com?owner=owner&repo=repo', + repoUrl: 'gitea.com?repo=repo', }, }); diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts index a68f92592e..cc16359d75 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts @@ -40,7 +40,7 @@ const createGiteaProject = async ( description: string; }, ): Promise => { - const { projectName, owner, description } = options; + const { projectName, description } = options; const fetchOptions: RequestInit = { method: 'POST', @@ -176,7 +176,7 @@ export function createPublishGiteaAction(options: { sourcePath, } = ctx.input; - const { owner, repo, host } = parseRepoUrl(repoUrl, integrations); + const { repo, host } = parseRepoUrl(repoUrl, integrations); const integrationConfig = integrations.gitea.byHost(host); if (!integrationConfig) { @@ -199,7 +199,7 @@ export function createPublishGiteaAction(options: { await createGiteaProject(integrationConfig.config, { description, - owner: owner, + // owner: owner, projectName: repo, // parent: workspace, }); From 701eefc71eb3aa22535e51e06fb3d3e9e79721e7 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Wed, 3 Jan 2024 15:18:58 +0100 Subject: [PATCH 084/204] Switched the logic to create a repository using api/v1/orgs/OWNER/repos. Adapt the test case to check if there is an org too Signed-off-by: cmoulliard --- packages/integration/src/gitea/core.ts | 1 + .../src/actions/gitea.test.ts | 19 ++++- .../src/actions/gitea.ts | 75 ++++++++++++++----- 3 files changed, 75 insertions(+), 20 deletions(-) diff --git a/packages/integration/src/gitea/core.ts b/packages/integration/src/gitea/core.ts index f188613d2a..40f1c0fb6d 100644 --- a/packages/integration/src/gitea/core.ts +++ b/packages/integration/src/gitea/core.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import { GiteaIntegrationConfig } from './config'; +import { Logger } from 'winston'; /** * Given a URL pointing to a file, returns a URL diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts index ee0457cac0..5486b5b69f 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts @@ -87,7 +87,20 @@ describe('publish:gitea', () => { it('should throw if there is no repositoryId returned', async () => { server.use( - rest.post('https://gitea.com/api/v1/user/repos', (req, res, ctx) => { + rest.get('https://gitea.com/api/v1/orgs/org1', (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + id: 1, + name: 'org1', + visibility: 'public', + repo_admin_change_team_access: false, + username: 'org1', + }), + ); + }), + rest.post('https://gitea.com/api/v1/orgs/org1/repos', (req, res, ctx) => { // Basic auth must match the user and password defined part of the config expect(req.headers.get('Authorization')).toBe( 'Basic Z2l0ZWFfdXNlcjpnaXRlYV9wYXNzd29yZA==', @@ -108,13 +121,13 @@ describe('publish:gitea', () => { ...mockContext, input: { ...mockContext.input, - repoUrl: 'gitea.com?repo=repo', + repoUrl: 'gitea.com?repo=repo&owner=org1', }, }); expect(initRepoAndPush).toHaveBeenCalledWith({ dir: mockContext.workspacePath, - remoteUrl: 'https://gitea.com/api/v1/user/repo', + remoteUrl: 'https://gitea.com/org1/repo.git', defaultBranch: 'main', auth: { username: 'gitea_user', password: 'gitea_password' }, logger: mockContext.logger, diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts index cc16359d75..b11d5bf765 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts @@ -31,18 +31,60 @@ import { examples } from './gitea.examples'; import fetch, { RequestInit, Response } from 'node-fetch'; import crypto from 'crypto'; +const checkGiteaOrg = async ( + config: GiteaIntegrationConfig, + options: { + owner: string; + }, +): Promise => { + const { owner } = options; + let response: Response; + // check first if the org = owner exists + const getOptions: RequestInit = { + method: 'GET', + headers: { + ...getGiteaRequestOptions(config).headers, + 'Content-Type': 'application/json', + }, + }; + try { + response = await fetch( + `${config.baseUrl}/api/v1/orgs/${owner}`, + getOptions, + ); + } catch (e) { + throw new Error(`Unable to get the Organization: ${owner}, ${e}`); + } + if (response.status !== 200) { + throw new Error( + `Organization ${owner} do not exist. Please create it first !`, + ); + } +}; + const createGiteaProject = async ( config: GiteaIntegrationConfig, options: { projectName: string; - // parent: string; owner?: string; description: string; }, ): Promise => { - const { projectName, description } = options; + const { projectName, description, owner } = options; - const fetchOptions: RequestInit = { + /* + Several options exist to create a repository using either the user or organisation + User: https://gitea.com/api/swagger#/user/createCurrentUserRepo + Api: URL/api/v1/user/repos + Remark: The user is the username defined part of the backstage integration config for the gitea URL ! + + Org: https://gitea.com/api/swagger#/organization/createOrgRepo + Api: URL/api/v1/orgs/${org_owner}/repos + This is the default scenario that we support currently + */ + let response: Response; + + const postOptions: RequestInit = { method: 'POST', body: JSON.stringify({ name: projectName, @@ -53,13 +95,11 @@ const createGiteaProject = async ( 'Content-Type': 'application/json', }, }; - - // TODO - // Create a repository in Gitea - // API request: https://gitea.com/api/swagger#/user/createCurrentUserRepo - let response: Response; try { - response = await fetch(`${config.baseUrl}/api/v1/user/repos`, fetchOptions); + response = await fetch( + `${config.baseUrl}/api/v1/orgs/${owner}/repos`, + postOptions, + ); } catch (e) { throw new Error(`Unable to create repository, ${e}`); } @@ -176,7 +216,7 @@ export function createPublishGiteaAction(options: { sourcePath, } = ctx.input; - const { repo, host } = parseRepoUrl(repoUrl, integrations); + const { repo, host, owner } = parseRepoUrl(repoUrl, integrations); const integrationConfig = integrations.gitea.byHost(host); if (!integrationConfig) { @@ -194,14 +234,15 @@ export function createPublishGiteaAction(options: { ); } - // TODO - // Check if the integration config includes a baseUrl + // check if the org exists within the gitea server + if (owner) { + await checkGiteaOrg(integrationConfig.config, { owner }); + } await createGiteaProject(integrationConfig.config, { description, - // owner: owner, + owner: owner, projectName: repo, - // parent: workspace, }); const auth = { @@ -216,8 +257,8 @@ export function createPublishGiteaAction(options: { ? gitAuthorEmail : config.getOptionalString('scaffolder.defaultAuthor.email'), }; - // TODO - const remoteUrl = `${integrationConfig.config.baseUrl}/api/v1/user/${repo}`; + // The owner to be used should be either the org name or user authenticated with the gitea server + const remoteUrl = `${integrationConfig.config.baseUrl}/${owner}/${repo}.git`; const commitResult = await initRepoAndPush({ dir: getRepoSourceDirectory(ctx.workspacePath, sourcePath), remoteUrl, @@ -228,7 +269,7 @@ export function createPublishGiteaAction(options: { gitAuthorInfo, }); - const repoContentsUrl = `${integrationConfig.config.baseUrl}/${repo}/+/refs/heads/${defaultBranch}`; + const repoContentsUrl = `${integrationConfig.config.baseUrl}/${owner}/${repo}/+/refs/${defaultBranch}`; ctx.output('remoteUrl', remoteUrl); ctx.output('commitHash', commitResult?.commitHash); ctx.output('repoContentsUrl', repoContentsUrl); From 17235fcaec32d047a64182c958aae5e4eab66bb3 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Wed, 3 Jan 2024 17:03:10 +0100 Subject: [PATCH 085/204] Removing the non needed import for logger Signed-off-by: cmoulliard --- packages/integration/src/gitea/core.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/integration/src/gitea/core.ts b/packages/integration/src/gitea/core.ts index 40f1c0fb6d..f188613d2a 100644 --- a/packages/integration/src/gitea/core.ts +++ b/packages/integration/src/gitea/core.ts @@ -14,7 +14,6 @@ * limitations under the License. */ import { GiteaIntegrationConfig } from './config'; -import { Logger } from 'winston'; /** * Given a URL pointing to a file, returns a URL From ca0ac99493605b1657f975b0a47103a38495097d Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Wed, 3 Jan 2024 17:04:08 +0100 Subject: [PATCH 086/204] Add a sleep of 2s to let the step to register the newly catalog created from the gitea server Signed-off-by: cmoulliard --- plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts index b11d5bf765..e8c7328245 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts @@ -216,6 +216,8 @@ export function createPublishGiteaAction(options: { sourcePath, } = ctx.input; + const sleep = (ms: number | undefined) => + new Promise(r => setTimeout(r, ms)); const { repo, host, owner } = parseRepoUrl(repoUrl, integrations); const integrationConfig = integrations.gitea.byHost(host); @@ -269,6 +271,9 @@ export function createPublishGiteaAction(options: { gitAuthorInfo, }); + // TODO: As mentioned by Ben lambert, we should poll an endpoint to see if it's ready yet instead of hard coding a sleep + await sleep(2000); + const repoContentsUrl = `${integrationConfig.config.baseUrl}/${owner}/${repo}/+/refs/${defaultBranch}`; ctx.output('remoteUrl', remoteUrl); ctx.output('commitHash', commitResult?.commitHash); From 8a88e91817f79258e6392acbd79338cb38856e7d Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Thu, 4 Jan 2024 09:54:00 +0100 Subject: [PATCH 087/204] Switched the sleep 3s to a while condition to check if we got from the repository created the metadata Signed-off-by: cmoulliard --- .../src/actions/gitea.ts | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts index e8c7328245..50b215f7d8 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts @@ -31,6 +31,36 @@ import { examples } from './gitea.examples'; import fetch, { RequestInit, Response } from 'node-fetch'; import crypto from 'crypto'; +const checkGiteaOrgRepo = async ( + config: GiteaIntegrationConfig, + options: { + owner?: string; + repo: string; + defaultBranch?: string; + }, +): Promise => { + const { owner, repo, defaultBranch } = options; + let response: Response; + const getOptions: RequestInit = { + method: 'GET', + headers: { + ...getGiteaRequestOptions(config).headers, + 'Content-Type': 'application/json', + }, + }; + + try { + response = await fetch( + `${config.baseUrl}/api/v1/repos/${owner}/${repo}/contents?ref=${defaultBranch}`, + getOptions, + ); + } catch (e) { + throw new Error( + `Unable to get the repository: ${owner}/${repo} metadata , ${e}`, + ); + } + return response; +}; const checkGiteaOrg = async ( config: GiteaIntegrationConfig, options: { @@ -218,6 +248,7 @@ export function createPublishGiteaAction(options: { const sleep = (ms: number | undefined) => new Promise(r => setTimeout(r, ms)); + const { repo, host, owner } = parseRepoUrl(repoUrl, integrations); const integrationConfig = integrations.gitea.byHost(host); @@ -271,8 +302,21 @@ export function createPublishGiteaAction(options: { gitAuthorInfo, }); - // TODO: As mentioned by Ben lambert, we should poll an endpoint to see if it's ready yet instead of hard coding a sleep - await sleep(2000); + // Check if the repo is available + let response: Response; + response = await checkGiteaOrgRepo(integrationConfig.config, { + owner, + repo, + defaultBranch, + }); + while (response.status !== 200) { + await sleep(1000); + response = await checkGiteaOrgRepo(integrationConfig.config, { + owner, + repo, + defaultBranch, + }); + } const repoContentsUrl = `${integrationConfig.config.baseUrl}/${owner}/${repo}/+/refs/${defaultBranch}`; ctx.output('remoteUrl', remoteUrl); From 458bf21b7f10bdad8ed2559c8f1c70d1d15773a7 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Thu, 4 Jan 2024 14:01:05 +0100 Subject: [PATCH 088/204] Added a new changeSet Signed-off-by: cmoulliard --- .changeset/metal-clocks-suffer.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/metal-clocks-suffer.md diff --git a/.changeset/metal-clocks-suffer.md b/.changeset/metal-clocks-suffer.md new file mode 100644 index 0000000000..f94deb380c --- /dev/null +++ b/.changeset/metal-clocks-suffer.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitea': minor +'@backstage/plugin-scaffolder-backend': minor +'@backstage/plugin-scaffolder-node': minor +'@backstage/integration': patch +--- + +Added support to create a git repository and publish a scaffolded project using a new action "publish:gitea" for gitea. The action currently supports to create a gitea org's repository - https://gitea.com/api/swagger#/organization/createOrgRepo From 39bc3d0d7d0913dc7ffc41a5fe103485f35ae5f1 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Thu, 4 Jan 2024 14:54:41 +0100 Subject: [PATCH 089/204] Updated the instructions to use the publish:gitea action Signed-off-by: cmoulliard --- .../scaffolder-backend-module-gitea/README.md | 162 +++++++++++++++++- 1 file changed, 159 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitea/README.md b/plugins/scaffolder-backend-module-gitea/README.md index 84b8ab6ea5..9d733e76ff 100644 --- a/plugins/scaffolder-backend-module-gitea/README.md +++ b/plugins/scaffolder-backend-module-gitea/README.md @@ -1,5 +1,161 @@ -# backstage-plugin-scaffolder-backend-module-gitea +# scaffolder-backend-module-gitea -The gitea module for [@backstage/plugin-scaffolder-backend](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend). +Welcome to the `publish:gitea` action of the `scaffolder-gitea-backend`. -_This plugin was created through the Backstage CLI_ +## Getting started + +To use this action, you will have to add the package using the following command to be executed at the root of your backstage project: + +```bash +yarn add --cwd packages/backend +@backstage/plugin-scaffolder-backend-module-gitea +``` + +Configure the action (if not yet done): +(you can check the [docs](https://backstage.io/docs/features/software-templates/writing-custom-actions#registering-custom-actions) to see all options): + +```typescript +// plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts + +export const createBuiltinActions = ( +... + const actions = [ + ... + createPublishGiteaAction({ + integrations, + config, + }), + ... + ]; + return actions as TemplateAction[]; +}; +``` + +Before to create a template, include to your `app-config.yaml` file the +gitea host and credentials under the `integrations:` section + +```yaml +integrations: + gitea: + - host: gitea.com + username: '' + password: '' + - host: localhost:3333 + username: '' + password: '' +``` + +**NOTE**: As backstage will issue HTTPS/TLS requests to the gitea instance, it is needed to configure `gitea` with a valid certificate or at least with a +self-signed certificate `gitea cert --host localhost -ca`. Don't forget to set the env var `NODE_EXTRA_CA_CERTS` to point to the CA pem file before to launch backstage ! + +When done, you can use the action in your template: + +```yaml +apiVersion: scaffolder.backstage.io/v1beta3 +kind: Template +metadata: + name: quarkus-web-template + title: Quarkus Hello world + description: Create a simple microservice using Quarkus + tags: + - java + - quarkus +spec: + owner: quarkus + type: service + parameters: + - title: Provide Information for Application + required: + - component_id + - owner + - java_package_name + properties: + component_id: + title: Name + type: string + description: Unique name of the component + default: my-quarkus-app + ui:field: EntityNamePicker + group_id: + title: Group Id + type: string + default: io.quarkus + description: Maven Group Id + artifact_id: + title: Artifact Id + type: string + default: quarkus-app + description: Maven Artifact Id + java_package_name: + title: Java Package Name + default: io.quarkus.demo + type: string + description: Name for the java package. eg (io.quarkus.blah) + owner: + title: Owner + type: string + description: IdP owner of the component + ui:field: OwnerPicker + ui:options: + allowedKinds: + - Group + + - title: Application git repository Information + required: + - repoUrl + properties: + repoUrl: + title: Repository Location + type: string + ui:field: RepoUrlPicker + ui:options: + allowedHosts: + - localhost:3333 + - gitea.: + + steps: + - id: template + name: Generating component + action: fetch:template + input: + url: ./skeleton + copyWithoutTemplating: + - .github/workflows/* + values: + component_id: ${{ parameters.component_id }} + namespace: ${{ parameters.component_id }}-dev + description: ${{ parameters.description }} + group_id: ${{ parameters.group_id }} + artifact_id: ${{ parameters.artifact_id }} + java_package_name: ${{ parameters.java_package_name }} + owner: ${{ parameters.owner }} + destination: ${{ (parameters.repoUrl | parseRepoUrl).owner }}/${{ (parameters.repoUrl | parseRepoUrl).repo }} + quay_destination: ${{ parameters.image_organization }}/${{ parameters.component_id }} + port: 8080 + + - id: publish + name: Publishing to a gitea git repository + action: publish:gitea + input: + description: This is ${{ parameters.component_id }} + repoUrl: ${{ parameters.repoUrl }} + defaultBranch: main + + - id: register + if: ${{ parameters.dryRun !== true }} + name: Register + action: catalog:register + input: + repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }} + catalogInfoPath: '/catalog-info.yaml' + + output: + links: + - title: Source Code Repository + url: ${{ steps.publish.output.remoteUrl }} + - title: Open Component in catalog + icon: catalog + entityRef: ${{ steps.register.output.entityRef }} +``` + +Enjoy ;-) From 1a49b1d137bdbe2ae46e98eb1320bf439e095211 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Thu, 4 Jan 2024 18:16:26 +0100 Subject: [PATCH 090/204] Added a TODO task as the code using while condition is failing like a mention to pass the branch name to the catalogInfoPath Signed-off-by: cmoulliard --- plugins/scaffolder-backend-module-gitea/README.md | 4 +++- .../src/actions/gitea.ts | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-gitea/README.md b/plugins/scaffolder-backend-module-gitea/README.md index 9d733e76ff..6b42eca9d0 100644 --- a/plugins/scaffolder-backend-module-gitea/README.md +++ b/plugins/scaffolder-backend-module-gitea/README.md @@ -48,6 +48,8 @@ integrations: **NOTE**: As backstage will issue HTTPS/TLS requests to the gitea instance, it is needed to configure `gitea` with a valid certificate or at least with a self-signed certificate `gitea cert --host localhost -ca`. Don't forget to set the env var `NODE_EXTRA_CA_CERTS` to point to the CA pem file before to launch backstage ! +**WARNING**: Please pass the branch name part of the `catalogInfoPath` for the action `register` till we will fix this issue (e.g `main/catalog-info.yaml`) ! + When done, you can use the action in your template: ```yaml @@ -147,7 +149,7 @@ spec: action: catalog:register input: repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }} - catalogInfoPath: '/catalog-info.yaml' + catalogInfoPath: 'main/catalog-info.yaml' output: links: diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts index 50b215f7d8..76ddb189a6 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts @@ -302,6 +302,16 @@ export function createPublishGiteaAction(options: { gitAuthorInfo, }); + /* + TODO: This code is commented till it will be fixed. + When we use the code hereafter, we got the following error: + "Unable to read url, Error: Unknown encoding: undefined\n at DefaultLocationService.processEntities" + as commented here: Still getting the error: https://github.com/backstage/backstage/pull/21890#issuecomment-1876733870 + + Such an issue do not exist using sleep 3s. + + WARNING: To allow to register within the catalog the new project, it is also needed to pass within the catalogInfoPath the branch name (e.g: main/catalog-info.yaml) + // Check if the repo is available let response: Response; response = await checkGiteaOrgRepo(integrationConfig.config, { @@ -317,6 +327,8 @@ export function createPublishGiteaAction(options: { defaultBranch, }); } + */ + await sleep(3000); const repoContentsUrl = `${integrationConfig.config.baseUrl}/${owner}/${repo}/+/refs/${defaultBranch}`; ctx.output('remoteUrl', remoteUrl); From 82ae17b19c8764ee6691f55b13258d0f59d07803 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Thu, 4 Jan 2024 18:27:24 +0100 Subject: [PATCH 091/204] Use the proper repoContentsUrl to access gitea repo and enhance the test case Signed-off-by: cmoulliard --- .../src/actions/gitea.test.ts | 5 +++++ plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts index 5486b5b69f..8936c34690 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts @@ -137,6 +137,11 @@ describe('publish:gitea', () => { name: undefined, }, }); + + expect(mockContext.output).toHaveBeenCalledWith( + 'repoContentsUrl', + 'https://gitea.com/org1/repo/src/branch/main', + ); }); afterEach(() => { diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts index 76ddb189a6..b8ab703460 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts @@ -330,7 +330,7 @@ export function createPublishGiteaAction(options: { */ await sleep(3000); - const repoContentsUrl = `${integrationConfig.config.baseUrl}/${owner}/${repo}/+/refs/${defaultBranch}`; + const repoContentsUrl = `${integrationConfig.config.baseUrl}/${owner}/${repo}/src/branch/${defaultBranch}`; ctx.output('remoteUrl', remoteUrl); ctx.output('commitHash', commitResult?.commitHash); ctx.output('repoContentsUrl', repoContentsUrl); From a72521bae8a6991c4c2ddcd25aeba1bfff415512 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Thu, 4 Jan 2024 19:03:31 +0100 Subject: [PATCH 092/204] Adding a new test case covering the url we are looking for when we register a new entry to the catalog using the action. Removing the gerrit stuff Signed-off-by: cmoulliard --- packages/integration/src/gitea/core.test.ts | 36 +++++++++++---------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/integration/src/gitea/core.test.ts b/packages/integration/src/gitea/core.test.ts index fe6381d52e..ba41fa4761 100644 --- a/packages/integration/src/gitea/core.test.ts +++ b/packages/integration/src/gitea/core.test.ts @@ -30,6 +30,22 @@ describe('gitea core', () => { const worker = setupServer(); setupRequestMockHandlers(worker); + describe('getGiteaFileCatalogInfoCntentsUrl', () => { + it('can create an url from arguments', () => { + const config: GiteaIntegrationConfig = { + host: 'gitea.com', + }; + expect( + getGiteaFileContentsUrl( + config, + 'https://gitea.com/org1/repo1/src/branch/main/catalog-info.yaml', + ), + ).toEqual( + 'https://gitea.com/api/v1/repos/org1/repo1/contents/catalog-info.yaml?ref=main', + ); + }); + }); + describe('getGiteaFileContentsUrl', () => { it('can create an url from arguments', () => { const config: GiteaIntegrationConfig = { @@ -94,29 +110,15 @@ describe('gitea core', () => { }); }); - describe('getGerritRequestOptions', () => { - it('adds token header when only a password is specified', () => { - const authRequest: GiteaIntegrationConfig = { - host: 'gerrit.com', - password: 'P', - }; - const anonymousRequest: GiteaIntegrationConfig = { - host: 'gerrit.com', - }; - expect( - (getGiteaRequestOptions(authRequest).headers as any).Authorization, - ).toEqual('token P'); - expect(getGiteaRequestOptions(anonymousRequest).headers).toBeUndefined(); - }); - + describe('getGiteaRequestOptions', () => { it('adds basic auth when username and password are specified', () => { const authRequest: GiteaIntegrationConfig = { - host: 'gerrit.com', + host: 'gitea.com', username: 'username', password: 'P', }; - const basicAuthentication = `basic ${Buffer.from( + const basicAuthentication = `Basic ${Buffer.from( `${authRequest.username}:${authRequest.password}`, ).toString('base64')}`; From 2b2fdccaa68bdb98826bce846d272574a35e0510 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Thu, 4 Jan 2024 19:15:53 +0100 Subject: [PATCH 093/204] Fixing the issue about the branch name not included to the url converted. Wrong: "https://localhost:3333/api/v1/repos/org1/repo1/contents/?ref=catalog-info.yaml" and good: "https://localhost:3333/api/v1/repos/org1/repo1/contents/catalog-info.yaml?ref=maincatalog-info.yaml" Signed-off-by: cmoulliard --- plugins/scaffolder-backend-module-gitea/README.md | 2 -- plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts | 5 ++++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitea/README.md b/plugins/scaffolder-backend-module-gitea/README.md index 6b42eca9d0..4a710a400c 100644 --- a/plugins/scaffolder-backend-module-gitea/README.md +++ b/plugins/scaffolder-backend-module-gitea/README.md @@ -48,8 +48,6 @@ integrations: **NOTE**: As backstage will issue HTTPS/TLS requests to the gitea instance, it is needed to configure `gitea` with a valid certificate or at least with a self-signed certificate `gitea cert --host localhost -ca`. Don't forget to set the env var `NODE_EXTRA_CA_CERTS` to point to the CA pem file before to launch backstage ! -**WARNING**: Please pass the branch name part of the `catalogInfoPath` for the action `register` till we will fix this issue (e.g `main/catalog-info.yaml`) ! - When done, you can use the action in your template: ```yaml diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts index b8ab703460..7905f42e4a 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts @@ -31,6 +31,7 @@ import { examples } from './gitea.examples'; import fetch, { RequestInit, Response } from 'node-fetch'; import crypto from 'crypto'; +/* NOT USED. See TODO hereafter const checkGiteaOrgRepo = async ( config: GiteaIntegrationConfig, options: { @@ -61,6 +62,8 @@ const checkGiteaOrgRepo = async ( } return response; }; +*/ + const checkGiteaOrg = async ( config: GiteaIntegrationConfig, options: { @@ -330,7 +333,7 @@ export function createPublishGiteaAction(options: { */ await sleep(3000); - const repoContentsUrl = `${integrationConfig.config.baseUrl}/${owner}/${repo}/src/branch/${defaultBranch}`; + const repoContentsUrl = `${integrationConfig.config.baseUrl}/${owner}/${repo}/src/branch/${defaultBranch}/`; ctx.output('remoteUrl', remoteUrl); ctx.output('commitHash', commitResult?.commitHash); ctx.output('repoContentsUrl', repoContentsUrl); From 73295aa0df6c4b4f4ceef997b42ce502a1237cb6 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Thu, 4 Jan 2024 19:48:35 +0100 Subject: [PATCH 094/204] Change the request to test if the URL is reposning after the repo has been created. We dont get anymore Error: Unknown encoding: undefined during register action step Signed-off-by: cmoulliard --- .../src/actions/gitea.ts | 29 ++++--------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts index 7905f42e4a..412d437a7a 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts @@ -31,8 +31,8 @@ import { examples } from './gitea.examples'; import fetch, { RequestInit, Response } from 'node-fetch'; import crypto from 'crypto'; -/* NOT USED. See TODO hereafter -const checkGiteaOrgRepo = async ( +/* NOT USED. See TODO hereafter */ +const checkGiteaContentUrl = async ( config: GiteaIntegrationConfig, options: { owner?: string; @@ -44,15 +44,11 @@ const checkGiteaOrgRepo = async ( let response: Response; const getOptions: RequestInit = { method: 'GET', - headers: { - ...getGiteaRequestOptions(config).headers, - 'Content-Type': 'application/json', - }, }; try { response = await fetch( - `${config.baseUrl}/api/v1/repos/${owner}/${repo}/contents?ref=${defaultBranch}`, + `${config.baseUrl}/${owner}/${repo}/src/branch/${defaultBranch}`, getOptions, ); } catch (e) { @@ -62,7 +58,6 @@ const checkGiteaOrgRepo = async ( } return response; }; -*/ const checkGiteaOrg = async ( config: GiteaIntegrationConfig, @@ -305,33 +300,21 @@ export function createPublishGiteaAction(options: { gitAuthorInfo, }); - /* - TODO: This code is commented till it will be fixed. - When we use the code hereafter, we got the following error: - "Unable to read url, Error: Unknown encoding: undefined\n at DefaultLocationService.processEntities" - as commented here: Still getting the error: https://github.com/backstage/backstage/pull/21890#issuecomment-1876733870 - - Such an issue do not exist using sleep 3s. - - WARNING: To allow to register within the catalog the new project, it is also needed to pass within the catalogInfoPath the branch name (e.g: main/catalog-info.yaml) - - // Check if the repo is available + // Check if the gitea repo URL is available before to exit let response: Response; - response = await checkGiteaOrgRepo(integrationConfig.config, { + response = await checkGiteaContentUrl(integrationConfig.config, { owner, repo, defaultBranch, }); while (response.status !== 200) { await sleep(1000); - response = await checkGiteaOrgRepo(integrationConfig.config, { + response = await checkGiteaContentUrl(integrationConfig.config, { owner, repo, defaultBranch, }); } - */ - await sleep(3000); const repoContentsUrl = `${integrationConfig.config.baseUrl}/${owner}/${repo}/src/branch/${defaultBranch}/`; ctx.output('remoteUrl', remoteUrl); From a6b552b40733bcbcec3bade18b57481620028568 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Fri, 5 Jan 2024 16:31:18 +0100 Subject: [PATCH 095/204] Adding a new rest.get to allow gitea to check if the URL of the repo exists Signed-off-by: cmoulliard --- .../src/actions/gitea.test.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts index 8936c34690..0ab2fd507b 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts @@ -100,6 +100,16 @@ describe('publish:gitea', () => { }), ); }), + rest.get( + 'https://gitea.com/org1/repo/src/branch/main', + (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }, + ), rest.post('https://gitea.com/api/v1/orgs/org1/repos', (req, res, ctx) => { // Basic auth must match the user and password defined part of the config expect(req.headers.get('Authorization')).toBe( @@ -140,7 +150,7 @@ describe('publish:gitea', () => { expect(mockContext.output).toHaveBeenCalledWith( 'repoContentsUrl', - 'https://gitea.com/org1/repo/src/branch/main', + 'https://gitea.com/org1/repo/src/branch/main/', ); }); From 317d62675d3ee8c7cfb4c61bccb79a66021a6c66 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Fri, 5 Jan 2024 17:04:59 +0100 Subject: [PATCH 096/204] Fixing errors reported by documentation quality check github job Signed-off-by: cmoulliard --- .changeset/metal-clocks-suffer.md | 2 +- plugins/scaffolder-backend-module-gitea/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/metal-clocks-suffer.md b/.changeset/metal-clocks-suffer.md index f94deb380c..648276d4dd 100644 --- a/.changeset/metal-clocks-suffer.md +++ b/.changeset/metal-clocks-suffer.md @@ -5,4 +5,4 @@ '@backstage/integration': patch --- -Added support to create a git repository and publish a scaffolded project using a new action "publish:gitea" for gitea. The action currently supports to create a gitea org's repository - https://gitea.com/api/swagger#/organization/createOrgRepo +Added support to create a git repository and publish a scaffolded project using a new action "publish:gitea" for gitea. The action currently supports to create a gitea repository owned by an organization. See: https://gitea.com/api/swagger#/organization/createOrgRepo diff --git a/plugins/scaffolder-backend-module-gitea/README.md b/plugins/scaffolder-backend-module-gitea/README.md index 4a710a400c..8f9d39f4dc 100644 --- a/plugins/scaffolder-backend-module-gitea/README.md +++ b/plugins/scaffolder-backend-module-gitea/README.md @@ -46,7 +46,7 @@ integrations: ``` **NOTE**: As backstage will issue HTTPS/TLS requests to the gitea instance, it is needed to configure `gitea` with a valid certificate or at least with a -self-signed certificate `gitea cert --host localhost -ca`. Don't forget to set the env var `NODE_EXTRA_CA_CERTS` to point to the CA pem file before to launch backstage ! +self-signed certificate `gitea cert --host localhost -ca`. Don't forget to set the env var `NODE_EXTRA_CA_CERTS` to point to the CA file before to launch backstage ! When done, you can use the action in your template: From e300ddacd903fb35713c614c50f74cfced627298 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Wed, 10 Jan 2024 10:28:04 +0100 Subject: [PATCH 097/204] Revert change Token to token as gitea checks token word only. "https://docs.gitea.com/next/development/api-usage\#authentication" Signed-off-by: cmoulliard --- packages/integration/src/gitea/core.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integration/src/gitea/core.ts b/packages/integration/src/gitea/core.ts index f188613d2a..6d11a4ffde 100644 --- a/packages/integration/src/gitea/core.ts +++ b/packages/integration/src/gitea/core.ts @@ -124,7 +124,7 @@ export function getGiteaRequestOptions(config: GiteaIntegrationConfig): { `${username}:${password}`, ).toString('base64')}`; } else { - headers.Authorization = `Token ${password}`; + headers.Authorization = `token ${password}`; } return { From 0c211065dd715ca2fe0a1d0b3584e241bb3a8b04 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Wed, 10 Jan 2024 10:40:39 +0100 Subject: [PATCH 098/204] Better documented the changes of this PR part of the changeset file Signed-off-by: cmoulliard --- .changeset/metal-clocks-suffer.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.changeset/metal-clocks-suffer.md b/.changeset/metal-clocks-suffer.md index 648276d4dd..4cba0ba35b 100644 --- a/.changeset/metal-clocks-suffer.md +++ b/.changeset/metal-clocks-suffer.md @@ -5,4 +5,10 @@ '@backstage/integration': patch --- -Added support to create a git repository and publish a scaffolded project using a new action "publish:gitea" for gitea. The action currently supports to create a gitea repository owned by an organization. See: https://gitea.com/api/swagger#/organization/createOrgRepo +Created a gitea module for the scaffolder. This module provides a new action "publish:gitea" able to create a gitea repository owned by an organization. See: https://gitea.com/api/swagger#/organization/createOrgRepo + +Fixed the gitea authorization headers (used by the integration module) to lower case the words: token and basic + +Added a new test case to the integration module to verify the url of the gitea repository created by the getGiteaFileContentsUrl function. + +Verifying if the basicURL processed by the readGiteaConfig function is valid From 71c68535ae3605cf4083ef264e7ce8440743cea4 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Wed, 10 Jan 2024 10:41:38 +0100 Subject: [PATCH 099/204] Revert Basic to basic as the word checked by gitea api is basic Signed-off-by: cmoulliard --- packages/integration/src/gitea/core.test.ts | 2 +- packages/integration/src/gitea/core.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/integration/src/gitea/core.test.ts b/packages/integration/src/gitea/core.test.ts index ba41fa4761..ac29c469b6 100644 --- a/packages/integration/src/gitea/core.test.ts +++ b/packages/integration/src/gitea/core.test.ts @@ -118,7 +118,7 @@ describe('gitea core', () => { password: 'P', }; - const basicAuthentication = `Basic ${Buffer.from( + const basicAuthentication = `basic ${Buffer.from( `${authRequest.username}:${authRequest.password}`, ).toString('base64')}`; diff --git a/packages/integration/src/gitea/core.ts b/packages/integration/src/gitea/core.ts index 6d11a4ffde..95caa24c06 100644 --- a/packages/integration/src/gitea/core.ts +++ b/packages/integration/src/gitea/core.ts @@ -120,7 +120,7 @@ export function getGiteaRequestOptions(config: GiteaIntegrationConfig): { } if (username) { - headers.Authorization = `Basic ${Buffer.from( + headers.Authorization = `basic ${Buffer.from( `${username}:${password}`, ).toString('base64')}`; } else { From a755bf4bf758486236e0e7db1bc7ce984c431061 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Wed, 10 Jan 2024 10:51:31 +0100 Subject: [PATCH 100/204] Remove the non needed code block as createBuiltinActions.ts is a new function exported from latest backstage distribution and gitea will be included when this PR will be merged Signed-off-by: cmoulliard --- .../scaffolder-backend-module-gitea/README.md | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitea/README.md b/plugins/scaffolder-backend-module-gitea/README.md index 8f9d39f4dc..c35138a34c 100644 --- a/plugins/scaffolder-backend-module-gitea/README.md +++ b/plugins/scaffolder-backend-module-gitea/README.md @@ -14,23 +14,6 @@ yarn add --cwd packages/backend Configure the action (if not yet done): (you can check the [docs](https://backstage.io/docs/features/software-templates/writing-custom-actions#registering-custom-actions) to see all options): -```typescript -// plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts - -export const createBuiltinActions = ( -... - const actions = [ - ... - createPublishGiteaAction({ - integrations, - config, - }), - ... - ]; - return actions as TemplateAction[]; -}; -``` - Before to create a template, include to your `app-config.yaml` file the gitea host and credentials under the `integrations:` section From b39db7dc78060ab6f509858e6954cb68b6dc9ab1 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Wed, 10 Jan 2024 10:52:37 +0100 Subject: [PATCH 101/204] Change to launch backstage with launching backstage Signed-off-by: cmoulliard --- plugins/scaffolder-backend-module-gitea/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-gitea/README.md b/plugins/scaffolder-backend-module-gitea/README.md index c35138a34c..fe42b2fbaf 100644 --- a/plugins/scaffolder-backend-module-gitea/README.md +++ b/plugins/scaffolder-backend-module-gitea/README.md @@ -29,7 +29,7 @@ integrations: ``` **NOTE**: As backstage will issue HTTPS/TLS requests to the gitea instance, it is needed to configure `gitea` with a valid certificate or at least with a -self-signed certificate `gitea cert --host localhost -ca`. Don't forget to set the env var `NODE_EXTRA_CA_CERTS` to point to the CA file before to launch backstage ! +self-signed certificate `gitea cert --host localhost -ca`. Don't forget to set the env var `NODE_EXTRA_CA_CERTS` to point to the CA file before launching backstage ! When done, you can use the action in your template: From 6f145d5097cc02b80977632342327c17cd9a47a7 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Wed, 10 Jan 2024 10:59:21 +0100 Subject: [PATCH 102/204] Removed non needed text from the template example and improved the changes todo in a template Signed-off-by: cmoulliard --- .../scaffolder-backend-module-gitea/README.md | 50 +++---------------- 1 file changed, 8 insertions(+), 42 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitea/README.md b/plugins/scaffolder-backend-module-gitea/README.md index fe42b2fbaf..f13602f2d8 100644 --- a/plugins/scaffolder-backend-module-gitea/README.md +++ b/plugins/scaffolder-backend-module-gitea/README.md @@ -31,7 +31,10 @@ integrations: **NOTE**: As backstage will issue HTTPS/TLS requests to the gitea instance, it is needed to configure `gitea` with a valid certificate or at least with a self-signed certificate `gitea cert --host localhost -ca`. Don't forget to set the env var `NODE_EXTRA_CA_CERTS` to point to the CA file before launching backstage ! -When done, you can use the action in your template: +When done, you can create a template which: + +- Declare the `RepoUrlPicker` within the `spec/parameters` section to select the gitea hosts +- Include a step able to publish by example the newly generated project using the action `action: publish:gitea` ```yaml apiVersion: scaffolder.backstage.io/v1beta3 @@ -47,43 +50,7 @@ spec: owner: quarkus type: service parameters: - - title: Provide Information for Application - required: - - component_id - - owner - - java_package_name - properties: - component_id: - title: Name - type: string - description: Unique name of the component - default: my-quarkus-app - ui:field: EntityNamePicker - group_id: - title: Group Id - type: string - default: io.quarkus - description: Maven Group Id - artifact_id: - title: Artifact Id - type: string - default: quarkus-app - description: Maven Artifact Id - java_package_name: - title: Java Package Name - default: io.quarkus.demo - type: string - description: Name for the java package. eg (io.quarkus.blah) - owner: - title: Owner - type: string - description: IdP owner of the component - ui:field: OwnerPicker - ui:options: - allowedKinds: - - Group - - - title: Application git repository Information + - title: Git repository Information required: - repoUrl properties: @@ -93,8 +60,8 @@ spec: ui:field: RepoUrlPicker ui:options: allowedHosts: - - localhost:3333 - gitea.: + - localhost: steps: - id: template @@ -112,8 +79,7 @@ spec: artifact_id: ${{ parameters.artifact_id }} java_package_name: ${{ parameters.java_package_name }} owner: ${{ parameters.owner }} - destination: ${{ (parameters.repoUrl | parseRepoUrl).owner }}/${{ (parameters.repoUrl | parseRepoUrl).repo }} - quay_destination: ${{ parameters.image_organization }}/${{ parameters.component_id }} + destination: ${{ (parameters.repoUrl | parseRepoUrl).owner }}/${{ (parameters.repoUrl | parseRepoUrl).repo } port: 8080 - id: publish @@ -141,4 +107,4 @@ spec: entityRef: ${{ steps.register.output.entityRef }} ``` -Enjoy ;-) +Access the newly gitea repository created using the `repoContentsUrl` ;-) From 25c3850edf06a1fb96139448d88ec3948bc95d40 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Wed, 10 Jan 2024 11:09:16 +0100 Subject: [PATCH 103/204] Removing import of node-fetch to use the node built in fetch function directly Signed-off-by: cmoulliard --- .../scaffolder-backend-module-gitea/src/actions/gitea.test.ts | 2 +- plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts index 0ab2fd507b..5e1e94b1b4 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts @@ -113,7 +113,7 @@ describe('publish:gitea', () => { rest.post('https://gitea.com/api/v1/orgs/org1/repos', (req, res, ctx) => { // Basic auth must match the user and password defined part of the config expect(req.headers.get('Authorization')).toBe( - 'Basic Z2l0ZWFfdXNlcjpnaXRlYV9wYXNzd29yZA==', + 'basic Z2l0ZWFfdXNlcjpnaXRlYV9wYXNzd29yZA==', ); expect(req.body).toEqual({ name: 'repo', diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts index 412d437a7a..c022bf4d08 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts @@ -28,10 +28,8 @@ import { parseRepoUrl, } from '@backstage/plugin-scaffolder-node'; import { examples } from './gitea.examples'; -import fetch, { RequestInit, Response } from 'node-fetch'; import crypto from 'crypto'; -/* NOT USED. See TODO hereafter */ const checkGiteaContentUrl = async ( config: GiteaIntegrationConfig, options: { From 107050672c69def8fbfe4131041d9514f76da6e3 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Wed, 10 Jan 2024 12:19:20 +0100 Subject: [PATCH 104/204] Add a function to test if while don't exceed 20s and test if signal is aborted Signed-off-by: cmoulliard --- .../src/actions/gitea.ts | 86 +++++++++++++++---- 1 file changed, 70 insertions(+), 16 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts index c022bf4d08..d38802d76e 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts @@ -18,10 +18,13 @@ import { InputError } from '@backstage/errors'; import { Config } from '@backstage/config'; import { getGiteaRequestOptions, + GiteaIntegration, GiteaIntegrationConfig, ScmIntegrationRegistry, + ScmIntegrationsGroup, } from '@backstage/integration'; import { + ActionContext, createTemplateAction, getRepoSourceDirectory, initRepoAndPush, @@ -149,6 +152,54 @@ const generateCommitMessage = ( return msg; }; +/** + * Checks if the provided function can be executed within a specific period of time limit. + * @param fn + * @param timeLimit + */ +function checkDurationLimit(fn: () => void, timeLimit: number): boolean { + const startTime = process.hrtime(); + + // Call the function + fn(); + + const endTime = process.hrtime(startTime); + const durationInMs = endTime[0] * 1000 + endTime[1] / 1e6; + + // Check if the duration exceeds the time limit + return durationInMs <= timeLimit; +} + +export async function checkAvailabilityGiteaRepository( + integrationConfig: GiteaIntegrationConfig, + options: { + owner?: string; + repo: string; + defaultBranch: string; + ctx: ActionContext; + }, +) { + const { owner, repo, defaultBranch, ctx } = options; + const sleep = (ms: number | undefined) => new Promise(r => setTimeout(r, ms)); + let response: Response; + + response = await checkGiteaContentUrl(integrationConfig, { + owner, + repo, + defaultBranch, + }); + + while (response.status !== 200) { + if (ctx.signal?.aborted) return; + await sleep(1000); + response = await checkGiteaContentUrl(integrationConfig, { + owner, + repo, + defaultBranch, + }); + } +} + /** * Creates a new action that initializes a git repository using the content of the workspace. * and publishes it to a Gitea instance. @@ -242,9 +293,6 @@ export function createPublishGiteaAction(options: { sourcePath, } = ctx.input; - const sleep = (ms: number | undefined) => - new Promise(r => setTimeout(r, ms)); - const { repo, host, owner } = parseRepoUrl(repoUrl, integrations); const integrationConfig = integrations.gitea.byHost(host); @@ -299,19 +347,20 @@ export function createPublishGiteaAction(options: { }); // Check if the gitea repo URL is available before to exit - let response: Response; - response = await checkGiteaContentUrl(integrationConfig.config, { - owner, - repo, - defaultBranch, - }); - while (response.status !== 200) { - await sleep(1000); - response = await checkGiteaContentUrl(integrationConfig.config, { - owner, - repo, - defaultBranch, - }); + const operationTimeLimit = 20000; // 20 seconds + const checkDuration = checkDurationLimit( + () => + checkAvailabilityGiteaRepository(integrationConfig.config, { + owner, + repo, + defaultBranch, + ctx, + }), + operationTimeLimit, + ); + + if (!checkDuration) { + console.log('Operation exceeded the time limit.'); } const repoContentsUrl = `${integrationConfig.config.baseUrl}/${owner}/${repo}/src/branch/${defaultBranch}/`; @@ -321,3 +370,8 @@ export function createPublishGiteaAction(options: { }, }); } + +async function dummySleep() { + const sleep = (ms: number | undefined) => new Promise(r => setTimeout(r, ms)); + await sleep(10000); +} From 48ecd88b134b3c79d70ee4ee6cad13b0a755cce2 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Wed, 10 Jan 2024 12:22:24 +0100 Subject: [PATCH 105/204] Declaring const { username, password } earlier within the file Signed-off-by: cmoulliard --- .../src/actions/gitea.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts index d38802d76e..35af81ce91 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts @@ -301,11 +301,9 @@ export function createPublishGiteaAction(options: { `No matching integration configuration for host ${host}, please check your integrations config`, ); } + const { username, password } = integrationConfig.config; - if ( - !integrationConfig.config.username || - !integrationConfig.config.password - ) { + if (!username || !password) { throw new Error( 'Credentials for Gitea integration required for this action.', ); @@ -323,8 +321,8 @@ export function createPublishGiteaAction(options: { }); const auth = { - username: integrationConfig.config.username!, - password: integrationConfig.config.password!, + username: username, + password: password, }; const gitAuthorInfo = { name: gitAuthorName @@ -370,8 +368,3 @@ export function createPublishGiteaAction(options: { }, }); } - -async function dummySleep() { - const sleep = (ms: number | undefined) => new Promise(r => setTimeout(r, ms)); - await sleep(10000); -} From 8c9b90bb3e7c38ca1be5b5288921800473355dfe Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Wed, 10 Jan 2024 12:24:49 +0100 Subject: [PATCH 106/204] Declaring the variables username, password earlier and change the message to pass the host for the missing credentials Signed-off-by: cmoulliard --- plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts index 35af81ce91..17fbf7ab0b 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts @@ -304,9 +304,7 @@ export function createPublishGiteaAction(options: { const { username, password } = integrationConfig.config; if (!username || !password) { - throw new Error( - 'Credentials for Gitea integration required for this action.', - ); + throw new Error('Credentials for the gitea ${host} required.'); } // check if the org exists within the gitea server From 842b9c00b79cb6b28ea689e1aed83b4caec662e0 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Wed, 10 Jan 2024 12:38:13 +0100 Subject: [PATCH 107/204] Removing ScmIntegrationsGroup & GiteaIntegration from import as non used Signed-off-by: cmoulliard --- plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts index 17fbf7ab0b..a2845a74c6 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts @@ -18,10 +18,8 @@ import { InputError } from '@backstage/errors'; import { Config } from '@backstage/config'; import { getGiteaRequestOptions, - GiteaIntegration, GiteaIntegrationConfig, ScmIntegrationRegistry, - ScmIntegrationsGroup, } from '@backstage/integration'; import { ActionContext, From 4ed5ffebdfb0670afb61e4949e8f28b0ca290c2c Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Tue, 16 Jan 2024 14:27:14 +0100 Subject: [PATCH 108/204] Removing the export prefix for the function: checkAvailabilityGiteaRepository as it used internally Signed-off-by: cmoulliard --- plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts index a2845a74c6..bd592cce4b 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts @@ -168,7 +168,7 @@ function checkDurationLimit(fn: () => void, timeLimit: number): boolean { return durationInMs <= timeLimit; } -export async function checkAvailabilityGiteaRepository( +async function checkAvailabilityGiteaRepository( integrationConfig: GiteaIntegrationConfig, options: { owner?: string; From 95b4910cf87076a5461a9ab64fd5f0d094e96b11 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Thu, 18 Jan 2024 16:12:30 +0100 Subject: [PATCH 109/204] Adding to tsc --skipLibCheck false --incremental false Signed-off-by: cmoulliard --- .github/workflows/deploy_packages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index c91fec948e..40351cba78 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -161,7 +161,7 @@ jobs: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} - name: build type declarations - run: yarn tsc:full + run: yarn tsc --skipLibCheck false --incremental false - name: build packages run: yarn backstage-cli repo build From d7b0c272c3f2535fccfbd00b89bd2bf2b83b2c66 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Thu, 18 Jan 2024 16:23:29 +0100 Subject: [PATCH 110/204] Revert "Adding to tsc --skipLibCheck false --incremental false" This reverts commit 11862cc739b2e2651a10d88169522e6fa16854fd. Signed-off-by: cmoulliard --- .github/workflows/deploy_packages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index 40351cba78..c91fec948e 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -161,7 +161,7 @@ jobs: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} - name: build type declarations - run: yarn tsc --skipLibCheck false --incremental false + run: yarn tsc:full - name: build packages run: yarn backstage-cli repo build From 9b0bf20dba51b75029916978de74e014416ce7af Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Tue, 30 Jan 2024 16:33:40 +0100 Subject: [PATCH 111/204] Removed non relevant information from changeset and splitting changes into 2 changeset files Signed-off-by: cmoulliard --- .changeset/metal-clocks-suffer.md | 8 -------- .changeset/thick-pillows-punch.md | 5 +++++ 2 files changed, 5 insertions(+), 8 deletions(-) create mode 100644 .changeset/thick-pillows-punch.md diff --git a/.changeset/metal-clocks-suffer.md b/.changeset/metal-clocks-suffer.md index 4cba0ba35b..c459be9fa5 100644 --- a/.changeset/metal-clocks-suffer.md +++ b/.changeset/metal-clocks-suffer.md @@ -1,14 +1,6 @@ --- '@backstage/plugin-scaffolder-backend-module-gitea': minor '@backstage/plugin-scaffolder-backend': minor -'@backstage/plugin-scaffolder-node': minor -'@backstage/integration': patch --- Created a gitea module for the scaffolder. This module provides a new action "publish:gitea" able to create a gitea repository owned by an organization. See: https://gitea.com/api/swagger#/organization/createOrgRepo - -Fixed the gitea authorization headers (used by the integration module) to lower case the words: token and basic - -Added a new test case to the integration module to verify the url of the gitea repository created by the getGiteaFileContentsUrl function. - -Verifying if the basicURL processed by the readGiteaConfig function is valid diff --git a/.changeset/thick-pillows-punch.md b/.changeset/thick-pillows-punch.md new file mode 100644 index 0000000000..def589d3f8 --- /dev/null +++ b/.changeset/thick-pillows-punch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-node': patch +--- + +Add gitea as new type to be used from integrations configuration From b4748b36eb875218ad7340ef19aa1f50bd83841b Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Tue, 30 Jan 2024 17:40:06 +0100 Subject: [PATCH 112/204] Refactoring the code to include the maxDuration within the function checking if gitea repository is available Signed-off-by: cmoulliard --- .../src/actions/gitea.ts | 60 +++++++------------ 1 file changed, 20 insertions(+), 40 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts index bd592cce4b..e7f62f51da 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.ts @@ -150,25 +150,8 @@ const generateCommitMessage = ( return msg; }; -/** - * Checks if the provided function can be executed within a specific period of time limit. - * @param fn - * @param timeLimit - */ -function checkDurationLimit(fn: () => void, timeLimit: number): boolean { - const startTime = process.hrtime(); - - // Call the function - fn(); - - const endTime = process.hrtime(startTime); - const durationInMs = endTime[0] * 1000 + endTime[1] / 1e6; - - // Check if the duration exceeds the time limit - return durationInMs <= timeLimit; -} - async function checkAvailabilityGiteaRepository( + maxDuration: number, integrationConfig: GiteaIntegrationConfig, options: { owner?: string; @@ -177,24 +160,25 @@ async function checkAvailabilityGiteaRepository( ctx: ActionContext; }, ) { + const startTimestamp = Date.now(); + const { owner, repo, defaultBranch, ctx } = options; const sleep = (ms: number | undefined) => new Promise(r => setTimeout(r, ms)); let response: Response; - response = await checkGiteaContentUrl(integrationConfig, { - owner, - repo, - defaultBranch, - }); - - while (response.status !== 200) { + while (Date.now() - startTimestamp < maxDuration) { if (ctx.signal?.aborted) return; - await sleep(1000); + response = await checkGiteaContentUrl(integrationConfig, { owner, repo, defaultBranch, }); + + if (response.status !== 200) { + // Repository is not yet available/accessible ... + await sleep(1000); + } } } @@ -341,22 +325,18 @@ export function createPublishGiteaAction(options: { }); // Check if the gitea repo URL is available before to exit - const operationTimeLimit = 20000; // 20 seconds - const checkDuration = checkDurationLimit( - () => - checkAvailabilityGiteaRepository(integrationConfig.config, { - owner, - repo, - defaultBranch, - ctx, - }), - operationTimeLimit, + const maxDuration = 20000; // 20 seconds + await checkAvailabilityGiteaRepository( + maxDuration, + integrationConfig.config, + { + owner, + repo, + defaultBranch, + ctx, + }, ); - if (!checkDuration) { - console.log('Operation exceeded the time limit.'); - } - const repoContentsUrl = `${integrationConfig.config.baseUrl}/${owner}/${repo}/src/branch/${defaultBranch}/`; ctx.output('remoteUrl', remoteUrl); ctx.output('commitHash', commitResult?.commitHash); From 2d0e7cdc69982cc935778519da106d243bea693b Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Thu, 1 Feb 2024 11:40:47 +0100 Subject: [PATCH 113/204] Add new changeset file to document what changed for @backstage/integration Signed-off-by: cmoulliard --- .changeset/shiny-jeans-begin.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/shiny-jeans-begin.md diff --git a/.changeset/shiny-jeans-begin.md b/.changeset/shiny-jeans-begin.md new file mode 100644 index 0000000000..d542747b34 --- /dev/null +++ b/.changeset/shiny-jeans-begin.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +Validate if the `baseUrl` is a valid URL From 06c1fce211ffd767298209407062736a3bf352b5 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Wed, 14 Feb 2024 19:05:54 +0100 Subject: [PATCH 114/204] Migrate the module to new Backend System Signed-off-by: cmoulliard --- .../api-report-alpha.md | 13 +++++ .../package.json | 17 ++++++- .../src/alpha.ts | 16 +++++++ .../src/module.ts | 48 +++++++++++++++++++ yarn.lock | 2 +- 5 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 plugins/scaffolder-backend-module-gitea/api-report-alpha.md create mode 100644 plugins/scaffolder-backend-module-gitea/src/alpha.ts create mode 100644 plugins/scaffolder-backend-module-gitea/src/module.ts diff --git a/plugins/scaffolder-backend-module-gitea/api-report-alpha.md b/plugins/scaffolder-backend-module-gitea/api-report-alpha.md new file mode 100644 index 0000000000..8cd41a271c --- /dev/null +++ b/plugins/scaffolder-backend-module-gitea/api-report-alpha.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/plugin-scaffolder-backend-module-gitea" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @public +const giteaModule: () => BackendFeature; +export default giteaModule; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/scaffolder-backend-module-gitea/package.json b/plugins/scaffolder-backend-module-gitea/package.json index c9e9a4e07d..13e57c9337 100644 --- a/plugins/scaffolder-backend-module-gitea/package.json +++ b/plugins/scaffolder-backend-module-gitea/package.json @@ -13,6 +13,21 @@ "backstage": { "role": "backend-plugin-module" }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } + }, "scripts": { "start": "backstage-cli package start", "build": "backstage-cli package build", @@ -23,10 +38,10 @@ "postpack": "backstage-cli package postpack" }, "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", - "@backstage/plugin-scaffolder-backend": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", "node-fetch": "^2.6.7", "yaml": "^2.0.0" diff --git a/plugins/scaffolder-backend-module-gitea/src/alpha.ts b/plugins/scaffolder-backend-module-gitea/src/alpha.ts new file mode 100644 index 0000000000..7c5ee49afa --- /dev/null +++ b/plugins/scaffolder-backend-module-gitea/src/alpha.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { giteaModule as default } from './module'; diff --git a/plugins/scaffolder-backend-module-gitea/src/module.ts b/plugins/scaffolder-backend-module-gitea/src/module.ts new file mode 100644 index 0000000000..658f240471 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitea/src/module.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + createBackendModule, + coreServices, +} from '@backstage/backend-plugin-api'; +import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha'; +import { createPublishGiteaAction } from './actions'; +import { ScmIntegrations } from '@backstage/integration'; + +/** + * @public + * The Gitea Module for the Scaffolder Backend + */ +export const giteaModule = createBackendModule({ + pluginId: 'scaffolder', + moduleId: 'gitea', + register({ registerInit }) { + registerInit({ + deps: { + scaffolder: scaffolderActionsExtensionPoint, + config: coreServices.rootConfig, + }, + async init({ scaffolder, config }) { + const integrations = ScmIntegrations.fromConfig(config); + scaffolder.addActions( + createPublishGiteaAction({ + integrations, + config, + }), + ); + }, + }); + }, +}); diff --git a/yarn.lock b/yarn.lock index 64bc6fc64e..3291d13beb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8497,12 +8497,12 @@ __metadata: resolution: "@backstage/plugin-scaffolder-backend-module-gitea@workspace:plugins/scaffolder-backend-module-gitea" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" - "@backstage/plugin-scaffolder-backend": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" msw: ^1.0.0 node-fetch: ^2.6.7 From 446dba2323bcc09522521e655be887ed38d869b4 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Thu, 15 Feb 2024 16:36:07 +0100 Subject: [PATCH 115/204] Only export index.ts and not alpha.ts Signed-off-by: cmoulliard --- .../scaffolder-backend-module-gitea/package.json | 4 ---- .../scaffolder-backend-module-gitea/src/alpha.ts | 16 ---------------- .../scaffolder-backend-module-gitea/src/index.ts | 1 + 3 files changed, 1 insertion(+), 20 deletions(-) delete mode 100644 plugins/scaffolder-backend-module-gitea/src/alpha.ts diff --git a/plugins/scaffolder-backend-module-gitea/package.json b/plugins/scaffolder-backend-module-gitea/package.json index 13e57c9337..f2623cc58e 100644 --- a/plugins/scaffolder-backend-module-gitea/package.json +++ b/plugins/scaffolder-backend-module-gitea/package.json @@ -15,14 +15,10 @@ }, "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.ts", "./package.json": "./package.json" }, "typesVersions": { "*": { - "alpha": [ - "src/alpha.ts" - ], "package.json": [ "package.json" ] diff --git a/plugins/scaffolder-backend-module-gitea/src/alpha.ts b/plugins/scaffolder-backend-module-gitea/src/alpha.ts deleted file mode 100644 index 7c5ee49afa..0000000000 --- a/plugins/scaffolder-backend-module-gitea/src/alpha.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2024 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export { giteaModule as default } from './module'; diff --git a/plugins/scaffolder-backend-module-gitea/src/index.ts b/plugins/scaffolder-backend-module-gitea/src/index.ts index 026da0735a..0b68069178 100644 --- a/plugins/scaffolder-backend-module-gitea/src/index.ts +++ b/plugins/scaffolder-backend-module-gitea/src/index.ts @@ -21,3 +21,4 @@ */ export * from './actions'; +export { giteaModule as default } from './module'; From 905f4708312f292796378c69f212cd8b125a1096 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Thu, 15 Feb 2024 21:07:32 +0100 Subject: [PATCH 116/204] Remove files to be published Signed-off-by: cmoulliard --- plugins/scaffolder-backend-module-gitea/package.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitea/package.json b/plugins/scaffolder-backend-module-gitea/package.json index f2623cc58e..101c629243 100644 --- a/plugins/scaffolder-backend-module-gitea/package.json +++ b/plugins/scaffolder-backend-module-gitea/package.json @@ -6,9 +6,7 @@ "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" }, "backstage": { "role": "backend-plugin-module" From 8db9fdaf29af2a059c9f70728bed531678d65840 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Fri, 16 Feb 2024 12:31:22 +0100 Subject: [PATCH 117/204] Add git repository to the package.json file of the module Signed-off-by: cmoulliard --- plugins/scaffolder-backend-module-gitea/package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/scaffolder-backend-module-gitea/package.json b/plugins/scaffolder-backend-module-gitea/package.json index 101c629243..17f30b226b 100644 --- a/plugins/scaffolder-backend-module-gitea/package.json +++ b/plugins/scaffolder-backend-module-gitea/package.json @@ -8,6 +8,11 @@ "publishConfig": { "access": "public" }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/scaffolder-backend-module-gitea" + }, "backstage": { "role": "backend-plugin-module" }, From f5e04e39d2e7da549309a21f008cc9f01c0b1e71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 16 Feb 2024 12:59:21 +0100 Subject: [PATCH 118/204] move away from deprecated types, import from auth-node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/smart-frogs-help.md | 5 ++ plugins/auth-backend/api-report.md | 76 +++++++++---------- .../src/identity/StaticTokenIssuer.ts | 4 +- .../auth-backend/src/identity/TokenFactory.ts | 5 +- plugins/auth-backend/src/identity/types.ts | 2 +- .../src/lib/oauth/OAuthAdapter.test.ts | 4 +- .../src/lib/oauth/OAuthAdapter.ts | 10 +-- plugins/auth-backend/src/lib/oauth/helpers.ts | 4 +- plugins/auth-backend/src/lib/oauth/types.ts | 3 +- .../lib/passport/PassportStrategyHelper.ts | 4 +- .../resolvers/CatalogAuthResolverContext.ts | 9 ++- .../src/providers/atlassian/provider.ts | 7 +- .../src/providers/auth0/provider.ts | 11 ++- .../src/providers/aws-alb/provider.ts | 7 +- .../providers/azure-easyauth/provider.test.ts | 3 +- .../src/providers/azure-easyauth/provider.ts | 16 ++-- .../src/providers/bitbucket/provider.test.ts | 2 +- .../src/providers/bitbucket/provider.ts | 7 +- .../bitbucketServer/provider.test.ts | 2 +- .../src/providers/bitbucketServer/provider.ts | 11 ++- .../cloudflare-access/provider.test.ts | 2 +- .../providers/cloudflare-access/provider.ts | 17 +++-- .../createAuthProviderIntegration.ts | 5 +- .../src/providers/gcp-iap/provider.ts | 7 +- .../src/providers/gitlab/provider.ts | 7 +- .../src/providers/google/provider.ts | 3 +- .../src/providers/microsoft/provider.ts | 3 +- .../src/providers/oauth2-proxy/provider.ts | 7 +- .../src/providers/oauth2/provider.ts | 7 +- .../src/providers/oidc/provider.ts | 3 +- .../src/providers/okta/provider.ts | 7 +- .../src/providers/onelogin/provider.ts | 11 ++- .../auth-backend/src/providers/providers.ts | 2 +- .../auth-backend/src/providers/resolvers.ts | 2 +- .../src/providers/saml/provider.ts | 16 ++-- plugins/auth-backend/src/providers/types.ts | 4 +- plugins/auth-backend/src/service/router.ts | 6 +- 37 files changed, 167 insertions(+), 134 deletions(-) create mode 100644 .changeset/smart-frogs-help.md diff --git a/.changeset/smart-frogs-help.md b/.changeset/smart-frogs-help.md new file mode 100644 index 0000000000..9c65c267bd --- /dev/null +++ b/.changeset/smart-frogs-help.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Internal refactor to no longer use deprecated types diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 40e5facbbd..8d81751b39 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -42,12 +42,12 @@ import { WebMessageResponse as WebMessageResponse_2 } from '@backstage/plugin-au // @public @deprecated export type AuthHandler = ( input: TAuthResult, - context: AuthResolverContext, + context: AuthResolverContext_2, ) => Promise; // @public @deprecated export type AuthHandlerResult = { - profile: ProfileInfo; + profile: ProfileInfo_2; }; // @public @@ -168,13 +168,13 @@ export type CookieConfigurer = CookieConfigurer_2; export function createAuthProviderIntegration< TCreateOptions extends unknown[], TResolvers extends { - [name in string]: (...args: any[]) => SignInResolver; + [name in string]: (...args: any[]) => SignInResolver_2; }, >(config: { - create: (...args: TCreateOptions) => AuthProviderFactory; + create: (...args: TCreateOptions) => AuthProviderFactory_2; resolvers?: TResolvers; }): Readonly<{ - create: (...args: TCreateOptions) => AuthProviderFactory; + create: (...args: TCreateOptions) => AuthProviderFactory_2; resolvers: Readonly; }>; @@ -186,7 +186,7 @@ export function createRouter(options: RouterOptions): Promise; // @public export const defaultAuthProviderFactories: { - [providerId: string]: AuthProviderFactory; + [providerId: string]: AuthProviderFactory_2; }; // @public (undocumented) @@ -226,13 +226,13 @@ export type GithubOAuthResult = { export type OAuth2ProxyResult = OAuth2ProxyResult_2; // @public @deprecated (undocumented) -export class OAuthAdapter implements AuthProviderRouteHandlers { +export class OAuthAdapter implements AuthProviderRouteHandlers_2 { constructor(handlers: OAuthHandlers, options: OAuthAdapterOptions); // (undocumented) frameHandler(req: express.Request, res: express.Response): Promise; // (undocumented) static fromConfig( - config: AuthProviderConfig, + config: AuthProviderConfig_2, handlers: OAuthHandlers, options: Pick< OAuthAdapterOptions, @@ -253,7 +253,7 @@ export type OAuthAdapterOptions = { persistScopes?: boolean; appOrigin: string; baseUrl: string; - cookieConfigurer: CookieConfigurer; + cookieConfigurer: CookieConfigurer_2; isOriginAllowed: (origin: string) => boolean; callbackUrl: string; }; @@ -303,7 +303,7 @@ export type OAuthRefreshRequest = express.Request<{}> & { // @public @deprecated (undocumented) export type OAuthResponse = { - profile: ProfileInfo; + profile: ProfileInfo_2; providerInfo: OAuthProviderInfo; backstageIdentity?: BackstageSignInResult; }; @@ -354,7 +354,7 @@ export type ProfileInfo = ProfileInfo_2; // @public (undocumented) export type ProviderFactories = { - [s: string]: AuthProviderFactory; + [s: string]: AuthProviderFactory_2; }; // @public @@ -366,7 +366,7 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; } @@ -381,7 +381,7 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; } @@ -395,7 +395,7 @@ export const providers: Readonly<{ | { authHandler?: AuthHandler | undefined; signIn: { - resolver: SignInResolver; + resolver: SignInResolver_2; }; } | undefined, @@ -409,15 +409,15 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; } | undefined, ) => AuthProviderFactory_2; resolvers: Readonly<{ - usernameMatchingUserEntityAnnotation(): SignInResolver; - userIdMatchingUserEntityAnnotation(): SignInResolver; + usernameMatchingUserEntityAnnotation(): SignInResolver_2; + userIdMatchingUserEntityAnnotation(): SignInResolver_2; }>; }>; bitbucketServer: Readonly<{ @@ -427,33 +427,33 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; } | undefined, ) => AuthProviderFactory_2; resolvers: Readonly<{ - emailMatchingUserEntityProfileEmail: () => SignInResolver; + emailMatchingUserEntityProfileEmail: () => SignInResolver_2; }>; }>; cfAccess: Readonly<{ create: (options: { authHandler?: AuthHandler | undefined; signIn: { - resolver: SignInResolver; + resolver: SignInResolver_2; }; cache?: CacheService | undefined; }) => AuthProviderFactory_2; resolvers: Readonly<{ - emailMatchingUserEntityProfileEmail: () => SignInResolver; + emailMatchingUserEntityProfileEmail: () => SignInResolver_2; }>; }>; gcpIap: Readonly<{ create: (options: { authHandler?: AuthHandler | undefined; signIn: { - resolver: SignInResolver; + resolver: SignInResolver_2; }; }) => AuthProviderFactory_2; resolvers: never; @@ -483,7 +483,7 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; } @@ -498,7 +498,7 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; } @@ -517,7 +517,7 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; } @@ -536,7 +536,7 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; } @@ -548,7 +548,7 @@ export const providers: Readonly<{ create: (options: { authHandler?: AuthHandler | undefined; signIn: { - resolver: SignInResolver; + resolver: SignInResolver_2; }; }) => AuthProviderFactory_2; resolvers: never; @@ -560,15 +560,15 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; } | undefined, ) => AuthProviderFactory_2; resolvers: Readonly<{ - emailLocalPartMatchingUserEntityName: () => SignInResolver; - emailMatchingUserEntityProfileEmail: () => SignInResolver; + emailLocalPartMatchingUserEntityName: () => SignInResolver_2; + emailMatchingUserEntityProfileEmail: () => SignInResolver_2; }>; }>; okta: Readonly<{ @@ -578,16 +578,16 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; } | undefined, ) => AuthProviderFactory_2; resolvers: Readonly<{ - emailLocalPartMatchingUserEntityName: () => SignInResolver; - emailMatchingUserEntityProfileEmail: () => SignInResolver; - emailMatchingUserEntityAnnotation(): SignInResolver; + emailLocalPartMatchingUserEntityName: () => SignInResolver_2; + emailMatchingUserEntityProfileEmail: () => SignInResolver_2; + emailMatchingUserEntityAnnotation(): SignInResolver_2; }>; }>; onelogin: Readonly<{ @@ -597,7 +597,7 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; } @@ -612,14 +612,14 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; } | undefined, ) => AuthProviderFactory_2; resolvers: Readonly<{ - nameIdMatchingUserEntityName(): SignInResolver; + nameIdMatchingUserEntityName(): SignInResolver_2; }>; }>; easyAuth: Readonly<{ @@ -628,7 +628,7 @@ export const providers: Readonly<{ | { authHandler?: AuthHandler | undefined; signIn: { - resolver: SignInResolver; + resolver: SignInResolver_2; }; } | undefined, diff --git a/plugins/auth-backend/src/identity/StaticTokenIssuer.ts b/plugins/auth-backend/src/identity/StaticTokenIssuer.ts index 41dc96e71c..17fbe1b180 100644 --- a/plugins/auth-backend/src/identity/StaticTokenIssuer.ts +++ b/plugins/auth-backend/src/identity/StaticTokenIssuer.ts @@ -13,12 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AnyJWK, TokenIssuer, TokenParams } from './types'; + +import { AnyJWK, TokenIssuer } from './types'; import { SignJWT, importJWK, JWK } from 'jose'; import { parseEntityRef } from '@backstage/catalog-model'; import { AuthenticationError } from '@backstage/errors'; import { LoggerService } from '@backstage/backend-plugin-api'; import { StaticKeyStore } from './StaticKeyStore'; +import { TokenParams } from '@backstage/plugin-auth-node'; const MS_IN_S = 1000; diff --git a/plugins/auth-backend/src/identity/TokenFactory.ts b/plugins/auth-backend/src/identity/TokenFactory.ts index f778077267..4c2e7bac7a 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.ts @@ -13,14 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { parseEntityRef } from '@backstage/catalog-model'; import { AuthenticationError } from '@backstage/errors'; import { exportJWK, generateKeyPair, importJWK, JWK, SignJWT } from 'jose'; import { DateTime } from 'luxon'; import { v4 as uuid } from 'uuid'; import { LoggerService } from '@backstage/backend-plugin-api'; - -import { AnyJWK, KeyStore, TokenIssuer, TokenParams } from './types'; +import { TokenParams } from '@backstage/plugin-auth-node'; +import { AnyJWK, KeyStore, TokenIssuer } from './types'; const MS_IN_S = 1000; const MAX_TOKEN_LENGTH = 32768; // At 64 bytes per entity ref this still leaves room for about 500 entities diff --git a/plugins/auth-backend/src/identity/types.ts b/plugins/auth-backend/src/identity/types.ts index fcfc0345cc..059b9fca84 100644 --- a/plugins/auth-backend/src/identity/types.ts +++ b/plugins/auth-backend/src/identity/types.ts @@ -37,7 +37,7 @@ export type TokenIssuer = { /** * Issues a new ID Token */ - issueToken(params: TokenParams): Promise; + issueToken(params: _TokenParams): Promise; /** * List all public keys that are currently being used to sign tokens, or have been used diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts index 1b6653d97e..48163e6e82 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts @@ -17,8 +17,8 @@ import express from 'express'; import { THOUSAND_DAYS_MS, TEN_MINUTES_MS, OAuthAdapter } from './OAuthAdapter'; import { encodeState } from './helpers'; -import { OAuthHandlers, OAuthLogoutRequest, OAuthState } from './types'; -import { CookieConfigurer } from '../../providers/types'; +import { OAuthHandlers, OAuthLogoutRequest } from './types'; +import { CookieConfigurer, OAuthState } from '@backstage/plugin-auth-node'; const mockResponseData = { providerInfo: { diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index 9b4afb4b2b..b5c3642024 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -18,14 +18,13 @@ import express, { CookieOptions } from 'express'; import crypto from 'crypto'; import { URL } from 'url'; import { + AuthProviderConfig, + AuthProviderRouteHandlers, BackstageIdentityResponse, BackstageSignInResult, -} from '@backstage/plugin-auth-node'; -import { - AuthProviderRouteHandlers, - AuthProviderConfig, CookieConfigurer, -} from '../../providers/types'; + OAuthState, +} from '@backstage/plugin-auth-node'; import { AuthenticationError, InputError, @@ -42,7 +41,6 @@ import { OAuthHandlers, OAuthStartRequest, OAuthRefreshRequest, - OAuthState, OAuthLogoutRequest, } from './types'; import { prepareBackstageIdentityResponse } from '../../providers/prepareBackstageIdentityResponse'; diff --git a/plugins/auth-backend/src/lib/oauth/helpers.ts b/plugins/auth-backend/src/lib/oauth/helpers.ts index 5e67b072e3..fef6dd04ae 100644 --- a/plugins/auth-backend/src/lib/oauth/helpers.ts +++ b/plugins/auth-backend/src/lib/oauth/helpers.ts @@ -15,9 +15,9 @@ */ import express from 'express'; -import { OAuthState } from './types'; -import { CookieConfigurer } from '../../providers/types'; import { + CookieConfigurer, + OAuthState, decodeOAuthState, encodeOAuthState, } from '@backstage/plugin-auth-node'; diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index b7205c9b85..76689abcdc 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -18,9 +18,10 @@ import express from 'express'; import { Profile as PassportProfile } from 'passport'; import { BackstageSignInResult, + ProfileInfo, OAuthState as _OAuthState, } from '@backstage/plugin-auth-node'; -import { OAuthStartResponse, ProfileInfo } from '../../providers/types'; +import { OAuthStartResponse } from '../../providers/types'; /** * Common options for passport.js-based OAuth providers diff --git a/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts index 44feb916c5..88d3dbfa04 100644 --- a/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts @@ -18,9 +18,9 @@ import express from 'express'; import passport from 'passport'; import { decodeJwt } from 'jose'; import { InternalOAuthError } from 'passport-oauth2'; - +import { ProfileInfo } from '@backstage/plugin-auth-node'; import { PassportProfile } from './types'; -import { ProfileInfo, OAuthStartResponse } from '../../providers/types'; +import { OAuthStartResponse } from '../../providers/types'; export type PassportDoneCallback = ( err?: Error, diff --git a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts index 7d22526193..a7e02c1c31 100644 --- a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts +++ b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts @@ -25,10 +25,13 @@ import { } from '@backstage/catalog-model'; import { ConflictError, InputError, NotFoundError } from '@backstage/errors'; import { LoggerService } from '@backstage/backend-plugin-api'; -import { TokenIssuer, TokenParams } from '../../identity/types'; -import { AuthResolverContext } from '../../providers'; -import { AuthResolverCatalogUserQuery } from '../../providers/types'; +import { TokenIssuer } from '../../identity/types'; import { CatalogIdentityClient } from '../catalog'; +import { + AuthResolverCatalogUserQuery, + AuthResolverContext, + TokenParams, +} from '@backstage/plugin-auth-node'; /** * Uses the default ownership resolution logic to return an array diff --git a/plugins/auth-backend/src/providers/atlassian/provider.ts b/plugins/auth-backend/src/providers/atlassian/provider.ts index a142d1de90..0cd95e19d7 100644 --- a/plugins/auth-backend/src/providers/atlassian/provider.ts +++ b/plugins/auth-backend/src/providers/atlassian/provider.ts @@ -14,10 +14,13 @@ * limitations under the License. */ -import { SignInResolver, AuthHandler } from '../types'; +import { AuthHandler } from '../types'; import { OAuthResult } from '../../lib/oauth'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; -import { createOAuthProviderFactory } from '@backstage/plugin-auth-node'; +import { + SignInResolver, + createOAuthProviderFactory, +} from '@backstage/plugin-auth-node'; import { adaptLegacyOAuthHandler, adaptLegacyOAuthSignInResolver, diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 95e83d2cce..c399eb08c4 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -36,14 +36,13 @@ import { makeProfileInfo, PassportDoneCallback, } from '../../lib/passport'; -import { - OAuthStartResponse, - AuthHandler, - SignInResolver, - AuthResolverContext, -} from '../types'; +import { OAuthStartResponse, AuthHandler } from '../types'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { StateStore } from 'passport-oauth2'; +import { + AuthResolverContext, + SignInResolver, +} from '@backstage/plugin-auth-node'; type PrivateInfo = { refreshToken: string; diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index 3883bbc88c..c09f307e96 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -18,8 +18,11 @@ import { AwsAlbResult, awsAlbAuthenticator, } from '@backstage/plugin-auth-backend-module-aws-alb-provider'; -import { createProxyAuthProviderFactory } from '@backstage/plugin-auth-node'; -import { AuthHandler, SignInResolver } from '../types'; +import { + SignInResolver, + createProxyAuthProviderFactory, +} from '@backstage/plugin-auth-node'; +import { AuthHandler } from '../types'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; /** diff --git a/plugins/auth-backend/src/providers/azure-easyauth/provider.test.ts b/plugins/auth-backend/src/providers/azure-easyauth/provider.test.ts index 1b7c51263c..f6a418b633 100644 --- a/plugins/auth-backend/src/providers/azure-easyauth/provider.test.ts +++ b/plugins/auth-backend/src/providers/azure-easyauth/provider.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { AuthHandler, AuthResolverContext } from '../types'; +import { AuthHandler } from '../types'; import { makeProfileInfo } from '../../lib/passport'; import { easyAuth, @@ -26,6 +26,7 @@ import { import { Request, Response } from 'express'; import { SignJWT, JWTPayload, errors as JoseErrors } from 'jose'; import { randomBytes } from 'crypto'; +import { AuthResolverContext } from '@backstage/plugin-auth-node'; const jwtSecret = randomBytes(48); diff --git a/plugins/auth-backend/src/providers/azure-easyauth/provider.ts b/plugins/auth-backend/src/providers/azure-easyauth/provider.ts index ca8cacf892..6f6fe72307 100644 --- a/plugins/auth-backend/src/providers/azure-easyauth/provider.ts +++ b/plugins/auth-backend/src/providers/azure-easyauth/provider.ts @@ -14,13 +14,7 @@ * limitations under the License. */ -import { - AuthHandler, - AuthProviderRouteHandlers, - AuthResolverContext, - AuthResponse, - SignInResolver, -} from '../types'; +import { AuthHandler } from '../types'; import { Request, Response } from 'express'; import { makeProfileInfo } from '../../lib/passport'; import { AuthenticationError } from '@backstage/errors'; @@ -28,6 +22,12 @@ import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityRes import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { Profile } from 'passport'; import { decodeJwt } from 'jose'; +import { + AuthProviderRouteHandlers, + AuthResolverContext, + ClientAuthResponse, + SignInResolver, +} from '@backstage/plugin-auth-node'; export const ID_TOKEN_HEADER = 'x-ms-token-aad-id-token'; export const ACCESS_TOKEN_HEADER = 'x-ms-token-aad-access-token'; @@ -44,7 +44,7 @@ export type EasyAuthResult = { accessToken?: string; }; -export type EasyAuthResponse = AuthResponse<{}>; +export type EasyAuthResponse = ClientAuthResponse<{}>; export class EasyAuthAuthProvider implements AuthProviderRouteHandlers { private readonly resolverContext: AuthResolverContext; diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.test.ts b/plugins/auth-backend/src/providers/bitbucket/provider.test.ts index 503eccffbd..66a8f6e396 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.test.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.test.ts @@ -16,7 +16,7 @@ import { BitbucketAuthProvider, BitbucketOAuthResult } from './provider'; import * as helpers from '../../lib/passport/PassportStrategyHelper'; -import { AuthResolverContext } from '../types'; +import { AuthResolverContext } from '@backstage/plugin-auth-node'; const mockFrameHandler = jest.spyOn( helpers, diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.ts b/plugins/auth-backend/src/providers/bitbucket/provider.ts index cfa30e9a73..4a7f3770a4 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.ts @@ -37,12 +37,11 @@ import { PassportDoneCallback, } from '../../lib/passport'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; +import { AuthHandler, OAuthStartResponse } from '../types'; import { - AuthHandler, - OAuthStartResponse, - SignInResolver, AuthResolverContext, -} from '../types'; + SignInResolver, +} from '@backstage/plugin-auth-node'; type PrivateInfo = { refreshToken: string; diff --git a/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts b/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts index f31d653b99..187c3b09a5 100644 --- a/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts +++ b/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts @@ -16,7 +16,6 @@ import * as helpers from '../../lib/passport/PassportStrategyHelper'; import { makeProfileInfo } from '../../lib/passport'; -import { AuthResolverContext } from '../types'; import { bitbucketServer, BitbucketServerAuthProvider, @@ -25,6 +24,7 @@ import { import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; +import { AuthResolverContext } from '@backstage/plugin-auth-node'; jest.mock('../../lib/passport/PassportStrategyHelper', () => { return { diff --git a/plugins/auth-backend/src/providers/bitbucketServer/provider.ts b/plugins/auth-backend/src/providers/bitbucketServer/provider.ts index 220f6db5cd..d66f7f33c0 100644 --- a/plugins/auth-backend/src/providers/bitbucketServer/provider.ts +++ b/plugins/auth-backend/src/providers/bitbucketServer/provider.ts @@ -32,17 +32,16 @@ import { executeRefreshTokenStrategy, makeProfileInfo, } from '../../lib/passport'; -import { - AuthHandler, - AuthResolverContext, - OAuthStartResponse, - SignInResolver, -} from '../types'; +import { AuthHandler, OAuthStartResponse } from '../types'; import express from 'express'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { Profile as PassportProfile } from 'passport'; import { commonByEmailResolver } from '../resolvers'; import fetch from 'node-fetch'; +import { + AuthResolverContext, + SignInResolver, +} from '@backstage/plugin-auth-node'; type PrivateInfo = { refreshToken: string; diff --git a/plugins/auth-backend/src/providers/cloudflare-access/provider.test.ts b/plugins/auth-backend/src/providers/cloudflare-access/provider.test.ts index 1abca4bdf4..95e4b0901b 100644 --- a/plugins/auth-backend/src/providers/cloudflare-access/provider.test.ts +++ b/plugins/auth-backend/src/providers/cloudflare-access/provider.test.ts @@ -21,8 +21,8 @@ import { CF_AUTH_IDENTITY, CloudflareAccessAuthProvider, } from './provider'; -import { AuthResolverContext } from '../types'; import fetch from 'node-fetch'; +import { AuthResolverContext } from '@backstage/plugin-auth-node'; const jwtMock = jwtVerify as jest.Mocked; const mockJwt = diff --git a/plugins/auth-backend/src/providers/cloudflare-access/provider.ts b/plugins/auth-backend/src/providers/cloudflare-access/provider.ts index 6313276508..fe271f9058 100644 --- a/plugins/auth-backend/src/providers/cloudflare-access/provider.ts +++ b/plugins/auth-backend/src/providers/cloudflare-access/provider.ts @@ -13,13 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - AuthHandler, - AuthProviderRouteHandlers, - AuthResolverContext, - AuthResponse, - SignInResolver, -} from '../types'; + +import { AuthHandler } from '../types'; import fetch, { Headers } from 'node-fetch'; import express from 'express'; import * as _ from 'lodash'; @@ -33,6 +28,12 @@ import { CacheClient } from '@backstage/backend-common'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; import { commonByEmailResolver } from '../resolvers'; +import { + AuthProviderRouteHandlers, + AuthResolverContext, + ClientAuthResponse, + SignInResolver, +} from '@backstage/plugin-auth-node'; // JWT Web Token definitions are in the URL below // https://developers.cloudflare.com/cloudflare-one/identity/users/validating-json/ @@ -174,7 +175,7 @@ export type CloudflareAccessProviderInfo = { }; export type CloudflareAccessResponse = - AuthResponse; + ClientAuthResponse; export class CloudflareAccessAuthProvider implements AuthProviderRouteHandlers { private readonly teamName: string; diff --git a/plugins/auth-backend/src/providers/createAuthProviderIntegration.ts b/plugins/auth-backend/src/providers/createAuthProviderIntegration.ts index 9143d63143..9846bb8bf9 100644 --- a/plugins/auth-backend/src/providers/createAuthProviderIntegration.ts +++ b/plugins/auth-backend/src/providers/createAuthProviderIntegration.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { AuthProviderFactory, SignInResolver } from './types'; +import { + AuthProviderFactory, + SignInResolver, +} from '@backstage/plugin-auth-node'; /** * Creates a standardized representation of an integration with a third-party diff --git a/plugins/auth-backend/src/providers/gcp-iap/provider.ts b/plugins/auth-backend/src/providers/gcp-iap/provider.ts index db4d9195b2..e0c5b62d47 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/provider.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/provider.ts @@ -15,9 +15,12 @@ */ import { gcpIapAuthenticator } from '@backstage/plugin-auth-backend-module-gcp-iap-provider'; -import { createProxyAuthProviderFactory } from '@backstage/plugin-auth-node'; +import { + SignInResolver, + createProxyAuthProviderFactory, +} from '@backstage/plugin-auth-node'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; -import { AuthHandler, SignInResolver } from '../types'; +import { AuthHandler } from '../types'; import { GcpIapResult } from './types'; /** diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 2551d334ec..899338adce 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -14,10 +14,13 @@ * limitations under the License. */ -import { SignInResolver, AuthHandler } from '../types'; +import { AuthHandler } from '../types'; import { OAuthResult } from '../../lib/oauth'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; -import { createOAuthProviderFactory } from '@backstage/plugin-auth-node'; +import { + SignInResolver, + createOAuthProviderFactory, +} from '@backstage/plugin-auth-node'; import { adaptLegacyOAuthHandler, adaptLegacyOAuthSignInResolver, diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index d467977094..b5e6e7127c 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -19,6 +19,7 @@ import { googleSignInResolvers, } from '@backstage/plugin-auth-backend-module-google-provider'; import { + SignInResolver, commonSignInResolvers, createOAuthProviderFactory, } from '@backstage/plugin-auth-node'; @@ -29,7 +30,7 @@ import { } from '../../lib/legacy'; import { OAuthResult } from '../../lib/oauth'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; -import { AuthHandler, SignInResolver } from '../types'; +import { AuthHandler } from '../types'; /** * Auth provider integration for Google auth diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index b004cb9651..f6a3a83bcb 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -14,10 +14,11 @@ * limitations under the License. */ -import { SignInResolver, AuthHandler } from '../types'; +import { AuthHandler } from '../types'; import { OAuthResult } from '../../lib/oauth'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { + SignInResolver, commonSignInResolvers, createOAuthProviderFactory, } from '@backstage/plugin-auth-node'; diff --git a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts index 5d75167e84..4200355c1a 100644 --- a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts @@ -14,8 +14,11 @@ * limitations under the License. */ -import { createProxyAuthProviderFactory } from '@backstage/plugin-auth-node'; -import { AuthHandler, SignInResolver } from '../types'; +import { + SignInResolver, + createProxyAuthProviderFactory, +} from '@backstage/plugin-auth-node'; +import { AuthHandler } from '../types'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { type OAuth2ProxyResult, diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index de6e7b1cfa..3a00de1f95 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -15,13 +15,16 @@ */ import { OAuthResult } from '../../lib/oauth'; -import { AuthHandler, SignInResolver } from '../types'; +import { AuthHandler } from '../types'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { adaptLegacyOAuthHandler, adaptLegacyOAuthSignInResolver, } from '../../lib/legacy'; -import { createOAuthProviderFactory } from '@backstage/plugin-auth-node'; +import { + SignInResolver, + createOAuthProviderFactory, +} from '@backstage/plugin-auth-node'; import { oauth2Authenticator } from '@backstage/plugin-auth-backend-module-oauth2-provider'; /** diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index 9bc78c48d2..40e837b0b0 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { AuthHandler, SignInResolver } from '../types'; +import { AuthHandler } from '../types'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { createOAuthProviderFactory, @@ -22,6 +22,7 @@ import { BackstageSignInResult, OAuthAuthenticatorResult, SignInInfo, + SignInResolver, } from '@backstage/plugin-auth-node'; import { oidcAuthenticator, diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 669914e7fc..463afc2bf4 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -14,11 +14,14 @@ * limitations under the License. */ -import { AuthHandler, SignInResolver } from '../types'; +import { AuthHandler } from '../types'; import { OAuthResult } from '../../lib/oauth'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; -import { createOAuthProviderFactory } from '@backstage/plugin-auth-node'; +import { + SignInResolver, + createOAuthProviderFactory, +} from '@backstage/plugin-auth-node'; import { adaptLegacyOAuthHandler, adaptLegacyOAuthSignInResolver, diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index ac636f92cc..c5ba57f090 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -36,13 +36,12 @@ import { executeFetchUserProfileStrategy, PassportDoneCallback, } from '../../lib/passport'; -import { - OAuthStartResponse, - AuthHandler, - SignInResolver, - AuthResolverContext, -} from '../types'; +import { OAuthStartResponse, AuthHandler } from '../types'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; +import { + AuthResolverContext, + SignInResolver, +} from '@backstage/plugin-auth-node'; type PrivateInfo = { refreshToken: string; diff --git a/plugins/auth-backend/src/providers/providers.ts b/plugins/auth-backend/src/providers/providers.ts index 36a24f4f6c..76ac51f662 100644 --- a/plugins/auth-backend/src/providers/providers.ts +++ b/plugins/auth-backend/src/providers/providers.ts @@ -30,9 +30,9 @@ import { oidc } from './oidc'; import { okta } from './okta'; import { onelogin } from './onelogin'; import { saml } from './saml'; -import { AuthProviderFactory } from './types'; import { bitbucketServer } from './bitbucketServer'; import { easyAuth } from './azure-easyauth'; +import { AuthProviderFactory } from '@backstage/plugin-auth-node'; /** * All built-in auth provider integrations. diff --git a/plugins/auth-backend/src/providers/resolvers.ts b/plugins/auth-backend/src/providers/resolvers.ts index 129c29c5e4..54c78ff182 100644 --- a/plugins/auth-backend/src/providers/resolvers.ts +++ b/plugins/auth-backend/src/providers/resolvers.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { SignInResolver } from './types'; +import { SignInResolver } from '@backstage/plugin-auth-node'; /** * A common sign-in resolver that looks up the user using the local part of diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 0da55ed3e4..d922034e2f 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -25,17 +25,17 @@ import { executeFrameHandlerStrategy, executeRedirectStrategy, } from '../../lib/passport'; -import { - AuthProviderRouteHandlers, - AuthHandler, - SignInResolver, - AuthResponse, - AuthResolverContext, -} from '../types'; +import { AuthHandler } from '../types'; import { postMessageResponse } from '../../lib/flow'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { AuthenticationError, isError } from '@backstage/errors'; import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; +import { + AuthProviderRouteHandlers, + AuthResolverContext, + ClientAuthResponse, + SignInResolver, +} from '@backstage/plugin-auth-node'; /** @public */ export type SamlAuthResult = { @@ -93,7 +93,7 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { const { profile } = await this.authHandler(result, this.resolverContext); - const response: AuthResponse<{}> = { + const response: ClientAuthResponse<{}> = { profile, providerInfo: {}, }; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 354387153c..40c693506e 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -110,7 +110,7 @@ export type SignInResolver = _SignInResolver; * @public * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead */ -export type AuthHandlerResult = { profile: ProfileInfo }; +export type AuthHandlerResult = { profile: _ProfileInfo }; /** * The AuthHandler function is called every time the user authenticates using @@ -128,7 +128,7 @@ export type AuthHandlerResult = { profile: ProfileInfo }; */ export type AuthHandler = ( input: TAuthResult, - context: AuthResolverContext, + context: _AuthResolverContext, ) => Promise; /** diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 44861207ed..a8876a7559 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -18,10 +18,7 @@ import express from 'express'; import Router from 'express-promise-router'; import cookieParser from 'cookie-parser'; import { LoggerService } from '@backstage/backend-plugin-api'; -import { - defaultAuthProviderFactories, - AuthProviderFactory, -} from '../providers'; +import { defaultAuthProviderFactories } from '../providers'; import { PluginDatabaseManager, PluginEndpointDiscovery, @@ -41,6 +38,7 @@ import { TokenIssuer } from '../identity/types'; import { StaticTokenIssuer } from '../identity/StaticTokenIssuer'; import { StaticKeyStore } from '../identity/StaticKeyStore'; import { Config } from '@backstage/config'; +import { AuthProviderFactory } from '@backstage/plugin-auth-node'; /** @public */ export type ProviderFactories = { [s: string]: AuthProviderFactory }; From ff40ada6ba27e6ea846cad9212a10e8c2c7f4f50 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 12:20:10 +0000 Subject: [PATCH 119/204] fix(deps): update dependency mysql2 to v3 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .changeset/renovate-755938a.md | 6 +++ packages/backend-common/package.json | 4 +- packages/backend-test-utils/package.json | 2 +- packages/backend/package.json | 2 +- packages/e2e-test/package.json | 2 +- yarn.lock | 62 ++++++++++++------------ 6 files changed, 42 insertions(+), 36 deletions(-) create mode 100644 .changeset/renovate-755938a.md diff --git a/.changeset/renovate-755938a.md b/.changeset/renovate-755938a.md new file mode 100644 index 0000000000..1182d57c16 --- /dev/null +++ b/.changeset/renovate-755938a.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-common': patch +'@backstage/backend-test-utils': patch +--- + +Updated dependency `mysql2` to `^3.0.0`. diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index ba39662e39..223bdc04ee 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -94,7 +94,7 @@ "logform": "^2.3.2", "luxon": "^3.0.0", "minimatch": "^5.0.0", - "mysql2": "^2.2.5", + "mysql2": "^3.0.0", "node-fetch": "^2.6.7", "p-limit": "^3.1.0", "pg": "^8.11.3", @@ -133,7 +133,7 @@ "better-sqlite3": "^9.0.0", "http-errors": "^2.0.0", "msw": "^1.0.0", - "mysql2": "^2.2.5", + "mysql2": "^3.0.0", "supertest": "^6.1.3" }, "files": [ diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 7d12ad8030..1516250232 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -54,7 +54,7 @@ "fs-extra": "^11.0.0", "knex": "^3.0.0", "msw": "^1.0.0", - "mysql2": "^2.2.5", + "mysql2": "^3.0.0", "pg": "^8.11.3", "testcontainers": "^8.1.2", "textextensions": "^5.16.0", diff --git a/packages/backend/package.json b/packages/backend/package.json index d7039887e2..0772fbaef4 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -93,7 +93,7 @@ "express-prom-bundle": "^7.0.0", "express-promise-router": "^4.1.0", "luxon": "^3.0.0", - "mysql2": "^2.2.5", + "mysql2": "^3.0.0", "pg": "^8.11.3", "pg-connection-string": "^2.3.0", "prom-client": "^15.0.0", diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 6848a0dd54..db5140f3ca 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -35,7 +35,7 @@ "cross-fetch": "^4.0.0", "fs-extra": "^11.2.0", "handlebars": "^4.7.3", - "mysql2": "^2.2.5", + "mysql2": "^3.0.0", "pgtools": "^1.0.0", "tree-kill": "^1.2.2" }, diff --git a/yarn.lock b/yarn.lock index 3394871837..3b511bebd9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3329,7 +3329,7 @@ __metadata: luxon: ^3.0.0 minimatch: ^5.0.0 msw: ^1.0.0 - mysql2: ^2.2.5 + mysql2: ^3.0.0 node-fetch: ^2.6.7 p-limit: ^3.1.0 pg: ^8.11.3 @@ -3483,7 +3483,7 @@ __metadata: fs-extra: ^11.0.0 knex: ^3.0.0 msw: ^1.0.0 - mysql2: ^2.2.5 + mysql2: ^3.0.0 pg: ^8.11.3 supertest: ^6.1.3 testcontainers: ^8.1.2 @@ -25436,7 +25436,7 @@ __metadata: languageName: node linkType: hard -"denque@npm:^2.0.1, denque@npm:^2.1.0": +"denque@npm:^2.1.0": version: 2.1.0 resolution: "denque@npm:2.1.0" checksum: 1d4ae1d05e59ac3a3481e7b478293f4b4c813819342273f3d5b826c7ffa9753c520919ba264f377e09108d24ec6cf0ec0ac729a5686cbb8f32d797126c5dae74 @@ -25990,7 +25990,7 @@ __metadata: cross-fetch: ^4.0.0 fs-extra: ^11.2.0 handlebars: ^4.7.3 - mysql2: ^2.2.5 + mysql2: ^3.0.0 nodemon: ^3.0.1 pgtools: ^1.0.0 tree-kill: ^1.2.2 @@ -27654,7 +27654,7 @@ __metadata: express-prom-bundle: ^7.0.0 express-promise-router: ^4.1.0 luxon: ^3.0.0 - mysql2: ^2.2.5 + mysql2: ^3.0.0 pg: ^8.11.3 pg-connection-string: ^2.3.0 prom-client: ^15.0.0 @@ -34008,17 +34008,10 @@ __metadata: languageName: node linkType: hard -"long@npm:^4.0.0": - version: 4.0.0 - resolution: "long@npm:4.0.0" - checksum: 16afbe8f749c7c849db1f4de4e2e6a31ac6e617cead3bdc4f9605cb703cd20e1e9fc1a7baba674ffcca57d660a6e5b53a9e236d7b25a295d3855cca79cc06744 - languageName: node - linkType: hard - -"long@npm:^5.0.0": - version: 5.2.0 - resolution: "long@npm:5.2.0" - checksum: 37aa4e67b9c3eebc6d9d675adcc9d06f06059ca268922a71273de389746bf07f0ff282f9e604d17fdf84c4149099b44e936ea2b621a6c4759a216621afa97efd +"long@npm:^5.0.0, long@npm:^5.2.1": + version: 5.2.3 + resolution: "long@npm:5.2.3" + checksum: 885ede7c3de4facccbd2cacc6168bae3a02c3e836159ea4252c87b6e34d40af819824b2d4edce330bfb5c4d6e8ce3ec5864bdcf9473fa1f53a4f8225860e5897 languageName: node linkType: hard @@ -34080,7 +34073,7 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^4.0.1, lru-cache@npm:^4.1.3": +"lru-cache@npm:^4.0.1": version: 4.1.5 resolution: "lru-cache@npm:4.1.5" dependencies: @@ -34108,13 +34101,20 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^7.14.0, lru-cache@npm:^7.7.1": +"lru-cache@npm:^7.14.0, lru-cache@npm:^7.14.1, lru-cache@npm:^7.7.1": version: 7.18.3 resolution: "lru-cache@npm:7.18.3" checksum: e550d772384709deea3f141af34b6d4fa392e2e418c1498c078de0ee63670f1f46f5eee746e8ef7e69e1c895af0d4224e62ee33e66a543a14763b0f2e74c1356 languageName: node linkType: hard +"lru-cache@npm:^8.0.0": + version: 8.0.5 + resolution: "lru-cache@npm:8.0.5" + checksum: 87d72196d8f46e8299c4ab576ed2ec8a07e3cbef517dc9874399c0b2470bd9bf62aacec3b67f84ed6d74aaa1ef31636d048edf996f76248fd17db72bfb631609 + languageName: node + linkType: hard + "lru-cache@npm:^9.0.0": version: 9.1.2 resolution: "lru-cache@npm:9.1.2" @@ -35780,19 +35780,19 @@ __metadata: languageName: node linkType: hard -"mysql2@npm:^2.2.5": - version: 2.3.3 - resolution: "mysql2@npm:2.3.3" +"mysql2@npm:^3.0.0": + version: 3.9.1 + resolution: "mysql2@npm:3.9.1" dependencies: - denque: ^2.0.1 + denque: ^2.1.0 generate-function: ^2.3.1 iconv-lite: ^0.6.3 - long: ^4.0.0 - lru-cache: ^6.0.0 - named-placeholders: ^1.1.2 + long: ^5.2.1 + lru-cache: ^8.0.0 + named-placeholders: ^1.1.3 seq-queue: ^0.0.5 sqlstring: ^2.3.2 - checksum: 45e479d0cbdb24ceb9d1846a1708ae2c33aa64f603f7899279b33560b1eec441f1b7a596075896f1305f701cfbc083bceb88bc72ba5d2f3656a3d6102611286a + checksum: 067353f8735d3e91654ecc01f562729c87f4fa141e870d524af06c5db98ac3341d91f3130357fead247833f41b1b93d1c6dd6e1f1b98687d28998bd9673b9ce7 languageName: node linkType: hard @@ -35807,12 +35807,12 @@ __metadata: languageName: node linkType: hard -"named-placeholders@npm:^1.1.2": - version: 1.1.2 - resolution: "named-placeholders@npm:1.1.2" +"named-placeholders@npm:^1.1.3": + version: 1.1.3 + resolution: "named-placeholders@npm:1.1.3" dependencies: - lru-cache: ^4.1.3 - checksum: c9317d1b479d6733b3baedfde209c6c866cf387c2d625837f93355fdb6a9055b1e8180b883fe00bcb20edb3ba4dd21128ec2f1ed8cb884385cef7698cbcadcc4 + lru-cache: ^7.14.1 + checksum: 7834adc91e92ae1b9c4413384e3ccd297de5168bb44017ff0536705ddc4db421723bd964607849265feb3f6ded390f84cf138e5925f22f7c13324f87a803dc73 languageName: node linkType: hard From 9eb7cacef753cebd7db62786d8a41d89e83df3c7 Mon Sep 17 00:00:00 2001 From: cmoulliard Date: Fri, 16 Feb 2024 13:49:40 +0100 Subject: [PATCH 120/204] Regenerated the API report Signed-off-by: cmoulliard --- .../api-report-alpha.md | 13 ------------- .../scaffolder-backend-module-gitea/api-report.md | 5 +++++ 2 files changed, 5 insertions(+), 13 deletions(-) delete mode 100644 plugins/scaffolder-backend-module-gitea/api-report-alpha.md diff --git a/plugins/scaffolder-backend-module-gitea/api-report-alpha.md b/plugins/scaffolder-backend-module-gitea/api-report-alpha.md deleted file mode 100644 index 8cd41a271c..0000000000 --- a/plugins/scaffolder-backend-module-gitea/api-report-alpha.md +++ /dev/null @@ -1,13 +0,0 @@ -## API Report File for "@backstage/plugin-scaffolder-backend-module-gitea" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; - -// @public -const giteaModule: () => BackendFeature; -export default giteaModule; - -// (No @packageDocumentation comment for this package) -``` diff --git a/plugins/scaffolder-backend-module-gitea/api-report.md b/plugins/scaffolder-backend-module-gitea/api-report.md index f83ae73fb7..373cc6258c 100644 --- a/plugins/scaffolder-backend-module-gitea/api-report.md +++ b/plugins/scaffolder-backend-module-gitea/api-report.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { JsonObject } from '@backstage/types'; import { ScmIntegrationRegistry } from '@backstage/integration'; @@ -24,4 +25,8 @@ export function createPublishGiteaAction(options: { }, JsonObject >; + +// @public +const giteaModule: () => BackendFeature; +export default giteaModule; ``` From e39eb80da9e7d66c3247503ee7cc2b5ff9c79a92 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Fri, 16 Feb 2024 13:26:34 +0000 Subject: [PATCH 121/204] better docs Signed-off-by: David Roberts --- plugins/azure-devops/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/azure-devops/README.md b/plugins/azure-devops/README.md index 6521f0e237..935e78c82c 100644 --- a/plugins/azure-devops/README.md +++ b/plugins/azure-devops/README.md @@ -70,9 +70,7 @@ dev.azure.com/project-repo: / dev.azure.com/build-definition: ``` -...and which README file belongs to each entity. - -Example: +Then to display the `README` file that belongs to each entity you would do this: ```yaml dev.azure.com/readme-path: //.md From fa7ea3f2f0810ae5b82b6829801208d2599f1f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 16 Feb 2024 14:33:10 +0100 Subject: [PATCH 122/204] break out the providers router into a separate file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/six-sloths-listen.md | 5 + plugins/auth-backend/src/identity/index.ts | 2 +- plugins/auth-backend/src/identity/router.ts | 17 +- plugins/auth-backend/src/index.ts | 2 +- plugins/auth-backend/src/providers/index.ts | 1 + .../src/{service => providers}/router.test.ts | 0 plugins/auth-backend/src/providers/router.ts | 155 ++++++++++++++++++ plugins/auth-backend/src/service/index.ts | 17 ++ plugins/auth-backend/src/service/router.ts | 133 +++------------ 9 files changed, 208 insertions(+), 124 deletions(-) create mode 100644 .changeset/six-sloths-listen.md rename plugins/auth-backend/src/{service => providers}/router.test.ts (100%) create mode 100644 plugins/auth-backend/src/providers/router.ts create mode 100644 plugins/auth-backend/src/service/index.ts diff --git a/.changeset/six-sloths-listen.md b/.changeset/six-sloths-listen.md new file mode 100644 index 0000000000..47886e3f77 --- /dev/null +++ b/.changeset/six-sloths-listen.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Internal refactor to break out how the router is constructed diff --git a/plugins/auth-backend/src/identity/index.ts b/plugins/auth-backend/src/identity/index.ts index a5e0dd4b80..0492836723 100644 --- a/plugins/auth-backend/src/identity/index.ts +++ b/plugins/auth-backend/src/identity/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -export { createOidcRouter } from './router'; +export { bindOidcRouter } from './router'; export { TokenFactory } from './TokenFactory'; export { DatabaseKeyStore } from './DatabaseKeyStore'; export { MemoryKeyStore } from './MemoryKeyStore'; diff --git a/plugins/auth-backend/src/identity/router.ts b/plugins/auth-backend/src/identity/router.ts index c9ae9e685a..c3c419e9ea 100644 --- a/plugins/auth-backend/src/identity/router.ts +++ b/plugins/auth-backend/src/identity/router.ts @@ -14,18 +14,21 @@ * limitations under the License. */ +import express from 'express'; import Router from 'express-promise-router'; import { TokenIssuer } from './types'; -export type Options = { - baseUrl: string; - tokenIssuer: TokenIssuer; -}; - -export function createOidcRouter(options: Options) { +export function bindOidcRouter( + targetRouter: express.Router, + options: { + baseUrl: string; + tokenIssuer: TokenIssuer; + }, +) { const { baseUrl, tokenIssuer } = options; const router = Router(); + targetRouter.use(router); const config = { issuer: baseUrl, @@ -68,6 +71,4 @@ export function createOidcRouter(options: Options) { router.get('/v1/userinfo', (_req, res) => { res.status(501).send('Not Implemented'); }); - - return router; } diff --git a/plugins/auth-backend/src/index.ts b/plugins/auth-backend/src/index.ts index 3943387e15..6c3f866031 100644 --- a/plugins/auth-backend/src/index.ts +++ b/plugins/auth-backend/src/index.ts @@ -21,7 +21,7 @@ */ export { authPlugin as default } from './authPlugin'; -export * from './service/router'; +export * from './service'; export type { TokenParams } from './identity'; export * from './providers'; diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 8173a7fbc3..b9543c275c 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -34,6 +34,7 @@ export type { SamlAuthResult } from './saml'; export type { GcpIapResult, GcpIapTokenInfo } from './gcp-iap'; export { providers, defaultAuthProviderFactories } from './providers'; +export { createOriginFilter, type ProviderFactories } from './router'; export { createAuthProviderIntegration } from './createAuthProviderIntegration'; diff --git a/plugins/auth-backend/src/service/router.test.ts b/plugins/auth-backend/src/providers/router.test.ts similarity index 100% rename from plugins/auth-backend/src/service/router.test.ts rename to plugins/auth-backend/src/providers/router.test.ts diff --git a/plugins/auth-backend/src/providers/router.ts b/plugins/auth-backend/src/providers/router.ts new file mode 100644 index 0000000000..9971466ad4 --- /dev/null +++ b/plugins/auth-backend/src/providers/router.ts @@ -0,0 +1,155 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; +import { Config } from '@backstage/config'; +import { NotFoundError, assertError } from '@backstage/errors'; +import { AuthProviderFactory } from '@backstage/plugin-auth-node'; +import express from 'express'; +import Router from 'express-promise-router'; +import { Minimatch } from 'minimatch'; +import { CatalogAuthResolverContext } from '../lib/resolvers/CatalogAuthResolverContext'; +import { TokenIssuer } from '../identity/types'; + +/** @public */ +export type ProviderFactories = { [s: string]: AuthProviderFactory }; + +export function bindProviderRouters( + targetRouter: express.Router, + options: { + providers: ProviderFactories; + appUrl: string; + baseUrl: string; + config: Config; + logger: LoggerService; + discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; + tokenIssuer: TokenIssuer; + catalogApi?: CatalogApi; + }, +) { + const { + providers, + appUrl, + baseUrl, + config, + logger, + discovery, + tokenManager, + tokenIssuer, + catalogApi, + } = options; + + const providersConfig = config.getOptionalConfig('auth.providers'); + + const isOriginAllowed = createOriginFilter(config); + + for (const [providerId, providerFactory] of Object.entries(providers)) { + if (providersConfig?.has(providerId)) { + logger.info(`Configuring auth provider: ${providerId}`); + try { + const provider = providerFactory({ + providerId, + appUrl, + baseUrl: baseUrl, + isOriginAllowed, + globalConfig: { + baseUrl: baseUrl, + appUrl, + isOriginAllowed, + }, + config: providersConfig.getConfig(providerId), + logger, + resolverContext: CatalogAuthResolverContext.create({ + logger, + catalogApi: + catalogApi ?? new CatalogClient({ discoveryApi: discovery }), + tokenIssuer, + tokenManager, + }), + }); + + const r = Router(); + + r.get('/start', provider.start.bind(provider)); + r.get('/handler/frame', provider.frameHandler.bind(provider)); + r.post('/handler/frame', provider.frameHandler.bind(provider)); + if (provider.logout) { + r.post('/logout', provider.logout.bind(provider)); + } + if (provider.refresh) { + r.get('/refresh', provider.refresh.bind(provider)); + r.post('/refresh', provider.refresh.bind(provider)); + } + + targetRouter.use(`/${providerId}`, r); + } catch (e) { + assertError(e); + if (process.env.NODE_ENV !== 'development') { + throw new Error( + `Failed to initialize ${providerId} auth provider, ${e.message}`, + ); + } + + logger.warn(`Skipping ${providerId} auth provider, ${e.message}`); + + targetRouter.use(`/${providerId}`, () => { + // If the user added the provider under auth.providers but the clientId and clientSecret etc. were not found. + throw new NotFoundError( + `Auth provider registered for '${providerId}' is misconfigured. This could mean the configs under ` + + `auth.providers.${providerId} are missing or the environment variables used are not defined. ` + + `Check the auth backend plugin logs when the backend starts to see more details.`, + ); + }); + } + } else { + targetRouter.use(`/${providerId}`, () => { + throw new NotFoundError( + `No auth provider registered for '${providerId}'`, + ); + }); + } + } +} + +/** @public */ +export function createOriginFilter( + config: Config, +): (origin: string) => boolean { + const appUrl = config.getString('app.baseUrl'); + const { origin: appOrigin } = new URL(appUrl); + + const allowedOrigins = config.getOptionalStringArray( + 'auth.experimentalExtraAllowedOrigins', + ); + + const allowedOriginPatterns = + allowedOrigins?.map( + pattern => new Minimatch(pattern, { nocase: true, noglobstar: true }), + ) ?? []; + + return origin => { + if (origin === appOrigin) { + return true; + } + return allowedOriginPatterns.some(pattern => pattern.match(origin)); + }; +} diff --git a/plugins/auth-backend/src/service/index.ts b/plugins/auth-backend/src/service/index.ts new file mode 100644 index 0000000000..d26055aa59 --- /dev/null +++ b/plugins/auth-backend/src/service/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { createRouter, type RouterOptions } from './router'; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index a8876a7559..459c347835 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -24,24 +24,19 @@ import { PluginEndpointDiscovery, TokenManager, } from '@backstage/backend-common'; -import { assertError, NotFoundError } from '@backstage/errors'; -import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; -import { createOidcRouter, TokenFactory, KeyStores } from '../identity'; +import { NotFoundError } from '@backstage/errors'; +import { CatalogApi } from '@backstage/catalog-client'; +import { bindOidcRouter, TokenFactory, KeyStores } from '../identity'; import session from 'express-session'; import connectSessionKnex from 'connect-session-knex'; import passport from 'passport'; -import { Minimatch } from 'minimatch'; -import { CatalogAuthResolverContext } from '../lib/resolvers'; import { AuthDatabase } from '../database/AuthDatabase'; import { readBackstageTokenExpiration } from './readBackstageTokenExpiration'; import { TokenIssuer } from '../identity/types'; import { StaticTokenIssuer } from '../identity/StaticTokenIssuer'; import { StaticKeyStore } from '../identity/StaticKeyStore'; import { Config } from '@backstage/config'; -import { AuthProviderFactory } from '@backstage/plugin-auth-node'; - -/** @public */ -export type ProviderFactories = { [s: string]: AuthProviderFactory }; +import { ProviderFactories, bindProviderRouters } from '../providers/router'; /** @public */ export interface RouterOptions { @@ -65,10 +60,8 @@ export async function createRouter( config, discovery, database, - tokenManager, tokenFactoryAlgorithm, providerFactories = {}, - catalogApi, } = options; const router = Router(); @@ -103,6 +96,7 @@ export async function createRouter( config.getOptionalString('auth.identityTokenAlgorithm'), }); } + const secret = config.getOptionalString('auth.session.secret'); if (secret) { router.use(cookieParser(secret)); @@ -125,96 +119,31 @@ export async function createRouter( } else { router.use(cookieParser()); } + router.use(express.urlencoded({ extended: false })); router.use(express.json()); - const allProviderFactories = options.disableDefaultProviderFactories + const providers = options.disableDefaultProviderFactories ? providerFactories : { ...defaultAuthProviderFactories, ...providerFactories, }; - const providersConfig = config.getOptionalConfig('auth.providers'); + bindProviderRouters(router, { + providers, + appUrl, + baseUrl: authUrl, + tokenIssuer, + ...options, + }); - const isOriginAllowed = createOriginFilter(config); - - for (const [providerId, providerFactory] of Object.entries( - allProviderFactories, - )) { - if (providersConfig?.has(providerId)) { - logger.info(`Configuring auth provider: ${providerId}`); - try { - const provider = providerFactory({ - providerId, - appUrl, - baseUrl: authUrl, - isOriginAllowed, - globalConfig: { - baseUrl: authUrl, - appUrl, - isOriginAllowed, - }, - config: providersConfig.getConfig(providerId), - logger, - resolverContext: CatalogAuthResolverContext.create({ - logger, - catalogApi: - catalogApi ?? new CatalogClient({ discoveryApi: discovery }), - tokenIssuer, - tokenManager, - }), - }); - - const r = Router(); - - r.get('/start', provider.start.bind(provider)); - r.get('/handler/frame', provider.frameHandler.bind(provider)); - r.post('/handler/frame', provider.frameHandler.bind(provider)); - if (provider.logout) { - r.post('/logout', provider.logout.bind(provider)); - } - if (provider.refresh) { - r.get('/refresh', provider.refresh.bind(provider)); - r.post('/refresh', provider.refresh.bind(provider)); - } - - router.use(`/${providerId}`, r); - } catch (e) { - assertError(e); - if (process.env.NODE_ENV !== 'development') { - throw new Error( - `Failed to initialize ${providerId} auth provider, ${e.message}`, - ); - } - - logger.warn(`Skipping ${providerId} auth provider, ${e.message}`); - - router.use(`/${providerId}`, () => { - // If the user added the provider under auth.providers but the clientId and clientSecret etc. were not found. - throw new NotFoundError( - `Auth provider registered for '${providerId}' is misconfigured. This could mean the configs under ` + - `auth.providers.${providerId} are missing or the environment variables used are not defined. ` + - `Check the auth backend plugin logs when the backend starts to see more details.`, - ); - }); - } - } else { - router.use(`/${providerId}`, () => { - throw new NotFoundError( - `No auth provider registered for '${providerId}'`, - ); - }); - } - } - - router.use( - createOidcRouter({ - tokenIssuer, - baseUrl: authUrl, - }), - ); + bindOidcRouter(router, { + tokenIssuer, + baseUrl: authUrl, + }); + // Gives a more helpful error message than a plain 404 router.use('/:provider/', req => { const { provider } = req.params; throw new NotFoundError(`Unknown auth provider '${provider}'`); @@ -222,27 +151,3 @@ export async function createRouter( return router; } - -/** @public */ -export function createOriginFilter( - config: Config, -): (origin: string) => boolean { - const appUrl = config.getString('app.baseUrl'); - const { origin: appOrigin } = new URL(appUrl); - - const allowedOrigins = config.getOptionalStringArray( - 'auth.experimentalExtraAllowedOrigins', - ); - - const allowedOriginPatterns = - allowedOrigins?.map( - pattern => new Minimatch(pattern, { nocase: true, noglobstar: true }), - ) ?? []; - - return origin => { - if (origin === appOrigin) { - return true; - } - return allowedOriginPatterns.some(pattern => pattern.match(origin)); - }; -} From 1cfcb469cd5d4237d333e0b5ddb3d642809c85a0 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 16 Feb 2024 17:28:03 +0100 Subject: [PATCH 123/204] chore: fixing knip report Signed-off-by: blam --- plugins/scaffolder-react/knip-report.md | 3 +-- plugins/scaffolder/knip-report.md | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder-react/knip-report.md b/plugins/scaffolder-react/knip-report.md index 3d42235f5e..3c2e8f3c3c 100644 --- a/plugins/scaffolder-react/knip-report.md +++ b/plugins/scaffolder-react/knip-report.md @@ -1,12 +1,11 @@ # Knip report -## Unused dependencies (4) +## Unused dependencies (3) | Name | Location | Severity | | :------------------------ | :----------- | :------- | | @backstage/catalog-client | package.json | error | | zod-to-json-schema | package.json | error | -| immer | package.json | error | | zod | package.json | error | ## Unused devDependencies (2) diff --git a/plugins/scaffolder/knip-report.md b/plugins/scaffolder/knip-report.md index d130aaeb40..6004665f89 100644 --- a/plugins/scaffolder/knip-report.md +++ b/plugins/scaffolder/knip-report.md @@ -1,13 +1,12 @@ # Knip report -## Unused dependencies (4) +## Unused dependencies (3) | Name | Location | Severity | | :------------------ | :----------- | :------- | | json-schema-library | package.json | error | | @rjsf/material-ui | package.json | error | | git-url-parse | package.json | error | -| immer | package.json | error | ## Unused devDependencies (1) From 504b54e4dd8f42b0f7615b722bf34003045d562a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 16:57:26 +0000 Subject: [PATCH 124/204] Bump undici from 5.26.3 to 5.28.3 Bumps [undici](https://github.com/nodejs/undici) from 5.26.3 to 5.28.3. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v5.26.3...v5.28.3) --- updated-dependencies: - dependency-name: undici dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 82e03f005e..b612c502e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -44507,11 +44507,11 @@ __metadata: linkType: hard "undici@npm:^5.24.0": - version: 5.26.3 - resolution: "undici@npm:5.26.3" + version: 5.28.3 + resolution: "undici@npm:5.28.3" dependencies: "@fastify/busboy": ^2.0.0 - checksum: aaa9aadb712cf80e1a9cea2377e4842670105e00abbc184a21770ea5a8b77e4e2eadc200eac62442e74a1cd3b16a840c6f73b112b9e886bd3c1a125eb22e4f21 + checksum: fa1e65aff896c5e2ee23637b632e306f9e3a2b32a3dc0b23ea71e5555ad350bcc25713aea894b3dccc0b7dc2c5e92a5a58435ebc2033b731a5524506f573dfd2 languageName: node linkType: hard From 4642cb7ac23211407877bcd780682ce2e9478e77 Mon Sep 17 00:00:00 2001 From: Andres Mauricio Gomez P Date: Fri, 16 Feb 2024 13:28:32 -0500 Subject: [PATCH 125/204] Added support to fetch data for Daemon Sets and display an accordion in the same way as with Deployments Signed-off-by: Andres Mauricio Gomez P --- .changeset/two-planets-beam.md | 6 + plugins/kubernetes-common/api-report.md | 2 + plugins/kubernetes-common/src/types.ts | 1 + .../kubernetes-common/src/util/response.ts | 4 + .../src/__fixtures__/2-daemonsets.json | 581 ++++++++++++++++++ .../src/components/Cluster/Cluster.tsx | 6 + .../DaemonSetsAccordions.test.tsx | 33 + .../DaemonSetsAccordions.tsx | 147 +++++ .../DaemonSetsDrawer.test.tsx | 68 ++ .../DaemonSetsAccordions/DaemonSetsDrawer.tsx | 76 +++ .../components/DaemonSetsAccordions/index.ts | 16 + .../src/hooks/GroupedResponses.ts | 1 + 12 files changed, 941 insertions(+) create mode 100644 .changeset/two-planets-beam.md create mode 100644 plugins/kubernetes-react/src/__fixtures__/2-daemonsets.json create mode 100644 plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsAccordions.test.tsx create mode 100644 plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsAccordions.tsx create mode 100644 plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsDrawer.test.tsx create mode 100644 plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsDrawer.tsx create mode 100644 plugins/kubernetes-react/src/components/DaemonSetsAccordions/index.ts diff --git a/.changeset/two-planets-beam.md b/.changeset/two-planets-beam.md new file mode 100644 index 0000000000..d500d22853 --- /dev/null +++ b/.changeset/two-planets-beam.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes-common': patch +'@backstage/plugin-kubernetes-react': patch +--- + +Add support to fetch data for Daemon Sets and display an accordion in the same way as with Deployments diff --git a/plugins/kubernetes-common/api-report.md b/plugins/kubernetes-common/api-report.md index c1d399b196..2f46ef90e5 100644 --- a/plugins/kubernetes-common/api-report.md +++ b/plugins/kubernetes-common/api-report.md @@ -275,6 +275,8 @@ export interface GroupedResponses extends DeploymentResources { // (undocumented) customResources: any[]; // (undocumented) + daemonSets: V1DaemonSet[]; + // (undocumented) ingresses: V1Ingress[]; // (undocumented) jobs: V1Job[]; diff --git a/plugins/kubernetes-common/src/types.ts b/plugins/kubernetes-common/src/types.ts index cd316af089..c4bb5fd32f 100644 --- a/plugins/kubernetes-common/src/types.ts +++ b/plugins/kubernetes-common/src/types.ts @@ -294,4 +294,5 @@ export interface GroupedResponses extends DeploymentResources { cronJobs: V1CronJob[]; customResources: any[]; statefulsets: V1StatefulSet[]; + daemonSets: V1DaemonSet[]; } diff --git a/plugins/kubernetes-common/src/util/response.ts b/plugins/kubernetes-common/src/util/response.ts index a86d90f780..b62fa162d1 100644 --- a/plugins/kubernetes-common/src/util/response.ts +++ b/plugins/kubernetes-common/src/util/response.ts @@ -58,6 +58,9 @@ export const groupResponses = ( case 'statefulsets': prev.statefulsets.push(...next.resources); break; + case 'daemonsets': + prev.daemonSets.push(...next.resources); + break; default: } return prev; @@ -74,6 +77,7 @@ export const groupResponses = ( cronJobs: [], customResources: [], statefulsets: [], + daemonSets: [], } as GroupedResponses, ); }; diff --git a/plugins/kubernetes-react/src/__fixtures__/2-daemonsets.json b/plugins/kubernetes-react/src/__fixtures__/2-daemonsets.json new file mode 100644 index 0000000000..1c88b9d8a7 --- /dev/null +++ b/plugins/kubernetes-react/src/__fixtures__/2-daemonsets.json @@ -0,0 +1,581 @@ +{ + "daemonSets": [ + { + "apiVersion": "apps/v1", + "kind": "DaemonSet", + "metadata": { + "annotations": { + "deprecated.daemonset.template.generation": "1", + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",\"kind\":\"DaemonSet\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\",\"k8s-app\":\"fluentd-logging\"},\"name\":\"fluentd-elasticsearch\",\"namespace\":\"default\"},\"spec\":{\"selector\":{\"matchLabels\":{\"name\":\"fluentd-elasticsearch\"}},\"template\":{\"metadata\":{\"labels\":{\"name\":\"fluentd-elasticsearch\"}},\"spec\":{\"containers\":[{\"image\":\"quay.io/fluentd_elasticsearch/fluentd:v2.5.2\",\"name\":\"fluentd-elasticsearch\",\"resources\":{\"limits\":{\"memory\":\"200Mi\"},\"requests\":{\"cpu\":\"100m\",\"memory\":\"200Mi\"}}}],\"terminationGracePeriodSeconds\":30}}}}\n" + }, + "creationTimestamp": "2024-02-14T21:00:28Z", + "generation": 1, + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "k8s-app": "fluentd-logging" + }, + "name": "fluentd-elasticsearch", + "namespace": "default", + "resourceVersion": "1769498", + "uid": "2ba243f3-a733-4b63-9db9-c9b8ce303a23" + }, + "spec": { + "revisionHistoryLimit": 10, + "selector": { + "matchLabels": { + "name": "fluentd-elasticsearch" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "name": "fluentd-elasticsearch" + } + }, + "spec": { + "containers": [ + { + "image": "quay.io/fluentd_elasticsearch/fluentd:v2.5.2", + "imagePullPolicy": "IfNotPresent", + "name": "fluentd-elasticsearch", + "resources": { + "limits": { + "memory": "200Mi" + }, + "requests": { + "cpu": "100m", + "memory": "200Mi" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "terminationGracePeriodSeconds": 30 + } + }, + "updateStrategy": { + "rollingUpdate": { + "maxSurge": 0, + "maxUnavailable": 1 + }, + "type": "RollingUpdate" + } + }, + "status": { + "currentNumberScheduled": 1, + "desiredNumberScheduled": 1, + "numberAvailable": 1, + "numberMisscheduled": 0, + "numberReady": 1, + "observedGeneration": 1, + "updatedNumberScheduled": 1 + } + }, + { + "apiVersion": "apps/v1", + "kind": "DaemonSet", + "metadata": { + "annotations": { + "deprecated.daemonset.template.generation": "1", + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",\"kind\":\"DaemonSet\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\",\"k8s-app\":\"fluentd-logging\"},\"name\":\"fluentd-elasticsearch2\",\"namespace\":\"default\"},\"spec\":{\"selector\":{\"matchLabels\":{\"name\":\"fluentd-elasticsearch2\"}},\"template\":{\"metadata\":{\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\",\"name\":\"fluentd-elasticsearch2\"}},\"spec\":{\"containers\":[{\"image\":\"quay.io/fluentd_elasticsearch/fluentd:v2.5.2\",\"name\":\"fluentd-elasticsearch\",\"resources\":{\"limits\":{\"memory\":\"200Mi\"},\"requests\":{\"cpu\":\"100m\",\"memory\":\"200Mi\"}}}],\"terminationGracePeriodSeconds\":30}}}}\n" + }, + "creationTimestamp": "2024-02-16T18:11:26Z", + "generation": 1, + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "k8s-app": "fluentd-logging" + }, + "name": "fluentd-elasticsearch2", + "namespace": "default", + "resourceVersion": "1981736", + "uid": "b923d035-e9a4-4a40-9472-f0f3468f30df" + }, + "spec": { + "revisionHistoryLimit": 10, + "selector": { + "matchLabels": { + "name": "fluentd-elasticsearch2" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "name": "fluentd-elasticsearch2" + } + }, + "spec": { + "containers": [ + { + "image": "quay.io/fluentd_elasticsearch/fluentd:v2.5.2", + "imagePullPolicy": "IfNotPresent", + "name": "fluentd-elasticsearch", + "resources": { + "limits": { + "memory": "200Mi" + }, + "requests": { + "cpu": "100m", + "memory": "200Mi" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "terminationGracePeriodSeconds": 30 + } + }, + "updateStrategy": { + "rollingUpdate": { + "maxSurge": 0, + "maxUnavailable": 1 + }, + "type": "RollingUpdate" + } + }, + "status": { + "currentNumberScheduled": 1, + "desiredNumberScheduled": 1, + "numberAvailable": 1, + "numberMisscheduled": 0, + "numberReady": 1, + "observedGeneration": 1, + "updatedNumberScheduled": 1 + } + } + ], + "pods": [ + { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "creationTimestamp": "2024-02-16T16:35:06Z", + "generateName": "fluentd-elasticsearch-", + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "controller-revision-hash": "86978587c8", + "name": "fluentd-elasticsearch", + "pod-template-generation": "2" + }, + "name": "fluentd-elasticsearch-mmkpf", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "DaemonSet", + "name": "fluentd-elasticsearch", + "uid": "2ba243f3-a733-4b63-9db9-c9b8ce303a23" + } + ], + "resourceVersion": "1970744", + "uid": "fc9f9070-1a3c-4e09-9cd6-8803a4ebfb0a" + }, + "spec": { + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchFields": [ + { + "key": "metadata.name", + "operator": "In", + "values": ["ucp-control-plane"] + } + ] + } + ] + } + } + }, + "containers": [ + { + "image": "quay.io/fluentd_elasticsearch/fluentd:v2.5.2", + "imagePullPolicy": "IfNotPresent", + "name": "fluentd-elasticsearch", + "resources": { + "limits": { + "memory": "200Mi" + }, + "requests": { + "cpu": "100m", + "memory": "200Mi" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "kube-api-access-8cwbv", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "ucp-control-plane", + "preemptionPolicy": "PreemptLowerPriority", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists" + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/disk-pressure", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/memory-pressure", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/pid-pressure", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/unschedulable", + "operator": "Exists" + } + ], + "volumes": [ + { + "name": "kube-api-access-8cwbv", + "projected": { + "defaultMode": 420, + "sources": [ + { + "serviceAccountToken": { + "expirationSeconds": 3607, + "path": "token" + } + }, + { + "configMap": { + "items": [ + { + "key": "ca.crt", + "path": "ca.crt" + } + ], + "name": "kube-root-ca.crt" + } + }, + { + "downwardAPI": { + "items": [ + { + "fieldRef": { + "apiVersion": "v1", + "fieldPath": "metadata.namespace" + }, + "path": "namespace" + } + ] + } + } + ] + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2024-02-16T16:35:06Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-02-16T16:35:08Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-02-16T16:35:08Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-02-16T16:35:06Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "containerd://958e4ec14b6a3557724fabeaac9c8f8fe2ea797337a4397e700209f6e482e898", + "image": "quay.io/fluentd_elasticsearch/fluentd:v2.5.2", + "imageID": "sha256:a5bf47027e067e0376708cae10750ea154b13356dfc0209610f5ea0bc7c16fe0", + "lastState": {}, + "name": "fluentd-elasticsearch", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2024-02-16T16:35:07Z" + } + } + } + ], + "hostIP": "172.19.0.3", + "phase": "Running", + "podIP": "10.244.0.215", + "podIPs": [ + { + "ip": "10.244.0.215" + } + ], + "qosClass": "Burstable", + "startTime": "2024-02-16T16:35:06Z" + } + }, + { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "creationTimestamp": "2024-02-16T18:11:26Z", + "generateName": "fluentd-elasticsearch2-", + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "controller-revision-hash": "5979b599f6", + "name": "fluentd-elasticsearch2", + "pod-template-generation": "1" + }, + "name": "fluentd-elasticsearch2-7hwwg", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "DaemonSet", + "name": "fluentd-elasticsearch2", + "uid": "b923d035-e9a4-4a40-9472-f0f3468f30df" + } + ], + "resourceVersion": "1981735", + "uid": "1b829903-2c3a-453f-a8f2-b6603eec0bbe" + }, + "spec": { + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchFields": [ + { + "key": "metadata.name", + "operator": "In", + "values": ["ucp-control-plane"] + } + ] + } + ] + } + } + }, + "containers": [ + { + "image": "quay.io/fluentd_elasticsearch/fluentd:v2.5.2", + "imagePullPolicy": "IfNotPresent", + "name": "fluentd-elasticsearch", + "resources": { + "limits": { + "memory": "200Mi" + }, + "requests": { + "cpu": "100m", + "memory": "200Mi" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "kube-api-access-rpkbp", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "ucp-control-plane", + "preemptionPolicy": "PreemptLowerPriority", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists" + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/disk-pressure", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/memory-pressure", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/pid-pressure", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/unschedulable", + "operator": "Exists" + } + ], + "volumes": [ + { + "name": "kube-api-access-rpkbp", + "projected": { + "defaultMode": 420, + "sources": [ + { + "serviceAccountToken": { + "expirationSeconds": 3607, + "path": "token" + } + }, + { + "configMap": { + "items": [ + { + "key": "ca.crt", + "path": "ca.crt" + } + ], + "name": "kube-root-ca.crt" + } + }, + { + "downwardAPI": { + "items": [ + { + "fieldRef": { + "apiVersion": "v1", + "fieldPath": "metadata.namespace" + }, + "path": "namespace" + } + ] + } + } + ] + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2024-02-16T18:11:27Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-02-16T18:11:28Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-02-16T18:11:28Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-02-16T18:11:27Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "containerd://3372cb5a300c87e5b1530b32fea7af35dce92470fc7c1cea47c714fac283feb9", + "image": "quay.io/fluentd_elasticsearch/fluentd:v2.5.2", + "imageID": "sha256:a5bf47027e067e0376708cae10750ea154b13356dfc0209610f5ea0bc7c16fe0", + "lastState": {}, + "name": "fluentd-elasticsearch", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2024-02-16T18:11:28Z" + } + } + } + ], + "hostIP": "172.19.0.3", + "phase": "Running", + "podIP": "10.244.0.88", + "podIPs": [ + { + "ip": "10.244.0.88" + } + ], + "qosClass": "Burstable", + "startTime": "2024-02-16T18:11:27Z" + } + } + ] +} diff --git a/plugins/kubernetes-react/src/components/Cluster/Cluster.tsx b/plugins/kubernetes-react/src/components/Cluster/Cluster.tsx index f2108ac1b7..d713b9a873 100644 --- a/plugins/kubernetes-react/src/components/Cluster/Cluster.tsx +++ b/plugins/kubernetes-react/src/components/Cluster/Cluster.tsx @@ -34,6 +34,7 @@ import { IngressesAccordions } from '../IngressesAccordions'; import { ServicesAccordions } from '../ServicesAccordions'; import { CronJobsAccordions } from '../CronJobsAccordions'; import { CustomResources } from '../CustomResources'; +import { DaemonSetsAccordions } from '../DaemonSetsAccordions'; import { ClusterContext, GroupedResponsesContext, @@ -151,6 +152,11 @@ export const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => { ) : undefined} + {groupedResponses.daemonSets.length > 0 ? ( + + + + ) : undefined} {groupedResponses.statefulsets.length > 0 ? ( diff --git a/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsAccordions.test.tsx b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsAccordions.test.tsx new file mode 100644 index 0000000000..9f38abf897 --- /dev/null +++ b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsAccordions.test.tsx @@ -0,0 +1,33 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { screen } from '@testing-library/react'; +import { DaemonSetsAccordions } from './DaemonSetsAccordions'; +import * as oneDaemonsFixture from '../../__fixtures__/2-daemonsets.json'; +import { renderInTestApp } from '@backstage/test-utils'; +import { kubernetesProviders } from '../../hooks/test-utils'; + +describe('DaemonSetsAccordions', () => { + it('should render two daemonset', async () => { + const wrapper = kubernetesProviders(oneDaemonsFixture); + + await renderInTestApp(wrapper()); + + expect(screen.getByText('fluentd-elasticsearch')).toBeInTheDocument(); + expect(screen.getByText('fluentd-elasticsearch2')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsAccordions.tsx b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsAccordions.tsx new file mode 100644 index 0000000000..834a86e837 --- /dev/null +++ b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsAccordions.tsx @@ -0,0 +1,147 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useContext } from 'react'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Grid, +} from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { V1Pod, V1DaemonSet } from '@kubernetes/client-node'; +import { PodsTable } from '../Pods'; +import { DaemonSetDrawer } from './DaemonSetsDrawer'; +import { getOwnedResources } from '../../utils/owner'; +import { + GroupedResponsesContext, + PodNamesWithErrorsContext, +} from '../../hooks'; +import { StatusError, StatusOK } from '@backstage/core-components'; +import { READY_COLUMNS, RESOURCE_COLUMNS } from '../Pods/PodsTable'; + +type DaemonSetsAccordionsProps = { + children?: React.ReactNode; +}; + +type DaemonSetAccordionProps = { + daemonset: V1DaemonSet; + ownedPods: V1Pod[]; + children?: React.ReactNode; +}; + +type DaemonSetSummaryProps = { + daemonset: V1DaemonSet; + numberOfCurrentPods: number; + numberOfPodsWithErrors: number; + children?: React.ReactNode; +}; + +const DaemonSetSummary = ({ + daemonset, + numberOfCurrentPods, + numberOfPodsWithErrors, +}: DaemonSetSummaryProps) => { + return ( + + + + + + + {numberOfCurrentPods} pods + + + {numberOfPodsWithErrors > 0 ? ( + + {numberOfPodsWithErrors} pod + {numberOfPodsWithErrors > 1 ? 's' : ''} with errors + + ) : ( + No pods with errors + )} + + + + ); +}; + +const DaemonSetAccordion = ({ + daemonset, + ownedPods, +}: DaemonSetAccordionProps) => { + const podNamesWithErrors = useContext(PodNamesWithErrorsContext); + + const podsWithErrors = ownedPods.filter(p => + podNamesWithErrors.has(p.metadata?.name ?? ''), + ); + + return ( + + }> + + + + + + + ); +}; + +export const DaemonSetsAccordions = ({}: DaemonSetsAccordionsProps) => { + const groupedResponses = useContext(GroupedResponsesContext); + + return ( + + {groupedResponses.daemonSets.map((daemonset, i) => ( + + + + + + ))} + + ); +}; diff --git a/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsDrawer.test.tsx b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsDrawer.test.tsx new file mode 100644 index 0000000000..51e70a3b9b --- /dev/null +++ b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsDrawer.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import * as daemonsets from '../../__fixtures__/2-daemonsets.json'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { DaemonSetDrawer } from './DaemonSetsDrawer'; +import { kubernetesClusterLinkFormatterApiRef } from '../../api'; + +describe('DaemonsetsDrawer', () => { + it('should render daemonsets drawer', async () => { + const { getByText, getAllByText } = await renderInTestApp( + + + , + , + ); + expect(getAllByText('fluentd-elasticsearch')).toHaveLength(2); + expect(getAllByText('DaemonSet')).toHaveLength(2); + expect(getByText('YAML')).toBeInTheDocument(); + expect(getByText('Update Strategy Type')).toBeInTheDocument(); + expect(getByText('RollingUpdate')).toBeInTheDocument(); + expect(getByText('Min Ready Seconds')).toBeInTheDocument(); + expect(getByText('???')).toBeInTheDocument(); + expect(getByText('Min Ready Seconds')).toBeInTheDocument(); + expect(getByText('Revision History Limit')).toBeInTheDocument(); + expect(getByText('Current Number Scheduled')).toBeInTheDocument(); + expect(getByText('Desired Number Scheduled')).toBeInTheDocument(); + expect(getByText('Number Available')).toBeInTheDocument(); + expect(getByText('Number Misscheduled')).toBeInTheDocument(); + expect(getByText('Number Ready')).toBeInTheDocument(); + expect(getByText('namespace: default')).toBeInTheDocument(); + }); + + it('should render deployment drawer without namespace', async () => { + const daemonset = (daemonsets as any).daemonSets[0]; + const { queryByText } = await renderInTestApp( + + + , + , + ); + + expect(queryByText('namespace: default')).not.toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsDrawer.tsx b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsDrawer.tsx new file mode 100644 index 0000000000..e0f64ed679 --- /dev/null +++ b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsDrawer.tsx @@ -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 React from 'react'; +import { V1DaemonSet } from '@kubernetes/client-node'; +import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer'; +import { Typography, Grid, Chip } from '@material-ui/core'; + +export const DaemonSetDrawer = ({ + daemonset, + expanded, +}: { + daemonset: V1DaemonSet; + expanded?: boolean; +}) => { + const namespace = daemonset.metadata?.namespace; + return ( + { + return { + updateStrategyType: daemonsetObj.spec?.updateStrategy?.type ?? '???', + minReadySeconds: daemonsetObj.spec?.minReadySeconds ?? '???', + revisionHistoryLimit: + daemonsetObj.spec?.revisionHistoryLimit ?? '???', + currentNumberScheduled: + daemonsetObj.status?.currentNumberScheduled ?? '???', + desiredNumberScheduled: + daemonsetObj.status?.desiredNumberScheduled ?? '???', + numberAvailable: daemonsetObj.status?.numberAvailable ?? '???', + numberMisscheduled: daemonsetObj.status?.numberMisscheduled ?? '???', + numberReady: daemonsetObj.status?.numberReady ?? '???', + }; + }} + > + + + + {daemonset.metadata?.name ?? 'unknown object'} + + + + + DaemonSet + + + {namespace && ( + + + + )} + + + ); +}; diff --git a/plugins/kubernetes-react/src/components/DaemonSetsAccordions/index.ts b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/index.ts new file mode 100644 index 0000000000..ca25150cbd --- /dev/null +++ b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { DaemonSetsAccordions } from './DaemonSetsAccordions'; diff --git a/plugins/kubernetes-react/src/hooks/GroupedResponses.ts b/plugins/kubernetes-react/src/hooks/GroupedResponses.ts index 7a81a74337..10cd3a8abe 100644 --- a/plugins/kubernetes-react/src/hooks/GroupedResponses.ts +++ b/plugins/kubernetes-react/src/hooks/GroupedResponses.ts @@ -25,6 +25,7 @@ export const GroupedResponsesContext = React.createContext({ pods: [], replicaSets: [], deployments: [], + daemonSets: [], services: [], configMaps: [], horizontalPodAutoscalers: [], From 3e57ca7b65df0fa69f2169339916d149b447582e Mon Sep 17 00:00:00 2001 From: armandocomellas1 Date: Fri, 16 Feb 2024 13:48:39 -0600 Subject: [PATCH 126/204] Add a better unit test for headers validation Signed-off-by: armandocomellas1 --- plugins/gcp-projects/src/plugin.test.ts | 30 ++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/plugins/gcp-projects/src/plugin.test.ts b/plugins/gcp-projects/src/plugin.test.ts index 8b53d0f0f0..e22854e221 100644 --- a/plugins/gcp-projects/src/plugin.test.ts +++ b/plugins/gcp-projects/src/plugin.test.ts @@ -15,13 +15,37 @@ */ import { gcpProjectsPlugin } from './plugin'; -import * as container from '@backstage/plugin-gcp-projects'; +import { GcpClient } from './api'; +import { OAuthApi } from '@backstage/core-plugin-api'; +import packageinfo from '../package.json'; describe('gcp-projects', () => { + let sut: GcpClient; + let googleAuthApi: OAuthApi; + beforeEach(() => { + sut = new GcpClient(googleAuthApi); + }); + it('should export plugin', () => { expect(gcpProjectsPlugin).toBeDefined(); }); - it('should exist the GcpClient class', () => { - expect(container.GcpClient).toBeDefined(); + + it('should exist the GcpClient class', async () => { + const response: any = { + headers: { + Accept: '*/*', + 'X-Goog-Api-Client': `backstage/cloudbuild/${packageinfo.version}`, + }, + }; + jest.spyOn(sut, 'listProjects').mockImplementation((): any => { + return response; + }); + + expect(response).toStrictEqual({ + headers: { + Accept: '*/*', + 'X-Goog-Api-Client': `backstage/cloudbuild/${packageinfo.version}`, + }, + }); }); }); From f1a5f33cec932292db31827d2757436ea29c232d Mon Sep 17 00:00:00 2001 From: armandocomellas1 Date: Fri, 16 Feb 2024 13:51:24 -0600 Subject: [PATCH 127/204] Add a better description inside unit test Signed-off-by: armandocomellas1 --- plugins/gcp-projects/src/plugin.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/gcp-projects/src/plugin.test.ts b/plugins/gcp-projects/src/plugin.test.ts index e22854e221..9d7a0541a6 100644 --- a/plugins/gcp-projects/src/plugin.test.ts +++ b/plugins/gcp-projects/src/plugin.test.ts @@ -30,7 +30,7 @@ describe('gcp-projects', () => { expect(gcpProjectsPlugin).toBeDefined(); }); - it('should exist the GcpClient class', async () => { + it('spy headers with identifying metadata', async () => { const response: any = { headers: { Accept: '*/*', From b6df34e40222d2cd47e1712f7cca606392cf3a73 Mon Sep 17 00:00:00 2001 From: armandocomellas1 Date: Fri, 16 Feb 2024 15:52:31 -0600 Subject: [PATCH 128/204] Package json deconstructed creating a new file containing only the version Signed-off-by: armandocomellas1 --- plugins/gcp-projects/src/api/GcpClient.ts | 2 +- plugins/gcp-projects/version.json | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 plugins/gcp-projects/version.json diff --git a/plugins/gcp-projects/src/api/GcpClient.ts b/plugins/gcp-projects/src/api/GcpClient.ts index e9375bcfa6..2b102dbd93 100644 --- a/plugins/gcp-projects/src/api/GcpClient.ts +++ b/plugins/gcp-projects/src/api/GcpClient.ts @@ -17,7 +17,7 @@ import { GcpApi } from './GcpApi'; import { Operation, Project } from './types'; import { OAuthApi } from '@backstage/core-plugin-api'; -import packageinfo from '../../package.json'; +import packageinfo from '../../version.json'; const BASE_URL = 'https://content-cloudresourcemanager.googleapis.com/v1/projects'; diff --git a/plugins/gcp-projects/version.json b/plugins/gcp-projects/version.json new file mode 100644 index 0000000000..11e109a959 --- /dev/null +++ b/plugins/gcp-projects/version.json @@ -0,0 +1,3 @@ +{ + "version": "0.3.46-next.1" +} From 7ae4cf6d13600cf21ce5ce8879ea488e9594da37 Mon Sep 17 00:00:00 2001 From: armandocomellas1 Date: Fri, 16 Feb 2024 16:17:56 -0600 Subject: [PATCH 129/204] Package json deconstructed dynamically with variable Signed-off-by: armandocomellas1 --- plugins/gcp-projects/src/api/GcpClient.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/gcp-projects/src/api/GcpClient.ts b/plugins/gcp-projects/src/api/GcpClient.ts index 2b102dbd93..484b57e1c4 100644 --- a/plugins/gcp-projects/src/api/GcpClient.ts +++ b/plugins/gcp-projects/src/api/GcpClient.ts @@ -17,7 +17,9 @@ import { GcpApi } from './GcpApi'; import { Operation, Project } from './types'; import { OAuthApi } from '@backstage/core-plugin-api'; -import packageinfo from '../../version.json'; +import packageVers from '../../package.json'; + +const { version } = packageVers; const BASE_URL = 'https://content-cloudresourcemanager.googleapis.com/v1/projects'; @@ -31,7 +33,7 @@ export class GcpClient implements GcpApi { headers: { Accept: '*/*', Authorization: `Bearer ${await this.getToken()}`, - 'X-Goog-Api-Client': `backstage/cloudbuild/${packageinfo.version}`, + 'X-Goog-Api-Client': `backstage/cloudbuild/${version}`, }, }); @@ -50,7 +52,7 @@ export class GcpClient implements GcpApi { const response = await fetch(url, { headers: { Authorization: `Bearer ${await this.getToken()}`, - 'X-Goog-Api-Client': `backstage/cloudbuild/${packageinfo.version}`, + 'X-Goog-Api-Client': `backstage/cloudbuild/${version}`, }, }); @@ -77,7 +79,7 @@ export class GcpClient implements GcpApi { headers: { Accept: '*/*', Authorization: `Bearer ${await this.getToken()}`, - 'X-Goog-Api-Client': `backstage/cloudbuild/${packageinfo.version}`, + 'X-Goog-Api-Client': `backstage/cloudbuild/${version}`, }, body: JSON.stringify(newProject), }); From ddd963a9409af2cda02d62bef017b58d9ab00201 Mon Sep 17 00:00:00 2001 From: armandocomellas1 Date: Fri, 16 Feb 2024 16:31:08 -0600 Subject: [PATCH 130/204] Changing the route that we need for the GCP telemetry Signed-off-by: armandocomellas1 --- plugins/gcp-projects/src/api/GcpClient.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/gcp-projects/src/api/GcpClient.ts b/plugins/gcp-projects/src/api/GcpClient.ts index 484b57e1c4..449a99a5a5 100644 --- a/plugins/gcp-projects/src/api/GcpClient.ts +++ b/plugins/gcp-projects/src/api/GcpClient.ts @@ -33,7 +33,7 @@ export class GcpClient implements GcpApi { headers: { Accept: '*/*', Authorization: `Bearer ${await this.getToken()}`, - 'X-Goog-Api-Client': `backstage/cloudbuild/${version}`, + 'X-Goog-Api-Client': `backstage/gcpprojects/${version}`, }, }); @@ -52,7 +52,7 @@ export class GcpClient implements GcpApi { const response = await fetch(url, { headers: { Authorization: `Bearer ${await this.getToken()}`, - 'X-Goog-Api-Client': `backstage/cloudbuild/${version}`, + 'X-Goog-Api-Client': `backstage/gcpprojects/${version}`, }, }); @@ -79,7 +79,7 @@ export class GcpClient implements GcpApi { headers: { Accept: '*/*', Authorization: `Bearer ${await this.getToken()}`, - 'X-Goog-Api-Client': `backstage/cloudbuild/${version}`, + 'X-Goog-Api-Client': `backstage/gcpprojects/${version}`, }, body: JSON.stringify(newProject), }); From d8a54d0a8342965f84677a5a8e395c5c015b0e27 Mon Sep 17 00:00:00 2001 From: Aramis Date: Sat, 27 Jan 2024 22:34:40 -0500 Subject: [PATCH 131/204] feat(catalog): add support for entity policies in the new backend. Signed-off-by: Aramis --- .changeset/thirty-bags-try.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/thirty-bags-try.md diff --git a/.changeset/thirty-bags-try.md b/.changeset/thirty-bags-try.md new file mode 100644 index 0000000000..7a8156e44c --- /dev/null +++ b/.changeset/thirty-bags-try.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Add support for defining entity policies in the new backend. From 31392e59b852f4b6384cc84bb03318ade9a088bb Mon Sep 17 00:00:00 2001 From: Aramis Date: Tue, 30 Jan 2024 10:59:28 -0500 Subject: [PATCH 132/204] update to a separate catalog extension and add support for replacing Signed-off-by: Aramis --- .../src/service/CatalogPlugin.ts | 40 ++++++++++++++++++- plugins/catalog-node/api-report-alpha.md | 14 +++++++ plugins/catalog-node/src/alpha.ts | 2 + plugins/catalog-node/src/extensions.ts | 17 +++++++- 4 files changed, 71 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.ts b/plugins/catalog-backend/src/service/CatalogPlugin.ts index bfe8ef5a16..5bcaaba1fb 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.ts @@ -17,7 +17,7 @@ import { createBackendPlugin, coreServices, } from '@backstage/backend-plugin-api'; -import { Entity } from '@backstage/catalog-model'; +import { Entity, EntityPolicy } from '@backstage/catalog-model'; import { CatalogBuilder, CatalogPermissionRuleInput } from './CatalogBuilder'; import { CatalogAnalysisExtensionPoint, @@ -26,6 +26,8 @@ import { catalogProcessingExtensionPoint, CatalogPermissionExtensionPoint, catalogPermissionExtensionPoint, + CatalogModelExtensionPoint, + catalogModelExtensionPoint, } from '@backstage/plugin-catalog-node/alpha'; import { CatalogProcessor, @@ -124,6 +126,33 @@ class CatalogPermissionExtensionPointImpl } } +class CatalogModelExtensionPointImpl implements CatalogModelExtensionPoint { + #entityPolicies = new Array(); + #replaced = false; + + addEntityPolicies(...policies: Array>) { + this.#entityPolicies.push(...policies.flat()); + } + + replaceEntityPolicies(...policies: Array) { + if (this.#replaced) { + throw new Error( + `You've already replaced the entity policies. If you want to add more entity policies, see 'addEntityPolicies'.`, + ); + } + this.#entityPolicies = [...policies]; + this.#replaced = true; + } + + get entityPolicies() { + return this.#entityPolicies; + } + + get replaced() { + return this.#replaced; + } +} + /** * Catalog plugin * @alpha @@ -150,6 +179,9 @@ export const catalogPlugin = createBackendPlugin({ permissionExtensions, ); + const modelExtensions = new CatalogModelExtensionPointImpl(); + env.registerExtensionPoint(catalogModelExtensionPoint, modelExtensions); + env.registerInit({ deps: { logger: coreServices.logger, @@ -193,6 +225,12 @@ export const catalogPlugin = createBackendPlugin({ builder.addLocationAnalyzers(...analysisExtensions.locationAnalyzers); builder.addPermissionRules(...permissionExtensions.permissionRules); + if (modelExtensions.replaced) { + builder.replaceEntityPolicies(modelExtensions.entityPolicies); + } else { + builder.addEntityPolicy(...modelExtensions.entityPolicies); + } + const { processingEngine, router } = await builder.build(); await processingEngine.start(); diff --git a/plugins/catalog-node/api-report-alpha.md b/plugins/catalog-node/api-report-alpha.md index 5be4b114c0..64382e56bb 100644 --- a/plugins/catalog-node/api-report-alpha.md +++ b/plugins/catalog-node/api-report-alpha.md @@ -7,6 +7,7 @@ import { CatalogApi } from '@backstage/catalog-client'; import { CatalogProcessor } from '@backstage/plugin-catalog-node'; import { EntitiesSearchFilter } from '@backstage/plugin-catalog-node'; import { Entity } from '@backstage/catalog-model'; +import { EntityPolicy } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-node'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { PermissionRule } from '@backstage/plugin-permission-node'; @@ -24,6 +25,19 @@ export interface CatalogAnalysisExtensionPoint { // @alpha (undocumented) export const catalogAnalysisExtensionPoint: ExtensionPoint; +// @alpha (undocumented) +export interface CatalogModelExtensionPoint { + // (undocumented) + addEntityPolicies( + ...policies: Array> + ): void; + // (undocumented) + replaceEntityPolicies(...policies: Array): void; +} + +// @alpha (undocumented) +export const catalogModelExtensionPoint: ExtensionPoint; + // @alpha (undocumented) export interface CatalogPermissionExtensionPoint { // (undocumented) diff --git a/plugins/catalog-node/src/alpha.ts b/plugins/catalog-node/src/alpha.ts index 3e958d1df1..91b5bb99b6 100644 --- a/plugins/catalog-node/src/alpha.ts +++ b/plugins/catalog-node/src/alpha.ts @@ -22,3 +22,5 @@ export { catalogAnalysisExtensionPoint } from './extensions'; export type { CatalogPermissionRuleInput } from './extensions'; export type { CatalogPermissionExtensionPoint } from './extensions'; export { catalogPermissionExtensionPoint } from './extensions'; +export type { CatalogModelExtensionPoint } from './extensions'; +export { catalogModelExtensionPoint } from './extensions'; diff --git a/plugins/catalog-node/src/extensions.ts b/plugins/catalog-node/src/extensions.ts index e2ee1b4f4b..7c63e51c69 100644 --- a/plugins/catalog-node/src/extensions.ts +++ b/plugins/catalog-node/src/extensions.ts @@ -15,7 +15,7 @@ */ import { createExtensionPoint } from '@backstage/backend-plugin-api'; -import { Entity } from '@backstage/catalog-model'; +import { Entity, EntityPolicy } from '@backstage/catalog-model'; import { CatalogProcessor, EntitiesSearchFilter, @@ -45,6 +45,15 @@ export interface CatalogProcessingExtensionPoint { ): void; } +/** @alpha */ +export interface CatalogModelExtensionPoint { + addEntityPolicies( + ...policies: Array> + ): void; + + replaceEntityPolicies(...policies: Array): void; +} + /** * @alpha */ @@ -68,6 +77,12 @@ export const catalogAnalysisExtensionPoint = id: 'catalog.analysis', }); +/** @alpha */ +export const catalogModelExtensionPoint = + createExtensionPoint({ + id: 'catalog.model', + }); + /** * @alpha */ From 0aba8f018ed590ce1ef233e90179088e98775794 Mon Sep 17 00:00:00 2001 From: Aramis Date: Fri, 2 Feb 2024 15:29:27 -0500 Subject: [PATCH 133/204] add test cases for the new transformer Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- .../core/policyToProcessorTransformer.test.ts | 90 +++++++++++++++++++ .../core/policyToProcessorTransformer.ts | 41 +++++++++ 2 files changed, 131 insertions(+) create mode 100644 plugins/catalog-backend/src/modules/core/policyToProcessorTransformer.test.ts create mode 100644 plugins/catalog-backend/src/modules/core/policyToProcessorTransformer.ts diff --git a/plugins/catalog-backend/src/modules/core/policyToProcessorTransformer.test.ts b/plugins/catalog-backend/src/modules/core/policyToProcessorTransformer.test.ts new file mode 100644 index 0000000000..46338ce6cb --- /dev/null +++ b/plugins/catalog-backend/src/modules/core/policyToProcessorTransformer.test.ts @@ -0,0 +1,90 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity, EntityPolicy } from '@backstage/catalog-model'; +import { policyToProcessorTransformer } from './policyToProcessorTransformer'; +import { clone } from 'lodash'; + +describe('policyToProcessorTransformer', () => { + const entityToProcess: Entity = { + apiVersion: 'backstage.io/v1alpha', + kind: 'Component', + metadata: { + name: 'test', + }, + }; + it('modifies the entity if the policy modifies the entity', async () => { + const policy: EntityPolicy = { + async enforce(entity) { + entity.kind = 'Group'; + return entity; + }, + }; + const processor = policyToProcessorTransformer(policy); + const clonedEntity = clone(entityToProcess); + const entity = await processor.preProcessEntity?.( + clonedEntity, + {} as any, + jest.fn(), + {} as any, + {} as any, + ); + expect(entity).toBeTruthy(); + expect(entity?.kind).toBe('Group'); + expect(entity?.apiVersion).toBe('backstage.io/v1alpha'); + expect(entity?.metadata.name).toBe('test'); + }); + + it('does not modify the entity if the policy returns undefined', async () => { + const policy: EntityPolicy = { + async enforce() { + return undefined; + }, + }; + const processor = policyToProcessorTransformer(policy); + const clonedEntity = clone(entityToProcess); + const entity = await processor.preProcessEntity?.( + clonedEntity, + {} as any, + jest.fn(), + {} as any, + {} as any, + ); + expect(entity).toBeTruthy(); + expect(entity?.kind).toBe('Component'); + expect(entity?.apiVersion).toBe('backstage.io/v1alpha'); + expect(entity?.metadata.name).toBe('test'); + }); + + it('bubbles up processor error', async () => { + const policy: EntityPolicy = { + async enforce() { + throw new TypeError('Invalid value for metadata.name'); + }, + }; + const processor = policyToProcessorTransformer(policy); + const clonedEntity = clone(entityToProcess); + await expect( + processor.preProcessEntity?.( + clonedEntity, + {} as any, + jest.fn(), + {} as any, + {} as any, + ), + ).rejects.toThrow(/Invalid value for metadata.name/); + }); +}); diff --git a/plugins/catalog-backend/src/modules/core/policyToProcessorTransformer.ts b/plugins/catalog-backend/src/modules/core/policyToProcessorTransformer.ts new file mode 100644 index 0000000000..ba1ce254c7 --- /dev/null +++ b/plugins/catalog-backend/src/modules/core/policyToProcessorTransformer.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntityPolicy } from '@backstage/catalog-model'; +import { CatalogProcessor } from '@backstage/plugin-catalog-node'; + +/** + * Transform a given entity policy to an entity processor. + * @param policy The policy to transform + * @returns A new entity processor that uses the entity policy. + */ +export function policyToProcessorTransformer( + policy: EntityPolicy, +): CatalogProcessor { + return { + getProcessorName() { + return policy.constructor.name; + }, + async preProcessEntity(entity) { + // If enforcing the policy fails, throw the policy error. + const result = await policy.enforce(entity); + if (!result) { + return entity; + } + return result; + }, + }; +} From 447d8b6fcf6ee080001643292df0c13f56de3b06 Mon Sep 17 00:00:00 2001 From: Aramis Date: Fri, 2 Feb 2024 16:42:39 -0500 Subject: [PATCH 134/204] add field validators Signed-off-by: Aramis --- .../src/service/CatalogPlugin.ts | 27 +++++++------------ plugins/catalog-node/src/extensions.ts | 15 +++++++++-- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.ts b/plugins/catalog-backend/src/service/CatalogPlugin.ts index 5bcaaba1fb..1fca11e482 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.ts @@ -17,7 +17,7 @@ import { createBackendPlugin, coreServices, } from '@backstage/backend-plugin-api'; -import { Entity, EntityPolicy } from '@backstage/catalog-model'; +import { Entity, EntityPolicy, Validators } from '@backstage/catalog-model'; import { CatalogBuilder, CatalogPermissionRuleInput } from './CatalogBuilder'; import { CatalogAnalysisExtensionPoint, @@ -36,6 +36,7 @@ import { ScmLocationAnalyzer, } from '@backstage/plugin-catalog-node'; import { loggerToWinstonLogger } from '@backstage/backend-common'; +import { merge } from 'lodash'; class CatalogProcessingExtensionPointImpl implements CatalogProcessingExtensionPoint @@ -128,28 +129,22 @@ class CatalogPermissionExtensionPointImpl class CatalogModelExtensionPointImpl implements CatalogModelExtensionPoint { #entityPolicies = new Array(); - #replaced = false; + #fieldValidators: Partial = {}; addEntityPolicies(...policies: Array>) { this.#entityPolicies.push(...policies.flat()); } - replaceEntityPolicies(...policies: Array) { - if (this.#replaced) { - throw new Error( - `You've already replaced the entity policies. If you want to add more entity policies, see 'addEntityPolicies'.`, - ); - } - this.#entityPolicies = [...policies]; - this.#replaced = true; + setFieldValidators(validators: Partial): void { + merge(this.#fieldValidators, validators); } get entityPolicies() { return this.#entityPolicies; } - get replaced() { - return this.#replaced; + get fieldValidators() { + return this.#fieldValidators; } } @@ -224,12 +219,8 @@ export const catalogPlugin = createBackendPlugin({ ); builder.addLocationAnalyzers(...analysisExtensions.locationAnalyzers); builder.addPermissionRules(...permissionExtensions.permissionRules); - - if (modelExtensions.replaced) { - builder.replaceEntityPolicies(modelExtensions.entityPolicies); - } else { - builder.addEntityPolicy(...modelExtensions.entityPolicies); - } + builder.addEntityPolicy(...modelExtensions.entityPolicies); + builder.setFieldFormatValidators(modelExtensions.fieldValidators); const { processingEngine, router } = await builder.build(); diff --git a/plugins/catalog-node/src/extensions.ts b/plugins/catalog-node/src/extensions.ts index 7c63e51c69..004a46445e 100644 --- a/plugins/catalog-node/src/extensions.ts +++ b/plugins/catalog-node/src/extensions.ts @@ -15,7 +15,7 @@ */ import { createExtensionPoint } from '@backstage/backend-plugin-api'; -import { Entity, EntityPolicy } from '@backstage/catalog-model'; +import { Entity, EntityPolicy, Validators } from '@backstage/catalog-model'; import { CatalogProcessor, EntitiesSearchFilter, @@ -47,11 +47,22 @@ export interface CatalogProcessingExtensionPoint { /** @alpha */ export interface CatalogModelExtensionPoint { + /** + * @deprecated Use `policyToProcessorTransformer` from `@backstage/plugin-catalog-backend` + * and `CatalogProcessingExtensionPoint.addProcessor` instead. + */ addEntityPolicies( ...policies: Array> ): void; - replaceEntityPolicies(...policies: Array): void; + /** + * Sets the validator function to use for one or more special fields of an + * entity. This is useful if the default rules for formatting of fields are + * not sufficient. + * + * @param validators - The (subset of) validators to set + */ + setFieldValidators(validators: Partial): void; } /** From af751400d679474aae497623075e973f479aa7c2 Mon Sep 17 00:00:00 2001 From: Aramis Date: Fri, 2 Feb 2024 16:42:47 -0500 Subject: [PATCH 135/204] fix api report Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- plugins/catalog-backend/api-report.md | 5 +++++ plugins/catalog-backend/src/modules/core/index.ts | 1 + .../src/modules/core/policyToProcessorTransformer.ts | 3 ++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index a38a964d48..e00436846a 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -421,6 +421,11 @@ export type PlaceholderResolverRead = PlaceholderResolverRead_2; // @public @deprecated (undocumented) export type PlaceholderResolverResolveUrl = PlaceholderResolverResolveUrl_2; +// @public +export function policyToProcessorTransformer( + policy: EntityPolicy, +): CatalogProcessor_2; + // @public export type ProcessingIntervalFunction = () => number; diff --git a/plugins/catalog-backend/src/modules/core/index.ts b/plugins/catalog-backend/src/modules/core/index.ts index 365def0bfe..54f16c99b1 100644 --- a/plugins/catalog-backend/src/modules/core/index.ts +++ b/plugins/catalog-backend/src/modules/core/index.ts @@ -24,3 +24,4 @@ export { PlaceholderProcessor } from './PlaceholderProcessor'; export type { PlaceholderProcessorOptions } from './PlaceholderProcessor'; export { UrlReaderProcessor } from './UrlReaderProcessor'; export { parseEntityYaml } from '../util/parse'; +export { policyToProcessorTransformer } from './policyToProcessorTransformer'; diff --git a/plugins/catalog-backend/src/modules/core/policyToProcessorTransformer.ts b/plugins/catalog-backend/src/modules/core/policyToProcessorTransformer.ts index ba1ce254c7..69b2788779 100644 --- a/plugins/catalog-backend/src/modules/core/policyToProcessorTransformer.ts +++ b/plugins/catalog-backend/src/modules/core/policyToProcessorTransformer.ts @@ -19,8 +19,9 @@ import { CatalogProcessor } from '@backstage/plugin-catalog-node'; /** * Transform a given entity policy to an entity processor. - * @param policy The policy to transform + * @param policy - The policy to transform * @returns A new entity processor that uses the entity policy. + * @public */ export function policyToProcessorTransformer( policy: EntityPolicy, From 9281ab3bb6c94417d1dbc6ece6f1a167e447be44 Mon Sep 17 00:00:00 2001 From: Aramis Date: Fri, 2 Feb 2024 16:53:57 -0500 Subject: [PATCH 136/204] fix api reports Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- plugins/catalog-node/api-report-alpha.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/catalog-node/api-report-alpha.md b/plugins/catalog-node/api-report-alpha.md index 64382e56bb..9243fa98fe 100644 --- a/plugins/catalog-node/api-report-alpha.md +++ b/plugins/catalog-node/api-report-alpha.md @@ -15,6 +15,7 @@ import { PermissionRuleParams } from '@backstage/plugin-permission-common'; import { PlaceholderResolver } from '@backstage/plugin-catalog-node'; import { ScmLocationAnalyzer } from '@backstage/plugin-catalog-node'; import { ServiceRef } from '@backstage/backend-plugin-api'; +import { Validators } from '@backstage/catalog-model'; // @alpha (undocumented) export interface CatalogAnalysisExtensionPoint { @@ -27,12 +28,11 @@ export const catalogAnalysisExtensionPoint: ExtensionPoint> ): void; - // (undocumented) - replaceEntityPolicies(...policies: Array): void; + setFieldValidators(validators: Partial): void; } // @alpha (undocumented) From 28a0882dbbe7c48d92649f3324f3321564109d0a Mon Sep 17 00:00:00 2001 From: Aramis Date: Mon, 12 Feb 2024 08:38:48 -0500 Subject: [PATCH 137/204] rename to transformLegacyPolicyToProcessor Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- plugins/catalog-backend/api-report.md | 10 +++++----- plugins/catalog-backend/src/modules/core/index.ts | 2 +- ...est.ts => transformLegacyPolicyToProcessor.test.ts} | 10 +++++----- ...nsformer.ts => transformLegacyPolicyToProcessor.ts} | 2 +- plugins/catalog-node/src/extensions.ts | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) rename plugins/catalog-backend/src/modules/core/{policyToProcessorTransformer.test.ts => transformLegacyPolicyToProcessor.test.ts} (88%) rename plugins/catalog-backend/src/modules/core/{policyToProcessorTransformer.ts => transformLegacyPolicyToProcessor.ts} (96%) diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index e00436846a..92a2fbb280 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -421,11 +421,6 @@ export type PlaceholderResolverRead = PlaceholderResolverRead_2; // @public @deprecated (undocumented) export type PlaceholderResolverResolveUrl = PlaceholderResolverResolveUrl_2; -// @public -export function policyToProcessorTransformer( - policy: EntityPolicy, -): CatalogProcessor_2; - // @public export type ProcessingIntervalFunction = () => number; @@ -455,6 +450,11 @@ export const processingResult: Readonly<{ // @public @deprecated (undocumented) export type ScmLocationAnalyzer = ScmLocationAnalyzer_2; +// @public +export function transformLegacyPolicyToProcessor( + policy: EntityPolicy, +): CatalogProcessor_2; + // @public (undocumented) export class UrlReaderProcessor implements CatalogProcessor_2 { constructor(options: { reader: UrlReader; logger: Logger }); diff --git a/plugins/catalog-backend/src/modules/core/index.ts b/plugins/catalog-backend/src/modules/core/index.ts index 54f16c99b1..e410c3824d 100644 --- a/plugins/catalog-backend/src/modules/core/index.ts +++ b/plugins/catalog-backend/src/modules/core/index.ts @@ -24,4 +24,4 @@ export { PlaceholderProcessor } from './PlaceholderProcessor'; export type { PlaceholderProcessorOptions } from './PlaceholderProcessor'; export { UrlReaderProcessor } from './UrlReaderProcessor'; export { parseEntityYaml } from '../util/parse'; -export { policyToProcessorTransformer } from './policyToProcessorTransformer'; +export { transformLegacyPolicyToProcessor } from './transformLegacyPolicyToProcessor'; diff --git a/plugins/catalog-backend/src/modules/core/policyToProcessorTransformer.test.ts b/plugins/catalog-backend/src/modules/core/transformLegacyPolicyToProcessor.test.ts similarity index 88% rename from plugins/catalog-backend/src/modules/core/policyToProcessorTransformer.test.ts rename to plugins/catalog-backend/src/modules/core/transformLegacyPolicyToProcessor.test.ts index 46338ce6cb..a38074723c 100644 --- a/plugins/catalog-backend/src/modules/core/policyToProcessorTransformer.test.ts +++ b/plugins/catalog-backend/src/modules/core/transformLegacyPolicyToProcessor.test.ts @@ -15,10 +15,10 @@ */ import { Entity, EntityPolicy } from '@backstage/catalog-model'; -import { policyToProcessorTransformer } from './policyToProcessorTransformer'; +import { transformLegacyPolicyToProcessor } from './transformLegacyPolicyToProcessor'; import { clone } from 'lodash'; -describe('policyToProcessorTransformer', () => { +describe('transformLegacyPolicyToProcessor', () => { const entityToProcess: Entity = { apiVersion: 'backstage.io/v1alpha', kind: 'Component', @@ -33,7 +33,7 @@ describe('policyToProcessorTransformer', () => { return entity; }, }; - const processor = policyToProcessorTransformer(policy); + const processor = transformLegacyPolicyToProcessor(policy); const clonedEntity = clone(entityToProcess); const entity = await processor.preProcessEntity?.( clonedEntity, @@ -54,7 +54,7 @@ describe('policyToProcessorTransformer', () => { return undefined; }, }; - const processor = policyToProcessorTransformer(policy); + const processor = transformLegacyPolicyToProcessor(policy); const clonedEntity = clone(entityToProcess); const entity = await processor.preProcessEntity?.( clonedEntity, @@ -75,7 +75,7 @@ describe('policyToProcessorTransformer', () => { throw new TypeError('Invalid value for metadata.name'); }, }; - const processor = policyToProcessorTransformer(policy); + const processor = transformLegacyPolicyToProcessor(policy); const clonedEntity = clone(entityToProcess); await expect( processor.preProcessEntity?.( diff --git a/plugins/catalog-backend/src/modules/core/policyToProcessorTransformer.ts b/plugins/catalog-backend/src/modules/core/transformLegacyPolicyToProcessor.ts similarity index 96% rename from plugins/catalog-backend/src/modules/core/policyToProcessorTransformer.ts rename to plugins/catalog-backend/src/modules/core/transformLegacyPolicyToProcessor.ts index 69b2788779..40a3e749ab 100644 --- a/plugins/catalog-backend/src/modules/core/policyToProcessorTransformer.ts +++ b/plugins/catalog-backend/src/modules/core/transformLegacyPolicyToProcessor.ts @@ -23,7 +23,7 @@ import { CatalogProcessor } from '@backstage/plugin-catalog-node'; * @returns A new entity processor that uses the entity policy. * @public */ -export function policyToProcessorTransformer( +export function transformLegacyPolicyToProcessor( policy: EntityPolicy, ): CatalogProcessor { return { diff --git a/plugins/catalog-node/src/extensions.ts b/plugins/catalog-node/src/extensions.ts index 004a46445e..df1b8b0d57 100644 --- a/plugins/catalog-node/src/extensions.ts +++ b/plugins/catalog-node/src/extensions.ts @@ -48,7 +48,7 @@ export interface CatalogProcessingExtensionPoint { /** @alpha */ export interface CatalogModelExtensionPoint { /** - * @deprecated Use `policyToProcessorTransformer` from `@backstage/plugin-catalog-backend` + * @deprecated Use `transformLegacyPolicyToProcessor` from `@backstage/plugin-catalog-backend` * and `CatalogProcessingExtensionPoint.addProcessor` instead. */ addEntityPolicies( From 0eda4bc6299a2110d2b32016a1d5326a4f2d5c7f Mon Sep 17 00:00:00 2001 From: Aramis Date: Mon, 12 Feb 2024 08:50:00 -0500 Subject: [PATCH 138/204] remove support for adding entity policies entirely Signed-off-by: Aramis --- .changeset/thirty-bags-try.md | 29 ++++++++++++++++++- .../src/service/CatalogPlugin.ts | 10 ------- plugins/catalog-node/src/extensions.ts | 10 +------ 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/.changeset/thirty-bags-try.md b/.changeset/thirty-bags-try.md index 7a8156e44c..f04d0160bf 100644 --- a/.changeset/thirty-bags-try.md +++ b/.changeset/thirty-bags-try.md @@ -2,4 +2,31 @@ '@backstage/plugin-catalog-backend': minor --- -Add support for defining entity policies in the new backend. +Adds support for supplying field validators to the new backend's catalog plugin. If you're using entity policies, you should use the new `transformLegacyPolicyToProcessor` function to install them as processors instead. + +```ts +import { + catalogProcessingExtensionPoint, + catalogModelExtensionPoint, +} from '@backstage/plugin-catalog-node/alpha'; +import {myPolicy} from './my-policy'; + +export const catalogModulePolicyProvider = createBackendModule({ + pluginId: 'catalog', + moduleId: 'internal-policy-provider', + register(reg) { + reg.registerInit({ + deps: { + modelExtensions: catalogModelExtensionPoint, + processingExtensions: catalogProcessingExtensionPoint, + }, + async init({ modelExtensions, processingExtensions }) { + modelExtensions.setFieldValidators({ + ... + }); + processingExtensions.addProcessors(transformLegacyPolicyToProcessor(myPolicy)) + }, + }); + }, +}); +``` diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.ts b/plugins/catalog-backend/src/service/CatalogPlugin.ts index 1fca11e482..114dbe6f61 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.ts @@ -128,21 +128,12 @@ class CatalogPermissionExtensionPointImpl } class CatalogModelExtensionPointImpl implements CatalogModelExtensionPoint { - #entityPolicies = new Array(); #fieldValidators: Partial = {}; - addEntityPolicies(...policies: Array>) { - this.#entityPolicies.push(...policies.flat()); - } - setFieldValidators(validators: Partial): void { merge(this.#fieldValidators, validators); } - get entityPolicies() { - return this.#entityPolicies; - } - get fieldValidators() { return this.#fieldValidators; } @@ -219,7 +210,6 @@ export const catalogPlugin = createBackendPlugin({ ); builder.addLocationAnalyzers(...analysisExtensions.locationAnalyzers); builder.addPermissionRules(...permissionExtensions.permissionRules); - builder.addEntityPolicy(...modelExtensions.entityPolicies); builder.setFieldFormatValidators(modelExtensions.fieldValidators); const { processingEngine, router } = await builder.build(); diff --git a/plugins/catalog-node/src/extensions.ts b/plugins/catalog-node/src/extensions.ts index df1b8b0d57..763e934176 100644 --- a/plugins/catalog-node/src/extensions.ts +++ b/plugins/catalog-node/src/extensions.ts @@ -15,7 +15,7 @@ */ import { createExtensionPoint } from '@backstage/backend-plugin-api'; -import { Entity, EntityPolicy, Validators } from '@backstage/catalog-model'; +import { Entity, Validators } from '@backstage/catalog-model'; import { CatalogProcessor, EntitiesSearchFilter, @@ -47,14 +47,6 @@ export interface CatalogProcessingExtensionPoint { /** @alpha */ export interface CatalogModelExtensionPoint { - /** - * @deprecated Use `transformLegacyPolicyToProcessor` from `@backstage/plugin-catalog-backend` - * and `CatalogProcessingExtensionPoint.addProcessor` instead. - */ - addEntityPolicies( - ...policies: Array> - ): void; - /** * Sets the validator function to use for one or more special fields of an * entity. This is useful if the default rules for formatting of fields are From 3d1954f80dae8f4ae327ae0813177dc0363731f0 Mon Sep 17 00:00:00 2001 From: Aramis Date: Mon, 12 Feb 2024 09:34:33 -0500 Subject: [PATCH 139/204] fix api report Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- plugins/catalog-node/api-report-alpha.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/plugins/catalog-node/api-report-alpha.md b/plugins/catalog-node/api-report-alpha.md index 9243fa98fe..e43d1bd49f 100644 --- a/plugins/catalog-node/api-report-alpha.md +++ b/plugins/catalog-node/api-report-alpha.md @@ -7,7 +7,6 @@ import { CatalogApi } from '@backstage/catalog-client'; import { CatalogProcessor } from '@backstage/plugin-catalog-node'; import { EntitiesSearchFilter } from '@backstage/plugin-catalog-node'; import { Entity } from '@backstage/catalog-model'; -import { EntityPolicy } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-node'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { PermissionRule } from '@backstage/plugin-permission-node'; @@ -28,10 +27,6 @@ export const catalogAnalysisExtensionPoint: ExtensionPoint> - ): void; setFieldValidators(validators: Partial): void; } From 5182f5bfdde84756b3d87de0765525ce8d2689c2 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Fri, 16 Feb 2024 19:51:51 -0500 Subject: [PATCH 140/204] merge fixes and update changeset Signed-off-by: aramissennyeydd --- .changeset/thirty-bags-try.md | 1 + plugins/catalog-backend/src/service/CatalogPlugin.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/thirty-bags-try.md b/.changeset/thirty-bags-try.md index f04d0160bf..98d6c5620b 100644 --- a/.changeset/thirty-bags-try.md +++ b/.changeset/thirty-bags-try.md @@ -1,5 +1,6 @@ --- '@backstage/plugin-catalog-backend': minor +'@backstage/plugin-catalog-node': minor --- Adds support for supplying field validators to the new backend's catalog plugin. If you're using entity policies, you should use the new `transformLegacyPolicyToProcessor` function to install them as processors instead. diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.ts b/plugins/catalog-backend/src/service/CatalogPlugin.ts index 114dbe6f61..255df84d58 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.ts @@ -17,7 +17,7 @@ import { createBackendPlugin, coreServices, } from '@backstage/backend-plugin-api'; -import { Entity, EntityPolicy, Validators } from '@backstage/catalog-model'; +import { Entity, Validators } from '@backstage/catalog-model'; import { CatalogBuilder, CatalogPermissionRuleInput } from './CatalogBuilder'; import { CatalogAnalysisExtensionPoint, From cd3e5f78d6ec7d8f8ba0855ddf6ef1e60630f1fc Mon Sep 17 00:00:00 2001 From: armandocomellas1 Date: Sat, 17 Feb 2024 12:10:10 -0600 Subject: [PATCH 141/204] Rollingback de way to bring back the package version Signed-off-by: armandocomellas1 --- plugins/gcp-projects/src/api/GcpClient.ts | 10 ++++------ plugins/gcp-projects/src/plugin.test.ts | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/plugins/gcp-projects/src/api/GcpClient.ts b/plugins/gcp-projects/src/api/GcpClient.ts index 449a99a5a5..f9694f266e 100644 --- a/plugins/gcp-projects/src/api/GcpClient.ts +++ b/plugins/gcp-projects/src/api/GcpClient.ts @@ -17,9 +17,7 @@ import { GcpApi } from './GcpApi'; import { Operation, Project } from './types'; import { OAuthApi } from '@backstage/core-plugin-api'; -import packageVers from '../../package.json'; - -const { version } = packageVers; +import packageinfo from '../../package.json'; const BASE_URL = 'https://content-cloudresourcemanager.googleapis.com/v1/projects'; @@ -33,7 +31,7 @@ export class GcpClient implements GcpApi { headers: { Accept: '*/*', Authorization: `Bearer ${await this.getToken()}`, - 'X-Goog-Api-Client': `backstage/gcpprojects/${version}`, + 'X-Goog-Api-Client': `backstage/gcpprojects/${packageinfo.version}`, }, }); @@ -52,7 +50,7 @@ export class GcpClient implements GcpApi { const response = await fetch(url, { headers: { Authorization: `Bearer ${await this.getToken()}`, - 'X-Goog-Api-Client': `backstage/gcpprojects/${version}`, + 'X-Goog-Api-Client': `backstage/gcpprojects/${packageinfo.version}`, }, }); @@ -79,7 +77,7 @@ export class GcpClient implements GcpApi { headers: { Accept: '*/*', Authorization: `Bearer ${await this.getToken()}`, - 'X-Goog-Api-Client': `backstage/gcpprojects/${version}`, + 'X-Goog-Api-Client': `backstage/gcpprojects/${packageinfo.version}`, }, body: JSON.stringify(newProject), }); diff --git a/plugins/gcp-projects/src/plugin.test.ts b/plugins/gcp-projects/src/plugin.test.ts index 9d7a0541a6..da5f89cbb9 100644 --- a/plugins/gcp-projects/src/plugin.test.ts +++ b/plugins/gcp-projects/src/plugin.test.ts @@ -34,7 +34,7 @@ describe('gcp-projects', () => { const response: any = { headers: { Accept: '*/*', - 'X-Goog-Api-Client': `backstage/cloudbuild/${packageinfo.version}`, + 'X-Goog-Api-Client': `backstage/gcpprojects/${packageinfo.version}`, }, }; jest.spyOn(sut, 'listProjects').mockImplementation((): any => { @@ -44,7 +44,7 @@ describe('gcp-projects', () => { expect(response).toStrictEqual({ headers: { Accept: '*/*', - 'X-Goog-Api-Client': `backstage/cloudbuild/${packageinfo.version}`, + 'X-Goog-Api-Client': `backstage/gcpprojects/${packageinfo.version}`, }, }); }); From 3b2db119b60b210e7a7d37b0f7a2b13726e5c559 Mon Sep 17 00:00:00 2001 From: Armando Comellas Date: Sat, 17 Feb 2024 12:13:12 -0600 Subject: [PATCH 142/204] Delete plugins/gcp-projects/version.json Just deleted this unesesary file. Signed-off-by: Armando Comellas --- plugins/gcp-projects/version.json | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 plugins/gcp-projects/version.json diff --git a/plugins/gcp-projects/version.json b/plugins/gcp-projects/version.json deleted file mode 100644 index 11e109a959..0000000000 --- a/plugins/gcp-projects/version.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "version": "0.3.46-next.1" -} From 1771120a72c118f0fea7a76926bd392464d4e0d3 Mon Sep 17 00:00:00 2001 From: JinoArch <39610834+JinoArch@users.noreply.github.com> Date: Sat, 17 Feb 2024 17:59:25 -0500 Subject: [PATCH 143/204] fix: update field props and field validation library Signed-off-by: JinoArch <39610834+JinoArch@users.noreply.github.com> --- .../software-templates/writing-custom-field-extensions.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/features/software-templates/writing-custom-field-extensions.md b/docs/features/software-templates/writing-custom-field-extensions.md index f22cecd22c..d53b8bdf2f 100644 --- a/docs/features/software-templates/writing-custom-field-extensions.md +++ b/docs/features/software-templates/writing-custom-field-extensions.md @@ -30,7 +30,8 @@ As an example, we will create a component that validates whether a string is in ```tsx //packages/app/src/scaffolder/ValidateKebabCase/ValidateKebabCaseExtension.tsx import React from 'react'; -import { FieldProps, FieldValidation } from '@rjsf/core'; +import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react'; +import type { FieldValidation } from '@rjsf/utils'; import FormControl from '@material-ui/core/FormControl'; /* This is the actual component that will get rendered in the form @@ -40,7 +41,7 @@ export const ValidateKebabCase = ({ rawErrors, required, formData, -}: FieldProps) => { +}: FieldExtensionComponentProps) => { return ( Date: Sun, 18 Feb 2024 12:38:19 +0100 Subject: [PATCH 144/204] docs/backend-system: split core services into separate doc sections Signed-off-by: Patrik Oldsberg --- docs/backend-system/core-services/01-index.md | 680 +----------------- docs/backend-system/core-services/auth.md | 8 + docs/backend-system/core-services/cache.md | 38 + docs/backend-system/core-services/database.md | 47 ++ .../backend-system/core-services/discovery.md | 35 + .../backend-system/core-services/http-auth.md | 8 + .../core-services/http-router.md | 58 ++ docs/backend-system/core-services/identity.md | 73 ++ .../backend-system/core-services/lifecycle.md | 40 ++ docs/backend-system/core-services/logger.md | 77 ++ .../core-services/permissions.md | 53 ++ .../core-services/plugin-metadata.md | 8 + .../core-services/root-config.md | 62 ++ .../core-services/root-http-router.md | 76 ++ .../core-services/root-lifecycle.md | 65 ++ .../core-services/root-logger.md | 8 + .../backend-system/core-services/scheduler.md | 41 ++ .../core-services/token-manager.md | 8 + .../core-services/url-reader.md | 47 ++ .../backend-system/core-services/user-info.md | 8 + microsite/sidebars.json | 23 +- 21 files changed, 804 insertions(+), 659 deletions(-) create mode 100644 docs/backend-system/core-services/auth.md create mode 100644 docs/backend-system/core-services/cache.md create mode 100644 docs/backend-system/core-services/database.md create mode 100644 docs/backend-system/core-services/discovery.md create mode 100644 docs/backend-system/core-services/http-auth.md create mode 100644 docs/backend-system/core-services/http-router.md create mode 100644 docs/backend-system/core-services/identity.md create mode 100644 docs/backend-system/core-services/lifecycle.md create mode 100644 docs/backend-system/core-services/logger.md create mode 100644 docs/backend-system/core-services/permissions.md create mode 100644 docs/backend-system/core-services/plugin-metadata.md create mode 100644 docs/backend-system/core-services/root-config.md create mode 100644 docs/backend-system/core-services/root-http-router.md create mode 100644 docs/backend-system/core-services/root-lifecycle.md create mode 100644 docs/backend-system/core-services/root-logger.md create mode 100644 docs/backend-system/core-services/scheduler.md create mode 100644 docs/backend-system/core-services/token-manager.md create mode 100644 docs/backend-system/core-services/url-reader.md create mode 100644 docs/backend-system/core-services/user-info.md diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index 9074998719..04d2e57ae8 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -8,666 +8,30 @@ description: Core backend service APIs The default backend provides several [core services](https://github.com/backstage/backstage/blob/master/packages/backend-plugin-api/src/services/definitions/coreServices.ts) out of the box which includes access to configuration, logging, URL Readers, databases and more. -All core services are available through the `coreServices` namespace in the `@backstage/backend-plugin-api` package. +All core services are available through the `coreServices` namespace in the `@backstage/backend-plugin-api` package: ```ts import { coreServices } from '@backstage/backend-plugin-api'; ``` -## HTTP Router Service - -One of the most common services is the HTTP router service which is used to expose HTTP endpoints for other plugins to consume. - -### Using the service - -The following example shows how to register a HTTP router for the `example` plugin. -This single route will be available at the `/api/example/hello` path. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; -import { Router } from 'express'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { http: coreServices.httpRouter }, - async init({ http }) { - const router = Router(); - router.get('/hello', (_req, res) => { - res.status(200).json({ hello: 'world' }); - }); - // Registers the router at the /api/example path - http.use(router); - }, - }); - }, -}); -``` - -### Configuring the service - -There's additional configuration that you can optionally pass to setup the `httpRouter` core service. - -- `getPath` - Can be used to generate a path for each plugin. Currently defaults to `/api/${pluginId}` - -You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: - -```ts -import { httpRouterServiceFactory } from '@backstage/backend-app-api'; - -const backend = createBackend(); - -backend.add( - httpRouterServiceFactory({ - getPath: (pluginId: string) => `/plugins/${pluginId}`, - }), -); -``` - -## Root HTTP Router - -The root HTTP router is a service that allows you to register routes on the root of the backend service. This is useful for things like health checks, or other routes that you want to expose on the root of the backend service. It is used as the base router that backs the `httpRouter` service. Most likely you won't need to use this service directly, but rather use the `httpRouter` service. - -### Using the service - -The following example shows how to get the root HTTP router service in your `example` backend plugin to register a health check route. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; -import { Router } from 'express'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { - rootHttpRouter: coreServices.rootHttpRouter, - }, - async init({ rootHttpRouter }) { - const router = Router(); - router.get('/health', (request, response) => { - response.send('OK'); - }); - - rootHttpRouter.use(router); - }, - }); - }, -}); -``` - -### Configuring the service - -There's additional options that you can pass to configure the root HTTP Router service. These options are passed when you call `createBackend`. - -- `indexPath` - optional path to forward all unmatched requests to. Defaults to `/api/app` which is the `app-backend` plugin responsible for serving the frontend application through the backend. - -- `configure` - this is an optional function that you can use to configure the `express` instance. This is useful if you want to add your own middleware to the root router, such as logging, or other things that you want to do before the request is handled by the backend. It's also useful to override the order in which middleware is applied. - -You can configure the root HTTP Router service by passing the options to the `createBackend` function. - -```ts -import { rootHttpRouterServiceFactory } from '@backstage/backend-app-api'; - -const backend = createBackend(); - -backend.add( - rootHttpRouterServiceFactory({ - configure: ({ app, middleware, routes, config, logger, lifecycle }) => { - // the built in middleware is provided through an option in the configure function - app.use(middleware.helmet()); - app.use(middleware.cors()); - app.use(middleware.compression()); - - // you can add you your own middleware in here - app.use(custom.logging()); - - // here the routes that are registered by other plugins - app.use(routes); - - // some other middleware that comes after the other routes - app.use(middleware.notFound()); - app.use(middleware.error()); - }, - }), -); -``` - -## Root Config - -This service allows you to read configuration values out of your `app-config` YAML files. - -### Using the service - -The following example shows how you can use the default config service to be able to get a config value, and then log it to the console. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { - log: coreServices.logger, - config: coreServices.rootConfig, - }, - async init({ log, config }) { - const baseUrl = config.getString('backend.baseUrl'); - log.warn(`The backend is running at ${baseUrl}`); - }, - }); - }, -}); -``` - -### Configuring the service - -There's additional configuration that you can optionally pass to setup the `config` core service. - -- `argv` - Override the arguments that are passed to the config loader, instead of using `process.argv` -- `remote` - Configure remote configuration loading - -You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: - -```ts -import { rootConfigServiceFactory } from '@backstage/backend-app-api'; - -const backend = createBackend(); - -backend.add( - rootConfigServiceFactory({ - argv: [ - '--config', - '/backstage/app-config.development.yaml', - '--config', - '/backstage/app-config.yaml', - ], - remote: { reloadIntervalSeconds: 60 }, - }), -); -``` - -## Logging - -This service allows plugins to output logging information. There are actually two logger services: a root logger, and a plugin logger which is bound to individual plugins, so that you will get nice messages with the plugin ID referenced in the log lines. - -### Using the service - -The following example shows how to get the logger in your `example` backend plugin and create a warning message that will be printed nicely to the console. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { - log: coreServices.logger, - }, - async init({ log }) { - log.warn("Here's a nice log line that's a warning!"); - }, - }); - }, -}); -``` - -### Root Logger - -The root logger is the logger that is used by other root services. It's where the implementation lies for creating child loggers around the backstage ecosystem including child loggers for plugins with the correct metadata and annotations. - -If you want to override the implementation for logging across all of the backend, this is the service that you should override. - -### Configuring the service - -The following example is how you can override the root logger service to add additional metadata to all log lines. - -```ts -import { coreServices } from '@backstage/backend-plugin-api'; -import { WinstonLogger } from '@backstage/backend-app-api'; - -const backend = createBackend(); - -backend.add( - createServiceFactory({ - service: coreServices.rootLogger, - deps: { - config: coreServices.rootConfig, - }, - async factory({ config }) { - const logger = WinstonLogger.create({ - meta: { - service: 'backstage', - // here's some additional information that is not part of the - // original implementation - podName: 'myk8spod', - }, - level: process.env.LOG_LEVEL || 'info', - format: - process.env.NODE_ENV === 'production' - ? format.json() - : WinstonLogger.colorFormat(), - transports: [new transports.Console()], - }); - - return logger; - }, - }), -); -``` - -## Cache - -This service lets your plugin interact with a cache. It is bound to your plugin too, so that you will only set and get values in your plugin's private namespace. - -### Using the service - -The following example shows how to get a cache client in your `example` backend plugin and setting and getting values from the cache. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { - cache: coreServices.cache, - }, - async init({ cache }) { - const { key, value } = { key: 'test:key', value: 'bob' }; - await cache.set(key, value, { ttl: 1000 }); - - // .. some other stuff. - - await cache.get(key); // 'bob' - }, - }); - }, -}); -``` - -## Database - -This service lets your plugins get a `knex` client hooked up to a database which is configured in your `app-config` YAML files, for your persistence needs. - -If there's no config provided in `backend.database` then you will automatically get a simple in-memory SQLite 3 database for your plugin whose contents will be lost when the service restarts. - -This service is scoped per plugin too, so that table names do not conflict across plugins. - -### Using the service - -The following example shows how to get access to the database service in your `example` backend plugin and getting a client for interacting with the database. It also runs some migrations from a certain directory for your plugin. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; -import { resolvePackagePath } from '@backstage/backend-common'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { - database: coreServices.database, - }, - async init({ database }) { - const client = await database.getClient(); - const migrationsDir = resolvePackagePath( - '@internal/my-plugin', - 'migrations', - ); - if (!database.migrations?.skip) { - await client.migrate.latest({ - directory: migrationsDir, - }); - } - }, - }); - }, -}); -``` - -## Discovery - -When building plugins, you might find that you will need to look up another plugin's base URL to be able to communicate with it. This could be for example an HTTP route or some `ws` protocol URL. For this we have a discovery service which can provide both internal and external base URLs for a given a plugin ID. - -### Using the service - -The following example shows how to get the discovery service in your `example` backend plugin and making a request to both the internal and external base URLs for the `derp` plugin. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; -import { fetch } from 'node-fetch'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { - discovery: coreServices.discovery, - }, - async init({ discovery }) { - const url = await discovery.getBaseUrl('derp'); // can also use discovery.getExternalBaseUrl to retrieve external URL - const response = await fetch(`${url}/hello`); - }, - }); - }, -}); -``` - -## Identity - -When working with backend plugins, you might find that you will need to interact with the `auth-backend` plugin to both authenticate backstage tokens, and to deconstruct them to get the user's entity ref and/or ownership claims out of them. - -### Using the service - -The following example shows how to get the identity service in your `example` backend plugin and retrieve the user's entity ref and ownership claims for the incoming request. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; -import { Router } from 'express'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { - identity: coreServices.identity, - http: coreServices.httpRouter, - }, - async init({ http, identity }) { - const router = Router(); - router.get('/test-me', (request, response) => { - // use the identity service to pull out the header from the request and get the user - const { - identity: { userEntityRef, ownershipEntityRefs }, - } = await identity.getIdentity({ - request, - }); - - // send the decoded and validated things back to the user - response.json({ - userEntityRef, - ownershipEntityRefs, - }); - }); - - http.use(router); - }, - }); - }, -}); -``` - -### Configuring the service - -There's additional configuration that you can optionally pass to setup the `identity` core service. - -- `issuer` - Set an optional issuer for validation of the JWT token -- `algorithms` - `alg` header for validation of the JWT token, defaults to `ES256`. More info on supported algorithms can be found in the [`jose` library documentation](https://github.com/panva/jose) - -You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: - -```ts -import { identityServiceFactory } from '@backstage/backend-app-api'; - -const backend = createBackend(); - -backend.add( - identityServiceFactory({ - issuer: 'backstage', - algorithms: ['ES256', 'RS256'], - }), -); -``` - -## Lifecycle - -This service allows your plugins to register hooks for cleaning up resources as the service is shutting down (e.g. when a pod is being torn down, or when pressing `Ctrl+C` during local development). Other core services also leverage this same mechanism internally to stop themselves cleanly. - -### Using the service - -The following example shows how to get the lifecycle service in your `example` backend plugin to clean up a long running interval when the service is shutting down. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { - lifecycle: coreServices.lifecycle, - logger: coreServices.logger, - }, - async init({ lifecycle, logger }) { - // some example work that we want to stop when shutting down - const interval = setInterval(async () => { - await fetch('http://google.com/keepalive').then(r => r.json()); - // do some other stuff. - }, 1000); - - lifecycle.addShutdownHook(() => clearInterval(interval)); - }, - }); - }, -}); -``` - -## Root Lifecycle - -This service is the same as the lifecycle service, but should only be used by the root services. This is also where the implementation for the actual lifecycle hooks are collected and executed, so if you want to override the implementation of how those are processed, you should override this service. - -### Configure the service - -The following example shows how to override the default implementation of the lifecycle service with something that listens on different process events to the original. - -```ts -class MyCustomLifecycleService implements RootLifecycleService { - constructor(private readonly logger: LoggerService) {} - - #isCalled = false; - #shutdownTasks: Array<{ - hook: LifecycleServiceShutdownHook; - options?: LifecycleServiceShutdownOptions; - }> = []; - - addShutdownHook( - hook: LifecycleServiceShutdownHook, - options?: LifecycleServiceShutdownOptions, - ): void { - this.#shutdownTasks.push({ hook, options }); - } - - async shutdown(): Promise { - if (this.#isCalled) { - return; - } - this.#isCalled = true; - - this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`); - await Promise.all( - this.#shutdownTasks.map(async ({ hook, options }) => { - const logger = options?.logger ?? this.logger; - try { - await hook(); - logger.info(`Shutdown hook succeeded`); - } catch (error) { - logger.error(`Shutdown hook failed, ${error}`); - } - }), - ); - } -} - -const backend = createBackend(); - -backend.add( - createServiceFactory({ - service: coreServices.rootLifecycle, - deps: { - logger: coreServices.rootLogger, - }, - async factory({ logger }) { - return new MyCustomLifecycleService(logger); - }, - }), -); -``` - -## Permissions - -This service allows your plugins to ask [the permissions framework](https://backstage.io/docs/permissions/overview) for authorization of user actions. - -### Using the service - -The following example shows how to get the permissions service in your `example` backend to check to see if the user is allowed to perform a certain action with a custom permission rule. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; -import { Router } from 'express'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { - permissions: coreServices.permissions, - http: coreServices.httpRouter, - }, - async init({ permissions, http }) { - const router = Router(); - router.get('/test-me', (request, response) => { - // use the identity service to pull out the token from request headers - const { token } = await identity.getIdentity({ - request, - }); - - // ask the permissions framework what the decision is for the permission - const permissionResponse = await permissions.authorize( - [ - { - permission: myCustomPermission, - }, - ], - { token }, - ); - }); - - http.use(router); - }, - }); - }, -}); -``` - -## Scheduler - -When writing plugins, you sometimes want to have things running on a schedule, or something similar to cron jobs that are distributed through instances that your backend plugin is running on. We supply a task scheduler for this purpose that is scoped per plugin so that you can create these tasks and orchestrate their execution. - -### Using the service - -The following example shows how to get the scheduler service in your `example` backend to issue a scheduled task that runs across your instances at a given interval. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; -import { fetch } from 'node-fetch'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { - scheduler: coreServices.scheduler, - }, - async init({ scheduler }) { - await scheduler.scheduleTask({ - frequency: { minutes: 10 }, - timeout: { seconds: 30 }, - id: 'ping-google', - fn: async () => { - await fetch('http://google.com/ping'); - }, - }); - }, - }); - }, -}); -``` - -## URL Readers - -Plugins will require communication with certain integrations that users have configured. Popular integrations are things like Version Control Systems (VSC), such as GitHub, BitBucket GitLab etc. These integrations are configured in the `integrations` section of the `app-config.yaml` file. - -These URL readers are basically wrappers with authentication for files and folders that could be stored in these VCS repositories. - -### Using the service - -The following example shows how to get the URL Reader service in your `example` backend plugin to read a file and a directory from a GitHub repository. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; -import os from 'os'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { - urlReader: coreServices.urlReader, - }, - async init({ urlReader }) { - const buffer = await urlReader - .read('https://github.com/backstage/backstage/blob/master/README.md') - .then(r => r.buffer()); - - const tmpDir = os.tmpdir(); - const directory = await urlReader - .readTree( - 'https://github.com/backstage/backstage/tree/master/packages/backend', - ) - .then(tree => tree.dir({ targetDir: tmpDir })); - }, - }); - }, -}); -``` +## Service Documentation Index + +- [Auth Service](./auth.md) - Token authentication and credentials management. +- [Cache Service](./cache.md) - Key-value store for caching data. +- [Database Service](./database.md) - Database access and management via [knex](https://knexjs.org/). +- [Discovery Service](./discovery.md) - Service discovery for inter-plugin communication. +- [Http Auth Service](./http-auth.md) - Authentication of HTTP requests. +- [Http Router Service](./http-router.md) - HTTP route registration for plugins. +- [Identity Service](./identity.md) - Deprecated user authentication service, use the [Auth Service](./auth.md) instead. +- [Lifecycle Service](./lifecycle.md) - Registration of plugin startup and shutdown lifecycle hooks. +- [Logger Service](./logger.md) - Plugin-level logging. +- [Permissions Service](./permissions.md) - Permission system integration for authorization of user actions. +- [Plugin Metadata Service](./plugin-metadata.md) - Built-in service for accessing metadata about the current plugin. +- [Root Config Service](./root-config.md) - Access to static configuration. +- [Root Http Router Service](./root-http-router.md) - HTTP route registration for root services. +- [Root Lifecycle Service](./root-lifecycle.md) - Registration of backend startup and shutdown lifecycle hooks. +- [Root Logger Service](./root-logger.md) - Root-level logging. +- [Scheduler Service](./scheduler.md) - Scheduling of distributed background tasks. +- [Token Manager Service](./token-manager.md) - Deprecated service authentication service, use the [Auth Service](./auth.md) instead. +- [Url Reader Service](./url-reader.md) - Reading content from external systems. +- [User Info Service](./user-info.md) - Authenticated user information retrieval. diff --git a/docs/backend-system/core-services/auth.md b/docs/backend-system/core-services/auth.md new file mode 100644 index 0000000000..4f1229a9d7 --- /dev/null +++ b/docs/backend-system/core-services/auth.md @@ -0,0 +1,8 @@ +--- +id: auth +title: Auth Service +sidebar_label: Auth +description: Documentation for the Auth service +--- + +TODO diff --git a/docs/backend-system/core-services/cache.md b/docs/backend-system/core-services/cache.md new file mode 100644 index 0000000000..0e26e8e1f5 --- /dev/null +++ b/docs/backend-system/core-services/cache.md @@ -0,0 +1,38 @@ +--- +id: cache +title: Cache Service +sidebar_label: Cache +description: Documentation for the Cache service +--- + +This service lets your plugin interact with a cache. It is bound to your plugin too, so that you will only set and get values in your plugin's private namespace. + +## Using the service + +The following example shows how to get a cache client in your `example` backend plugin and setting and getting values from the cache. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + cache: coreServices.cache, + }, + async init({ cache }) { + const { key, value } = { key: 'test:key', value: 'bob' }; + await cache.set(key, value, { ttl: 1000 }); + + // .. some other stuff. + + await cache.get(key); // 'bob' + }, + }); + }, +}); +``` diff --git a/docs/backend-system/core-services/database.md b/docs/backend-system/core-services/database.md new file mode 100644 index 0000000000..e083f0b44d --- /dev/null +++ b/docs/backend-system/core-services/database.md @@ -0,0 +1,47 @@ +--- +id: database +title: Database Service +sidebar_label: Database +description: Documentation for the Database service +--- + +This service lets your plugins get a `knex` client hooked up to a database which is configured in your `app-config` YAML files, for your persistence needs. + +If there's no config provided in `backend.database` then you will automatically get a simple in-memory SQLite 3 database for your plugin whose contents will be lost when the service restarts. + +This service is scoped per plugin too, so that table names do not conflict across plugins. + +## Using the service + +The following example shows how to get access to the database service in your `example` backend plugin and getting a client for interacting with the database. It also runs some migrations from a certain directory for your plugin. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { resolvePackagePath } from '@backstage/backend-common'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + database: coreServices.database, + }, + async init({ database }) { + const client = await database.getClient(); + const migrationsDir = resolvePackagePath( + '@internal/my-plugin', + 'migrations', + ); + if (!database.migrations?.skip) { + await client.migrate.latest({ + directory: migrationsDir, + }); + } + }, + }); + }, +}); +``` diff --git a/docs/backend-system/core-services/discovery.md b/docs/backend-system/core-services/discovery.md new file mode 100644 index 0000000000..455e398d8b --- /dev/null +++ b/docs/backend-system/core-services/discovery.md @@ -0,0 +1,35 @@ +--- +id: discovery +title: Discovery Service +sidebar_label: Discovery +description: Documentation for the Discovery service +--- + +When building plugins, you might find that you will need to look up another plugin's base URL to be able to communicate with it. This could be for example an HTTP route or some `ws` protocol URL. For this we have a discovery service which can provide both internal and external base URLs for a given a plugin ID. + +## Using the service + +The following example shows how to get the discovery service in your `example` backend plugin and making a request to both the internal and external base URLs for the `derp` plugin. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { fetch } from 'node-fetch'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + discovery: coreServices.discovery, + }, + async init({ discovery }) { + const url = await discovery.getBaseUrl('derp'); // can also use discovery.getExternalBaseUrl to retrieve external URL + const response = await fetch(`${url}/hello`); + }, + }); + }, +}); +``` diff --git a/docs/backend-system/core-services/http-auth.md b/docs/backend-system/core-services/http-auth.md new file mode 100644 index 0000000000..7af30c3251 --- /dev/null +++ b/docs/backend-system/core-services/http-auth.md @@ -0,0 +1,8 @@ +--- +id: http-auth +title: Http Auth Service +sidebar_label: Http Auth +description: Documentation for the Http Auth service +--- + +TODO diff --git a/docs/backend-system/core-services/http-router.md b/docs/backend-system/core-services/http-router.md new file mode 100644 index 0000000000..e976d2c06c --- /dev/null +++ b/docs/backend-system/core-services/http-router.md @@ -0,0 +1,58 @@ +--- +id: http-router +title: Http Router Service +sidebar_label: Http Router +description: Documentation for the Http Router service +--- + +One of the most common services is the HTTP router service which is used to expose HTTP endpoints for other plugins to consume. + +## Using the service + +The following example shows how to register a HTTP router for the `example` plugin. +This single route will be available at the `/api/example/hello` path. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { http: coreServices.httpRouter }, + async init({ http }) { + const router = Router(); + router.get('/hello', (_req, res) => { + res.status(200).json({ hello: 'world' }); + }); + // Registers the router at the /api/example path + http.use(router); + }, + }); + }, +}); +``` + +## Configuring the service + +There's additional configuration that you can optionally pass to setup the `httpRouter` core service. + +- `getPath` - Can be used to generate a path for each plugin. Currently defaults to `/api/${pluginId}` + +You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: + +```ts +import { httpRouterServiceFactory } from '@backstage/backend-app-api'; + +const backend = createBackend(); + +backend.add( + httpRouterServiceFactory({ + getPath: (pluginId: string) => `/plugins/${pluginId}`, + }), +); +``` diff --git a/docs/backend-system/core-services/identity.md b/docs/backend-system/core-services/identity.md new file mode 100644 index 0000000000..eb7d1f4ac7 --- /dev/null +++ b/docs/backend-system/core-services/identity.md @@ -0,0 +1,73 @@ +--- +id: identity +title: Identity Service +sidebar_label: Identity +description: Documentation for the Identity service +--- + +When working with backend plugins, you might find that you will need to interact with the `auth-backend` plugin to both authenticate backstage tokens, and to deconstruct them to get the user's entity ref and/or ownership claims out of them. + +## Using the service + +The following example shows how to get the identity service in your `example` backend plugin and retrieve the user's entity ref and ownership claims for the incoming request. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + identity: coreServices.identity, + http: coreServices.httpRouter, + }, + async init({ http, identity }) { + const router = Router(); + router.get('/test-me', (request, response) => { + // use the identity service to pull out the header from the request and get the user + const { + identity: { userEntityRef, ownershipEntityRefs }, + } = await identity.getIdentity({ + request, + }); + + // send the decoded and validated things back to the user + response.json({ + userEntityRef, + ownershipEntityRefs, + }); + }); + + http.use(router); + }, + }); + }, +}); +``` + +## Configuring the service + +There's additional configuration that you can optionally pass to setup the `identity` core service. + +- `issuer` - Set an optional issuer for validation of the JWT token +- `algorithms` - `alg` header for validation of the JWT token, defaults to `ES256`. More info on supported algorithms can be found in the [`jose` library documentation](https://github.com/panva/jose) + +You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: + +```ts +import { identityServiceFactory } from '@backstage/backend-app-api'; + +const backend = createBackend(); + +backend.add( + identityServiceFactory({ + issuer: 'backstage', + algorithms: ['ES256', 'RS256'], + }), +); +``` diff --git a/docs/backend-system/core-services/lifecycle.md b/docs/backend-system/core-services/lifecycle.md new file mode 100644 index 0000000000..aa547d6cc9 --- /dev/null +++ b/docs/backend-system/core-services/lifecycle.md @@ -0,0 +1,40 @@ +--- +id: lifecycle +title: Lifecycle Service +sidebar_label: Lifecycle +description: Documentation for the Lifecycle service +--- + +This service allows your plugins to register hooks for cleaning up resources as the service is shutting down (e.g. when a pod is being torn down, or when pressing `Ctrl+C` during local development). Other core services also leverage this same mechanism internally to stop themselves cleanly. + +## Using the service + +The following example shows how to get the lifecycle service in your `example` backend plugin to clean up a long running interval when the service is shutting down. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + lifecycle: coreServices.lifecycle, + logger: coreServices.logger, + }, + async init({ lifecycle, logger }) { + // some example work that we want to stop when shutting down + const interval = setInterval(async () => { + await fetch('http://google.com/keepalive').then(r => r.json()); + // do some other stuff. + }, 1000); + + lifecycle.addShutdownHook(() => clearInterval(interval)); + }, + }); + }, +}); +``` diff --git a/docs/backend-system/core-services/logger.md b/docs/backend-system/core-services/logger.md new file mode 100644 index 0000000000..015d2055d5 --- /dev/null +++ b/docs/backend-system/core-services/logger.md @@ -0,0 +1,77 @@ +--- +id: logger +title: Logger Service +sidebar_label: Logger +description: Documentation for the Logger service +--- + +This service allows plugins to output logging information. There are actually two logger services: a root logger, and a plugin logger which is bound to individual plugins, so that you will get nice messages with the plugin ID referenced in the log lines. + +## Using the service + +The following example shows how to get the logger in your `example` backend plugin and create a warning message that will be printed nicely to the console. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + log: coreServices.logger, + }, + async init({ log }) { + log.warn("Here's a nice log line that's a warning!"); + }, + }); + }, +}); +``` + +## Root Logger + +The root logger is the logger that is used by other root services. It's where the implementation lies for creating child loggers around the backstage ecosystem including child loggers for plugins with the correct metadata and annotations. + +If you want to override the implementation for logging across all of the backend, this is the service that you should override. + +## Configuring the service + +The following example is how you can override the root logger service to add additional metadata to all log lines. + +```ts +import { coreServices } from '@backstage/backend-plugin-api'; +import { WinstonLogger } from '@backstage/backend-app-api'; + +const backend = createBackend(); + +backend.add( + createServiceFactory({ + service: coreServices.rootLogger, + deps: { + config: coreServices.rootConfig, + }, + async factory({ config }) { + const logger = WinstonLogger.create({ + meta: { + service: 'backstage', + // here's some additional information that is not part of the + // original implementation + podName: 'myk8spod', + }, + level: process.env.LOG_LEVEL || 'info', + format: + process.env.NODE_ENV === 'production' + ? format.json() + : WinstonLogger.colorFormat(), + transports: [new transports.Console()], + }); + + return logger; + }, + }), +); +``` diff --git a/docs/backend-system/core-services/permissions.md b/docs/backend-system/core-services/permissions.md new file mode 100644 index 0000000000..d979932c2f --- /dev/null +++ b/docs/backend-system/core-services/permissions.md @@ -0,0 +1,53 @@ +--- +id: permissions +title: Permissions Service +sidebar_label: Permissions +description: Documentation for the Permissions service +--- + +This service allows your plugins to ask [the permissions framework](https://backstage.io/docs/permissions/overview) for authorization of user actions. + +## Using the service + +The following example shows how to get the permissions service in your `example` backend to check to see if the user is allowed to perform a certain action with a custom permission rule. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + permissions: coreServices.permissions, + http: coreServices.httpRouter, + }, + async init({ permissions, http }) { + const router = Router(); + router.get('/test-me', (request, response) => { + // use the identity service to pull out the token from request headers + const { token } = await identity.getIdentity({ + request, + }); + + // ask the permissions framework what the decision is for the permission + const permissionResponse = await permissions.authorize( + [ + { + permission: myCustomPermission, + }, + ], + { token }, + ); + }); + + http.use(router); + }, + }); + }, +}); +``` diff --git a/docs/backend-system/core-services/plugin-metadata.md b/docs/backend-system/core-services/plugin-metadata.md new file mode 100644 index 0000000000..f045ed7f19 --- /dev/null +++ b/docs/backend-system/core-services/plugin-metadata.md @@ -0,0 +1,8 @@ +--- +id: plugin-metadata +title: Plugin Metadata Service +sidebar_label: Plugin Metadata +description: Documentation for the Plugin Metadata service +--- + +TODO diff --git a/docs/backend-system/core-services/root-config.md b/docs/backend-system/core-services/root-config.md new file mode 100644 index 0000000000..7cdc10f38c --- /dev/null +++ b/docs/backend-system/core-services/root-config.md @@ -0,0 +1,62 @@ +--- +id: root-config +title: Root Config Service +sidebar_label: Root Config +description: Documentation for the Root Config service +--- + +This service allows you to read configuration values out of your `app-config` YAML files. + +## Using the service + +The following example shows how you can use the default config service to be able to get a config value, and then log it to the console. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + log: coreServices.logger, + config: coreServices.rootConfig, + }, + async init({ log, config }) { + const baseUrl = config.getString('backend.baseUrl'); + log.warn(`The backend is running at ${baseUrl}`); + }, + }); + }, +}); +``` + +## Configuring the service + +There's additional configuration that you can optionally pass to setup the `config` core service. + +- `argv` - Override the arguments that are passed to the config loader, instead of using `process.argv` +- `remote` - Configure remote configuration loading + +You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: + +```ts +import { rootConfigServiceFactory } from '@backstage/backend-app-api'; + +const backend = createBackend(); + +backend.add( + rootConfigServiceFactory({ + argv: [ + '--config', + '/backstage/app-config.development.yaml', + '--config', + '/backstage/app-config.yaml', + ], + remote: { reloadIntervalSeconds: 60 }, + }), +); +``` diff --git a/docs/backend-system/core-services/root-http-router.md b/docs/backend-system/core-services/root-http-router.md new file mode 100644 index 0000000000..bbd42b743b --- /dev/null +++ b/docs/backend-system/core-services/root-http-router.md @@ -0,0 +1,76 @@ +--- +id: root-http-router +title: Root Http Router Service +sidebar_label: Root Http Router +description: Documentation for the Root Http Router service +--- + +The root HTTP router is a service that allows you to register routes on the root of the backend service. This is useful for things like health checks, or other routes that you want to expose on the root of the backend service. It is used as the base router that backs the `httpRouter` service. Most likely you won't need to use this service directly, but rather use the `httpRouter` service. + +## Using the service + +The following example shows how to get the root HTTP router service in your `example` backend plugin to register a health check route. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + rootHttpRouter: coreServices.rootHttpRouter, + }, + async init({ rootHttpRouter }) { + const router = Router(); + router.get('/health', (request, response) => { + response.send('OK'); + }); + + rootHttpRouter.use(router); + }, + }); + }, +}); +``` + +## Configuring the service + +There's additional options that you can pass to configure the root HTTP Router service. These options are passed when you call `createBackend`. + +- `indexPath` - optional path to forward all unmatched requests to. Defaults to `/api/app` which is the `app-backend` plugin responsible for serving the frontend application through the backend. + +- `configure` - this is an optional function that you can use to configure the `express` instance. This is useful if you want to add your own middleware to the root router, such as logging, or other things that you want to do before the request is handled by the backend. It's also useful to override the order in which middleware is applied. + +You can configure the root HTTP Router service by passing the options to the `createBackend` function. + +```ts +import { rootHttpRouterServiceFactory } from '@backstage/backend-app-api'; + +const backend = createBackend(); + +backend.add( + rootHttpRouterServiceFactory({ + configure: ({ app, middleware, routes, config, logger, lifecycle }) => { + // the built in middleware is provided through an option in the configure function + app.use(middleware.helmet()); + app.use(middleware.cors()); + app.use(middleware.compression()); + + // you can add you your own middleware in here + app.use(custom.logging()); + + // here the routes that are registered by other plugins + app.use(routes); + + // some other middleware that comes after the other routes + app.use(middleware.notFound()); + app.use(middleware.error()); + }, + }), +); +``` diff --git a/docs/backend-system/core-services/root-lifecycle.md b/docs/backend-system/core-services/root-lifecycle.md new file mode 100644 index 0000000000..be981e9bd8 --- /dev/null +++ b/docs/backend-system/core-services/root-lifecycle.md @@ -0,0 +1,65 @@ +--- +id: root-lifecycle +title: Root Lifecycle Service +sidebar_label: Root Lifecycle +description: Documentation for the Root Lifecycle service +--- + +This service is the same as the lifecycle service, but should only be used by the root services. This is also where the implementation for the actual lifecycle hooks are collected and executed, so if you want to override the implementation of how those are processed, you should override this service. + +## Configure the service + +The following example shows how to override the default implementation of the lifecycle service with something that listens on different process events to the original. + +```ts +class MyCustomLifecycleService implements RootLifecycleService { + constructor(private readonly logger: LoggerService) {} + + #isCalled = false; + #shutdownTasks: Array<{ + hook: LifecycleServiceShutdownHook; + options?: LifecycleServiceShutdownOptions; + }> = []; + + addShutdownHook( + hook: LifecycleServiceShutdownHook, + options?: LifecycleServiceShutdownOptions, + ): void { + this.#shutdownTasks.push({ hook, options }); + } + + async shutdown(): Promise { + if (this.#isCalled) { + return; + } + this.#isCalled = true; + + this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`); + await Promise.all( + this.#shutdownTasks.map(async ({ hook, options }) => { + const logger = options?.logger ?? this.logger; + try { + await hook(); + logger.info(`Shutdown hook succeeded`); + } catch (error) { + logger.error(`Shutdown hook failed, ${error}`); + } + }), + ); + } +} + +const backend = createBackend(); + +backend.add( + createServiceFactory({ + service: coreServices.rootLifecycle, + deps: { + logger: coreServices.rootLogger, + }, + async factory({ logger }) { + return new MyCustomLifecycleService(logger); + }, + }), +); +``` diff --git a/docs/backend-system/core-services/root-logger.md b/docs/backend-system/core-services/root-logger.md new file mode 100644 index 0000000000..09b7929efe --- /dev/null +++ b/docs/backend-system/core-services/root-logger.md @@ -0,0 +1,8 @@ +--- +id: root-logger +title: Root Logger Service +sidebar_label: Root Logger +description: Documentation for the Root Logger service +--- + +TODO diff --git a/docs/backend-system/core-services/scheduler.md b/docs/backend-system/core-services/scheduler.md new file mode 100644 index 0000000000..e4c67a1199 --- /dev/null +++ b/docs/backend-system/core-services/scheduler.md @@ -0,0 +1,41 @@ +--- +id: scheduler +title: Scheduler Service +sidebar_label: Scheduler +description: Documentation for the Scheduler service +--- + +When writing plugins, you sometimes want to have things running on a schedule, or something similar to cron jobs that are distributed through instances that your backend plugin is running on. We supply a task scheduler for this purpose that is scoped per plugin so that you can create these tasks and orchestrate their execution. + +## Using the service + +The following example shows how to get the scheduler service in your `example` backend to issue a scheduled task that runs across your instances at a given interval. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { fetch } from 'node-fetch'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + scheduler: coreServices.scheduler, + }, + async init({ scheduler }) { + await scheduler.scheduleTask({ + frequency: { minutes: 10 }, + timeout: { seconds: 30 }, + id: 'ping-google', + fn: async () => { + await fetch('http://google.com/ping'); + }, + }); + }, + }); + }, +}); +``` diff --git a/docs/backend-system/core-services/token-manager.md b/docs/backend-system/core-services/token-manager.md new file mode 100644 index 0000000000..b62499462f --- /dev/null +++ b/docs/backend-system/core-services/token-manager.md @@ -0,0 +1,8 @@ +--- +id: token-manager +title: Token Manager Service +sidebar_label: Token Manager +description: Documentation for the Token Manager service +--- + +TODO diff --git a/docs/backend-system/core-services/url-reader.md b/docs/backend-system/core-services/url-reader.md new file mode 100644 index 0000000000..b9616a029e --- /dev/null +++ b/docs/backend-system/core-services/url-reader.md @@ -0,0 +1,47 @@ +--- +id: url-reader +title: Url Reader Service +sidebar_label: Url Reader +description: Documentation for the Url Reader service +--- + +# URL Readers + +Plugins will require communication with certain integrations that users have configured. Popular integrations are things like Version Control Systems (VSC), such as GitHub, BitBucket GitLab etc. These integrations are configured in the `integrations` section of the `app-config.yaml` file. + +These URL readers are basically wrappers with authentication for files and folders that could be stored in these VCS repositories. + +## Using the service + +The following example shows how to get the URL Reader service in your `example` backend plugin to read a file and a directory from a GitHub repository. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import os from 'os'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + urlReader: coreServices.urlReader, + }, + async init({ urlReader }) { + const buffer = await urlReader + .read('https://github.com/backstage/backstage/blob/master/README.md') + .then(r => r.buffer()); + + const tmpDir = os.tmpdir(); + const directory = await urlReader + .readTree( + 'https://github.com/backstage/backstage/tree/master/packages/backend', + ) + .then(tree => tree.dir({ targetDir: tmpDir })); + }, + }); + }, +}); +``` diff --git a/docs/backend-system/core-services/user-info.md b/docs/backend-system/core-services/user-info.md new file mode 100644 index 0000000000..5ca717ddd5 --- /dev/null +++ b/docs/backend-system/core-services/user-info.md @@ -0,0 +1,8 @@ +--- +id: user-info +title: User Info Service +sidebar_label: User Info +description: Documentation for the User Info service +--- + +TODO diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 146d8db059..40eaf235c6 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -387,7 +387,28 @@ { "type": "category", "label": "Core Services", - "items": ["backend-system/core-services/index"] + "items": [ + "backend-system/core-services/index", + "backend-system/core-services/auth", + "backend-system/core-services/cache", + "backend-system/core-services/database", + "backend-system/core-services/discovery", + "backend-system/core-services/http-auth", + "backend-system/core-services/http-router", + "backend-system/core-services/identity", + "backend-system/core-services/lifecycle", + "backend-system/core-services/logger", + "backend-system/core-services/permissions", + "backend-system/core-services/plugin-metadata", + "backend-system/core-services/root-config", + "backend-system/core-services/root-http-router", + "backend-system/core-services/root-lifecycle", + "backend-system/core-services/root-logger", + "backend-system/core-services/scheduler", + "backend-system/core-services/token-manager", + "backend-system/core-services/url-reader", + "backend-system/core-services/user-info" + ] } ], "New Frontend System": [ From 9aa01eeb0819fddc01d751fffd3c1a3368e5431c Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Sun, 18 Feb 2024 12:42:04 +0100 Subject: [PATCH 145/204] Updated BEP 0004 Signed-off-by: bnechyporenko --- .../README.md | 67 ++++++++++++++----- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/beps/0004-scaffolder-task-idempotency/README.md b/beps/0004-scaffolder-task-idempotency/README.md index a7e592af39..28b052e288 100644 --- a/beps/0004-scaffolder-task-idempotency/README.md +++ b/beps/0004-scaffolder-task-idempotency/README.md @@ -4,7 +4,8 @@ status: provisional authors: - 'bnechyporenko@bol.com' - 'benjaminl@spotify.com' -owners: +owners: + - @backstage/scaffolder-maintainers project-areas: - scaffolder creation-date: 2024-01-31 @@ -153,7 +154,7 @@ export function createGithubRepoCreateAction(options: { username: owner, }); - await ctx.checkpoint('v1.task.checkpoint.repo.creation', async () => { + await ctx.checkpoint('repo.creation', async () => { const repoCreationPromise = user.data.type === 'Organization' ? client.rest.repos.createInOrg({ @@ -168,19 +169,16 @@ export function createGithubRepoCreateAction(options: { }); if (secrets) { - await ctx.checkpoint( - 'v1.task.checkpoint.repo.create.variables', - async () => { - for (const [key, value] of Object.entries(repoVariables ?? {})) { - await client.rest.actions.createRepoVariable({ - owner, - repo, - name: key, - value: value, - }); - } - }, - ); + await ctx.checkpoint('repo.create.variables', async () => { + for (const [key, value] of Object.entries(repoVariables ?? {})) { + await client.rest.actions.createRepoVariable({ + owner, + repo, + name: key, + value: value, + }); + } + }); } ctx.output('remoteUrl', newRepo.clone_url); @@ -204,7 +202,7 @@ Checkpoints will allow action authors to create actions where code paths are ign This will be provided on a context object and action of author provide a key and a callback. ```typescript -await ctx.checkpoint('v1.task.checkpoint.repo.creation', async () => { +await ctx.checkpoint('repo.creation', async () => { const { repoUrl } = await client.rest.Repository.create({}); return { repoUrl }; }); @@ -215,7 +213,7 @@ It's going look like: ```json { - "v1.task.checkpoint.repo.creation": { + "repo.creation": { "status": "success", "result": { "repoUrl": "https://github.com/backstage/backstage.git" @@ -224,6 +222,41 @@ It's going look like: } ``` +or a failed attempt as: + +```json +{ + "repo.creation": { + "status": "failed", + "reason": "Namespace is not valid" + } +} +``` + +DatabaseTaskStore will provide two extra methods `saveTaskState` and `getTaskState`. The type of state in API will be +represented as `JsonObject`. + +Task state will be stored in the extra column `state` in the table `tasks` with the next structure: + +```json +{ + "state": { + "repo.creation": { + "status": "success", + "result": { + "repoUrl": "https://github.com/backstage/backstage.git" + } + }, + "repo.add.member": { + "status": "success", + "result": { + "id": "2345" + } + } + } +} +``` + ## Release Plan