From 90db93f223765858e37a74ca8a7d7ea0adab5dff Mon Sep 17 00:00:00 2001 From: Alexander Kaserbacher Date: Sat, 15 May 2021 13:08:31 +0200 Subject: [PATCH 01/42] do not post to errorApi in case getLastBuild or getFolder API call threw an error we do not want to show an error popup for the user here but instead move the error to the card Signed-off-by: Alexander Kaserbacher --- plugins/jenkins/src/components/useBuilds.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/jenkins/src/components/useBuilds.ts b/plugins/jenkins/src/components/useBuilds.ts index 5a6f8b9f08..e818d3f5a6 100644 --- a/plugins/jenkins/src/components/useBuilds.ts +++ b/plugins/jenkins/src/components/useBuilds.ts @@ -48,7 +48,6 @@ export function useBuilds(projectName: string, branch?: string) { return build || []; } catch (e) { - errorApi.post(e); throw e; } }, [api, errorApi, projectName, branch]); From 93068f04e7ce4505b7b88d765a58eac1ba29979f Mon Sep 17 00:00:00 2001 From: Alexander Kaserbacher Date: Sun, 16 May 2021 11:59:48 +0200 Subject: [PATCH 02/42] capture API errors in Jenkins card and show WarningPanel Signed-off-by: Alexander Kaserbacher --- plugins/jenkins/src/components/Cards/Cards.tsx | 13 +++++++++++-- plugins/jenkins/src/components/useBuilds.ts | 3 +++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/plugins/jenkins/src/components/Cards/Cards.tsx b/plugins/jenkins/src/components/Cards/Cards.tsx index f19ed608ce..31df25820e 100644 --- a/plugins/jenkins/src/components/Cards/Cards.tsx +++ b/plugins/jenkins/src/components/Cards/Cards.tsx @@ -17,6 +17,7 @@ import { InfoCard, InfoCardVariants, StructuredMetadataTable, + WarningPanel, } from '@backstage/core'; import { LinearProgress, Link, makeStyles, Theme } from '@material-ui/core'; import ExternalLinkIcon from '@material-ui/icons/Launch'; @@ -83,11 +84,19 @@ export const LatestRunCard = ({ variant?: InfoCardVariants; }) => { const projectName = useProjectSlugFromEntity(); - const [{ builds, loading }] = useBuilds(projectName, branch); + const [{ builds, loading, error }] = useBuilds(projectName, branch); const latestRun = builds ?? {}; return ( - + {!error ? ( + + ) : ( + + )} ); }; diff --git a/plugins/jenkins/src/components/useBuilds.ts b/plugins/jenkins/src/components/useBuilds.ts index e818d3f5a6..f119da0838 100644 --- a/plugins/jenkins/src/components/useBuilds.ts +++ b/plugins/jenkins/src/components/useBuilds.ts @@ -25,6 +25,7 @@ export function useBuilds(projectName: string, branch?: string) { const [total, setTotal] = useState(0); const [page, setPage] = useState(0); const [pageSize, setPageSize] = useState(5); + const [error, setError] = useState(); const restartBuild = async (buildName: string) => { try { @@ -48,6 +49,7 @@ export function useBuilds(projectName: string, branch?: string) { return build || []; } catch (e) { + setError(e); throw e; } }, [api, errorApi, projectName, branch]); @@ -60,6 +62,7 @@ export function useBuilds(projectName: string, branch?: string) { builds, projectName, total, + error, }, { builds, From 8b13d2e0e318452964c83cf726c5bc473cb561f5 Mon Sep 17 00:00:00 2001 From: Alexander Kaserbacher Date: Wed, 19 May 2021 21:06:27 +0200 Subject: [PATCH 03/42] Show title in WarningPanel depending on the error returned by the Jenkins API Signed-off-by: Alexander Kaserbacher --- .../jenkins/src/components/Cards/Cards.tsx | 24 +++++++++++++++++-- plugins/jenkins/src/components/useBuilds.ts | 15 ++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/plugins/jenkins/src/components/Cards/Cards.tsx b/plugins/jenkins/src/components/Cards/Cards.tsx index 31df25820e..010afa8990 100644 --- a/plugins/jenkins/src/components/Cards/Cards.tsx +++ b/plugins/jenkins/src/components/Cards/Cards.tsx @@ -24,7 +24,7 @@ import ExternalLinkIcon from '@material-ui/icons/Launch'; import { DateTime, Duration } from 'luxon'; import React from 'react'; import { JenkinsRunStatus } from '../BuildsPage/lib/Status'; -import { useBuilds } from '../useBuilds'; +import { ErrorType, useBuilds } from '../useBuilds'; import { useProjectSlugFromEntity } from '../useProjectSlugFromEntity'; const useStyles = makeStyles({ @@ -76,6 +76,23 @@ const WidgetContent = ({ ); }; +const JenkinsApiErrorPanel = ({ + message, + errorType, +}: { + message: string; + errorType: ErrorType; +}) => { + let title = undefined; + if (errorType === ErrorType.CONNECTION_ERROR) { + title = "Can't connect to Jenkins"; + } else if (errorType === ErrorType.NOT_FOUND) { + title = "Can't find Jenkins project"; + } + + return ; +}; + export const LatestRunCard = ({ branch = 'master', variant, @@ -95,7 +112,10 @@ export const LatestRunCard = ({ latestRun={latestRun} /> ) : ( - + )} ); diff --git a/plugins/jenkins/src/components/useBuilds.ts b/plugins/jenkins/src/components/useBuilds.ts index f119da0838..be573ef605 100644 --- a/plugins/jenkins/src/components/useBuilds.ts +++ b/plugins/jenkins/src/components/useBuilds.ts @@ -18,6 +18,11 @@ import { useState } from 'react'; import { useAsyncRetry } from 'react-use'; import { jenkinsApiRef } from '../api'; +export enum ErrorType { + CONNECTION_ERROR, + NOT_FOUND, +} + export function useBuilds(projectName: string, branch?: string) { const api = useApi(jenkinsApiRef); const errorApi = useApi(errorApiRef); @@ -25,7 +30,10 @@ export function useBuilds(projectName: string, branch?: string) { const [total, setTotal] = useState(0); const [page, setPage] = useState(0); const [pageSize, setPageSize] = useState(5); - const [error, setError] = useState(); + const [error, setError] = useState<{ + message: string; + errorType: ErrorType; + }>(); const restartBuild = async (buildName: string) => { try { @@ -49,7 +57,10 @@ export function useBuilds(projectName: string, branch?: string) { return build || []; } catch (e) { - setError(e); + const errorType = e.notFound + ? ErrorType.NOT_FOUND + : ErrorType.CONNECTION_ERROR; + setError({ message: e.message, errorType }); throw e; } }, [api, errorApi, projectName, branch]); From 2300f303a5ff435937c2b10259adc15168069312 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Thu, 20 May 2021 13:58:58 +0200 Subject: [PATCH 04/42] signinpage: Show page in case of auto pop-up Signed-off-by: Himanshu Mishra --- packages/core/src/layout/SignInPage/SignInPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/layout/SignInPage/SignInPage.tsx b/packages/core/src/layout/SignInPage/SignInPage.tsx index 6a8dcf454e..781700fa42 100644 --- a/packages/core/src/layout/SignInPage/SignInPage.tsx +++ b/packages/core/src/layout/SignInPage/SignInPage.tsx @@ -110,6 +110,7 @@ export const SingleSignInPage = ({ if (!identity && autoShowPopup) { // Unless auto is set to true, this step should not happen. // When user intentionally clicks the Sign In button, autoShowPopup is set to true + setShowLoginPage(true); identity = await authApi.getBackstageIdentity({ instantPopup: true, }); From 4860f242d3b9b348fd15cd7ac375357e19f8ab87 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Thu, 20 May 2021 14:02:17 +0200 Subject: [PATCH 05/42] signinpage: always execute login logic on component load Signed-off-by: Himanshu Mishra --- packages/core/src/layout/SignInPage/SignInPage.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/core/src/layout/SignInPage/SignInPage.tsx b/packages/core/src/layout/SignInPage/SignInPage.tsx index 781700fa42..cfe543f9ab 100644 --- a/packages/core/src/layout/SignInPage/SignInPage.tsx +++ b/packages/core/src/layout/SignInPage/SignInPage.tsx @@ -89,7 +89,7 @@ export const SingleSignInPage = ({ const [autoShowPopup, setAutoShowPopup] = useState(auto ?? false); // Defaults to true so that an initial check for existing user session is made - const [retry, setRetry] = useState<{} | boolean | undefined>(true); + const [retry, setRetry] = useState<{} | boolean | undefined>(undefined); const [error, setError] = useState(); // The SignIn component takes some time to decide whether the user is logged-in or not. @@ -139,9 +139,7 @@ export const SingleSignInPage = ({ } }; - if (retry) { - login(); - } + login(); }, [onResult, authApi, retry, autoShowPopup]); return showLoginPage ? ( From cc592248b7e4bd1e5c41dd798640b8abb4ca4e9b Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Thu, 20 May 2021 14:04:43 +0200 Subject: [PATCH 06/42] chore: Add changeset for minor followup on signinpage Signed-off-by: Himanshu Mishra --- .changeset/lazy-gorillas-remain.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lazy-gorillas-remain.md diff --git a/.changeset/lazy-gorillas-remain.md b/.changeset/lazy-gorillas-remain.md new file mode 100644 index 0000000000..f0fdb9bedf --- /dev/null +++ b/.changeset/lazy-gorillas-remain.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': patch +--- + +SignInPage: Show login page while pop-up is being displayed when `auto` prop is set. From 3ef3485b01d0b97c9687b86d7b356ce6bc2957e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 May 2021 12:30:26 +0000 Subject: [PATCH 07/42] chore(deps): bump @gitbeaker/node from 28.2.0 to 29.2.0 Bumps [@gitbeaker/node](https://github.com/jdalrymple/gitbeaker) from 28.2.0 to 29.2.0. - [Release notes](https://github.com/jdalrymple/gitbeaker/releases) - [Changelog](https://github.com/jdalrymple/gitbeaker/blob/master/CHANGELOG.md) - [Commits](https://github.com/jdalrymple/gitbeaker/compare/28.2.0...29.2.0) Signed-off-by: dependabot[bot] --- packages/backend/package.json | 2 +- plugins/scaffolder-backend/package.json | 2 +- yarn.lock | 37 +++++++++++++++++++------ 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 4044a6632f..d48d3d7523 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -46,7 +46,7 @@ "@backstage/plugin-search-backend-node": "^0.1.3", "@backstage/plugin-techdocs-backend": "^0.8.1", "@backstage/plugin-todo-backend": "^0.1.5", - "@gitbeaker/node": "^28.0.2", + "@gitbeaker/node": "^29.2.0", "@octokit/rest": "^18.5.3", "azure-devops-node-api": "^10.1.1", "dockerode": "^3.2.1", diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 031319bb14..80d7b58276 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -36,7 +36,7 @@ "@backstage/errors": "^0.1.1", "@backstage/integration": "^0.5.3", "@gitbeaker/core": "^28.0.2", - "@gitbeaker/node": "^28.0.2", + "@gitbeaker/node": "^29.2.0", "@octokit/rest": "^18.5.3", "@types/express": "^4.17.6", "@types/git-url-parse": "^9.0.0", diff --git a/yarn.lock b/yarn.lock index befc88cefa..d21f684b52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2093,7 +2093,7 @@ dependencies: yaml-ast-parser "0.0.43" -"@gitbeaker/core@^28.0.2", "@gitbeaker/core@^28.2.0": +"@gitbeaker/core@^28.0.2": version "28.2.0" resolved "https://registry.npmjs.org/@gitbeaker/core/-/core-28.2.0.tgz#87e6f789faf55d726f75d05e470020e4bbb8e08c" integrity sha512-GfXknRVtdDPqt0WfxILbR4AOb/yPyBC9jDtPSh1+B8JB5HP+ursA70RJnGKFfP/aOHyikJE1MH/sdA7QBbRAKg== @@ -2103,14 +2103,24 @@ li "^1.3.0" xcase "^2.0.1" -"@gitbeaker/node@^28.0.2": - version "28.2.0" - resolved "https://registry.npmjs.org/@gitbeaker/node/-/node-28.2.0.tgz#7c18630af5d0bcdf6e8d986f7c6265c3068bc9b3" - integrity sha512-rtaXEEwL2aX33FDD427gj+KHinJJoVvaii44R9QyleqvoHLqUx3UUGUkFpbbLpL/lVg4t4pjze7dkRjvig1EMQ== +"@gitbeaker/core@^29.2.0": + version "29.2.0" + resolved "https://registry.npmjs.org/@gitbeaker/core/-/core-29.2.0.tgz#d1ad60e47e03fe9dcc05ce73099cbd506865ec77" + integrity sha512-VvJMzAquoOAjHX/MGfFuXJkWtxOlDeY7Q18gHYQRL6ON9a1+L8aYRIzoCAWCemAKuMdP1lAU/29Aw7ehfwJAsg== dependencies: - "@gitbeaker/core" "^28.2.0" - "@gitbeaker/requester-utils" "^28.2.0" - got "^11.7.0" + "@gitbeaker/requester-utils" "^29.2.0" + form-data "^4.0.0" + li "^1.3.0" + xcase "^2.0.1" + +"@gitbeaker/node@^29.2.0": + version "29.2.0" + resolved "https://registry.npmjs.org/@gitbeaker/node/-/node-29.2.0.tgz#62e32df1f404b87c70bb8b8bda77f8ac5ea4cfcb" + integrity sha512-WnFJs5aCUfBZEOUa2tbzq4Swh8KGyy7IlNn3KVsRpSi+IIkzGNIjJW585GOqT1nUxYN8mNZil5Q3TjQyhaRisA== + dependencies: + "@gitbeaker/core" "^29.2.0" + "@gitbeaker/requester-utils" "^29.2.0" + got "^11.8.2" xcase "^2.0.1" "@gitbeaker/requester-utils@^28.2.0": @@ -2122,6 +2132,15 @@ query-string "^7.0.0" xcase "^2.0.1" +"@gitbeaker/requester-utils@^29.2.0": + version "29.2.0" + resolved "https://registry.npmjs.org/@gitbeaker/requester-utils/-/requester-utils-29.2.0.tgz#8b69da14dcf4c719e4a476167fcc47173ba44460" + integrity sha512-B/xrNmcgCVFUPAA6EjGZ39o//VAVUCaI15tDRl3LCx0hLxce2JOAZNaKKlTqe+4F6xNYIkcuRl28IQS9BPcaAg== + dependencies: + form-data "^4.0.0" + query-string "^7.0.0" + xcase "^2.0.1" + "@google-cloud/common@^0.32.0": version "0.32.1" resolved "https://registry.npmjs.org/@google-cloud/common/-/common-0.32.1.tgz#6a32c340172cea3db6674d0e0e34e78740a0073f" @@ -14357,7 +14376,7 @@ google-p12-pem@^3.0.3: dependencies: node-forge "^0.10.0" -got@^11.5.2, got@^11.7.0, got@^11.8.0: +got@^11.5.2, got@^11.8.0, got@^11.8.2: version "11.8.2" resolved "https://registry.npmjs.org/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599" integrity sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ== From 4d38bab75f57f0fb80ffdc5e8b8ef65d421cab11 Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Thu, 20 May 2021 17:44:24 -0400 Subject: [PATCH 08/42] fix(github-deployments): handle deployments without commits Signed-off-by: Phil Kuang --- .changeset/two-cameras-repeat.md | 5 +++++ plugins/github-deployments/src/api/index.ts | 2 +- .../src/components/GithubDeploymentsTable/columns.tsx | 11 ++++++----- 3 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 .changeset/two-cameras-repeat.md diff --git a/.changeset/two-cameras-repeat.md b/.changeset/two-cameras-repeat.md new file mode 100644 index 0000000000..467f33adc4 --- /dev/null +++ b/.changeset/two-cameras-repeat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-github-deployments': patch +--- + +Handle deployments without a commit diff --git a/plugins/github-deployments/src/api/index.ts b/plugins/github-deployments/src/api/index.ts index d7e7e2fca6..a5d824914e 100644 --- a/plugins/github-deployments/src/api/index.ts +++ b/plugins/github-deployments/src/api/index.ts @@ -60,7 +60,7 @@ export type GithubDeployment = { commit: { abbreviatedOid: string; commitUrl: string; - }; + } | null; creator: { login: string; }; diff --git a/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx b/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx index f050af836c..a4aad47657 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx @@ -66,11 +66,12 @@ export function createStatusColumn(): TableColumn { export function createCommitColumn(): TableColumn { return { title: 'Commit', - render: (row: GithubDeployment): JSX.Element => ( - - {row.commit.abbreviatedOid} - - ), + render: (row: GithubDeployment) => + row.commit && ( + + {row.commit.abbreviatedOid} + + ), }; } From 2cf98d279aeed11065bbc1ce79dccf7d7d4b4f09 Mon Sep 17 00:00:00 2001 From: Jacob Bolda Date: Fri, 21 May 2021 17:34:43 -0500 Subject: [PATCH 09/42] config-loader resolve config from cwd Signed-off-by: Jacob Bolda --- .changeset/four-jeans-tap.md | 5 +++++ packages/config-loader/src/lib/schema/collect.ts | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .changeset/four-jeans-tap.md diff --git a/.changeset/four-jeans-tap.md b/.changeset/four-jeans-tap.md new file mode 100644 index 0000000000..e4508cfa18 --- /dev/null +++ b/.changeset/four-jeans-tap.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +Resolve the path to app-config.yaml from the current working directory. This will allow use of `yarn link` or running the CLI in other directories and improve the experience for local backstage development. diff --git a/packages/config-loader/src/lib/schema/collect.ts b/packages/config-loader/src/lib/schema/collect.ts index 611413931e..e12a4c9309 100644 --- a/packages/config-loader/src/lib/schema/collect.ts +++ b/packages/config-loader/src/lib/schema/collect.ts @@ -124,7 +124,9 @@ export async function collectConfigSchemas( ); } - await Promise.all(packageNames.map(name => processItem({ name }))); + await Promise.all( + packageNames.map(name => processItem({ name, parentPath: currentDir })), + ); const tsSchemas = compileTsSchemas(tsSchemaPaths); From 6e829c9a4e2a56d87d3a15817323e13a914b6055 Mon Sep 17 00:00:00 2001 From: Alexander Kaserbacher Date: Sat, 22 May 2021 10:23:36 +0200 Subject: [PATCH 10/42] Add tests for WarningPanel Signed-off-by: Alexander Kaserbacher --- .../src/components/Cards/Cards.test.tsx | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 plugins/jenkins/src/components/Cards/Cards.test.tsx diff --git a/plugins/jenkins/src/components/Cards/Cards.test.tsx b/plugins/jenkins/src/components/Cards/Cards.test.tsx new file mode 100644 index 0000000000..67996f4dde --- /dev/null +++ b/plugins/jenkins/src/components/Cards/Cards.test.tsx @@ -0,0 +1,94 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { LatestRunCard } from './Cards'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { JenkinsApi, jenkinsApiRef } from '../../api'; + +describe('', () => { + const entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + description: 'This is the description', + annotations: { JENKINS_ANNOTATION: 'jenkins' }, + }, + }; + + const jenkinsApi: Partial = { + getLastBuild: () => Promise.resolve({ timestamp: 0, result: 'success' }), + }; + + it('should show success status of latest build', async () => { + const apis = ApiRegistry.from([[jenkinsApiRef, jenkinsApi]]); + + const { getByText } = await renderInTestApp( + + + + + , + ); + + expect(getByText('Completed')).toBeInTheDocument(); + }); + + it('should show the appropriate error in case of a connection error', async () => { + const jenkinsApiWithError: Partial = { + getLastBuild: () => Promise.reject(new Error('Unauthorized')), + }; + + const apis = ApiRegistry.from([[jenkinsApiRef, jenkinsApiWithError]]); + + const { getByText } = await renderInTestApp( + + + + + , + ); + + expect(getByText("Error: Can't connect to Jenkins")).toBeInTheDocument(); + expect(getByText('Unauthorized')).toBeInTheDocument(); + }); + + it('should show the appropriate error in case Jenkins project is not found', async () => { + const jenkinsApiWithError: Partial = { + getLastBuild: () => + Promise.reject({ + notFound: true, + message: 'jenkins-project not found', + }), + }; + + const apis = ApiRegistry.from([[jenkinsApiRef, jenkinsApiWithError]]); + + const { getByText } = await renderInTestApp( + + + + + , + ); + + expect(getByText("Error: Can't find Jenkins project")).toBeInTheDocument(); + expect(getByText('jenkins-project not found')).toBeInTheDocument(); + }); +}); From 5baf2ff0f9b7bb569819b8e581d077190c4ed279 Mon Sep 17 00:00:00 2001 From: Alexander Kaserbacher Date: Sat, 22 May 2021 10:41:31 +0200 Subject: [PATCH 11/42] Add changeset Signed-off-by: Alexander Kaserbacher --- .changeset/smooth-wolves-sort.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/smooth-wolves-sort.md diff --git a/.changeset/smooth-wolves-sort.md b/.changeset/smooth-wolves-sort.md new file mode 100644 index 0000000000..f196927073 --- /dev/null +++ b/.changeset/smooth-wolves-sort.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-jenkins': patch +--- + +Show error in Jenkins card for errors exposed by the Jenkins API From 6f17a5d3a0e430f1b26966a5dc4da3ff2de97b9a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 May 2021 13:08:06 +0000 Subject: [PATCH 12/42] chore(deps): bump webpack-node-externals from 2.5.2 to 3.0.0 Bumps [webpack-node-externals](https://github.com/liady/webpack-node-externals) from 2.5.2 to 3.0.0. - [Release notes](https://github.com/liady/webpack-node-externals/releases) - [Changelog](https://github.com/liady/webpack-node-externals/blob/master/CHANGELOG.md) - [Commits](https://github.com/liady/webpack-node-externals/compare/v2.5.2...v3.0.0) Signed-off-by: dependabot[bot] --- packages/cli/package.json | 2 +- yarn.lock | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 12d8373ba5..e83ec108a5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -111,7 +111,7 @@ "url-loader": "^4.1.0", "webpack": "^4.41.6", "webpack-dev-server": "3.11.0", - "webpack-node-externals": "^2.5.0", + "webpack-node-externals": "^3.0.0", "yaml": "^1.10.0", "yaml-jest": "^1.0.5", "yml-loader": "^2.1.0", diff --git a/yarn.lock b/yarn.lock index 64aeb6eedc..92eed77e30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18740,7 +18740,6 @@ minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a" integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ== dependencies: - encoding "^0.1.12" minipass "^3.1.0" minipass-sized "^1.0.3" minizlib "^2.0.0" @@ -26572,10 +26571,10 @@ webpack-log@^2.0.0: ansi-colors "^3.0.0" uuid "^3.3.2" -webpack-node-externals@^2.5.0: - version "2.5.2" - resolved "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-2.5.2.tgz#178e017a24fec6015bc9e672c77958a6afac861d" - integrity sha512-aHdl/y2N7PW2Sx7K+r3AxpJO+aDMcYzMQd60Qxefq3+EwhewSbTBqNumOsCE1JsCUNoyfGj5465N0sSf6hc/5w== +webpack-node-externals@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz#1a3407c158d547a9feb4229a9e3385b7b60c9917" + integrity sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ== webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: version "1.4.3" From 6391d60d0a03bde21550933011f42d4e5a79d149 Mon Sep 17 00:00:00 2001 From: AlleyWei Date: Mon, 24 May 2021 14:39:00 -0400 Subject: [PATCH 13/42] Create/Manage/Explore Image Updates Create/Manage/Explore Image Updates Signed-off-by: Alexandra Wei --- microsite/blog/2021-05-20-adopting-backstage.md | 6 +++--- .../blog/assets/21-05-20/1000-engineers_v.2.png | Bin 0 -> 46121 bytes .../assets/21-05-20/1000-plus-engineers_v.2.png | Bin 0 -> 55936 bytes .../blog/assets/21-05-20/200-engineers_v.2.png | Bin 0 -> 35250 bytes 4 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 microsite/blog/assets/21-05-20/1000-engineers_v.2.png create mode 100644 microsite/blog/assets/21-05-20/1000-plus-engineers_v.2.png create mode 100644 microsite/blog/assets/21-05-20/200-engineers_v.2.png diff --git a/microsite/blog/2021-05-20-adopting-backstage.md b/microsite/blog/2021-05-20-adopting-backstage.md index 1aad5ce0bd..191422004b 100644 --- a/microsite/blog/2021-05-20-adopting-backstage.md +++ b/microsite/blog/2021-05-20-adopting-backstage.md @@ -72,7 +72,7 @@ Within today’s complex development environments, there are barriers both big a After talking with companies who have already adopted Backstage, we’ve seen a few common strategies for getting started. The different strategies are based on the size of your engineering org (which often also corresponds with how fast you’re growing). -![200 engineers](assets/21-05-20/200-engineers.png) +![200 engineers](assets/21-05-20/200-engineers_v.2.png)
### My org has ~200 engineers (and is growing fast) @@ -93,7 +93,7 @@ You’re big enough to start feeling the pain, and only getting bigger. Onboardi - At this size, you might not have a dedicated platform/infra team in place yet, but Backstage can provide the framework for centralizing and sharing knowledge — from managing compliance requirements to finding the right API documentation. - This allows both new and old engineers to collaborate more efficiently, easily discover best practices, and cuts down on duplicated work (e.g., a new team doesn’t end up rebuilding a database that already exists but nobody can find). -![1,000 engineers](assets/21-05-20/1000-engineers.png) +![1,000 engineers](assets/21-05-20/1000-engineers_v.2.png) ### My org has ~1,000 engineers @@ -112,7 +112,7 @@ You’re officially big. Lots and lots of teams are managing lots and lots of so - The service catalog also helps your developers visualize your existing software ecosystem, beyond the software they own. And with Backstage Software Templates, every new software component is already added to the catalog. - Even at this scale, a small platform team should be all it takes to build and maintain your own version of Backstage. At Spotify, we have a 4-person team responsible for our internal version of Backstage, which is used by all of Spotify’s 1,600+ developers. -![More than 1,000 engineers](assets/21-05-20/1000-plus-engineers.png) +![More than 1,000 engineers](assets/21-05-20/1000-plus-engineers_v.2.png) ### My org has 1,000+ engineers diff --git a/microsite/blog/assets/21-05-20/1000-engineers_v.2.png b/microsite/blog/assets/21-05-20/1000-engineers_v.2.png new file mode 100644 index 0000000000000000000000000000000000000000..de8ca430314cb01ac022f140d31bb9fb616c10d7 GIT binary patch literal 46121 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 VRQ~K)@3&}r4Ry_Q%E*op{{zCR0C)fZ literal 0 HcmV?d00001 diff --git a/microsite/blog/assets/21-05-20/1000-plus-engineers_v.2.png b/microsite/blog/assets/21-05-20/1000-plus-engineers_v.2.png new file mode 100644 index 0000000000000000000000000000000000000000..a2054a589c4ba650e7d377d4502312573338a7d1 GIT binary patch literal 55936 zcmeFY^;?u*^e#L|t0;|%w3J9mBQYQ$jeydiATq=d(%mX8igXMhB_%L)DGDeO14s>} zfYKn{aP~9!Ip@6RA9#QHdR<=3FwEY2?N#@^)_TIVHI>QEUpo(j!N^qZDd@mpL?JNP zIV=eg_|1XnO(F1u)ajm~3k*g_0sY78rB1N*GXu+ctZlqAKU{gkUGBT;Qrg@v!^g$>=Vs-f!*%JBi zN4XgK6Fp4}Ma7Sn;x|b>FSy?LWuSZc2K_7N*W=qhqmJVjRi5^CO$x2~@6J^|loae* zK5p!-w9Z>PUR~Q%A8YdQI0p_1gQ3#ui+OOb;9jIa2+zS{x$HlX?xC9@Q<9-DX|DUD6Mf}fQLDuj;#(?vQ|1pODe~iH( zwv7<%EUA$zxrdGKf6Se`&&GHBExx}!zTXnx?>N@$F$`uUfZSc3prr(V$W^84CJ>yt z>3)gqIMK$`bhE+R`_i=)Zx6CH-}<8b!@T(Z=RwUUy*^s7nt>tQkz!>jdGL-Wy+V9BcO}+Q;|}9g{oIE2^#FUxzpoZ-hUhiFo-i`` zQGYhW%#Zfexzb&E~Dt{!(e zP2%64ugOUHlhGpHEw%5qFI!R&Ca>lF8ypILb|}?cNzcC0U%z-Pcxp+4R!!>;2v;)K zQ`|O47vANnRcIsTFc_+drlz?|TicF*7x7>bnrB~>M+do+T@ENO%AbozrZQ@Jn3{a6 z4za67oKJTjgjf5Ss4k?$_dnxhfuX+QasYg+$Ps^{W^urfzToY&Nft?HjHF+40y#|E z6PG_w$OrF3UaVZdR?pW%WNSr_Fh_^N0^ON$X#=a1D_QF_{I`eSecP4k=%aO+mPc9b zGgUrI1vP|j2|K#4=sb+;Jnlv?4;RPacdDi0s)C?9h&OK?zkExR)AZ;?gU{imj07Dc zLCkPVV+jW>jEnc|8=^|pL951flF~JP%8#GR`t5XWB@_|$cfB#sXD5JBKgVS=uEy-@ zpdCdu5*Qcd+0`Qy9r0HpArYw0M0kZQAw%;U$-_SCou$Gs-NH*fQxd|iejbi%6seB& zA`?y1oz)2Z6*WIK1dwOVW7{=T1T%bUAgFu+79V}KQxpO}H$Gcp^H&zbpzARE+pVt* zOKO$yKFSpRI^mUTjQ)Ax+BblKJs`Y73)6mfwmUhHJ9e^;26@Dl=`m;Sv>)8W#0SyAL> zbWPF`g?}F0(zvx6kO2R7xiJ*DpRQ)oE(FVS_+uUoFu{p>cY z`*XIWcrYqfocS2Z(-%lU<`hne+fXB)?1u@YCIylZ;LU+~eMGjr;BFpCotdhx-un&az+ap6=nLEyjpZ9WR^MwbtxDRV$^z~hmeIkI#LuK68e9-T$33B%b&VKllSjNY%-tqV1*7yzsM^U$WrnFGg@ApB% zo3q1;XTDfIFh>ZJ49A(WXHXmxB~Fj{F1?%jPE>3cKXO2Y?)2B{WOGQq4JjVn`b-EP z1peDpa5z^E+-u4iLNLh^9dwdgO5+F=T;BdLaEwKES%w=HiZ6Me**=t9NHzfwU11uDq?b1+_BUcseV zs)_{6mZ35NsDhpx@(p+@IQ>oN{o%j&{Y2R$kTiQ|LO(|baZv(gaEanng^p8o`}-B- zCi7)r%Zl_bvkM(b$xfH+0_MHG1)V+s66R&#B*ZHGmuu=lvaEL;%pYjt(KnVn&uJvT znP8d_R*yk8DsSQT5%tt+_L?{75f0c(8JsO;^x0p zlXMMdAY1}SW~?DPl$Bg^acdv=hCtZaI&dsJJ{)L5{VeWNM~RYKy#tEVRmNQp;r+r( zjMa9Ydzn>1TtYy`B;T|30?&LX9gQAYx2g(xP$XSL>rYiJ#lL7Ha$-er&79R?}hT%42Dy(JF(gVX3o>Z_wf z|M^uyNX+g3CkgSM*_KKk83BJDIC2Ra6xjk*-jNX;;@-2Iy+@G|L=RA&3;ctNbnr-! zR5v}qd8Q`Dd_qOZg`e{CW)VU*pDY8ra_3Bi_s~H)s(pyPznB=?`lZ*Z-m@{=dGzhf zKrc5F^ZvsfG9YVq9^ri)Ir9_3|DA2nL63Z&LV#suF-X|5N08#YAI`JD#beGwq;Q-l zIl4zhE%6-{{{adpASw=L5-AGwBt}}mMB2|#1r?Cy4fOsRCr%pD0zG2$1{VY(U6fp1 z1}Fp(h5KH6Ah|3)TYdh#2*>)FWbMJ%b} zx3SpF&xBSJh9g>?Mr=O>F!pJMSd z>w2!s$^Acx2+jcAG$sM$#Z6m0%;+0|lK@q2ze%#T9-z>byFg^KdyoyHqm3@iXJa$( z=06%gWz5o6wRxKwk*-Kjf{g{&b`%VW&(&x|g1(u%bx3%1Nw;d9BurHke4YtD$B7$U zByv0NJ*jX?zHJcZSy|3Tp&l_mc7HST^motPbF*x)9}*bBsU-;VAy$MaUEH_i-|G?2 z4Oc($QKT=%;B)705*-|f(LTDRt_{3)7UU0rytdLRkOQ9=N+Wq}#zHP9KjaERkn$Q- z-rm++i;S$}$VHo|kpybu5Zn3OYIYN0q|Eqpo=SPOf+j_JfOh`Um`Y0%Ll|&j@@>%_ zKUI`#Haq0R3k3sn9o5cSuDd%KMeGFdQ7F3yw!Yjn)KL|yQNdwB>?=`n*NmJgZ(G{) zAah_yA(w`;{OBPvdYCDro0VA$I6)QG^(JqezdKZuQ#aep_kAY6#Qb^7caW+oA!NF%Q=R zG|YzrxZiN8x{#K?=~?V(^@U**Ahjk@Ghm6ZiDe+3#L~EU{=Qv7Ms1CcteZR0%%nx( zxyXJHPj_8sT3ubaPbry`;Owel!c7GjE~4^{aNWYK&R3*=i zW5|&x`GtZ6;1qjUBYH#(_*4^ML%_d=+;(^Ps(*9BbmbsNUQQI%&N8L4o{Qgax5$;e zV*L*e{}&`mVL9inqfsm%u{-RrRj7vkU04SY5 zcS{0DXMaiRiMOQl!s}vow9j1WB6_t{j0|u(GzNxFfwuDu0@C`IkZqk88gR!pv+EUJ zYPVb4twDN=moz*o6xAk9ZkatEZEHR;ZArbfmU*0gVd}$y%fjf&o&HmUUH*mqkk}e~ zi-_AX+0kKtA`&pcB0RclTg{&1yC8xFOj6@xR3XJN=aA{$M2=D(wA38`J;+GBzPH*f zHjx4ns+G6HVa&Ox_s72T2}=XT!(F3GwZn}bDhI#BnoX1JA_6T>h|IaVTqCawj9PZQSZD! z_Rd!S-ODQ9TMn!IVw&zf)<&E49DRMkv313n_tbOWT)lusH{O{Vc}qQJMAYF|;?Qgm zCIV!P-+7FI4A<*Z;jxv|A@K*-UM9z3`FcROpj2!fPd`B)$pMCUn68uVQ55vO!zhz4|O$fWwL2Z;T)(& z4jM6MmW9D`D}QGk4*a2aREZrgmmymz5cTyMux5yh@n0%jsCAi4AXH#j42tjf0`^AF zxw&CC_n;);XXS8ygTBdVp^GeE-wm(9#y11M_%q*5%rX^;teM_s&w{7DaQQ^{F!sO; zGPfsL9g_;**{h}LOYmrh>4ptwarWGXp5Lij_^Z!wtgf%sTMX=vla~rc#SVZ`ASI6r zJ=ZyukA7)Sk3I z`9}gKQ+j`X=5w#iou##@S~w4anqu#0l_p>a-I4lU0I|*~E4o-GUgNh+W%GHlcV*Y5 zf_MF7q5l4u08@Iv!-IvNGhqzMH!*42>g$JyUsyX!`v5V_a6pFDxkuUciTwOO_On1{ zrc$6s9Ak8AI#MIQi)K>Hj=O*A-wIi3NfQZT{hF*_cdP05V$jz6n)EnAxffj-$;X9s zr-zAVUV1xIb7`B^!Vp#DKR$@F>4>e)V{$z5$NCPGj9T{keruiS_ z@V@a5qtLlkzu3EbC_$lPvfIIQbsOikssnjeB`L?F>r7+)mb)z<{yxNc2eq!foA=l6 z)@}6Hr+4?Kst&A$V84g0qVZrT6I&!|Ey3Bic0_0Sk0M*j~8vfN>kFfm45dwQ@E)9iymj?YbD2& z&3>O{MWsZ_t&C^`mpdjiD!FF3um)VEh?XHwPB9o2l(qw319%);4B|&Tw7IJN%GF4n zG(oDdPwD$L`qnJkl|2XHE1s;>P}Rtee!^{`0xBluNx7#=2AKx!OfPYrEK}8Q%1w9-xV#w8DB4F5!sqAm%FR|J8lIatTNYEzGVqhc@X<~=M zS(;~}PcctAOA1#`)qTd($q>toNTx}@-hxajjU-te8Nf0byN)6M zm`VFl*p8sOBf!D^>1RJ4eY&SHdD@L9%{qmbCZ!U1BfZTIUJnmXVBX!m zezIYhV>T}|b5TY~m&q68KM=iFSmd`z23}+yk6ul<7`P76T==I!qbsp*o59-rNyB2T)VIAdoFD-p~i3x1^@jiScr=-k3_>p|1~ZEhX6U z&phlogDf_s{*w4lkp%pItIOe-#7BL}EVB59nxW?KhuZhQh`M{(Dj{-~X zOjRHM%+gGzvyJFi;MQSNy7OZF<%%xgqL)RzU{Q$>#5!M4O6Ka$S)wJZoJ|AOHAi>s z6)6G!2oas|RLg3AK%P?-8lzrS5;K3R6-7ryB*?hm+uLYE3bVw8{c{UHM738TGY#PB z+;Q5@W^mmy*6QxL)Y>tG?GKWgC9=@^*?xh$j5)Q*ItZ z&oM4pmUykb-umq5+rZs>8Md5>oCciiJVf+pR!HSAn;AZf`=84rvFAHA0`@m&TuwAT zPj4R|7P_xersu}}oJlm7DYA0e82Hpt8H)=j=U}dvAUdF40U*0&iHMvAV`-Q7XVS2Y z*H=4ar%WK+$<3tJAhFxx$KEYjH?pMWB0bi+_9o<#^plp;CK*?5=!CRb<%zZSj$8Dw z8K~G2sFF>lkbh_1swIuKv`Mtv*gZKx`vu~L@0H%6r@wM@8lOXX#^=Ngmp{;%LCyZ) z5Hk=W4O9VTnihfGBbR~fvEfx4!~15@_9{F5_p!GQ4^sNhy*Vv*H8q9(Hw)minjMvc zu*~vPD0&7pkVanP37A2#7Xf(7NW(&9%)Lm51{UC6;a6{#ceJ$F{D@55PLZ(82>eVh zuABdV&Yz|~y!je`t^CP9i!^!%s*WY1+A|x-&yfAf$GUtql+$nn$mj><&GHdSBpNpT zAivSSi|X%tFV5Hl3FAfi#Iw^#L#5vf7`szhLRgeqtPMUa@DJd=iTUvf$=LR8&+W~} zA6*D98~bnk+{(2*&Xr?2^>VNS+o8O4CLsZ$GEn=pB313A@6Ni(}acd{FG7 zCzP}F__~}tI&2S^vwW;jiW(Es~`nvWRnRC?u&{81g-hFqZUG|piBl8774FtNZ63lXoX&+ z^L@NkRl8!Dd5`{Kz^dYDbB$={y-it9Gl-$oWqn{Z;->Il?mZn*8__vK6UL3}sGU1= zN@|236%ZhVeFQvIM5~#mhs~oHxMsEQ(*96?@f)ox+b+phc(XQM3Rt3@qb=EfRK_Z=5t`D87kwnw8ExGPp~YB;NAW5x{u8 z59Qh>oV=#5L0wZC^v?J%o_iz@MR$QqSa=Bqi<&)6&3>No0H~hP+S)rO^g)fyWX=}8 zvHqBE6mvq|MJ6c#2}OH3T9)$~u^Hza1F)HJoD3|Ke{YvWQD#ePxM_ZB_h|r&8cy-r z>V5LjQ{kZP$W*BSQ;!z{B;-*P;QNs}syyAscGo?6#ws=~vcrTNN3FuH>ihd^G^(Y3 zqhlFwa4=6TxjTu;ZW`a|v+FN%sPq3Js0^$OnRFa3eK{}ONTU;WLx*C=64o45fv!uf zhRMfJ6FcQvQ3qbQSR-Hlg`Ul#s0$Q-VsVv2FIW;66nun%S|X6YChePzmH>WTvzhYU ze%%=#J68t?v}u(yf{^C!B;~UN?Zfa#tUYBm)z+EVjOM)#rf)K$*<3UC2yw)PcnZB- zaa4@@oj@4)6yl;E{Xw&k!M2n`d3-k3LqVCIb1DP*UV&T~os;sk`J|>Q_`HHzfuAV= zxHgN`Rut|ghm-7v-=E$(Q>?%?)Nney3el<3-!PORq}LCZv2b0xPbqIlyFZA_B0%#_ z{hI-fpo3OQ;Zk4*dVB0t+Rv(w&``JF!St;(JM+yv?uJSC_?l1)HFj)A$}XQx=Z+Nx z%6EFXZRV2nKQ2fm`yKRr{iRMtAi8vm5Y~+gh7{~ zS4K`Nozfb)Ro1su0X!)v?4_}0Q^dILe(b2E?cWJbd;5dWB3kosFTy_E!iODuH|_0a z-@bLOYsmapgH#p$eB{|UPfm8o|s?dLGsH@vD};G+t0j4ui>KG6C1 zGy~OE@&a+z_;3NPFB%o!5P@HYoS>%dg{zY%xgw)v^~cAD3JUaG)vb}_T#RBe8K85T zt|hD~7*MVW2x2pD#}psmUhCr4dtI~B;69}y_SUHN6sT;X|9{FR%HbXU?uRkUzUdF2 zGo0CsUEA85y{2a$<%l2TyLlY9gMOA}-u1wz5V-lc$$|m%2)+67J$z%n3QIRqIC`b4 zEVXjju~b54$G)%nF3tV?K`alIkuos z%=SXuondP^Ibn7Xv`~r$U0 zioR}Lj5P5mMi#G6$~Ss8C$*3evN0E%&&^CaKis*Vj-lG5eb& zd(>W2S!o;C-nKR>#(Vm8H6yWc7BnGOy@HuxMk;`yz?E<6f=WO-3F!toS8EyxX?y?1 zw=soOhk7fmH}6QYwEf7bf3?}@lRW{=1QipPV}qHL*!N*Nv3sVPiOkQj`1j8e_2-+)C9cjnPMev|)vRDb8U zFRL}Fh6#ZjRH&j$r=eygiGY(Jx*Dw#t;RA=j*_jJWPYL&`6vnSFt!j@kyIVU!nenw zMR*#oUgcBV`|uO{mRBOjI$(!}U@-FUjo|1wRRQ|*<~ zW?h41*0(y_Yv-%!cSd_g&FK-d+dp2&m0#K|IdI*XvS645Q8CYchR!m=>-*iTV`QYh zXnFTfKrZY^qvb;3P|Yw`NOmKu$^U*D74;k;UQT4@uhCRIs1f{sH&_etQG-wfbd9P= zCyRH~O)&bg`ts5A>Tmt0(eWf2)9NALB(=C4eoR3`cWzRjTzF%%5{Uui>3H_j3o|E8 z%_iTm#zcky`xLJYQYx$&A6<}Y8WZ*7?y=!guTKGB1V#}~6ZHM$&j*={5h(5B>-wIaVOI?(NFm1;Ew}4fbJ9SUL57)jq zm-UYQ#|OVKxodKmqyte5*w+!vA~jm0%1#9bh(A7AG;8*)BPE8R&>&6kbLWofsygz0 z78xRzbJks8INb_!y%$-tvzQs6x^tbv{!8Yb58BR%T|WL(!0|(OKWo2VWyapf?q{ID z@{~6NDeK9_uvKw&0aR1oofcnMGTY2>(di8$jybURqMrwIC#j~VIIe$?S1)pMda~cM z_1V}#t1Kx%)t$j=*RExoe&K<2W?YGJm$VPv*q81*Yva+Y`kG>jm zUx9vpOY7u;_cwlqgD!MwY3$KHi}J~*!loBk+Dm~d`2haGxdHiG@5#TMxNxvh@@oV2 z=yvaV_-6QOy{nS2Odh-mSNNj0yDaB$%90MR)V0(n!eRUjg4KsXEk?=1$8xzi~vqC%5e!6*#A-#bC8vP0y>Cd+_|4SU6B;= z5&`=bMyi-;TF z)X%(U-f+ZUNzQ|X=Lan2EF}6L7YIt*Nokq{NGaKKZrQaQr|PYPn-aZJk$u zoyS)pEMy>hmYTYb&Sl)|ax)~S6(f%%4^^z>9vsT0s?mtl=1*~4`JsuQ_wZ=ddzir- z%h1vRfab~chaCUU)lM*KXp|+e8dTcbcX*b*1D!z}JRbnUd;vL?PzDO`cSZ&R_xpq+ z1a25Oru_Tx>X7X1cx3rbP@As1re+Rk%u`IJff8Uo=rpEbMpY=PXNP5J=HM&e z!>fjk^PkJriSL4z^GGU~&EdHe@OvGX#%W=SHvl(9@d6gLUOKkc-jsB^>`@v`d`)os zn3T=ix;fb?os@e9!jf!i*X)BDlSfR}-}O zdsB3YtnK1w9p1l@CDRs~dtnIDHqrZvvUBH?YWF0rI1;@SBYYQM+V`K-Txd;u{NMf? zA#;w+bX6j67Tc?j*Gnu9>RD@ofwlyjpMq79ho9~mj_A$8S*9qFW0yna*xHt6Ol!&} zD$^OY`Ml`4Cdn~=kJhNTb9IRi4)`m&VCWpiA|e=^0w_#5FWzDG3prP!m|%pzjEa3K z&8Hp%>h)XilGea5+7lLS0^vRb+ii^##Q2Y%bgts7W&S7IVnZLxbidfKS4T&boHTo@ zu-k3nt?*A0IDwY@=&mO^2#kg$;z*1Q9zq{VE$8#W;DyJ865G6X(VPCD7ql*swI*v< zI_Si{i%q%OrN}J3bRCyyBMHd{38gjHXl#a8pA6kt!Y0g>*vL*@mP;(-Vw$f-j4nao zE0Tae0rQ>_qL@0!0E@X`D^PnL{`dzLo~gy%H2VH%S}HkPEM&vQm316(1MUUpQbs#i z3FHL-AY2qGZ7Q(vx<(t_StLw%V#!fzPTuVrFAKsE3x~xgL!6N-6vT`i=J~db54i;! zA5eYBFB?N&(zq{$SX>7cctr20Vvm28VPgiWX?e+>S&kfLl?`J4$M}AtYZIVhPc2E_ z*()mfF6g1Q?<(`91c6M*7xQ$7f=ctM<+73Cot_%$esE zZSj9^4)U?cIewzIRir7a-1T!Ytv;sKzP`>82{nqLTz%^k`}3|1LRrM_p8C@d+EYYO_Z1CL+Ib3su&ZSRCLZ-+6hqb*1l$rBqdc2ICi5_ycCvZ4VbZV-R zlLogl_r!;%KBST&9k3BP&=ewP{Hy~9?4NtW=%dw2Xw>O@D1z4EB^OYI1 zksLJUA!3B!<7G*@4lL=f6hz*6sgPi%P@sqCKvi`?TS(e{KYCiql{-c1Cn!T510+gj z6nefq-K41IWh7BQGQi2X`q&5^1c-opZ-S2^)l9@kej`@Ji$@lbO-@@2=OQ0|HI~0n zYPz7#fe6~W+VEG+u77;9w;KxwGkcZ5y&Xk|{|#UAW|954zCZMMUt^y&oSExf2H*=r z1!5JOCG8w+dLw3c0yrHi=*r22b?y&~w-y@Aklf=sdI;`ooeMhpk>8Xw@I^EXvYlN+#mj-Z|C=SRI0t!jR7-aQ}c@pUfY1V&S`K1 zEkKSu0LExHtAbMa9c#uLp1D14r;d&jkL3)0uY|qWcQN9*%KaL))kje zBr%Wp@YVpW57j7kc9p6hM?PW+j`?O!Sd~hZ$oDNc5gJ-$x%LP^Y*2Uy)3cB4cfi6{ z*YJMQLD@w89_Ypk=59R}8kKZ3ayQ%=>AQs8`FU4YGNFO5@QF2Hx7XY#SxvCc%2U%( z1x(d`yK#kEc=Zp@HnP)!(rcdn{`{rt3+~R&ycAN5P8hX;zMk_bI>4JGk<4c^Yk|%{ z67MiW`w|kJKM(l2vDb93(kcio^xYAA2c92Tm>Tk{%7WdvT-H8tF(N^oRz|wn&143k zEal^4?v^(Y+{8^xLZ)?aD8_fi!+`FS+G*-k5+M>A2H-ZFA*r4=0*wpiW5B%zp!9ca zT=ZpW4m;e)ohr5i{9^}S$@(7S%NwhB|3wnIz(tlOrvS?6c#% zo+rD%#vG5jR?WSC-|w}>$Aeo!3K3SPM=UDddDNa}$sv8K;PQEkJC*wZUS8F_TK+xV zC#}IF&u#_3Rx&07TJ&kpmVT_(_hk>9$SzjG^AIaCDyq14_@~|;$vTqsHNp88$~;-H z(C&J4ne@`(I*p+kcf;rbsOPw~Z;SGfN7RLLXbX2)G*4H_Sz44%iwu7?G}TrDBUD}f zf0PU+CVW2rS;fEPnFFGWFm!wHyMW^i+v009k?7I-+jM8Zx#ZtG%w(ZvffEo?oS&7G zdoSX*0efQpSgyHgEJ{Ci@@WC!eW+(Y7B&Zx1AMlVIu5(EPJ|AxY*K#GILSB6Qf)?J z(_AKZvJ={!=Ngf{E?QrlTRa{PiARXU{CtxAtG{`cEWL4^W+~PDuB}sa$AJle16aL2 z*W{CpWZt^!+u!Fj)m~~+;%zsq?)9(kjV9?BIfD=@dO zZ@cTCEQ_6ednQZ$0xY6g?{#J_fq||Hkw-_YsK^BEv~^cNi=MvjkI3`_K%4o(S1=2L z)Z|X*AvhOU4#l+(T2-Q$xtZn~!<#WA*I9kTiPWdM%)dK)=pg%5TgNn}JScXjoeeAH z>>_d5gEjP}(^GRfT}E0UXETSx#+>ELK#piiqBfVdJ$kQFlPJMz(a6fs=>r$%5GkX3 zZ1``oR_;_(Mnb(3d!LFuDr8bE0MkcpK3GjliY(06xp2W~_onEPqu<5@LZ{`t4H?7c zJIV{@riu*8A1^+{x=+y8!+<)IxuZw(1dKn9A6J5aB>_VG2b zLbfXfj?WYmI>TJJI04q$NMXTVu8?BT#Ls^v>np>69Fs zAMcWWBUtSo&lq01>gL7{m(qAPQbnHmj>7Q4r`tYu~%^50tUgh<&JQPyY*I)?upBZ^6AM0ov&FA5=BBieK89m`$clrIJ(WP z?M^#qy@;8Ve-wLwsc^qZxZ6uwJ((EK^x5u2KV<&!OV4roUZb=O?`35l(b%_~QCFgT{M>NPQ2 zq5F(f^)c31obr>>xK!r+jvbXCsL}GbILlO!R>)GYD$L|w{iW~k5@7bbUz#`ULd)s) z@{GY4-0R9#5mZR&m^wDwaV#0Jz17-EBEc#Zz8ymg3RqbulFuxW*r@nk<4{WgWh55o z=9aEohV%ZNj>nX7rJQ~h)96)^LoO&j3fwp55O}gH= zKji3qqSJRbk_2IK=c6yO155P2rbYlUyX!QGh=AwEZTF4^3ie1bM|QkyH$J*GPc0Y# zP)3DygA&*KVlvl3_>NzSPCb~7_5}b&(6>$v4qr~Xto zQ{}88=GMq!!${P$Mt|xE9yRJu@Vl(cMa`Qm^+S4Ph9}Q;spDH7yFwpe_#bCMHE~?R z7Y0iQ1N!wWxq1;b?Hy#>W42;(Y~n9d7yFcKsz=V>a$r0y3-A#}9`AVcUv{jMZn8Mx zUO**wBfhz{X^gqi@kM*6cQ+Vj7oOYsg-&4N@xVcNPUd@(|**_D7D=y zo4e54catK@e>383;lY>K%5CZ6Hvw-&JiRYTId%2cg3bhZmLT(9szW3D1tT`$D3Q~u zsgv%!7#Xv><%1@^QPqa_#8>RbH8*7_C=fjWT-waJ-1X_13N9p{3UNH1RUQbZ^ApIm zSqLv(U9D;K%uf1m768obmdRA@VloZMVF>7{0yuFvuAb+h$ql}-_4>Anm;c@4Z^|dq zxREGB^Sz}6B4X#t!v~Ic*bWb4PqvMD$wEQ3ekp?fsSCE?6ZzQviS>?v6cMHA_LH&Z zzr&XGzs#6|6+d`m5|G9V+npoatX%7(1a_{(B^# z!PSt~qHV-7+H7ZWeXeuruz|k6a?C%)U1ry0NBX+8MKq_LtzM{l-@TV>6C{Vz+oB1` z6|CWIP*dS)x1)OHl#|5{HfC5vJ#WvMdC@c3D&l!w<*LB5|J&Hj$qJ!cO1#d{VixI$6tn zK?bUr4pciw(yxp0*MBIfK8++YvwxH1I3qe#8MMP;CS;x#;Evti{H1cr7_+pmT9W)A zbQt?2a_QtCf~PXm5@WkAwEAT)-RY#Rhv-}&H`KM{sskMYJ$_kT4lHROM^dNYmmOE< zy#S#vWE=E72JzB5JiSFWd7#P>NF$bH$wymkC{gM>$D$%M=aRjFDNFujLZDvGjsZy!JrYA zO)N)YqWETRH7M5FGKP1BR)hIt=bY2z#`hGd{`E+g>}fYzWG=~Nj+>=|!6=dk5sU{-n-AInEz^JXPhyzC^I*`EQm9Y>qCbQG;`H+_R#;j-Ix+h-h`zNzmEESc zA>-0(UmfB*$TmroIapH!*ENqxm@HDOSR+NXg_Brsd`pq4nGnblRQ~%)_F><(Yx{0( zB3cTK?wcK+0~ON*WlEh%8CtkrUcZY@EzB{nqVk2L^ zI>yxh65UVz?heTW>EAfKR4%Rp%y~R&%JU?)!LQ%JNpB=CnN>~A^z=x$?Z{ZK`3=d; z)s+>MzE>TT@G0>qsp6irmR(c3&0tNKn=4OF=Jd&+CC+_9&W`R>Y4$t~LG%NM%N>W_>=R;Z%J$zEV6hq6rl(-q+v#xzVzlefMQ>Vr0+5ezN#o1Lu}s zu$jhCN@B{9KR-H)#w{Pjsog_IjKjfB3M2O}QbIeXgb>%CV9J z$_w{rZ-a9jw1(By2VEo(9ycQ)&*DJkrwelEARE_$y2=zE?ImvVKf7b@HMsT$^XJOi zv0gm=R{>%J#NoL2-R=52Utg)+dlIyHOY9Z7JPCy;Wk@La$#PSjzb+_>bM11VJUe^q zHT_rSxG3gN1BYG`Ivb%+xU~jmoJP~+1e^r*G39eF*r;zzz~fJnU1m1qEgaX+JDZXX zB{tRU|8lj9r2nc~%CNPQGhUj0rzwXw{+;(Y8G&MbP?Yr=gUbVr%i1K*%AJ#aRH8nO za{gkfFyQHY@#37P_|I)e>1p=4@!%GzpD(ZeKPR$Ka6sAld?S03^oab~D63k6QU4vX z8^1MX&s#Y-i;>S{A8Ey{2Cf*_?Dy*CJztR-S;K@h35YT}i@lQIabFo(FsD$pLD$lg z)ab`Kpu`_qto?*55{3#)2L*CcmUI7-VSfK9!vUqzadCUwJoh&hxcXKl3rH0#3Sn2S zGe`_d?4n#&Et_f`I6idq+dCfJms20@44-1Q(k?(_1W35+L*=H#ik=JU2M7O+6fSzC86i_L;0V$k9 zrZy|{fk?*^px?)jG)iqC%^|8*|O>$mm-=@f>6 zSX5?Q*7;Y&S~x@YWd~-;JdRbr-PdxbUI!BCrsfLDUv4TekMkEhj!vga#L2gZ3U=bD z{w;{D3uUbT=D%`fUsdY*ykIBXX6?=~Wvh??1UzmY0$iMs;J`Rr$yHh+o zY-@LeC~TFH<-RJoeG<8s{OQZ=cUcod`NB=oMF~V3-OaxfLJkGFi2= zHol;Lape#CaWuutJh!i8mJa-?CwLAh&4i-V>z>>n@ngU7&F|bC=W+643GGxFY;tOO zsoMIB=wK6=0m<7?NE0_-{yPagj=rECqfZpXNwx%Bf>7g5j*z?@i379v{CtfUUi7+N zTXc}_N{<@RaqSxiW_%VBPR;x;GNrNPKUBnCJ+}BHN5fCZ<<7ZEY1AzIJvft9|I~n!)>`t_w0Y6-u8@j~1U7jlFxd1|# zcslu^{4@Lq6gL~JD#Mi}!dIAv=@dXs)**JA)5wRj(|eIIijpboqVBtFE)vnvX)$qg8(#?mL z{-P{zn9YXOg-$m8d@~`K#TR(^W&GFgla>oqoNLka^Lts%{%^w|*QmepFAuV_iD@q- zIZ;027F4?-<`VMmxJgPv;P0&Hl3h<6hCr68z%xRIOhjKBbtzqw;^1oD8s%ZEpTXM- z_Xp<^$)F5d8JTmH7&|+~{Fx99syE{;%aN1e?jtrrI%Ra0)&6o5 zM0SyO-(`#ptzAM#yhsUM0-XcmUg}?Z2BvP=#4y_Crxzwc46y)&e+L1}xbhhR)XE599d0X-;o2xAMf-YH4RggX_D4#5u|i(Ro`_OR^csd; zN_}WH#YZA%$H!2#xHnYU>zdPh+;xG;X8hKlqt1$E5{l22fA&4Y{v`R?vN5e(Jze%BNazC zDxD*h&e1$K>hJ%&;~i(`KIghVbsg5LtvopM24AqT%$89`^yWQxyw#{b01RFh^>6d& zfG!|zYbvr*RsZ5TEK%$M6=ut!cA@>bwV0Ls<6mrg;4r$dI~`vJN>ygDrJA}4XP<1X z-80(`No;fGPNf zRW7mr~GFIQxG@whQwU-QSe!t=4h^A3~ZDd#SOtCOrg)q zmCf0@U&rP0Y(ZMuUo7H;@rbuZdCJv$RtqOZ? z_6+)+P~$dF+X}s5IcDXjmi!0365hxhbo|8yNFuKy0WGokMB+eydusyo4G}vH8tdex z(9t~PsqiQ`%;3HA{kDTwUfqxSB`0fwEwYLiaM2Z@HSUfk?FEJR8kwX#q6uO~yR*wu6DY}#eNvpc$x?6s|# zf(X0h=~D6R$G_2mD=K?Afwkt#RJ9y&HtdGBubNI@wj}ZF;4Ayrb4J%N2z%cf) zcNgF$15@*_vXiI_GhRB6eb9UEN{J%1Y@K$~@nYE;CIhDpOAT)adf@^9@$%68BDcb;ndgCea<@(5Zf3k0vi42BVLEzKV>I2J)a@<4qngsqH(i0FV^k+~< z@FK>|JhZj038MJ0s(ra~TC|g1RBb)rXr{#+d2CP^o=9D3GBZoid&Xu0Sw?ij!YBRj zI(JqC2Te5(L>!wRvcC8td99gs51|}@li2t>LEzl+WZ&?bI02TQ1LAY>52VUmf}5c znGg63%W~_NdpH)^PUO`>V6}!5qx)F1GS5;vMQZ)-Rh&k1$Td=45>FY1h<;}27NyRD zt5lm#wb0!yBSlU6+Mvn?gR*yS#5@>Upk!T_rTC0bJrDoOWSu46#Ut7RZ-xBB(A=^o z5vN2v=!qqoEdw~dhpgtqI6;f{t~cU^3x_ds;um>xA|5@dM|O$O6OuKsA5~gh#z0(Q zM)9n4p2?ccRrV99gT`I<&Yab&9O8X*u5A!a3*}IFR3)N~?aS13Q(40^hC@F9!6J4| z&#>FG3R;#cVaCC|9vS2Tr_(2f&0(5v`3GoNWnMA2aZjgJfP`Bb=3FLe*~UqRy*Qme zzzHg--SK+=fWqkUDaVOQ6a2zI6$QVklW^sA)`FYfw$ib%Ze$ND9P*8(C(Y^JgAa-* z51SuY*M!{+%fZ(p->w&o)IPGs5D-g~CR)$>;-OBn?KfVk7Y#Fdw@_lKiYq=FRs!#; zd=myUp?0a;*(~9LnNZY~?W`#kIR1)}JuI5XkHUx%MYntSV}KCZ1o;e|5Kk1zJ7^KdGqc{gmJElEEPj zKAju;U8^poPkPJE=)B0#F!J}iwe20LSOL&+7Rn0*40dqg)4do zWGSddTWI8D+i`Pv+~H+2{pSB#I-1HH?(UsXb#sB(iC}+nx(gndOhS9K$0Fr9vofE< z*>E;p+WVU=w@kknmTU{&r)2FW+dx5wAIDs*G`*%0s6LOJZjB2dK*NjA^QVHo$*Wg7 zZ?IzJ;Uc*`8oc?pq)wKG^vr@BI!cs61b$3}yqwT5yd?mLe~N`AUDH=Wxzu>;9M%*i ztTdgM^-%=9KYj741-NsHqX}e=N%M|rYSQ$dlG~@>TN6|oEuqJSi`ar+MF+gK4w+SY zY^wU`LqONfbv!NN!l-#L!I93b%b|YEJRw>v8b)^N6kGw4$?rgNj(e@VUhtN>_)6@^ zqQuGEh+_#xVFYjl2OGHw?XZM1%)Hh8 zRYfBf^*2I0!qb3zD7Cj98LRi$^ELPRqUB+#t`NZ{GhOPP8c_7y2C3z+AFtne3P@%l z_p!s6LL~6c4c23UTM(&p?;o-t{Hsg&cx22;?~j>)u+y3PI7c;}nwFU>NMb8f7_P63TQ$qls2`qLjOG# zYJ!tG7fCVx-fVyP%Ho!56c_(%wu)dxD23QM_s17biUo#jG9YVuhXAAN%4 z_)~w>wFTOgp65P3F}mqzY$}vEWw{BW5YnWZVebw+r9;>40!ftx`53`?>Y7Poq35Km z1Fa3mISF1Ni5?ug9!|~$r}{A#hqDup(>)-~$6Ah+8axLm5s6BE}do&fjxjC7sPNT>IyA1chPBB|rJy@($%Zpx=W znPSu^VEK_3+^GrKq;audC|g$|(GN7d|4dYV_WoF<>=3d@sVWC$dUf0%9P%CWKSdet z9n#tYBl+*jJO%1xyM*#LRla9~Twx|HPS&k$7B*j-st}%96%)_z_wm}k>EnhVLV@!G z(jq6LaJ;sOB-VwTZ=3UcFK0Oc z&}!>6N! zoztq2-}yRXpIy+sMqTsjc?AX+^ynWqi092kpX?2w+#CwR7MejmNV}3~er?Wp zF_xo^MoYAKx3ox{PC=MM7I$yPTaPjAa)G!#z)fc`sB&+6Rudf0Q1SNK9f_vUZhU>1 z6~XVyfs82PD?DM_kK_coW;!`)c6^{$e3u%?o)c?Ts;p6`J@x{%(%Ys1T7FM%NHd{O zo2N@XL?|82WH9W72qxM5#jJ%LI&5~>_oA`I6;o@BXo<`Y+vaB4s{xFAu2dt{=iQP5 zf0}1*hT5|VmJi)im~B+ZsLwhqzWIS}3^EOQ6>wf~@nW-}6Jc|4nI1N0>R^#nvo@<8 zC%TZ2Q$w__y-~Wyffhl(Yo9qjZ(!I@^>F@b#^8#)_g6EJoBw;u=qb>=E73u5euuse`{S%XL8~6n1Jf%uDvMiNp-w z<(N<=lVzpd$|EYPnL0eHDJBga>2Ii2|%1PUsqrQRvqmu z|5su5PXwq%=Nt-$Z^!JUGglizoCRynV-TTLe6^Nh9v_Fpu2VGC5?Ao4n-HFz)=6j$ zycu|>*M+nTiad1cakjMRS?xd#Rki?a0PUld)1uyN)1t|LrMPS9cl6H4-A`lO50Jso zeJbq-9mNPOSoe7APkyKBF7HfPvf3$^@52%^yW7XunYj0`PrToozyPh(W9L?567-ugMcro?H$WP5+`r$6Xb<%k7!u2KK&EVnFawy|=wGZZJ=8SB$Hh{|*nIip zV;U1Q^wp-(+nbhI)BRe5H=?bm%4A@XIP2eL%^)DeV=FgFM|vp&GB&k?^`#@C^e_Ff z3C$*eP|K`Rb}`#GF5#zuj+1%%-WX6l|Fnm+e+$t7+C#83_q&<3W^RA~La4SPEGC2T z#RR=|uY17&F{{Bm$U|E5Sp_}&-Ng8jHPw!6EH(^vhMgjp>-LaUsVIF0?j4-kF}Pn- zC@Nc@<{4c!yW`-jkr!^~a7P>NRp!ju@Bgj~!2VTLEkmw!Vm#k5?uUcrz+!GZ$D}q* z@Rh#|AM|fEdfzJmaW<(7S(GfD%^WkWeg;WBSW_2fU3_Ng2WZ7$Yz#_T4Y5R$@$`A# z_CU*UN;&{iq)J%Ex?)o2nGE4p7LCY}8f|)U*2c*6*6k%G5jAE#kzfooD{C2c%>!H$ zvTzth9kGamlN0jI1jjThJem8CO$32S|2;uXna2Z7slQ1pFRq`8hPHpLS& zhqI&yt1X{^^ZR|${d)Dyt>qTB;f}+k8{`d_HH5=2^3WBt(?UufCS5&w<+b^JYS>H2 zocoP9Pb$q~tzAF;w%wNMr!TSsY+@rs#Rx z%VU^Vd^XeAz*N-ZMtAczBe74O4RzH(!!}jLcc@qv5dG#?*(~xBw(B+CKU5^_H`gs9 z79AViH-s1l^gx5Z-P)7xFPhqT2nh1rJKg{s*p#=3X#qP3_9|kC>gZ`o;L4e5q#G%H z(7cG7^nbTaYwgU0B~#EQwf1fOG=#Xc98#HFLR%L9^ah!9Wztn|*m<((>o{o8xKw_Z z=Nvj5hW8p0i#VnWlHXGDcpkBKK$hTOCzVYLlJP^O_YRA!tJ4Aq$hWNO20Oy|7S!h3 zrs_)H{xSCj5Vw~A$sylkce50G?sCuSI_4z#b!`%>jLWgrU@Ex2O#s50>Lxww*IT3! zNaoX(>4TB8B2cVUrj9Y5jCgJT6O_8>$W2|2=tz{f5%&Qc8h`dkf+7JL)Jkqdxa9nR z-P(QGQhfeAT*^j8^Tg5$FJ%n2VT{DsQ-j6zq{>D?VLeT1JAEC-neUCxQ#=lRmhLx; zk{ynKu4x(bhyQ}`{%&BIH~@DA*k| z^$?IP`huL+zEo#N;4jy;$sAjLIfr18a3Wvj=n?r@K-wzv$(!{Rl*8{@K|+mAd|xD{ zj7<2Srvz6lBZo1h?1IvYGc$beDcL_*z8AOC0m$>TGvAdOeO!r`Olz$!yl17?36GEM zsuCXl!8(usO1)YJ{yzz=bxJAaImMeS%Ug6nF)Ox2(aazTnS^Rt@y~2XCalDI6>fAQ zn9>7tioqnLTYop_t>_Qp58navSl|5S%Y^^IkA+z?l-=E1#ezl@JwE3dm*fVjEK<)~ zg3hq)Yv?5H|6i$6{#P7Wf7u8nYh{>{XKty_SVhXwmUgMvJctU9r zL}KWAjRY)gSDX}qZTKBBAIykF{F+0f`*!$OAv*@{ejn?;>>9RLsmJ}y_r{!}o=mT? zGn@i^9M#2*t|)>7U>EW*{igu~A!lLvU#)!jZvmwnUP78Yugn)kXSLblFpEL&R4deB z6k5x{${c`Zd?Wb_hfiOR4AcjhLC=!1$MrpaG2b)^*e&(;+#T-~_Bp@TZLyFuN&0Y2 zhoIHT;{ASN2cWAC4PO!qS&Tu^zpKoyqX1ZX5cTi>;{W@OD;igB0f5>&s*kGL@F2Sh}q zjW+CRG0T7{8^k>0HL0{{g2nE=GM1y?Y^`el#!WPczl)Rixou3Nby7BXP8ye9f(@)` zTJ?Pu{Qa@(jO)h*emgUE)F8E~*ViznS!uWOZi+A!kVJYaSg3jTqpC5R>1Ljhh_L7Z z;reVhEB)FJs~LEkG?P3~XRv zgk(m%rt92XePj`bm^E`fE9uM&vO!1G@((om9Ne4uW@)i{2(t?ETzIq`=>6a_s1|Y_ zAb(p+nDtlwGvG+=Y%81P>n?p${3z(QjJ z71A}Chv4rujUfru?#A&APD{{7Te4;^9@#MFG=3{7ztFO`Yh)-FT!-^U`c}OwFW7V~ z{LF3s(HM3si-G7x{v;Sk=-e?&yZcmfF4*MC#r1PdS!~!+W!*44Yp5bx3b@_ls^2-# zv!ug3SjC88J|cAFcwB3JAd;CVhyKYa<98WQ=Lp|OT8kY4f9eP8q=e1wc8xM>|;UoRTHV=qxW7hddd!g2PW{o3B+mnaG9v;rd464lO zC0oF=6$In-Ka_igW*tEaZ<;1V@773vVIpCCvB1+@RpP~Mb9LqYg^*n8+mQgi!N3=< z@K@&>u}0ydGi>T=QVNwtbi7$3^(p-Zf3_C%x3MvUR&ZVo-8C^IyjL`!i#QpfrCb=hSyYfJarj4`V@{MB#eI(di}uu!Zg%c8&Yh)cY+Yd&); z&-#!>TYj@6Y`oR+Zi;-=bhV=D3jhyV@*M;b-k3CLJeFrhNruOIo-n+%+J8ArV!7p% zPPp0p;u<4lIA7S${eeW^B;%X8VdXXEzlY%eaD^wc(8zP~N3;@xoYP+bU@>V#s|Y}` zyjyFG^24(j8R8^X`3yydZ@G(6<4*HkX{9CPjz*uC|FZ`>Z|KRkGpV9MBSkBkk3 zCFc`c(#B@@(=`?{otJJOT6aWZ&FwGIsY+~VGA?8=<}Q%PMBk`a`%rh^Qp&bcTy2sI zV%)L5(6j3nxVO~i9e2DxkoE1qa`*u`D^2;};LKu&sjoI-@8}q4zt{Z5Pib&t5)d=9 zEI5b#wcOE`jo};XB(YX-IqooA^~_Jj^u0Ew-_8grEG)F zly45pi-~IFJR$06++;3=yvCc4JZEP)9630fliU1U!Z{@YQKe@Kmd1?M3JR#uMQy)#G6>-T8@ z%>SLoKJ-2I&bDxP%i`WZ*FY5~COi}{C1acY0Db=JghM{dcz3w_vwZJ(-}e&>L38XE zbw`vnP_&H63V!YJ>)@TK=9v7WKAA5pUke2hQdXljR;3J8h_<(um6Qa`5cJEN{i_^f znK{F?6n>}GI}fn4--qLnj9|Sfp2+8AImreKtaq&j@1k^2jwjscZE-A!X%a1{o8(Yd z;C_In2i^mL+w#Lf@b_YH&>=$mLamQ#)(h9uT>PY#@+pkDpr6>cS{r8e54?F^l{_Gn zBTcoo)>RxxD`L%|p@GV}=~pwPld#;~R5>ffy3@89Au}Ox}Tz)Sum7 zwKZrb648ubW)m0kdw1b-@9lG2E_|Y37$`JEEm!6f! zn=;%qbo5_YJ&mhyZ+{oK^WP1O0Bpsx#m7B|c)MIoSO#b19xi7AW>4a(l-?Z(1hH_D zOY5kj^6h{ruzs3>{?e0{pvzUGD7_((%y zyI(S8Ppw(Kdg0A4I<~f&D{O+h>=NA1cT!QuXuIbK>y?EAvz z-S#yP!S|$Liggz~dS<2}kVA)ADS`Bv|exZ81?ptw&*B}^x)53}oqbR-(> zu1_;(E;ZQH=bI^ZTa;zpUFw4+wm2Cm7A1e!hcK&8UDg&zi8Xsaf;(0IRBP?PU(sKm zB?bySh0pfAh}#y6^$*p7Mzt$_`U!O;>#9t3RW{~~pFN{?!-Vw6uMR5SNu5nZzqNN@nxjOVmg^w;$PUr9ykrM z7MCd-KjiFe37V=mIDEmK;oS`+Sr0rZA(qpUD-5=qs5$?Xe%7Q-w_)oFlGl>^t%CLQ zT9;yWp%NMOSZusYPRtlfPihav>`q_YR7>-t(62v*>0NI15&ocvTOs}9DyOJE^Ss@G z6rs`Y6>;Q~LtnHPphs?+P^x6b6O;xQ#OaEoTrVQ7 zKr+dycgXfC0yTjF$0JRIO2~%8x%aD~?sUd{m*N$U{aN5<_ns_bmdKVws?R7%rMP2C zrOXWG`9>)!0z5}#>^|whOu_ud@?!?Jr}_u^UY)$855EtISFGikUthGJnKw9hKB^DO zVOyGy$4rtOdl|gq#*;fh)$++%sY%@YGW`zV+z#xp418C9%i14D4QeVs^bOz(ezYmk zX&_x~k7LL9^=q6YT2j-)PU5C#IiRHgy=Ll;q<)_eQ}sZ8O_=RSO>FsxglVCISqZ?9 z%`RvlJv(H1j^i{Wk=e_EE8+D^O|u$eJ~eaARN|vdQJpQ2k+Qb0r%xcw_M8hkbz^lFDz#xh9;@->h9q^McJUE_ z`Gb{&rs8uWA^FBWeCr{Wtl#H6;hr$(>F##GP+%zDq)oapL3gBSAoN16YAG>q9XjJ- z?i!Tp)G3L?Z@l<@)29PjeC9`)N^O*zLi3^?-rI)z7n=M&ghW}5BWuONhYZoK`@Xm4 z@PZ2X-D!}ds-lc}FLU&y>Bk1U@%LOQlc1B|1R73zQV9eyI02d2b;H7p$UAoj{Y3=B zJyukR3Ie?Q(Tb)~Af-$2Vw&Slmq;_SkSpFGpd$wYR8}V9`E9lGWBMC+Lof+x3L{PE zp?*mPLK8vj2g4UD8(zD9?L6{w#nlrX#|X(q$DIn0Rjk%I{SsP?MB_}HY#%Gc*8|a)R#N+kp!q1@6$mMl>*F>7jWd3B zGI(HN*%f+57ORHT?pUtIKM|wPMbJZRUo>?;n&o8r>0u`T0(LBkYRi? z((N$F)VZSjdS90Zi(bSo^P$Y!qlVDLS!v zmf?w6ipY$E<5XIqsj%onhGS=W2OqPi;62#V!us%uJLl@{Y3*?8a{ZXY*4uJ0@ld=Y z6IQ210^$i{`OKQ)2HzWe$RqA(lC1DVKO^Tb$OLO}xyVHAjp-+UZ+XXlrmR09gaf(1 zgl8J&TvQGCS&_bVBUpR+7(HEbG+CCt*vr};pCHs#`_ z%wX*_3PoK0&f+=MQ}-WCpEj(eFs{o$R99{haxQ0`JP7f4yeMjwPb6yV+t6yRw7HUP z=tiCma^Usk#YOsFOptB_t~Eic&lc&Dqg@pAtq1(XAO+1gxYztJO=B+epq-e$`u!An zI$qqL=VChtyw#^RBJFH)=aNq*jCWO5r)D2ugVM+>rDq4T#xt_XX?*i})UtVjyiP=& zGg?9plJ|75tK*W52Qj;_OGR8`%a{{w>-?6>*COB=gOS)+$Uvpxe$7HIuhqVd6?!uw zye*0xjo-$GVa1&P=A|;G)5kQMx2M4w}@Bqa-d`;&dd4^;@S6-gghvRtm1khN_ok&p;ioDM!RxB%YWIvz`?yG3tr_I48C2fN4RoujOe$2RL=GMiC zM+UXzkz?TW`fU1rYns<^`07}RmSP;%+B^G}Sk-D~EM8mx(CG9b<~X`=NMxb)0Hs1` z!m{>TDf1pb?|mPl;`od~tNdlV&{H?>7&FfGtW!7B7+M z^}NL*nRv5KsPy~R=yJ^Bs=$-1Xbg4E7F?w5?S}kPWX_pT&uLniT<36e2vT;M-;X@1 zz-LZ)tPmSV$5(NZri3rkdn^xC)CO`=e(CCU@ESZRZZ=W~sWgSc-I!&e)=lAt;Z5SQ z!5Ba7014>kML#8b13i8#>Meq=sA)W|H4b_J7d#ZAxrw(>a2p9zb&T zn#;xwGlvSvz~%S|brqM}nJdiFw!~$(8ZpEoRPKomI&ql=9155b>I1UK z_{{c)iXu1UmVQ~o{$5KyUO*V<)p#em{_dgsk~!YgN9fGfhl{KE%bD9a4g!7ES%^pp z*iqZYan~vyrvq`bt!SbiIMxdO%Ea&G?GXP}|3eWJOUAR=z4`g+>K;nJFr@`~mokPL z2+H~Ghq^y=>+UhK9rn~HH(Uu0&MT< z!|?L=P5I>Gv)tW~_k;@aWp*SS01ZR+9^V+q!VgTDAJ=Ak=`;X{!qr~AuS?RZOn7xI zHFtXcQoMnGP4FR*=4lO`e;*wo|GZFCNCINBg<6zQHMTp23L^XSTcFG=r)zZ z&;6fxsfj5e=Ng5sQdigQ2oo`%b2;HK^<2S&-ovSzDTGOLixpgi67hbScLzxg1AXi` z@2UwPvI+)#tXY-U1?bJZ`i2SFiNHBAOp_Q`a>~zypASE`-TJltLtkoR&^XH4&v*z) z!|`SRZhXnVY<@1YkY?j#+A(x%ZN9MujF!_H2|?YoZwx!KPLDM`MYZ~l2}c`33@R(N znT;~w7#{IVD7xFqK02X$8Zykhfiz*a8OqsYwymk^Gj<^xX=Ib|{fM!D{yWAF0ffMs z&2A2`rGA&&jkb61=C8^x0s|>;H!j}C*ye>!>GAX*x5hF3!s*kF&!E!bXoPb!07~xX zxOI-(QbLzq#Pr8vB>@ODE|8@gp|#I}7ws{*b$_3c?x<9TftB>h!dy>A^w`w!n90Hv z=f~cbfFK}W`~r77=r0m0*F=4d(pvM%HQf(kR`B|OnOSsmE1T*b4D1M}4TJhlTOSNA zAIU!??dm>O)Ftz{M04Zn;9~cND??w8W z)$mlS0FNs*GgeL=n`aBlZh7c*!2r;36qa`Ds(_^$_bjR29YEiFRhuG0)%;X;;}k?-n)u<)2ykPv7Fd& z+Ua3^>ppFM^l+0CSURW%_b*MT!R3W)=*zRKx{fDTcRlxj$?RmeSPY!3_)!QY$E9Z2 zWbN^R*-&Acql+9xjHCk<(!?AdqW3&2AEr`fx?70Z-EBLatlZsiI~ zp?}Oj5?=H|x|Z*g^$QkYlqKLmyn?m|`v+yr{LLOJn+jqf%vCHspRI`QpTnKgV% zsVLtN^@6z^m8>4ZANh*lTMUk5_*;J93NTGUX*!635^2@<)+b#ozi?h0_Z~Lx{d9P9 z2mR%N*1(}>JXJcBMQdoffWh_KS>xMRaCFN|78JybiI32mx}pkXf@O7Yu7XC+M3WJ{}Uh!#_YtQvMuvzu7pvF5m~k4)-yi7MS#p z#8^Z7_Y8%CJzw4KiRBc#UC7P1uadnc!-PNj($*pPW>9sd;)4~b#7uFBmZKxBVe#xw zH*$dX{1~J+ogddH`4Y{_dI}>`(05!@s`{vV-UAJ=ZoSDJMhqs2?}T_Oe}Opz^Jk?E zdb#MxU+eG((yC%ssy~cF6dWy+x9w0AW5L|0t=PUV3tmDBA?tr+rS6o!nR#|d8BMZ1 zlvj<9=f}Gh5Y09)eg-cBNALuo7U&-Ny>k6Bohnxu`<5w5qLG{Q2h59IHL0s!GF-bv zE2+HjW`-qLYFQGG7tdu`y(#p7TyS#TwMWUKsd&ruc&nB8(~-BM8=(toq*`hTE4=s@{lv&ZN(~CvM?~yZj1HEaE6ynpkHNF4A+|{l?o*tQ{mAm zPpeUAVcclxUJvutH5mB?YWZn$Nud_W5QWDed`ppcoe`x!eZggSIuv)^e9@^_Cg6_QcAy|;?FQMI^*b68IWl|a)mtRKaFRe4)#LU!t*bSYYQ~h=!CV$m<|Marp%jbqSFJ6UrC<{d}Cz3DW@I86DT2daRV6SRbkj4E1k*6c> z4TQlrfXo$1yj2O8LTJW40uws(z-1A6{r=~Nls|yF1Hu75lP2J4emm!`-z?$G?H#3Y z0p@%>*q^lNA6z$}+b$X@Lq30aTdZ=q6*enfOf*3Rq`PlNg~GyEZa*i3`=4=-)_zo3 zVoTYbOjRP*8ojD0)q>KtC-NGrQPW0PT6Yu(4jZ68hQ2QrEYV%d3ri72nlwY*^wm&amAmfLrSUJWKm2{q_L6K>o1Q4K*ExN|-+1?cCh9yiAA3_Q8_h_(B3Blprh{thkS0ih!f1QZ-ZdnQpbOO1i6L4x5<&d6= zvO{#yX2+~IRgEveIy5nvjjjwPZvXiNdl`CuB|I7toVciZonW-3)XMLKzSFFb)!Jo@ z2BtuKwzguG!^JL_4Pm$Y(eQUR=7{P?tj|!8=B6oqLCaNTB0f$+K_m}8=P)b@o|Ly#v0cE;@5sSSp1?Q50j_+wb zvMfQ!GuB->uYFN}U_rmJ8Tv($&mb}EX!iIQg+#U>U%1kKWn2ezrYJ%TEPvR411CvPIS$F|*^B(^^$r5r>gxsYDVM zV0_Oh53uaa{V{8HRiT9UcM!KXHlj#i9m^skz;iT=@nYm$V@Y#N!e9m2{Zrv~IB$dg zZ?A2QKo;T4@Z}(b3^b13yM0Pm+FLF8eEAgF^ShDvjkzJ7<7>K^Tm9OWtEqf%^U1e7 zX6S-!Bm-ZgV=YLAd+j&p<|W&lv^@Ic1-5b(ujENcbWmJ01Z|vkaBT-$g0N?-JINZS zgQ4z!MTAk=zb)1u4y2Bj^E{#)TL1gj#W50= z5TcN6nJjWL-@2#00|(ymKEGhI&U8znC}v75h3^5i83$Ycb{B%#>=OV*GrGVcF|)}q zJD?=plst?3K*^;blCkk+bbeTLjle+)L;wsM+o%xMlHXYI)}y#uxD+-S2bdV#1saJW(R5WI>Ftev#zD(0Pf?~@ht6u z1JMbaFo~cpaH#%k0B1Hz`Txdk8-S`my_V2<|E>pHLg3ccJi7A-<3-Oex>k62Njq)m z+TF$b0i;2EIC0kk^uGA^hIm+K%d(KWk}STkFO^cMiVIArhF$^|9zBmj=kZ{vr!tP) zm&wV_Fvd!Dth+>}<7MR*nRNrl(IyjF%GyCcWFu`735`Ej#L|+Q$HD)2+K?J0m8&aCx7sQFMxLc2B?1hv<@-6zH6os2fdf6$ zzz-w7*|KqhN<${ExY|S$$tSD;gQtY{bJext&vx=aHiCRbcS4B-w%|}6>)_8 znWg46*x)#8KBj#79@sW&#y+n(qmPSvF98 zS}fuvtgW|^Fhr=v`{*O_puR6eCsfKUJn2g1BD6`yU1}lYdN?6-8(qu3mwEJB9nZEj3Gpdx0 z*}EG=6d0t!ihAOcV}(rp1!8Qp$I)F_l)}VFVIBAmEA~4%urakRic*=nQUVKtF%%nJ z7=uO$hjaC28XS736(Xr1Ljn!lSB}6A>dsAhPu2X-AYC`oF<;``%7-r5yog#`b`@-a4@-3%1~- zTvJ6w1lR2x=BSJc z)!9iKIlczVLdido{#W~0E3rOdAq`CIRMGJH0S;gY3HWS0U_ZaWBFe4D7P(6@4?Y7I zPzBT?>CM=&-}pNP`)+ORN)1U&SK1VltwZBx;w8RH+^I>}&pVm29LBJ+8aDdybjM{& zt4q3tN)}Wqdh4%2c&E81JkY)-33kW0&$C&j+>dl$ad=ni5`Jb10$s*H0OMjga&nQQV5!8cUlI%7zjZF3zDG#jQ< zsAMR6`$x3vq#-OuIJLy*X~ID$`!I<gbodZZHkp!5j6(m}=|dl`EbSbnT@Q>{iTQ+XCG~^ITYN8;*BdpNY-C zzA14pX4{jvoOH%>TeFNg zZiT5;eCu>AP-iklySYjj`N;g!+mj>MCbkCQovw(T|auM%&V2;C!W#Q5r z=okbSDQ-qvCR+>`(KrE#@tx!}-R+un6wD`3t!qa6SaFB=yhMwkjtt|T<4BJnE3jw4 z6Cw%!Mqyr+2^MLLdX(H({E_DQi9wPdxF-|LqlV;AXC-H5Z>Bt^qU`UlkPk{@#@N!% zT7?+U-spjbb|a5?Yqgdq;aCzeCV5!#NpROzC|?+sO)t`^rT0Yjlp-c;2cwkmnj zC;vrT_PQW>+fKMk6w@DP5iJ+%X_NmX9UP4JGFi^%NNLCo=W07_#@K2E_DhGEx_PN6pZEiZ6_h@rR+G;S>~MccG2 z+;r%jfWz*cJX{aeKWzRSymi;!d)Sh=RzpOiVke->+ET>P-ou6tq|NQFPfFWts~eho zg#Z#y?NYuPCkhm!%C|QX7X9h}UrCfkPVO$*QbHb{jSMUc`7lO1z`vkG&`R+&l^xOA z2-An5O_FRbrNMyu?~8dQTw&pOIBO%+JRj7@C?>#`;6&D3Gnv){yjpM z4hsj)Ael7bV|&PwbU_?$&EM;W`jM#rZF8ccv9lYWmy5{p`$y?Jx)>wv!v{9?$R>om z{$|bDg2~JMxw45?n#h}qrm}k{!a**8>dMNro5}0J1(TH>RVpCtkc8sLz6m@?Yup6}CTC{tGq_+P_NbZB>J@#kt6aY?` zfAv-H-I@<~>@*98;NI0pPfln=kw}UWeC5B47XAqmrE>?nytU3^+nDF6&Cr>c6jnr? zWd3lT>q_#EmpV_m=B-w;q=Lt8pPtht>#S`I9SE|rI!?G^iMj_PJ4~pmFrn-QnUS$~ ziLlvUiWPow7IYgD5BktPv-`24{}aDPKi4OuMIrMGzO3@QP$Ddart zSvKXF=AVbx1iqSBRTk)`PT^cUsFL`p3Q`V>*8E(iL|3r_Sw;Om*7f`SdcnDW`>SLk zS^y=HD}wO7DN!WdO|rHN3y>DGG-(ctXlH*!N7ie5&vZI7txB~8By^=1Vyhdt*M7>W z;Xg=@-jQl9D<#cX8{Y(G3Pg(J_5!t4ZXvy=)IUgX9*!(}p;@)A$cfCy12GHJh%Vvu zVXkRu0K<`2@&Gk^g1&tkz(iN6L6OcQA$hTDbj%r+`ToM;Ns`!uv*7p};907UI39yJ z>1ZN0ZYB(yUp47)-W*wiP={?)zhi5b?}9q(dIcjmKrjJ#W=L`V^#+yC%q8$D!Ylx5 z8Yv4VM^a}n_ao8IdrWPEx0b~(0yDn4v|so(*sM_B zLhSnO>RA9q`HWL?yD0fqlDd(+=66#LH#Pd(o9X@sLBod-XyChjU3qej%{nLjJmg?@ zEp_}6>IG{>pGIve)su`|+)6GIgA2xNML71JNA))6j<=CtPDahwL2Q?O|$$xxjUSL$7_I!KgWM+unV6=C*UF)f?PAj&W0&V z{25O=Ov%_A0ngKxw8QXj%ByST#%JX%rUt#|zBkzq5ffmofx)AJ>D|WB!t0g_emggw z+Vr=(exAp*b4V%5!BTtG67Nw_J$Z5_-ExoltMQXu0p6_r2*p`K9eDI3eAy88KT^P} ze`M5GbCXVT{FS65AV89P3mv^-qDVIiN8Bd~GfsDHs5Rysz6!c=G00~aXK|QF%)gE~ z=Yvy|&NjjuopZu8TS8Pb;40%AuULo<@m%0Vy2G;`=$oRGW8hU1WfI%+Bf9nfxMX_d zULY$Uf`aG9Tq;ykZ|M8R$kebxuhsv^0eAi<2LJ=l=*5lb%q9Y*6YH`j+Mb^dd}gJl z3hC*VYUO9`8S8x*VfUPiYvD}cC69=pWbo7Ar=b#(AE%|*s`#T8&#rj>Uwd!;73CN8 z4Ud8_fXL7Yk^&M^A|*XELy2@MNJ&eB^hl?Y(jcLfbb~<&(n^QGSGqd{-gANX{jB#- zc-DI62bXKO=3M9O{OrBY*~ehDg%bsTP&@qJ*`ZS*m%-AFEKm^Ff3_ImPX7^6$OdO~ z3^oJ(&+OWT|B~GrJ~8}`uOUsjRIz`$OPD46*>G}l6lQH-LP6!+Qx4%%L2k-fU ziHSE8W+cI35jg4o;&3h6$15J4xsv5`l{KWZfOg3Y2ycm>(*E=e1{V0B~7ROIF@euX}aUvNqd>rx#g47(i3%CW#Jot)@sGFv2TS+sp!RfeaL?&p%(i< z-Q!SFQt+namNeSLPVft&en-q{@9e?s{r|k;CMCu|^O?fhx)BUIU}g4j|0T@>&n?6q zs>OJju^yLypxIBw5qUkufs854lt;K>#7f(NGkZGG@1KwvCe*j6J^qyMkP>^xBFZD+ z?dJn~(l;IZy#~VbvLX)$xG=xGRriZ?kU}6Sy54RY9N)OG%O}yX`vh8T(H)575&a(cC{rTKp%gh8y0+yr&kk&Rlqxy#I%NYOOHQMbl}6EfkY+Py5ynH|yYp z4K4MLnuORE0r82e(%(OMrhfYU)~A%-z$P5s)i z^5}5UQt8u7RxDjO*!LMQ8lSYRa|4w|FYI$Xc(#5i0)_v&z8O$3OLoG7aM)+@3pSk5wbxJ(NF=V6* zH2{@7I|bI^F?4P}(O|-wZH>kqr$(OzUuSXOrDrXPQpybE-U~_^c#h}Ha0t%w^Yv>AFS}mR`w#|dJ+&R{Zu$G&^Ww$AZIhj*2wn?oYDL!1f?$Vb zPsSIm;jxqC$_)*p@6A|l9Cmo|f z@c5>wQW2N6veMwAX@?1fIM_(P@Ls&3b86f8;vq%8{>xVY@V|loL{IhF{f7nq<6?7F zzucp_k_#Hsf>Pgap!Pt7gPir>g25#jBfPSB1d#oAFSb-A>jNC*=`iUvVn>-nC+!q! zJ?W5J=UFBD^!Fl}dX*~`F`}-g^0!7cz3LyZcLLk}y&VE*P2c||_z`(=>MbIkrWP)6u}tEV1dGz1=+V0W81?RG9}lUW2s-L9YA#}a zsheD<@@VSK{OXNhvxe3_PEZy9)k%7ppk(<+CNK+Zunk&xF~REWo4**t+rybYJVKG4 zsJEFc0*d!S8+e$T%)`9)N4T)xI!844cd@WmtuIHOJ1wbBg9N}ABSHFSla@t}a+096 zgOWnm{15oE^t6{^Her|vF?|)0JqBS4Z|tZ%+372jVyKK)RYDqi+V8ClEv{4uEu z02B$C{#QoGfm&tq2|jwzA#^CV?(gt>c^{ZjO{cv6{^k%dzBIbAls~pkGn>%157kju zJ;s7XtnB|Z2NeD*?u!VOTO>W!Z}x1m*6`-x6h zsfEtz!|!ZeunQ*MN3S8BYQEvIeQZwea*=!{)^738kH>zUK_Sux!ndF7ODYh2eIJS; zJJ}1e6C7*LRTlIBFay03!{sYAP4N3jmw?qpgNP1FJBPLr_9 z9R5DX!p&ppiQP)KFKHFIuHSxg!^~v@QracAZbH-lu)3lqB}99)HEEzq^OgL<{pt3$ zp>hwm91G>MfQq@8x6_(VLg+Oj#i5ix-wSHw$D(52Q^CoVZ94!#^ zl-Bv{OsMSnya+bF$ex@&dx^0xvWKJYad}w376;cKN)9kMV2i$gipIm)ulv^BREs2v zd%h0(t~G0@v;1y{4$o$U$YCaV&F_d3Rc(tsFcXW9q6%>bM@3t7jhoBo?XIIz?IT+g z)XKK$G0RFIH}X!2-!W6^OSE^#;YeZRb$bAbUKDNdk89hb{A}9TtwJB3_$1LLbUKy?|?@kB@uc|9h;U zRja_p=ZHt(8OV!9MdQEUD17e3HqJW7Hus9#jil^m)x*TJp7X=BH8F>&Mc$T;kT;60 zUqJB4{>w4p9&x^sD>#VztE0GpDE>QdR=TAq4zmBlU+P9=c)nkKQ3m2X*i`$ipa2_+k8=WUJPH_lSac{ySU{DQg=g4#&mFD8vX zd=jLP(qlFv$T0k$?N(Go=#Mw-=oFbAr{nNoJq}X0HIvwR`3oE-fin4JFoE{Zw%?TB zUWnjF!_+Hz;jwi!xqiQHcrb%Wca1?1E&rhy9&4sr{esJropuF}b<56TZPC=~_au)2NIE{Ty z>Sk!u)Ud1sHA~)fS@XsulcwCrXa29slgk#1;T99eJZDx4P+I2N+o0RUzVutFf_u6| z@vpr;jbK22KV;!5H6r~291kJyhI-v{X7Pc#bhb*u133i&HDT3s`*6xLH&c5;6U8F= zt+y<_dWvE0dJN~F`!F7*nJ0yB;dOt|!IJ+A!2m`gHPg4XocME2L3_k)1S!%Xpv-># zm4(ZVFAKr6yI*B$Y99->2u_4xH;pPieI}kFRbr{FJqenR23hnJ;;6x;K#%LMk2^&N zq@QiMjcyOjm_Zane7M>j5~3unnvuSedYa0hjSo6ezG)5-?x7sjZ;1eX3DWNMHB{BW z74*O~9S-Q~kBOe#p2A>!*Yo|C)d)O0%T4z0v@maJVjV@j!k#II%#PRRb~`xURCAnd}X#lY=+mU&o5%QbUFbt@$EM0`Y4A@CPQRg z5yr+WG}+iCp?_^MQ2!`t4k&31VuF@YAvv-YRvGSBZ#e1E;j*0ct5VNIrFT}O1|pke z5I6|mqEh&F+x|0^q9Udlb1q{oc5-bE){o|Nx9B{TM!nul9Hnh~2`G^rb}z{9p;m8x zr4H=Qd$MbbWo^mHI((Qjb3*Q$a5VYig4TrBbW6QORja3OVYnO5_}uY4X$jlS+modN zt}8smiYdh|;ITiUYE?(VFmIX)0502obLPFD6D`$p-6WMgOwLeYX%lFV)xWjq|65Bp z@BuUHslqCpSu0Ng!ABJm`wuu84zoJH;gjS~c!sy4KJ*yWx8Ab}<;u;CdbEgfK2wo! zZ32|f8lcmZrG#(0$m9_Lh-==wC%#87dp@8AlZL;1ot8vcpLq4z>>-E7vvSW*A54-J ze?I<6XQh8eM0_ZC$I9^$^%u+{U7R?L>bcW|GO=TOJb*USg{og%kGuMhQo zc*0+#{A@SP_Xy`Axx&pU>}cT{Ln@BsnqjYym)*4Ow{x!p2|dRypZv$kf=(E^b>Fpy zDFqOsP83`LleaHceaR>@mIriYk7kuVD^46ob>S7QMN82lUr%E2oiE32R2=gFXjRL)t1u>=~m7hP8g5bY!~9C>iRyw zSL!5(iz$d##+CbN-S=XR*65mhPCTkGHY4F3Rf4PF47w4OFn}mAu2W&5!n^1Ap_<{CMp+`-W~T8g*AeG#{Sbo|brX82$F) z2rkZWf>1?i2z54AROZpE@u`t&nOE z+vwZGXa_7F`vZ?0@6`Ldi>QyFPu3}0Pt}{&LtHz$g=y9=F-i03B-zg$FgOz6P!Jcw z@XuN!Lx^tE(%zx28xwLXcC!1(Fx3#TNbwy*8uv}VAdWo4lP;^SpJ$Ek>LejN3z^X7 z!V@C(zUfmx)q0HFJUxXuUh+wS2G@-}6TkRehJh$QsqD!&TM^I3lk@qMYwwzH^S_%D zzrFt`pvVNnH0zLf#NvLWG_{vFy38xVz-YA?E!eM;L2a@H&PO)Hf8@o+zX*cI*Zh{Z z(M8R)ShW?Of3wsc9S27Cv~PRo)xERjjH6M8xzYV1F4EkUcWu#vpN97yJ@^uKXi)@b zw`zUt6I^9Nt3GJoxViyCOdsE=zNEYKWcPY-@Whf8Ld=?fq%;Nm_BbJxdL7<@#KZ*s zV+$EmA?}A=MUwQmX5hr&0tipO-IZJl40E+N`F`rzBs2aR^MSXF!Oz`JJjv6|L@$LZ z&)=D!nWP4%CaFUn4#$bJGh@ag^6%?UlKQjwQw4j(3NCsw&Tw4ob5ZzlPqL9`yF=!y zgmBbb=se*|`_@T36Q0=5il23LAA~CuX5%0U<}?jPEnCN#P-ob?QjLOh5kn953vLq- zn^536nfme^y0+q?%Rv*=*Y)1-eh>x*hF8ftREnYebkw7tM~o6TMuU@jpWP|2M`SE> zf9l^Mo^@@i|CY%81N1PTJOtZ{j9q#^TrCw>?&_ov?mb0^AGhyNC*Ms}vo{en-+49I zl1^1HHO-+*|0PO73Q2WHJX5psYP(5mfotn4zKL=US}@dA?8kb=b~E;5o?QFnBdr6D zTV;|Wzxg6KqR;u6iGH6bB!o}YnFcq2)*`k5y>qez9)8tr-zPMmQwxeYN?~vBD&i5F zOByB|vYGY>C4ox{My}vUV$I96&VG~Tnfq!$g=5+Zm+aq+CE?6A=Qbvuqi`}X@H#I8 zJLTd`oMXFh5>|pzxv89G9F_bYLLW3*=+SeAa8lgAW`}V>2FND74HBXmj{%UkU_G#| z5V`&JmZ%=WX)brR{o5p~86I$yM09F&V%!H9(=TgJ+vZh|W~|!q?>RC__~0)A*vnVMd;Qj}=%SLijI-ateEy|r>Ry~|juB8VyGp7iY6_?kXGJ&Zyh9$*|7UiC9@qcPw9L%KL}@KGSBn9=^5{pzsqgs&(cucJ zr7JgDgH--eX1(DN(LBRdyJ#LA5;V|LNgmPK0gbvCVYh0h*Ea8vZuD(>Sx|)OXNJrq}^?4K2P- zYCOR)^FLSD!umu!Voz#v)VjjAF|9^|RgolY`8+rOKvJGj>ZmXWIWeR&vPrkyw zO|hqg^s~u#OIV`(2dxnQpjF3h01;n+zCH_`OnYL*A>4C+;_Jc1FKRozGQHs+8N-_V zD*k)9Y5tM}9FQqe9ZbMiJ$-(hN|i{?8AQ)O1DY+r`cMsUk+ishE%S-y>*u=dpeK}L z9*vKsnMn-k0Ur~wW0nwD_?0Vy!>^(acmN26@O$f?{2?OZd~6iTxEJvS)zA*E&v~hv zk@@k*`q(MW*4$^X71nwT|9K3z#km>KpzH~#B|B!;_tQ;n_e~n&GZkHDU9;97?z-(F zlM+oZJE7GyCIPj+K&quVF#i99I&kcNFU74x*GGeUls@;d+NxJhOi6GOHT+3Ksj zXx6pXcZ_^W3LJ-Ru?M}YJ~a#8gL*R_Fw9{!MafUoI{$EZ=5?R*L^DfOZ4e*rr$ z3lc^Yq1Ze*<8^BOW*BLo?ocRU*^NGijohM;ZATL846{ zNpV3r!20|#(=rwV=ag0gk3CRL*QuA;J6HF7g#7*&vu7&QqCXCK7`_icuYi6PduXc8 zqmheq@iT33F9X?EhZQ0>3=ix+QmMMVUp(~p--{{0qoIS7JP!y!ZqeZz2T}Q_K|HhD zyUgGe(Fn&qHgL-Y6h9T*{AU|*7!v!ZVo^&v#d`b8ZRV?Pf4^tc8UH;}$Z#g$^JsMa z{->_N|E4rHxzJ0bzo^?!AzBBJlBu$FLzH$Lx2C5*aPd9dNhtOyp|6*j%O(w&0kZQ4 z>&5jszQw?%IUwNwP_@ZF88?GUBmF{dP%lMqG?~?(k6#XoCtRs4xcK#Jgv6^BXfC`% zc9j!wySwp8e0|xY$=@M@;K%^lL?w0;wU??F8^?=vz6}eWo0Q)x_6&?bOQ*C(FN@Hw z3c!EJENlEbTUKxf)FQ$`zn}kza8s3|MHaB-=PJoTpGcvG0v=yWh39C0Pzrl<@yH$= z60>fX`HHgqggq7we8HB%2>q1#P0vWqZ?0*4!!Bz4e{#XGD z685lGWCUV*j{R!xY#6zl1OjX7@Mcq{!G8{<#4yeqtZp>y_!`Kf)OmM!$2*pIXQ^-gX0_I3-{$su(@RUbu?j8-$?G{{Ndx}WDWip}d zA>Xz0<9Xk;MdP^9H}MZ9C9uBY1Q3_Oe?4~WR50}-x|W$t@i1N9Z|Q6O-CM7@N5Gc1 z{SfdH5V{X6N@0Q`8YBTrxc@BQPA3xy|LGy^95oB^;q*k)A=i0j;p@!zRGpIyTKxCX zZwx?s|Et8n{J2f-byZxqb;s|2XAU;R+DmqtiI0fiF*U5aL{R4N^WVN{$b>~X_V^8M zK|_*@^k>57_pb%MVasi0JSIqqIm}cV=Y11 zk`7lZVJn;YGh>jsBm3vKl?vnL`@&)!P0c?>4|^vQo%)Vny^rl^PxtC|+L)AF^lE9j zH$1=eq%VDEQ--Pljx%iF<)YVe#(VEIrdj0p@;b`tu(G%`7tup*Jp?$e#$AQ3IFF98S6oJ=;P-(Kc}(6 zn!?2#EDH0_9LWkP2(=IBOxk2N9}Q$K|Ll=(IuZugv%Y;!v05L~tENs>AiOS_!LPP% zg>elQ58Z#tmisG7a5Oc=Fi}0sZD)FJCCX@0X{ypR^jEVrg9wyKBq3i%`2um;Z*+!boa#ND} zyPsIsVILL2&Dt6X32MhbMu@@)P3DJzy?3kC*GzdiJ@E(T z9(Bw_dZUAioup^;ni?woRivmwZnWVzd<|B>2rkA{v(iKU`8h`Pu6ZS>%<6&j z5Efem8(lw^-n*8%Gu5GP)jM3+&Zmv9&S&Myjr*wPLTY>Y`)k)aNLNp@E{zlBjl0G>WhUXi`ikX%;hWm95hF ziI2+!)FNua=~%9(e<)SWcsY}kNaMwD>Y{T^veLi#Q5BQuc>P_x0EW&{ktS`#&)mDm z`(bn}2NTKTzQ!Zh{(Pc(y6t{wRL=??iAznLej90UCs^(7ut1b(@3W-2DkdY-xvD(N z?zY2Vx`OJt3+!Q>0GMBWKahtPIXO%uwB3eh1MGUpQ@iPe$cOBvr~)8z-SQvi%nv`J z$ZkcPHsHUpQ0$!t~_)9K-73 zo?Q>}*E6(Enk}7eo!$`O)e8k)IIJM;FIN9b++bjdk5FsmZ}(uI#wRUG99g~mYLjah zqXGvmq?j_O{3C(D=7F;xX9Q_KvBKKblMfiKDS} z%0`B3tNh2CPTQ%#g`3uoqp1dOTV5gPjA5-0v80a&=-gH$`|XJc(;9YWF-tjHm_ z!CiNb-cs6Z4mV?Tm#TPTQ(mhzsho=t3(1wqr_Ce$ zY7LfOT1QxhYO5c!$rgxuUr6POG-rQznWjMdXg+}kG<<<7u*7Jy4gxl$t#7wyd)JT( z4wC?i=MVrCud)V>Y#j1c^}8bM;}54oQg*CMnjYa9c9FrU&;M>O4w_Ih%IG*A z$o#VQYde%RSUd{p3#LbYgPQ=7<_`k9ITG^W>@fc2=KUZfSj5w7_SM`lm_5lRvt4hG zEQ)1X^HnkvWfPp(Bu-;8De5;fB~0A?D0a_ZSU=q7(098^y*a-}y;-YauKhSH>;|Ue z6ERAvb(7LWWiK}uzFS;&{o=cyN6KBI9c|1;51FqQo4g-2`Pu+KH^Kk-QzCS6(_cSD z7gd6oxktEQ+gQQ3)xo8QbhN$osyNHBg3L_TSb67p(I|L(v)Ve8J87eNvRIvqHW=W%1-oYGs zn@3RvDfo+b1=;Le30?#q&tbV}wniSpuQMQyMlX(c?+b?Wg;VXz(EVKRtlggwcQR2; zvO5gC9S5(S8=VpI_>~>dpuOYhH&vP4E`v7R7S&u!Ast~C>DlaZ9>m^ zB<*aLnb>E#a}*5nW!?*319y7&L+T?zt``|f4n#b^f{8@g|1~o70DnQMWkTY$iS%T- zn}&sr&Om$Z?|SR$Pew8^!|Uk}9GzMy_{bu&pRLObu_7ZEX~mMJo!f6O*Cn5Yzi3RQ zz#f$WXsFx$Jn|7tIE}$3bhG$?$Hy^h+nem#50(<$0U|w@@7w( z7lNyKR9+C*n4gCHox3M4>R7Z+^@HFezJ~i*Fqc^de&GiM{Rl6CNSx39S`GdhkI}`z zw$nTNo~S<;#pI5rEwx6vDC^EyBwC*kS9a*XLGkH?Gs?Rd^BzOW4Ml2q4#)B-h_%VC&_e>NW2l@~|$jv~gxH8QG>=wbrF0G3^Yh zGM7PUh%DF}9%ob}sEgP`R+YJ>yr%H;ybjG@j-Q<2Tg1Cl{>~q z-=mF}M_#7B7hF8PUM84G@qv7CV3XglRF+hh=j8y_I6Wp%gF8LoqFtF6);QGFL4VXL z(I($ObiXWa-VtZJl@T3B>5PaQ|C(8I%R$NDw6j65#Zvfe$N1dkX|$Uw&0yswIdKOf zGSS4Fykb!$B0QybZIBp0%jC`!9f`l91tM;T``-0TosLly+x09vO60cN;sh1jx$RmL zv2CVcjk_3G&bBA1=^vB7&oCv+#D^w0?@StLl-+8hQClBjh??Dg6*0}-9Q>39_#GIE z5mMWLS6o?ew5wjnP6+;{_}+2D(8TmUZE*9KSA+~VSGLFHrb+2XOahXOq+y6_ikL)R zKBtG}h^6V7Gqr=$quoe14$dX`=6T?Uy*%FOxYx|sb`$B|>unL~!trq4ywcPzJ$XXc zA0x=HYK1j=muXU}@|b&3^BQ5MYm-zT-HX0uT)aHcc{}7)zBsR+a5!wvdObJ!Ly9V< z;Uu#v!Zb-CX5W`Wq9D&quvp_!{87=JTHd+z}^X`A4)LL76Xo(Zwu@ZT~ zI<>WRCe`BMz4yVQ0+Bfr)?^XJ-_H$7WDz#KDZJaGZMEe`XX@<@GMPk+yWPHLW_msw zLLbU@1S(3YpJ_+5=)Yhg`J(i&fZ<0-H40NSU%$d~OL~d$nLe5akM-a&TeFszJ8`ro zS|A)}h3sZr5H}5nEQejAsRh}N42*t-U6N~>TK1wQ#=<-&Mj z1%ml{yro!M$9#i$inp|%(>ZBk{;rt2hjui%@nNSxMrX4u`T_m*c%2HnfMoXwxTwiP zFjaOgA7io@XSM^puol``*kwY^S zzs_FKy4$U%qFzDWEh{9iJqmkQ$ z*Ywdy!h_bOY6^WVv}qcHbf|>6=JbMYmrF<~VNa^%vb28wC@O!8pzqw)bLh6k`^`<^ zPw%(#D)1}gByieO0Xq2%1IP@9C66jF@;MkUW6H3%pt97+`HIDKAQ~oNkwsVV1oP>- zq!Le_U31IGCSRC~@5y;^c_#C?qD(D`7lya*h5y>_$14A)n2?tWf`AG4Q(KO#QOr}a zRYSwJq*~3y`*w|5UQ`8%-gdyduncU3<~^|(%F{oicrsF)oke2$ySZvL{XQaWi}`C& zZjiwCzKQC4jko-2=+w4D?8*UvUMYxxm-!r1NlC0fp@&@5bHmlN$9QeA6Qqo0d22i- z%)h9$W=tYE#Y^ytpmZ=nJ|Y2D`nYAT6oEA9EHX&)Sq! z<9|!MV{K57E^|IT5# zlh(odYKHEt+VdECN>da-Xqk+VnlSYb6OE^-pJ0r<_Z**)aJikb-(r6}Pdq<+a=YLr z5>wmvc-h=@xy&B#0ok*D@KXior_z@{jh*3cC@W^FoUFR-n`WE&D559K$>+b8H6&30 zn58;NqRKZVeKC+$lVK^5`^sSSWB81~psAj`S4`PlfLk6wkvI?P8Kq5WO8?kq-VVor zsi^?$?ng94L@C~K^j#GasHztjF8rAZ#eaMD4uM*+c6_VCzD<9?%XUi9@L&MTga(|z zjRA@!5eg}mq6*5gv2kZAzhb@^MO9F7&1DupWhi6D8O5qE3FtxbtY+@3Tm0|Nu2Np< zA*@SO+z2tP9Sy)ehw)jI^IGSOSx?f;#CftJwj|zqaOLKL?U=8z*AtI`fQ%-e`@)>I znQesGSCr#{P$I=$pnp_P6dS70U4OnA*Dp$Y%{7BC_j6X*dguYa+Cx7eeepMp2WULb zX9KOqKGr0fKb$5wpNn<=J)|tz&Oq^@Lj=<1_Mf*o@4!AES@AJvQ&)24I(PnIj~*2^ z+ufU9%kw}JL|1Rf8>fxGnbIf?05OV>JRznG6^rcawqr8i@4~oi#dTUvaf^i8Hf?lu4@L}T{enf{1m8JDL zGbR`=?s7P5K4w>IoOo3YINph9`uK(Hd1MhSFAz(3^ zG^!x=kv?+3AQTIwtB(Yv8&`Uk1!6+DX|~YAwHWIx{unxtKeEk)4>0E+!UI^wB?5pv z;tl{N7bHa`^mC5Xh(PheZc_wD7I?nI^A2&+_P95(Lu;fR6Pet#729gnq88xB*E zG=4REutvh{Cf|vCS^l|b^?qPk3%DUL7uIFJ)O2rSv!VUZ<4W}0f>*L06e0?iSn{+v zrgOp-ZST(YHkb%zX%+p-5w=(5eIj5X*)Le-@AEfpGwLtJf;9#(Ew?4rnxH(FMk=Iz zUqMW8Pd_r0f~sc7g)_1Cm_}fB-u%@7QCP}e4#GC`v?R6b;2{7nKhIA+DM?-bOHqB^ zaw}&R5Dg?{250+xJ?;laj7iOOB5U9?LB;Tah@Uy%szC%KHO+PdmYEG17(RfG)_{~` z)Z&PDC8&w?f^f`|%Vl-xFuiA7Xwk6j+3yajXJdlZ{^MsWVKru1`e=K?ulU`y!In3r z|BARrL#iqYY1|7WV8@*K-&Y)2wI1T~Z3Uk|=!+}EZb^})gsJ)rB(lozA8?+WSy%LP ze64}cVoecIF2;J^{MmctfeD4aaYW4?y!N0#F%Sb4jQe}f1EVy7q@IH zsg^-yttm%1e2U>!IKnOlVqJf7H|4m*`)dCFTZPwPYFu9dQz!?C*z$W5)FxY_Q;yGG z;E;l)(Q@SC|Kxlp=~&d=tE;>7iz5*}!+^H7$Jw`*ehLD1*(2{Ys?$pUOV;v0p;=^A z720S|geCd_jP%18q!TLeH6n2JZGay!otRrKu9xooq_fVEWg`6)?Hc}T46A;{$t_pe z%hd|wR|+5x44O-Q_Y4yaM^?jXGntt>kPvg9$)CnHP@0MaKixz_N1O!0DH?2rI;%$% z|D}srr6;I?(X4R1tw%)dEiv$&UeV^h2Qiw&LQldqm>=-Izv2dm7be$~6qoT>bDgYP zSeOU}b^%P{qCRrw#dDrB_kHL!-Ln29-^NQiM(uA(e^1M@=EFebuXmD!A6sd}2!D_; z7>v1FSZCs=SmLzpi#b^Df_Ru_*V$jiAoxf?F2jct?^eUHfQC_}=shTiCxO&XFbm{g z3z#&-2qjtzN&$HZ+nw2DU%OQMpHM%)8?)qk$oVU zwa{^Vldk)SGKwXjaJkT~Gl0&P?&bDLF<97V1xOxK8TF7Ll{6eT_~(F#EwT1Ht-$Gg zo^QnUa0J~~zps5p!U^jffc)$SSYe_eSoGIJ;>lZT9;{N)j|1$o?%uEvl{Tg)*KOYI zcENU90Y{VJ{yzCYK}bPwgsHx}&O`$6q%o%U0kBM^BZ(cWZqo;tFh64G)jv>If=7rY zvfo0}8O8UGeSnhkIaPt$n&IBClUCH(J2I1p22dD2_-?gCXnV0NNM<5$^qv(3Lu)v9 zX%o}l%As*5O`Ro%3`IV^v_92RW*ZFlR1EMRM)q@n$g@W=Ud{`(lrMK7CR_r)i8Y1A zcIR{!HD>XRl91eV(44^g?pbin*~w>`915?RgLzu-Z@yHT^LZ6adqdh$#;fq_BLeoQ zl>EaN|0K(h5IA}Vz2k-+?ff{JNn|X96*fQaAkQuI@xowYP+$yc3LM#ya+H% zJ_sQzKnhx#vYkermA0*V8%U0i2$$%=dQq&#!vK@M^k?hQn7bh(V?EpT?C|Tbr#z7S z>pbMh@(laToLbk|;ze9l6x4nhOHGM%tM{ePbx!_8<0}Ws0>Z&Y0g;v%)U{Q~QDSEX!&oP2wi$}n z;^za3rB4=xnwOp$& z5fY%n&sx`Nd3Wn#EV116M_+W06~-4wD9VY5fL<7D3Gh$lI@5cmhq6&;Te(I14>Bw< z&i>@5Ea!GKr=6wk_e`+OL@bL~fVd$GvSRWOY^6vB_8CIWPHUary1Thgr~&L3vGAt6$iA~{U|ZxOvKye1d#e-r>q<0){+=3( zv!4x#H@?#{`a50e;c1`S2X4ZmMPZ9gDfNlJ;y!KSN9{09U$H zpd~L?Q4*JeboR@TxWVv}QAQ6%(me7-oq9<(e<>>bV-Hu%bLpy!x0yfVvQhZNs68N$hV*{*gs7K zg_-#PIkL&102+?oXcX;Hno5nL_RysYIc{C;FbDA??4vZqhFk#F<;L^?u3p+&Y$?KT zMb5dHx|~0{4ui$h12xNh2@=IjddQTPa)IL7-)VQPE(vAe1*$e_0G7Uoma7RR5Hric zlcsI2H@mJU9onBgKRGU|%c??ySlEKo`hB68oA$W71&S)f^>WJTms7U3#yQJM=wCYP ztyawiRgbivM4k)h!6!ZjAnXGXM~v7(r1$b)0wXb>@OB_dKzOMj#eM|n>w2eUJKZgx)_1W%SCFJ9>xi$tyZG!#ZnZ)F2T1h^ zPy(+DyO0I*hV6pja|2MZhCgRynuzURRsyEu2gn2?%LK)r0iu)c;tM|N(*e|Szy}3e zx<^-5?=}qPeivd!YES~mf%k$~B^U)p#Dg+85}-hm=?pO}69sX4$sare?LmO;L0L1H zD9|a8igYTbz8MdFdmmC~7+IE(fR~mc>pPqp9dQa=NW68RN;&MlBp{hoGbrpMZnlTB z=7L#(KQ;0eQG>r374kT8=aQJtZ&XM0yKQ?{sUW}`P&v23zuwtQnBKc4AjvF=!UbRE z|L>QNEB!HnE$+Vu`BQF~2g;H;w{r3XsSQo~sRV=`p2Bvh#tWY)66hHVazJmSpzDGD z{RAPb%gLld<`^UbLAe=*RRpwsfG9w~UG4HqDd@`}c@%um+JnlnT^$Dd>|hB<^BG}< z^Vn&-(U=3k6u;R)h>?T?MA-mpfmfo@&~%W1u@&lyj)8w4jQEFnauWm?x%gj$J$(rM zRqgt}5{e#DBLpH|92z2?Vz!V#!y{U%e5Te~;kc_!3w|#V;EfXBEpd3-v+*l)ekF~r zGhByBctHL>6J3d>kh=2P?qFC>h*2y&9Q5sem!m>~EliQq9;c1@ueBk&qAawm_DjKN zmv*w_r$kN|aw$q&ej5Tk+b$2~7%vHv1EErw>7|I7%A-OE$HqYH`soO)&FFH4dEnjI z-#n<66vA)8VDoa2P?)NqLassedq18_@y6{ZGJ##n5X0rV>A||ESabCao+LoH1|$`r zUy*{m*;Gsoy4S^aITo6@Mhr9=wt`Dpf)bvhkl*tQgPf+sYb%_A*W|nW{gUA~z-|fW zpj99izv5?p8z-vQPnen0Y&YuMgBbGyDuVjG>nF;Nga-W?0&*g-pVIoMrSKAuVhEf_ z^g!&Czz3y(d%Fq!FGpJVFDIJ^$xdbvfLqCNgO?i+d^sSSClP4{J>O%6qd5+VMBWD8 zH5kk#4-!efd?pQDM^SAkIRt}*>^CMymLfE7m`XPEKx!t_!vkn|KLKbX1#ID0YDyDN zZlylT@;)>;3=di!4DUs6Pf2e_yPxM$VnOMa+AluFgQGCaWvY9B6ta~YDr z3qm%sdRI0G~fGG&w{ z%jLY7AX^DT4MMwpYS6=a-cDt|Ee?By(_0QII~HkmX!5}^Dgz?l|0 zFM;+Y0j34A=2B3(x%H5V^|}SpK|}LH40-ze#UPORLDeWB(;~rIS%s##v&(*|5piLV7^yu1lKj1qsq$4sNfh*cHO0$E zmqE@SSjAtUMice@7gP&?DWV`V!cqmQdZ1+5t>~T_JU5D#0OKY@xYVz#|4I({ax1Gb zJJ>P7Pud|I0i$5|c*26rB8dK7Yict^4*lc`1Hnu`04VK+tDF~ScgI10$pI6XgT0Ry zcz_Icm!uxG?(IpB$Cg@ESENdyxYY))z4t_M;9Q6W292Hujb8A6VbTUu%Hq$p0o{DT z4(;8o#a|GN?!y8MdjX9CeMnt{!ojfMqh<&h7x3}Jh1>)5(MW;@0-wyU|NA^tt$;oS z+z^k!N8&ZKO6bFODM#QxYR~@PP=E}i|F_UeGdJ#BT%Sa1A@l6k7{o~h85QXg=g(jlR=gvcPEpfI#ZcPcFg0+K_El*rI2X%Iuh zPzuuB4exP|*ZsWj{rv;qFAv+s=8T+itaYr|*S^*frmd+$ahBmM3-|{p5Bak@hE6cpWoqb`=m+PfEDXj9yQ?U#=bp4Qb}lB_#Yf;sR=A1v zCefc}6oToliM>3J`=4yw;GdS~i$p=?te7@6uK7~PCs-r+3D0fvIYaow#s8R!@Ifws97qHGoK>eMC4A6+)W}Ns z;7@!JPDJ=b8!w0;e84mW%M(7p&TyV5CVb)&{NG2xVE?PD|22mHbsT~X{Qnpk7N0wT zfaUM1_bDb?jhm?=QOz&AC};PI!&w-HYX@KHl#@sf)<|oTt7Gz7Ct-In&P2v$b#T?| z&x1k9y^SK~1^ljyTTYDq>f-#^FD7Cb_68xiX+OwP6;F^=b3K2-xVyW*()8hM>~?fk z$0CcyNXppT1HZ~?(Kwk+K9#I1ApoK|=~biewV*1=?)?1!RwnX>ze2!;wH z#8Iv!6hdTc(bTWZX|;AY3Wn}=TfL(-;pB&}EAShQV@$&(3D4x1%X&Oc5L*0&cww?M zw{WCqs4G%}f>Qp&n>ff$Z*Tke$&7M@5qDU34^H%;toE9fhRV6bq z*jwEwiGY0*CWwyPcv&8hQs-t&N2>AU>ZoFe)XYa`yo=B92L+S@Om3WzKKx@vjqNbq zXmTQ0fgB-?U^D0KNure1z;h)3K1U~E49*Z0O9gvvK*%qcr!hCCr#{(XDm!dRqAbMt z(oXvZdk<^e=wUeZzgKX8E6^my;@TdLvrU7?EhdZ(50gipo@R51Y?-MTSVl%wOOyR1 zhT&rfYW-IF+YZCG$f71&QxkKLI`;N35xMwhsw5T{X-Jln77$RHJT`XO8Z5HouR5OR zy~oQ{mvo&7_mHby1`2l!0PDkv5Z=z}hAO2HVc=tfVLlL&o?Ic;lMyWfNnL^n0%qj@ zkW2qZh`y2V3-n)6hdtUYZ##^SKAOkmNm)etnI)R*YY=E+G4Tp$`;Mzk=x7{;7 z(zS~ll_(gd|F8bC!;rXFU%xtl8#WOxbHI z%8ZG%0upThO}`0d?u>J9E)C{{#1+~8WTfUxLj+r-%4h+-qY(tX3l`vO{}tmNL5v1$ zHykeviADWatR^cGjYuH3w$|U(r-9W6G6mh9SeaWG)nY^+x%SdXR2SvA-OkHrfqkR= zyPZ^7s{0KzLW2wtaE5i;&F+KV~&Y!3SVwivSUnt=s zRzrbZUlhwxi7iqeDc>HMyPM!R$2KoLxRZ5T(^mx>jMVR<>7gNpwJH(ps4ohsZO(M~ z{akNY-0>T*N-H{ALHkoJ+( zU+IID7`d`&HDc7r4tT7V)3fk$8hSS5E-Ssqd7ZVpC-3S~#xL1bUx78){M}=6urs)Z zh7MplWU@Z1Rn8OB9Cnzi@gD8>=CO+hJ?)h$*bEe8UL;*9vgs)fFwyGSTX&3q_#~j`0LxhUx5OI zAjcwz`Abyw{c~(UVITa)E3nzJD~t2BxnsMeF#HR`)k9f&+$;umBpqX4PKvjheyZi3 z4)Y;Dvc4=2ehG&UxVpNT4uM5_LyM$M{_8bM)+G8Sqx)+ve#L{Wj0Pj|#CXfoEAzow z5xvE!6|BqjS3Qd}@`PJk$HrFc~~aV36BL z?D$%NF;RPe$&i$)CqF-t?jMC5_1?Xf$(naN9-eVyTGv0F8aamj*0}1IYy2P$Q^;uTYO$U`XiJ*7B>u&SRwS@yu3(zUgx{s|v7> z8vU_2xFjWPpPleFI?)=6zWV*jd-;8Xe6V1f2CLMmCkg^Oy! zv&>i0a6!O%E`tczuti9Na++w8?h9aN9vu*NhLsHL%yl5MVC`RXQ^d<*p=cM+{8tBW z!A?B{f!%;H)U`A*7U#FHIPWvgNDN!}s{{@0Pca!3&eL+!Ey;f6O9{0p70VaO1Il*5 zwYUh7%!QCEJFXKKVIMCOY*9Vqzmv`+ot&!G!1p#wzO*YP7c_5^kl098R}1t3TgoNG zzy^^k<8aTts`??Dsi-_V>B!IjpZwGJ7bGyKGa@)O!S4LKFp5kH(swz)ueR9;Mgk`! zpON!;IYA{zUQvR4A4_zyYM!+H`qdBgROE+Q{dt!Wk!|m;hMvW}`eBmHG)r)aQkdXh zSL(Zq4FKEcTMb+(rXbPx%}RBCOqt9{=JE@P1gF*`$f0t*l(oFO|IIuNDStiS0)Pd~ zhK@d-?)$1EXDMj@%{&6FtjZkbmAuRS=vWiK{Z$(FU{z0K}h|5YnSm>DnkS{4#d0@5~a1B5?P>CKluR|Q|# zc(P?-3wOE!Uim5+{QJMN7GmQmKoliDEMzdkjp?6=aBVuO=X6vZ?0@MWIjPR?#Pnz| zbCs3<$8+5)MuE#=UAuMcSij{0r_4L>^o*-CfME$h2&V(RT24(zwGH`rj8RvW2mIM# zqtHsAX+(~(-q*?V$^H+lkC9)s!?{2B$lH{6S1Zc|n4WJ)rgLC(J!4;Mv-p?&1MM72 zL6j6V@QV)Te(bc(dp1q`Ym_ZX%dlMHpe%l6gqUyoznnb|C~{d9Qsf_$$j$Vqckigs znTQv$U>nC%ik{x1#LH=41q#9vVAg9jkd{B+%DJAkwL({MTQ+<%0xUE2`&U%_H9J-= ze&7iB_mD$>O9pn5DM7R1Pgf-=pr_JS@C{t^54c7Ky5GG9MDFp#mE26dyI4D$_gGC6)M8UP~!JrXuc@=_2Ar5w_gkt_nOs(0v$(NC!#Npm&@2+wC39KmWdl2S1t>< zM(Vkcaya5+gyBoBaCVe<{GEduQQOzuzwH6m7_LmXO(UsGhaxZ>2VqO?3sxjV>>xO3 zKY^U#5niA13N>+Ud6cWE8W#u*{>31XYAX`{hAO>F0CO6^ts)^MD-o05t!IkYuS_5!k`IPc5VyUlY{W8TM0_ zcsxJpymX!ajwCz;0{=DNzG?kiJfIPF81ntug`rz~X%h6q@PpseS$(HVv9b01nEn6C z$Ea{<6F=rzGjcIpiNg<*_tGQ)BfxZnXo*&Wlz_1ZPE%veIM?Co(t{1=lxIZ5+e5ZQ z0|_bk6fZ}S#v!7Kmy;_2io@N2HoGAPO(Gf-i(>}Zyy?Fp#{fU-69uVA%UZ0q>U@!o zs#@?q><=N9#nakhtX%X#l>dUP8W(zhW10hm{vv=cJ&)OW3bmlpW%Ck=(q+E1-ojmE=qS*B?Ng#6^s zb(jYsKyqff%0w93B$U~a1FI-dP(%3F5O!b+6i(53WdW~B%{2z^^RWSy=mVR1eVJev z>UgR_d2kCrQQt@is<>f?iPioVtDUPO^9=pykqf}^!$2+6k+$U9+gtpO=0x%Q zgxkvjnCt9;K%`Q|YseGYe&HsZon2PSA=FD&c%XCP{ux0Rc9cTmwKag=d|C&HacJn) zU;5xyeDo*2v{;|d^ya^HRDb>pQLQ!)RGdEFNO(L!vZHm~XYbf*;@C3NBSN-r`R*~u2ptuD^Q_p#CK`-Wc2?>hZVB|h%r?hUT(+MFVfz4nJ+uIQx!=~H9dtN)lA()Z_!_WiNq>lUVNZ*M1hfNJKd@%IMkRCs1j>xywo_#|~u^Oox8k zb0zjUf7gi!V(YNj*Q}w24#|o9Cj&otsXoP37w%glk#^VUd~BwJ8SiF<6}yOeKUvSb zohw!FqLl_gB*+BWI4&C-fLFCQQUJl!oIA?_5r{Wn55y#dkt$sFT~6RGf2*8ei+Gn) zDkyCofMdLndi;EL+^aqh)JQ=EWSA6*B|x}hQ>_`%L98F`ntjH#Yga4p0jrB(C%h6{_diT1P&Eo?*pQG>jY{MX1uL0<4T@L&A`H47q`a&&tmL-%@ zIUT-zasPFNQsyvqtmjV_?tHhel_H5nCL(Z$)<0>xQmt*Z^P(Uit;SU zrDc<=AyOkNX2tJd+{J3SMQ!Xga=X&S?N#0-TFX(zJ!%%wZcG@f)iu6^rw)F*HjQr) zO3+@$`|TX2o;K#0_*GQAq_3y5!N2qKUQ5F+ut%sIM*@Y;gl`7Xjn{H6?`BUx)yyVt z{)CxAPRI&cMS#BFr@brkR2gVhHT2rF>2Y)bh{_q46urWKnM=uQ7Z3^GJPP&aN)R+n za??E6-6)yDH=&y&ohR*2%HK@7D4#P`s5vQ@-5wS)5;r$?N*Pda>hGik$aYj9D<-dG z9ELWAJe&)I26@K5$kC(tKX=%{}Q=5B5z3E}O z;W-PfYeyBoGyFsf*U{`FH-%*E^Mzd2w+9Q3TH{S64{H`37&bSRy=sAg-a|Df-Potc z=2mlq^(VH>ogO5ZZGuNU!Un`=T3L}lEx??p;@vY!x<`iH14GeYBir1YqE7|W$%Ruy zrJR56w)w=R7YpC#62;rhSn>U7YXbEID2c}nBoY3e5ZIQJN0ZzmD~-izMD5+(f^hV} z*(yhup0n1b-MBE$_7ufN-!HRIP4DI_GT;9zQrr)hHkP?OR&Rt|{%& z9&wr@6d3tt@_gEQWH{qWs1R}dckhlp;ddTmkodz}W0GG=f`G-gjWWQp6`+8|r_LA( z_XJSy1Yn(X6TUQ1r(cF@c#tpYb_=4SZ0D9b9~8uneYo?0CMv!dCDe7&GPi(N6;zIyr7E`NseRuOZ zqAqzWeiZJeQ)lQJWI`sa;T(fmPfvb!Kuq7e^8Y+js2_<7 zN89O*FyheLXz~`WChY$8=qkfA$P{68>seOLi{6p5M~T0e6V`)SFN&9ZO&o-Tq`*Xbv_$fpXd&oKiu2dXvK0ai+7>3U1BYa_pt z%)`MaDoM|E==PQhqiDX$1uL#Q-rpbI!tqULQToh!O`W|q*;RL1e_B6W1jX0CZaXS{ zT>U-N=7Zb!l&*njo4vzQzv{(Q%7bJ>^SYiV3c2g_R~kSX_|v;%cbx0D>10(eR=Y0U ze`*SYajFXgGFofoy$OiTt>;AtjQx6pM?{;t=o3QMBb^iH45d9#=Q^k# z9{_Tx$xasbj$pk>&yiGozlLIZv_PnSAe(!lN}qY9PekZ|k3s(8B{nruN$RsouW18% z)_!pWrqYXDK0SQa-S^?uYqkLqn@A^zYoy^Xd!Kn&H=}0-nW?kgYVA|ShIwI}KjUu! zu%2c;GMT;5{da)&o0z-PN&)a2qHfQ6@@1*kVN^81|!eX&_n*pzX z9%_(kbR!se5N9V&k1m_^o~40zTU$@aW>^ga1}Ksd(V$3(ew}(obh4L7yUlj`Xl`;A zhNWlqMm9W_V*kO(j?zp$S?r%W8@N(6lUF3cgFy9vwwQiwe+~97?Fw)(@iYoW`N)16 zvxJU=G4t$W(TuQ*^6Jw4cx7sMluFG>)%%GxR}vWBgpjsm^hRh6s+edM3pku1EcjrS zy!(-x&pOY1sHDAbM+70ihtzM@8h3o4C%QZ}ctLhL(~*onV|WZ*mK!P@)_Jg$(>C@D zi1cX|pM68ih6ol;AXfl}SNz(KQdk_Dn;AYT(J;J4^H{ax_Qbvk-O%~VzqLKr(v~L1 z`n}Ld6MlvGu<#E(C3Tc$yIsLj@t4X|98KM!QlUa*@fB9C!o|GR+Gp*o zTv5XQ`;_&5(r&BCnjH_8I6ovAe?rDip4Os$fV1m}^ z_5ykGwC=eVz~Xf2ek3`c9GdhZ!vxuhF?&g#r4RX!@S+#3Nm@iTG6{%}JKw&`M7EWy z`i)miPU&k?;fd#Zc6MDA8Zh;j5b)2w2Qb20op5Cw(N40f@Mb)F@4j2nT?lI<$1Cv1 zsTR};qib(k-dP7Wr-r%S^H!wSv-woB)mUbG-nh2ib8*zUd$91zu6xN?R5fEXh&saz zt=%^nAn>`gK5=>|AXdMSx{0ORut{C0U0R&K1Sb;YhT>y994bOVSuwVg=sF;saXlGT zb*)3(#~0Kp9DHe3_oIJt2Bs-T(j}JfO%+KDqVq)~XKgXk$NR-glFE4&Za}e@o*a*a z4t9cASp?OMaKF;y(@bVDa{up+7N9l}c~DA$m8T-Dcl`E!^&3i|&38kgT7ix@N(>Q> z)J}RTGR(>)CHYF57o~d91$V1+=gO56C|DRohAo0#%Jp)D|Mwm!>%hC=fzrJ+*J>c_ z31MsV6{Ses2qaFi6OQj43}=0!)$xH{Q{1Z2bT?F2i+R&YxvXRZ2+ghB+!62cEPpw~ zo*MQp3vvevKy*wQ*pPRKo7WlJ2zSF7w5SmGzOejf7JxRb#bCngu`xiB#SG#?7Ix`T zN$cJxD-0SSqU;MFicK&MR!YOvV<8Fq-XOJoZB0(m_!9*`{K4M4YGd7``p;cN!ae!# zq8+*@etbZaCvV3MX639J$3e}UGKWQnT|Vxb^5o;gVR3o$FxI)3(Y&1uBHLS(pa?K( zN>Qw~s9FAiux+(oYaYapxj-7xI+89mf3}t-aP<}-z#1|Mm_ZXle%4gZ{?f_51nBk! zfD4e0vH?RRlr|W>E?F|};ho@faI`c?|NXX8yNr*?!Cds9MS%9GZaBAcdW(BSvB(XC z>XdB@R2>XoQJV5NM&D9Glw8jcO@#D!9raGqZHbDMNRWeIGwA?NJQ5}KEUHa1=|1t8 zER+0#dxWX~vRG=_DScDY)(dc&OV+R`+O+NlQ0^2(%YG=!_v4jbACoMJezrx{c~`}6 z?O{P0XE)7_Is~%yEYG#Ay`3DHkUeo8l)Y3)uWE6&uB@;yaT*M`on zYWbD%R~?a;pwD*8i(bX4xfVxrBNo9UQ@+xf{jm4)Z1TI4+M}gHzgPFHBkD?@x6AGx zcloi|OfP8$i(K{tfYowKaqr84ZYtCE#4H51JZ$b~O~{;7T&{oiYl*r1jDIeKxElTe z#kNY)>86{3kCg$!85sK=C)sEAV@(MNW~8srcAm-XZsC#oz|C}VY;ZVxYf}q-i3R-? z_zT3NRteMVim;0;O`&eUw|<2tmi{t%uvBV(el+)|Oz)bIJv-x_Hs8rW=Y{vF-)G^aM0{yx4|aN4qQtKi z-gCVSSBjYFbsHVU4MzRqWRN!h-it!lq0iLLGdXxwf*vR3JVdhc5k0ygvBV66MQULK zfQ2msB#a2m))J8)NU9V(7ie~M6sk-cs|VG8ddRFZtP8GS1x(-9?z9$7Bo$+lt7ia6TEc=_yXFd4?aPHlLF@m?|9(s(kR1 z&yv4=K-o$0woMhW{K6ve{?6%F%5pTbbA2N}1#HzOPWmiMOpP4*L{x&ua*PM#02@*p zuY>xHglSVkHa!#sD!>;=+*ZWrX)+d6OcY%5fMoUw<*DUW&pW*X7b#~!!=mx6+85CR z@x`O9W;-_#hamhYXjWQUTA|5HV||>wi|+d^l(_N1T;G*cY08r+$-u)EH0oK(O)>elj}IE}NH z#+uKAky@KyrWLUS{De~U&OHGT{-Q4VMJEAfKvJ@L_aqKb zhvwKo*vIP-`Ep*@j$|xG$Fs!egYP<(U&n`Yx~DUy!_-v-02E1dPLQkkrQS ze;cpPV*aBMtmv-=d0*;PbW~@rP?E{9-vSWSzYL@`?;*hK@gl-Q9_N_se~2}7%in(e zZsJXu{lSYhHWkeW&+pQ8UYQ{KV<@5;az>uzGAR*`D*=!Y*ixOWt9BTk5g(R>0$}^| zvBH-vK!G^9FWEGB+D&#E=jbR7X!Sq$T$OR))%P;`S?4&#&iX5N13$*_0l+&2ECMdC zEz=xyfY3?r=nmI-h46!7*5NUwfPkovbWK7m)oU?FPLrdx+k_J%m!(m}XqB!TufLYL&~5*;6!Z1Ymm|>b zP2liF|2p!rAG6|6j-JpB{w+tLJGlQ12*h>?cAO$N*)Bw z(#<4TFJq#_3hfV2LxA0Eso+dw!f`ou#-|>CzD+96omJ%Q>Q=7?LWeLKXhZt}UT)u@ znXUs=8&>_RClgRpq-xL7FD~ekP;9`7>HrnZFQV<6=@eU_yM*n8li+xvU8L0nlp*My z6W>{-eJ%S!|TAp0E-~8ZOMMTqRX|TWNkOt!%y*w4H1M0G3 zw7~|xjnRoT^(dE2bbvB(S>3g$(7X>$AoIGF+GL70r>)FR)N)GxVS1E48A8-q_t-;X zQJ_dI`OsN5PA35x=EwvSFdm8IawRU6Bw#`E*Z>_>G4cRZ+st;fg-!By=hQhjyHcA^ z#_R?`hxjHFvI(Sxu zHD&4T-3af(&O#wa&}6uN;lm(J;K0o<6k9JyW|M9ph%tA9!3`pS;|3-j9@BHooh4Sy zxI6`g!UHi`gQ1;fFj4hXgBq_}z(E_*HQ1=d>IV$)_cCY{=avSH_dsJw1;iLM;W-_j zk+`fCg+mW>JH1h@aIqWqi&I(_^_`wM7I&(9X#$UW;KZ0@DCft!;^i{)uWbEjTIV|> zB{rAqwbJE#3HBNr%<5)w)iV@H1+#-XzYW+PczX6gLhhOCN*m66=X=hZ#iPmjDqQrD zv1ie*pkXYD(-b4OPSCyjzYnLw%SF;sgbn9({p_T`Kg76dgMzMSK%@T7I!4I%;OfhW zU8TsZ@O8b0t+3U$*#g7&ntaA@B&v6PuVrny+Ofj$P=mz(C6E4(HY<{1Nhi(J)Z-$p zQCF46JC>k5Tw*$v)Ad+HDCI`09x#Wge!N^r($OpTg1kbRxixc$1sL*#;lGOdqFm|6wY# zNnMYHd~(YKH`K3GLVyva&$*z-Ps|4O9U*gi84A(Oc7~@9kk$qLQedQD9?S}4foA)-=*|UBo)e>6s{{JuYJ!^74XHj&8_ol! zCUFwG^S=C9BK3fw|8Pv=KOx9@0p`yQcy!fyPBtpt((U!@y$L0qT4TbVU&wwJ^hAD) z3W>On3ltU0G`!9NnjiOQsb4Fb^N(9L*g!$T=I~3sq>ifTU2eWb2-@ENR|A63*wI~8 zt7O(t&(O75kOvJvesS&YS>jEri4bi?nRoP)z0o!hTDj3`VnzNnFRBNQx`rV3AA?fV zqekExj{y%@z0ADrIx(HK1(I&N@>9qN3%e!^z1Dkdr5^5rX*D^~8c=-IEamhiGb+}C z2k}7aorF}S4#G99(;nq#ClLj#o&6~;9Si2ko--k$&KH=qkAb-|9e|#`ki}RQp~kn~ zI1>1PB0rJZS$v(y6z+CH72N_m3hiiWv~J56|7ow9RRMz-1NE=$@YP}n3BNjSK4bNt`34a68=77Cjmp}~h+v!s>Wr;m{`WeYZ@Oey&L!JCM%g~iVa~_lQlgaTT1)oaN1TbIxW-upwNAd+5DH8 z;=N$qjMDex&-RwKW;}8-H>$`=O4~%6uUw9zmG4hx*`M0d@^f;aBd*=7_m$}I{&~+Z z=nph{xg*9|_zakvrb?2Nvgef1E6r(^<69JVEa+Xnv`5#PWoC{1QamS)3+d05mzjgq z1(BuCQi&c-Le4Q%pp$h}9Mf~w4$~SIvYY6)fu^H9f?`|IMaq?s2<7(33GsiGGOZZK5b@JmqVAk9fi0 za!gIE6u$u(@rn4(5y-5ZQlARn1mM;%H}G=BEKNZ-9%~fuC<4RxIs(~=uq=7si-f%PU+XJ_nUDwOJ%|n&$wTQOyz1R%= zwHVt=CBK>I4qz8;b8N!3x*^(egsD0(ZI62a zLMcsM*&j3=eRho} zCtZBk(v*e=P2C6iF51)gNA|-R>}wutFCTC9)$Z0?=>2x@AD>v4DCvryaLqWsc}Feu zei{WSEDD-jLp$N+qJ(NnsVTotJ@)YynI!TEre8;uhU{gODxx}40x@p}SyHR3yS++_ zy-J1SbVOrvrLVGF@$M7;Jd%tTZ6WD;;a2ov?eOv5p|zCxV(Zx;GwH-SueQ7$wgI{{ zl&f;T(JOpEjDdT{=kQ#SNQm{UDTPB_%ZcdoNS3J#4#n}7-7qlqm<|?10YO@{?!OC~ zBr?cQz`U9KeMIuJE=Kf4t99g`G4~+M8|yU1C8bk=@*3-;@fAhP%_%pND=zmLM+VXB zk7V#>LT9-X9!?ck@9K|E6i3p*qips2QQErFK{tp)a#m|k^}jw0ekWw%@I`}b!Tf(f zjG}$>ZZIz{<$oqm3`pg>DsIDY^o^`Aew>MGVV>L`H||buZa-{pD8c24hvap>gvxmpo&rHPO4m_6!^KK5g9R( zjEG8CqMfbFB*zB4WMqabnSY|AQ8v0>H{dLiHd!B4Ukxl%nN#_W1XIz^IczYR4l-S4 zV7i3C%T4fFvFh1v(cw21QFN4^4j^mfpXoUppq;-9Xp-tRdF>8LI{qK6b;~O#cHVGS z^i4a=Z|lc6qL5=v&)*+REgq{!+9s~lMtL#*z8y?u_4X^>b{0pob1$6Oe+DA|X_LSh zZ+{@h{hRN2LEdu@e%0m31Sv9wrl34}J2x<6CjLO*yk)M+A!UPX*h8A-cpbk~x&Rnd z`?1uA$;_vnyS<%El3$RxVKH8Zg<}@x=PjXC6WVc=~sb`E_=2tzvZ}&WTv*7f@t6RJ6cUKihH^~m_lE5787k|E;QkzttgsKvU zTi&h`8>7M{Z(JP5rd=p428WhhN=qhu4_C9<$Gt4@2b%MlkU3iPz+`b*8cX7 z+gX^~TS{H_8)E5PmHFk7cplcP1&|iT9~;0Rz??Ate5Nt>iWRcP<6Vew)XNcwKfs2* zR8LCn)zo-{lKi;nQlu7Cf1tlp-*PMtMuoVSo7PI=AY>{1y5rq_#>YeaW|@y7>4J0r zGhF?Y=oGZ8QeB)~QZj(DI{F`}6kf8* zRI~v&5`gH)@~+jTDzPRG#D|-W)5DOh zp8AtS$wp=XF(YN`EoK9p#%l});1Z8^$Y$MG)&yKBU>mm~OKAb@&fa_qM9=JgS+`z( z;l3uCp~Qk}^AIU@bnK82lmS%mVw;3!by*R&O}u?KJ`A?B^Wri@mh=};=v~Y>O@Dlm zNS9~XN~FR)Fm;xTF`H`Zl`c$+ehJ&hO6U zK`&D)`yyGbC7>zEy)w_T0Bg{{k~lnY)YPu6-*FCe)h=PQiPTgJO;|drWQ?@|0z^O* z7&PwOaglPH5COa%__Ut3pBnN`>7R5*mvki+aw?Zarydjmo{E)`ug}_&U!!>qsa@lc ze&!No&tgCPQ|_|WtjyT3SBd_v$PS0hr1WrpD}#B&=XIQ0%In&CvdAqP$@Ph8kMcrQ z;b`%j{Kq_|w;t_?gX$W`0AV>)FjCt|8f5?B#jfLDl`kr@Gx7$%?rx-oC5Dx`Wmhj8rJzjzTFFwrz@hv$LZ32csa}CX%}j# z|FAmE%Vhoo5M%$X5_C=;5axlZdTOmxQ1p74A*t)n1OmemG?O_;jC{;1ZMRR*-)Xnm zo+o)Z9fr_7&FGwznEGlZC1>}b#H|V+fTMfrb}-nIUVrWz0X;#tp8o4 zFX=Q7wIYC6}?I1IVeF_80Sj ziq$e={5Ui3WlFwok!q5(>0&kk+B~gz-Cio4n=UAY7Rb(8pdrt1SL`rHVIjxdhva*c zFXY#MNi%45TUceHc;>)ldZO1?mw`wm?iqD#bI{(rcgMUoKya^&>bbZPy|S~RgPU_lOL_NJq(j6$*#txJGBy7TL$0+g90mz ziwerq7xYHtentlW%sDZ&|30AK>}0QCm*2Z{nPC%G=DMSHFAy3ox>KQNr%(ebx$MYR zylCBLOjPO62sjb-voSdRq}h1TO~qoZ9H1ycOKr#nFJi-7)@>jYrhBCX9t zFcHu@mcb3n?O!I_Y73fA|5PeyJ+-!A2Qd<5a(U6Oz_7wvNkD>+vg)$0Qb}IuxO@A( zqAE>Pmvd@bOS4jZjiBErv%@S5%dM8~(i#VIv32%YDRmP@WNjZB7Er{*z0{2q zh!nk~U$FkC{e#)Z(qM41Ex9B(!FF$0t{c7pP3j_ml|axrUKv#48p@5`7tCr53hwsi z-?P@GmcO4wN!yWakwPJokRC0Ne}#&HHXPh<>1Ih{?1THQ+(;ys2zBn%N$gnh_aZ~j zLG3y<<6{t7Zh`q_z7bpNr={y|rwi?v#%`JfUl2KN*pPsc=uALFVY#854g#kt3Xx;3 zDmyqg*K^4iZ`M%IQfich$0|DvKd5*JA0kDQ(h)QF?fHNR@aCwKa`T_yw%$8v>rP5M~mo@{E z(M2PdWPDb6ECbqQq)!Vlh(a?xaEz(0FX5VlvjAQp$y^wmVnZo_X-l z4~rLpmdKIP0@G#zTL&5+>wTzeiGDimsrM%i zHVqmvo~4hi*(W!ylD06^?-!PouyQdc@YgKH#xkB0gAQ+keP~on zK3V^Ged3GQamQvl1$Np`{J5qxkQIi3j-Ne9N0XFRCDYe`8GKOTd80L_-_iLVHu;+N zt;NO(rlB_8&Cm)o!&052n&+Omm}LN9X3csl!_9wd{xq@%E^TkJ-`y?9r67=ygC;K+ z^GuXm`zO7a;gsoTTO)uc$;)AP&40uEr|2(|!9J6A!mVFUR`)Iroxd^O%UVxTpU*z& z*#!@enxW6qo1T#u`XZQM>>@Sw5n-A<(P!HY+6*$CG@oqNDrxg%57M?$5>LMtX(qc* z*=!(RQz2mSZva2ta1C_7yDH-fe=>-;j6`~KI;@|+Q4zMyJrY`Rg48xv*6yz?rn?}! zp3IBwJ)8C^<7x=pRr!vXrD@*AgEskaByKy} zcimv$y=&QTEqlU4#mj6%&TgC)CP>=|YREHa7s(MFRlj_SKB-v~*Sp9AUSw{Dl@6Jf z&pgj?b!b$Jya#fkS+@M24vc$rHyq{C@xD~*bzNap>CIHh7I)D~4iPm*v=KQ!1?(eq zW~D(2&>;QG&2D4%r6wJ;8gDm?YHysQTfkiim*W{%A$^!vSZn==WwRy6_qE+XY=Otc z)#6uEtqVgB)Hr0^!Z*+;wL4&t;Q@4lx#8{>HsHQdo&^tsNb$PKwySnc$kI_#v@iM* z+g+i;c63p9*y}v|sM4HGWE^YgJ1Ygkz4Wxuy}a0fxyy;q*T?3H?;Zbn<#nT4#_^SA z_gRKNp%qP9S1E!zKMix0eaX``=kw+{9cB1aa<{g>l!G>4ZaZ52Iw&+BQ9(&v7zZ~) z7RxiS{rGxV_d3NdJ%Mc3Z#j0L!ASnDFxqyZ??J?U?W6$O#0iZnr!9}bnTrL1m3*a8 z`cvcaXREKATXI%ai+qlAWe4EIFgSF!5O^;1Jl6PS~Y*|y(W|`_U%SU;e=-tBx$Q(tD&5x=L zw)FdLl;)&>GYqiv!5~+$4lcMNb7FR^NE~k#8(wB~>3%d+f4uV}e=uU=&=)^Z$9}kf zP>m-!9#T?MqJ_=<_NVgj5Ns8QA58(;y9MW zzITA%IMrt!FgXekSN&UUTqJpn#x#TF0Hw^6P{-V^g$;^L^|A0g_-QIVV8K;_HkNW9 z%QiQ-HTTA>CG=KC?F`+bjL*K_fo!LT!OoP^>ey>(S+&~B#cv*Bt?Ap9P2A99Ksj^P{LOlVkKrDXI%c zVxp)eH^*#Vy?8*aD785^9`MkmTUmT6xe@cB`9Kjt+mUaUWP-h%PJXdVYcw?ruFxGA z8hwSuuZyprJb2P)bsWoaxZnJtKG~#TuTmvO?Sqj5K*bGQfXnk&?tz<8rJuP-7OTRF zogFC1Iv*U)%0(PVU%zd7Y zCiRopt<#fVTD>H3S`@sQ5A&B&#ws|*4pVE3p3eq4rr#_JHlt04GKebhW{e{?fIjm? z?xVUTLWz+BFRMgFo3X6Zb*DHtgyq%%4N6s*E`b)qcarAh(^J)&P zv}vtDx@s)@rFzuFQt{C*7byZgTU=CbbJPh-Bmb2ALQ^wNw4{1$5~Jjm7}0}$QHI(y zkL8$J4kJJFDU?@sHgYRJph%~_xV!DMZ}VQ93hcrF=gM)bz={-|PAjK}kgsFR)C!oU zBfYoop*v{#WqxuPEQ`9mRai_FOoE)w6rOw|>ou)ESXddXaUC`S#J>MMD9}dX7F_Lt zx5t~2$ka{Mpxw`JNTs~sLSorTLn)T&qeOJ6^td|nLmRkGjuEfv7!a4(CubymbIU38 zY}*URkY3t4PrLj!M~3@VEQN*p@*Z6v`aJAg7W^(9b-q03ve|#=|UVn8OlliY1FSIAA zas);2t3MfIo$j;W6XhMFfLdb`i>Gw*r@jZiNUfLJ<20lOvP{7pk$pNXp3Qd}5$cv_ z1J6#IP+?U%j!NeMUs0|1$@i48rQHmPpy?1lH!Yp3k8Qb|28 zN={EoqM7X8B>QwfN{x1(d3E)q#P$zrbP(-WP@k9lf-i%v3mqIlU8P7x-_`K2%izd_ zRDqhg1;j~9XR7oxx-O50TERKR{0bP08-eo(CbIsy$l2XMKC!24I03))L4dvwp>#=2 zOawEBiZAQycsUQm7Li-ry>RWB#Sw=tWE}hJuuZMWK{FC5WJyHvgJO9W`ETBBpE~|Q z^0LcuA-~@y)m%?d+pnk%(6((a$ks!c8a=&2*W+)UKBB1mslTXcMm<3dACZLq%G_hiPcP*uQt zcb+#Xl44=#BXi3K?)8%$hid6_#J3~)lN`(o5W(r|bT+h$!Xpx$2Bg7SD;3unb?KEW z>0%6kpIS`o7&)&eDOb>^_IATPmveJ`zumb7do&7Co?HQ%M2Nq5o;jmJA#&yhGJnc% zO6BN8*Yre{4z=Vk@#gp&@>IanjZU*Uv~(zP#lg1uSQ+NIcTr_b@ZW`?yW+*GS&=qPVl>eO@)@1t~K`gKCbXC$?=o|(<4t(HUdI%y4ckv zIPO{fR92<50XbztuO*!xw>><_a<4M&Z2b>#>!SO0+*O7Py^!u}v+ zY9ovG`-^<1vth9JITX#eI_O9{G7%yv9=5A@M|exP6*8R!YM-EImlgeK>>|ObFWlxZ zvfbQC8YR}Py1?|ie*~VOh0yj+O2607L!3-6G%ykv&U(G`cbLnAZx+5anj+yTKF+I- z0XnuY6~UX4k62gSlJ36S*ttJiZeS|@JfAv-ZYcIc^oKxssB5#>}fDBweD$g=KnY`?W zU5e2?NrA-Tgfan`ccIUDK`JjCCdZ26aqQGM^$>eu#x4;U-8=q#;7)5|R*JtxVaM(X zRZcwxZ9OvewRyH6*FXk+aomPB+kl(4M(nkUIcW#0;^p`6!Un@)_17g{xNCvQ{3jp9 z45<3@5HBXL@E7u&sCVDzIdLm&_-AF8$8d20{cV8pa|nF&iQK6z|D)fq>vz#fZ?yoF17m+vt-!f?RRsfpXN07l{q5zEfm=AJ2BY*uW z|IG_Z{%`26km}2m(yx!Lm)O-^W}I;xP!8r1I^pNLrZ-O+Sd(|)fae38a~ss=Tu>>heYe2g(s&#$(Z=-TYC1KwWqfnCvvClnuYCUmO5)dT z*T8b9X`97wpR*#_zR`#|on&WG57~Tq*E@gjRukf>#YK$|?po`<=0Rd3bH;!6MKa2U zSU;LdjW`NqWZlQIO{RL?PJfW7Yrre(s;t}n59-@oS<8*q6{TP9+gqJiWL5nb(L%HO zc(*)eL{Z@Z8O4++OzV{Ma=3+xf%ZIF)UpiFYsi*!w-M#rgRo@ps zeni}o3?V}aC4?fRTh;E@mhDffNlz9%B^@^1FQX)gbHP01F<_4LIBqW(;3us3*X20J|6YMan3&b?7j9HpKC3;2G_o0qvjHNRrXar*U$Ekk)?&E0 zuf)!|2=luP==Z^^CYwW-HMWjt+^MK+z&CtfPgYvL_2qhKYWe%eB+t#-TWanwj>fmllECjQgcdic2m2rj#Z4vp|+{od{?|5v!DJ=NXT!Q*%Bn((;gvCTs^v<=cL zKZOQ!aEb@9NXdDM)b^OzUS5E({q|lM?Y@3{BFx*K10%X1zg5OhTmI%}T#q>=;$NJ7 z&fASI+j208^I5g@_4%Fp^iD+aEOH^jPd8DJP6z1?Dt9!abiqn1}!o-RVXF0u~u zpX%FzNY+QM7N(x%6=}=Rw`O67j34Fi-i8ihQ|0nfE)ofc1zwhNJvwRME?;qyF+h;c z>au1YOJp94GBNN*x6*N=)uQCAyw9Q;O7?e8tLI}_2FfbZ$oxE&YjyWD0LgS+9C{y~ zI5TXUbU5k#<&Xl$N{9Z3i>1@&YL~-nTY3qdVFnXCZ=0qR#-9JSJl3!lIeB5Fh^1Re zduA1E?-P~7K^faAk~nXS0)O)lx`ahos9CcO&VSpkrPa^>B1F}vb0Q` zHVWFCz-AwJey9P(Q!tHj_R{T`$i4MS()8YKmt;FtM0jx1wM7FVYdREdK{^duIo_g-cv35CtPP)SjWU6h_T8vTNA4vVYS{Xk@wrXY0Rw3L4ZXF za>XOt;2q5>5INvFZO7l7ix(D=O-+;xwtiwq{&aHCcB-WB$f-fw4WqMX>PZoi9@no| z8P8nc?7rm6TA|v`iZMn7B7ae&&XRSwX!;XySJ%>|OQ`00F4YJAoIJ^xT zAwy^>B1o3SqPwro&7?s+UP9M_m$cGj`8@LD(yfsW`u%hb3F>YS?S5bN(mE&-zQj6h zsXImH(qEo`T$?cPwBo*CwH=w11yiKmn;E;CQjy$he;AY84MNHEnZZC?^Oe#)MRTPF z?#;QS^_RQp)dRT@P4I1gZSC=lEcx*1;KwsIzg0gTF&UX&kcZ=KUx%pk+)&f7ItyCpUK8f>(f^FLf8N*fE`q4H zxSC1-!8XNrxX)^J_C;E$zXwO!c_SXahP99fHeZK*I1VyiHS+xIHnjX;zSN)I+n6IZ zBo1t<+X%rJe01wi_u5=5vq-hDYrk{uBVsANI-SwX=5SS_%-bUW)rOFwgY8@RWY6)6 zUW&X~Yx*h9r+sFh2HhVU9jA2n)=qWjkt@cP@{cG^2NGlmoov0o#G$YgI|N(&(k85E zcLZbw=5pK_+FN*qIM5~{+6!V*{WJX$_Nn-}qx+3bkMAD)IXwn;*6yj#ThsZ93C3O% z4EkcEYX{dGGnT53Kt`tq)LY@&(F#i}Lh;Ya9=vqxuGRL1QvdYUk7>71C39rex zL$!}m4HtN8-NEg!HJx#1^TbuAiQ7%jYnf!S=d#IW!@O2}ws%i6k6P|P70776L!z81 z|7_>s`*g~>`nGkMJP{*de=WN2CPpQDQ{6SZe+HyVw3f^Q4UGB@Seo3C`Ef)gzX3dH z_jD4wHW&BzoON4Ls*qv~81~B4>%MY&*MtHu>C>2=vYI!)!w$PUv^0LoC=?lY$bohs z)xm&Z(SR@JvfyM&P>x*XxsRiyRITash(#AMeG*fN`n$@p(7&uC;cFbnM!!(vZ+57E zZQ3?7vu6I$@1<<{LLx6NP8IDh7ic4|NGq;6gVVPB6a2CxZ?eOpM#A_7+aSN;R^wgS z4qM~*Z^#uv)Y~mWpMlv zmyA;dEkqoEsCVb@C`RR2Gev-NQ${0O{&KG9PitC^l+D{GQqSA*b5$&>C(*iVl2S+M zv7ZPp(h?32T?tekyGk0V2J1OVN@uhvDnXD($tk$)f*o1KK_~BsGN?TwJX$uTg8-9w zgoTUHr39vAN%7~L}NXEP3%{~tV7jOiqumk-TiZ*#bs;g zYSO~D;*Q+7t1(Wb1orNUVdy{pukSvJm9A5!p-0@(ME9j%`jRbq z1$NfOvTE*E@m{8cic^a<#xVdO?=OXrpSR(;We(-bfxZ^|SogM%#JKt%St~E{C(mE` z!6e|%f;mP24|)e2;ZXp8!Xeqg4=%Erpa&~jw&*I;>31FyO}KVfrz(G=Z~EP)YnmPT zb z9qo%}&hY~Gn-y1-i6Yw`n#q_Lu}_cX zO;<`*I%w{wNO8a8@TEpn^V$`el=L*0g?Vjd7Tu2%>yz>r`8#mI4o5II3^m4SwM-Zi zgk>u}P-cv?3~{<4C7QXebbOrw+2NV9lD`VK=2DN1?@AWjCEmB9_ae~Jnuc-F|4M05 zPazGV2#zzk-Ym*X!z#zp@42YDXU`A5WSoYX?sYo8hLDUCc)f5tDeKyjRq zQDGK&w11`B8zY=S&-O$xrTt@D72V`n(jQy>r_`liCwxh#DjIxQ#Xq9;N(zDq5-SzV z7!Rtw%=9SwzOyzfZGH%mp6@g=rAvfBYmqvcB49US2WL3j7> z!NtC|9vQ~S<>+Gx@Ua#e?{8s|dNTLmg~kr2f%h{s-1YSaCAWR3bfHAZx9Xsh-1XkM z0KeXzFB2}>u7)&Zcl7Xu&YcF|2S2J7{9_^`hcD+c;ZzcQ#T7_m$O8vo(O>g+i9C%# zeUTBO!VwscKfCnY@<89?NJHMY@u#1%b$kD=fDAR_Lm-QRAu9wHq#+&%1`H@9|QKi?F@pYZ1$GqYu60KPJKWdC~0*vIgT8~ip#s<=-|9%9>)zCMVKT;b|2)>hqedCvU8v|!>+O4MS}_gJedw4iN!wdR zUI*ct&F*>2j*X42$K#SgGtC;a9`- zQ7_vYAZTx0rW0~vy30PvEpSn$KoFm#HRIXfZdzBx&B1^%2?8wyLkLdHyuD@P`TMZe z)sv6^R*`F0$J-lMR|{lW#gn`{E63y`e7PdC2MNC*WI{}k4h)-%2`kYJRH_cUrE%Re zcff30T6*iANyP)8d*d?G(zQ zY3k<;&6rQP??azdO_mHV?LX6zsvWPf~R}QKru1Z%XO&d^j)7D5fTlcxB^+!%ysM#GN6b zP^t_H=kbs~Pc190vgAb{;IoTwxqG-xMD-WT(~htr5;X$Y!U*{zhLqKV2U=9_Z}d4R z33!E)ztzisa~t~7M_&=_X>YZkvEk+BaNC`FVa2#0zRu=QlklPN2ZEc`$|r+{N5JTg zHo5-hvdUu;tC}S1lC0vN>tRl}rSmKtB&9){x^BZN*Wk=D?c4vo**)}=3KX08w@1KZ zqlZD4SqX|88Tm)~++YRlT+e|`RmzIahN^~w7rbK=Zv)Sz=s~w@r8;f%b zE4f6cTupOY$OiFRuO8IN`-~a9HGcMLNc$IZqgU!(szryB% z1mB%<1A~hGv+co!TXLDAJ`!IJsgPN^WpY_WMlCrTq`6ahS*vnua};7({lbfZ&Fi1z z$ScUKS$|~SeMLS-jYYS_xeknn$+xujJlLm1*a@H()e(-!hfc)-km1!D!{Yx56!pjU zxq7*+5PoVg5QKQclS?5sQAD!`%JtWkiPI+|dit`AFGCM&Crmtu+%=e_v z;B>>Zj}oK4;Bp8!mRfb`G-u3-?do1hOs8FMS}Ln`ut_AwE4p0Q#7p?@hS%A!b?>de z;1zKrXkP&zIP=H8Dn!W=1N}$rTNlC!w;|ltBiFcGvwSJU@-hn@?e|Gl>|Yvsqp=kK z+*648o_e)Y{}BC=fXKoyNq!#9nS-xm%v$O9v#E-W-Bc##yKM+kLQRha#FW*Lmgn!& zIlpf^U2(^Vlh4BZT`>E*hNfs~*I@QNGU!JI}whxaw|(pg6j3}MD9t3H1amNE9;SHTP?29DAOrRE!xy56Kk)% z!F>uNO1w_Ptqt?l9)A z<5rM8yd&S%j@(1S>N}F`t!T)bNG;ekR@N>vP;jebm-$Ty7oMs}Ka z-4KVU9ws-EC61}2Fn(At4rg_h>N*-wk2BuM+Oobo3S3d}FJ= zkaAV8xX2&M6xLHyr*UU}-0*#Y2cCh&RAxHYRK$sE3NIja2ESK%;OQ!3eb-E8Fy{21 z^S-NpQ+}u3-`_%_Wzv@V!i-@PsC+u%;9GamP7UtZP1z1W7>q}_Rt>!QB>egTatG9M z**!U2tRqMlZhAPS`VyV=j0cpoPFYwQ>iI%64BY%DS}~sn%c9mT6ek4o0^iC&s@8@8 zlv-~v;25K0auzq}i7>xl$ZGs{?pIZI!#IWYpo5w2?x!4ljvywU1z9Si18QRKg=VfDN!;l$|v_>69@_3>Ij8E zMUyK+BHiuC4WUencri=3`HPQyKfaG3ZtTD-vcuh~69>?k3?dP@wJp@+C^!qB&_xS0A7>rwhnR`6gIJcmNBfgb`t03y{7(!$&ALu9(fp@Z@VVNvB#Fp|rS>~r zltZj|=p(&B6~GM?88rki7~Y-I)AQz)o=`AX=gpF%_J?|kfR1&Qo!GZVOJ4WH+4{S69X{7lGz_;B82SWWaz133EVesRseKg-l zp1Lz=@!0J!r$E3%;pt7?MTU!sUYZ^mb59K@0pJaJ&n^83vXmHIBx@8Yw*O5yZmswp zsc`FW4?|b#pTU^CI)wcS;OA{%>;}e2m|EH#^L@Da*LT&VC^gk^#uT|0BI2!KtvN^S zKhR^XNF4$txbs{mms_jNV^l!8qiepzAD-=9caye1>n)yzT2{1a(r$rx5p)?+a9x(F$aD+I@k-P*bv!`dn#XV>%+l z&~H$-Lx1n4y~kGa6lGn)(qe0$z$H4>naPHHy&&+p07JfFoGlN!&{HoDR-qef?YT7h;cym!c?9g@?zOQ`*JOopM_V zIY){gI!%vohgbp}UWZwUAD8V1ZUbpRXT?s}*9`x<}ggpwP(!nKo;Et~a;jbx?4D z7^vn%ufrib>Ip{~X3IkN$VBFoi&5!#nQTX}rz{X29a4*zZT#T|qLvm;NMdn>M{>0ce`X^M*KIla_%yEI@p z?~%(r|Nlg!)Qu<`*r|1Kx{&sPQ#s-^pYiu7!e{i_tQ(JW_VEj8`M;P zKG2s9GOE86=ReFH{*R*y|FuG1@LbD029Tr7HH0J7u&M+HgUxMCIBItqR~$59rzEHO zv$$BVsyj^&^EFmScxHo74T^oU8_wphK~^@Duks5HcM?ERRgLlC=lR=P0UO#@@pr(g zU1Gsu2W%0-QO&1FoNQfdf9$RbDOiZOhTW4cO72MdbOsQdmTKDwZ-A-!L21%Hs3&T7 zf>c4`2mbU&oW*0Yz+1oQkb%%%+wvE(3}4Q^Z6{Kl8Nn;L?BP+!Syo=6L9$BL6}4b~(lGf8if4U{YMihi5^4DSLn_fY?%{ngBuU*%f%@oth~tj5(HmS~ ziJgsply~vG`dc+JF^M`>$XqsGmAwvRFY*3b#g{^{k9&}ejYT2%WE3l|J~S+@gCn&@ z%;xA@ksRb(z|wcZ30rHF7)M@(V)t|=nT2=r4|%jZU+(~CU6ixiutG4`*xse=7v|qL zSI6?>z8%p%+Lq(tXsts947zrg*lS(AI4#4un?4g4u=|?Me;&V&dvFSt7T>x(3{^UB zW~m@LKo-r%#&OTR1N28!|K3LAJe6K(H<;QB(EH6FCcZD`1uQ83NQv?crudtRK)Kfw z0RUM@g(HGjuJM0$c!-GIhM*MErv}(29B9?d4?`Y_N!k42_PHVPt70K0;hsic9uxw9 z?|#8VcV@AOCQ6wvu zhpTDhA-sBPy3zLN@uTCXp; zZ~Av)Fw-)L7F4S>B6v)jU5r_37ZZKWEncW|FGB3d$(X}2fNRq8{N@4=R+kGbweEbe z@L|`C*elOt`3emjt4R3yl0dY7Ujn%@a3sMHBp!pi=qEU)qTC{Hc-h9iWlmI{m;qA#7EF4Sy1-u#NHF>-}1%l)2Mu4uUifS(%eg3DN)imE1H zb^*J(8QaAm&bMv=RXFx++LTfX)m3E=ZA z$jsc=rL%5jb+NB#ZAk1-=j!Rco;j%_YUjAMig5wyLi4|$dVU0L@J#uAXyD?kiAG?O z$?f(J1}{333M_IO3WRNk>K8psl?Y!JHg{t<>B$WSWl?6i*WA;TdZn0asa$)mMRbK< zxv+i>0Lc0THoN%D+WANSm|Wcwc|+R4bUQXpmL&ojLtw8Cp^fW=8Y4yFOv^d7Wzsh1 z*d@|3?CX|NnAo*q@Ouxp-RJ(iqM#;CVz1Zvton5?uyFxaI(5R{yjRPtkz~BIu&Ry7 z>zGTEOrj$_+`8u=@20)l^$g-HO>H?)F>}{-ujJz6TNR9$DQZqr_i&yPSVG!SKa`K(h2%S=Y|tfL4YW(I2PhSCe6 ztA;AbeQ`qUCl5ELT?~Fmi09M@#BIV@omdaWwzeBDzrU8Bg=^M!anSzI{pXR?edVB_ zsN>>oC<2rgmY-Te0*LxOlpReE{T^-sI3?_snv8h5RcDU49obmT?-IqZS@3mu^;TK3 z@lx=~h>b99h4&lo0?eNTxx~X)qK^Mo$cN7kK3rpO5aYXlJ0?lYyrzwra-|E!UN-m_ z##{mi=Nbv0{nan}=S4cjP6{3nzJ)`ci=EXy`9b*ZQRJ2HYz&p;U!WK^O1ut3Auk>Y ziUEU2kwU$aSWGn7JLpf}${MyM-*r#q!A!G2{$uLTA#{~T zg^CUw_!yikzmMa_YhU-ZlHX^!ImVijD$KkhACLxqmuEZ*BCu+xWnF&mWYhV?eWr4# z-9W}^_b2XYc@Mi88>iOJTzy~5@|W&Mosv&_O%}`=^XnQ?{Owr)ANdruhfqhlP0_0_ z5V85XFlfDywvJ+vPBLfGE%pcyr3%FX>v3H)5jQXE<76qf9N%uK6ejHg1}C4U_;Zefsc>bsB27Kz zLmy$SUl$|i#+6HBg!oZju}-$tXS~}#gHi`(?ZwLH_ALQq=VL~XkjoDEn$9CykRF^_6sj)yYbFUy%)b{r`vfxm z$R0bg-O9-v4ATt&v6dB^64y~vB}93>KX8`Kc$c;VnbQ()SLS-3z@)AT3zCKERuIp_ zf*7ID5g*RG&M0seoKO*)gee6>dkqvBkgGb81(*hm+=ceVVwCqdFDxBMORgw@4_AGQ z&S8?lL!b~`585AwZeLLt+^75I11Q0d*S2H2@n|0MaC6FV3%Tf+$5NH?8= z;~){cz6;ktO6JHMkq>YAwtw3RE(!DjU(g5GjY_1QaOb~M zaGL3!d2EmAJiYbzfYt-;Dkn^zp;3B~@22=m$`kqj$2Eb$cX3(BqObfP?80b}PuCv= zB(-(~nH7vi*V2k)BLkIJZtC$&jqN6<9xnEGX|yYr99wgP*>gsZDR&5bVE)(VK-wiC zaIFea&oQ5AhMv^D#^Qt0(SKS4;B;{%l4$M19oU>0F~7r9Bt3l6+)~kApNGapw)&iO zJC1Q)xkdhIp$wA@%0zDtopLyoy)#ESsxpIx$lK9j zyVBTEf#01mZbKujztPE@QMvQL;K`Ugly8P$xFXAUHoK<}C zoG+e}uAT&7cSOV_)Q6|T4d{jz{z1TfO3Q@t$@vKBM2}_v+|Ni$eFhP<=H8<%N571r z#FWS&L!ZTXD{!n@fzFuA6`@V>F9G@4&CSR_ZIp%w-pV~PJO~m)I80WPVlbSWNGezD z6EZl~>GQq&mJMyP_xH*?wum-m*h;?c^9ea8$WJAsOH_oY$z2S13^M~KMB#kxF33T3f2E0 zeE4K$?9ub03YG)k+@y;G(-m|c#}R#be?f{;2D7iZs~qT{8jg2S9tehoQ*-^u0934r z?Sf{7_)ET;BE8a`>7lY(kE)ler=c*Zo!m<%%J)HJJQ~a_{gFef7=&nDJRpvxC9hP7 zT_S<>QMXuVK5zfhvvFw1a!hdn@>jW{mpvCBE_0=bL<;@6bduWH^&S~O)a!RC;KC0$CKZ3ffZ~;gU^P@pWb=SFz zn!f~Wb~c5}YEi7yjdXsZFGO}qu|M@hWArE@E=d5ii@C=5L~PfGFPu?(rxHLLpg1;@ z!ASlRRGSEp4!OLF#UrjP>SxiiQ)|qf>41!iN0$wta-Hym!wxk@;a+@aGxvQEX1!nX z#!+l@#jx%6lIb1A95^A8@}TDW2>@RyUnmJokKB`&bg48DrB;mBPs;A^rZ<4&Qtm7% zH?uVI8AzTNI0Y7rD#l6PL`&gR9EiC2|F!Te;X}ihJ)JBzmkwdr86?uVXnat$v_*qKpS}}q$Sl4mWnBxU00#m zTD9c|tt&k0!kfl@sHl%5vK0N;I>6|F6?LyqT&=wg#H8`;^OUW&sGX*0o}6Lmo|iT^DOyW%h-! zh^R{5JhU(3I;dD7tXXk<82Nf2oJ}DBK%@&$J}U(5p&k+h$crQ1K=4eeoAcje8H+kH zl7E16GXX?tZ*{;PZ-7(0D020c8+%wh4p}Z4z-{`E)c->Jql*qPSrD2AiNs5wJ^hT% zW)_Qt0H*QH6oBvFZh>^Zhi(dho48`8fSl|&4`5^iG$jt;e|0#-4RlerZSpX>=Q!Gz zFtjh92%zIUE)8sRQljBvL6;Lyp7|$0M4VE{U>_>q*s;X!CxrBcELPyrgjR4xzM^X% zwIh3TvSMV#xbnvsw(!v5Ti$t5@2H*>dAFvkKb;m;n70R-ayatfDLThLha*8 zih8W0!5ajt(fSk+u^j~XW)PKFB->WCi6}T*NQbH8KtH&w;>UAE{x0RMa4)K--r)%M z6@x>L4pxUL6Ry-Dux>HPHb7sY_uvl?;jhPB_UO;XbRU95#)J`C&{Q2X8t5Ye_%?zN z+89YOSP!ho8qfiA1x|Ynh5Wia}8NkVAD;zctX`k)K}5Z zy_0R52nVb+_7mySbpf!X5s%P|7?BV#t)Gn}?z>#mJ3$i>fv$!lqBQL^7|@mpEHO~_ zOGCS1dLHer_<5wFrV2e1qLXxEfF*%!Iqcd_G%?yVxaQc`wgNm7T@$68gm!u_An>(g)*;UlEKeEPbH;*8tk|gs7L>V zI?*%K(S9L-UzO0EsL%9xqz?Q2bM00<4aQYMl^d5MBG;ZZ(7$`@%iO&q0N%45wggiL zEG(&PY*x0T=J_$GZvPbRU)?!aDJIUX0YgymG1E_I7r1yO1jqu2EpmyW2OFPf3eD@g z(qxIEm21=ne74_Zh-CB5#dYj-rON^~1?L}?v4P7MFZ|IE&AT9~@FNQ2Skgy-YF0&m z>VmaDGBFD6Vn4uslP{n*iR_0r1q|0-726JYxKLe2n;RQ@k2o;2a zy{Ctgl-&Q97$k3@L#d0pqWNh6s-8*=mCkis^05rc$Mn50ESp-uRR~5n2z4avfK)3b zivBs#R*)XHN7b8yE3X1J4OIWQBmMo0O-D$s5QDnvsn7qOSZ zFs+i70S1gbJ{vgPcI=nv^SGLA&?ex8)oETQOPVKp|4RrWx>~{RhFsAqe zG_p8!r`(0gMPTAo8WLBoQLa9mF<*8!(4RUj8pKEZXn;r(bu{{5(D#VEP5scCOH#3R zin4#qCU=IjNmNDxnLHMUCJ(!)Z8w6Up(-aB>3zs9FqDHsL9GzD3)v zmQUP+I2J8u$qWigD8kgK%}~tKg;d|zI$XS9@BEPlGo7d2u5O?rQ&+!kOQyA#h02@M zo~{}MD49fkI~#gC;XG-_rGseeh=PdTMEg=H*n!ujwXp$v@NJWGj@vB%=M}aDd ze6w)W%cyJ#78@JK!ZG_EILfV)@P*CDILCxCg;YZo9+&zrmY;`#jX(7B!Z4*?PrbD$ z$NG2FQ^j=Wof-7bL@`tdGEo9~ zAYXw>G$*|U;EEM>2%>>XBvP~fEuQEX{J~J=bVAMU6B+0ldeTpv#Q?DrsKSUA2|JFq z?%NZ{4H?7P6$?Hp1&A>$FZI?D(hj}edU!E;oRJ2@)=_o>RN9Cmj#A0RW>pXa5sGw2 ztf-4Eo5~$bwW$L>qXcRow*iKy?zQAGAU~Aq(i9d+Jx7ub=r+?qkTc_=8KiWt*V&Pg z=zzWAr~aXy64*Fg9^0jk=*2WGP9J!QX&M}nwadsqn@Vvz$U0dCjT+!c2BY10b;v;F z%))h$e>FU{XW`~|=z#kPb+Yj!q%$1?;RCpNL?c{JKs6J~puEmLnt)y?Nko;Y>*{{c zG&aX}ow;%s;2iLvKOzMzIQ@T$iCp>r^pZMB#DlOionSU(2+15PP^3zix>x^R&J>b) zO4Umq{q2Quc#y7y1#4^mf}qhkG~ZpxXugt{nH*4`NenxP#1Iyt%NLjnM%>`5N5}}p z8c~`O#+3*PD+I6dejZfQRVv0`Hi*A}Ms#bXnR{?7ZT;-my-L8v_-0ix z47-aoEA-1Mu&{?y3_pT5ni2Y0BU=&ui00@M;OEDKBq92JLb^2iksHxW(C-A&-Ow*f z3au{u2$BDP8mewKd=~$%LSJi7FCy*hoBw_1lmEUP7xUj*+&KR4>hNFqZ@mKF t^P?5P{3H<-!;j_gf2;ie!9hNk(9$*C67wm1XMv`AUQJu|rHX05{{iMdgiZhe literal 0 HcmV?d00001 From 8b62311cbb43b4e766d20b7c4f3c8441721b4b93 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Mon, 24 May 2021 12:45:33 -0600 Subject: [PATCH 14/42] Replace `quickstart-app-auth` with updated docs Signed-off-by: Tim Hansen --- .../docs/tutorials/aws-alb-aad-oidc-auth.md | 2 +- docs/tutorials/quickstart-app-auth.md | 371 ------------------ docs/tutorials/quickstart-app-plugin.md | 4 +- microsite/sidebars.json | 1 - mkdocs.yml | 1 - .../commands/create-plugin/createPlugin.ts | 2 +- .../commands/remove-plugin/removePlugin.ts | 2 +- packages/create-app/src/createApp.ts | 6 +- .../templates/default-app/app-config.yaml.hbs | 2 +- 9 files changed, 10 insertions(+), 381 deletions(-) delete mode 100644 docs/tutorials/quickstart-app-auth.md diff --git a/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md b/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md index ba0aef89ea..0567a6bbb6 100644 --- a/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md +++ b/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md @@ -167,4 +167,4 @@ Once it's deployed, after going through the AAD authentication flow, Backstage s -[monorepo-app-setup-with-auth-ms]: https://backstage.io/docs/tutorials/quickstart-app-auth#the-auth-configuration +[monorepo-app-setup-with-auth-ms]: https://backstage.io/docs/auth/microsoft/provider diff --git a/docs/tutorials/quickstart-app-auth.md b/docs/tutorials/quickstart-app-auth.md deleted file mode 100644 index d54b21f7c4..0000000000 --- a/docs/tutorials/quickstart-app-auth.md +++ /dev/null @@ -1,371 +0,0 @@ ---- -id: quickstart-app-auth -title: Monorepo App Setup With Authentication ---- - -###### January 8th 2021 - @backstage/create-app - v0.4.5 - -
- -> This document takes you through setting up a Backstage app that runs in your -> own environment. It starts with a skeleton install and verifying of the -> monorepo's functionality. Next, authentication is added and tested. -> -> This document assumes you have Node.js 12 or 14 active along with Yarn and -> Python. Please note, that at the time of this writing, the current version is -> v0.4.5. This guide can still be used with future versions, just, verify as you -> go. - -# The Skeleton Application - -From the terminal: - -1. Create a (monorepo) application: `npx @backstage/create-app` -1. Enter an `id` for your new app like `mybiz-backstage` I went with - `simple-backstage-app` -1. Choose `SQLite` as your database. This is the quickest way to get started as - PostgreSQL requires additional setup not covered here. -1. Start your backend: `yarn --cwd packages/backend start` - -```zsh -# You should see positive verbiage in your terminal output -2020-09-11T22:20:26.712Z backstage info Listening on :7000 -``` - -5. Finally, start the frontend. Open a new terminal window and from the root of - your project, run: `yarn start` - -```zsh -# You should see positive verbiage in your terminal output -ℹ 「wds」: Project is running at http://localhost:3000/ -``` - -Once the app compiles, a browser window should have popped with your stand-alone -application loaded at `localhost:3000`. This could take a couple minutes. - -```zsh -# You should see positive verbiage in your terminal output -ℹℹ 「wdm」: Compiled successfully. -``` - -Since there is no auth currently configured, you are automatically entered as a -guest. Let's fix that now and add auth. - -# The Auth Configuration - -A default Backstage installation includes multiple authentication providers out -of the box. The steps to enable new authentication providers in Backstage are -very similar to each other, the biggest difference is usually configuring the -external authentication provider. Please see a subset of possible providers and -instructions to integrate them below. Steps 1 & 2 are described separately for -each provider and steps beyond that are common for all. - -
GitHub -

- -### 1. Open `app-config.yaml` and change it as follows - -_from:_ - -```yaml -auth: - providers: {} -``` - -_to:_ - -```yaml -auth: - providers: - github: - development: - clientId: ${AUTH_GITHUB_CLIENT_ID} - clientSecret: ${AUTH_GITHUB_CLIENT_SECRET} - ## uncomment the following line if using enterprise - # enterpriseInstanceUrl: ${AUTH_GITHUB_ENTERPRISE_INSTANCE_URL} -``` - -### 2. Generate a GitHub client ID and secret - -- Log into http://github.com -- Navigate to (Settings > Developer Settings > OAuth Apps > New OAuth - App)[https://github.com/settings/applications/new] -- Set Homepage URL = `http://localhost:3000` -- Set Callback URL = `http://localhost:7000/api/auth/github` -- Click [Register application] -- On the next page, copy and paste your new Client ID and Client Secret to - environment variables defined in the `app-config.yaml` file, - `AUTH_GITHUB_CLIENT_ID` & `AUTH_GITHUB_CLIENT_SECRET` - -

-
- -
GitLab -

- -### 1. Open `app-config.yaml` and change it as follows - -_from:_ - -```yaml -auth: - providers: {} -``` - -_to:_ - -```yaml -auth: - providers: - gitlab: - development: - clientId: ${AUTH_GITLAB_CLIENT_ID} - clientSecret: ${AUTH_GITLAB_CLIENT_SECRET} - audience: https://gitlab.com # Or your self-hosted GitLab instance URL -``` - -### 2. Generate a GitLab Application client ID and secret - -- Log into GitLab -- Navigate to (Profile > Settings > - Applications)[https://gitlab.com/-/profile/applications] -- Name your application -- Set Callback URL = `http://localhost:7000/api/auth/gitlab/handler/frame` -- Select the following values: - - `read_user` (Read the authenticated user's personal information) - - `read_repository` (Allows read-only access to the repository) - - `write_repository` (Allows read-write access to the repository) - - `openid` (Authenticate using OpenID Connect) - - `profile` (Allows read-only access to the user's personal information using - OpenID Connect) - - `email` (Allows read-only access to the user's primary email address using - OpenID Connect) -- Click [Save application] -- On the next page, copy and paste your new Application ID and Secret to - environment variables defined in the `app-config.yaml` file, - `AUTH_GITLAB_CLIENT_ID` & `AUTH_GITLAB_CLIENT_SECRET` - -

-
- -
Google -

- -### 1. Open `app-config.yaml` and change it as follows - -_from:_ - -```yaml -auth: - providers: {} -``` - -_to:_ - -```yaml -auth: - providers: - google: - development: - clientId: ${AUTH_GOOGLE_CLIENT_ID} - clientSecret: ${AUTH_GOOGLE_CLIENT_SECRET} -``` - -### 2. Generate Google Credentials in Google Cloud console - -- Log into https://console.cloud.google.com -- Select or create a new project from the dropdown on the top bar -- Navigate to (APIs & Services > - Credentials)[https://console.cloud.google.com/apis/credentials] -- Click Create Credentials and select [OAuth client ID] -- Select Web Application as the application type -- Add new Authorised JavaScript origin = `http://localhost:3000` -- Add new Authorised redirect URI = - `http://localhost:7000/api/auth/google/handler/frame` -- Click [Save application] -- Google should display a modal with your Client ID and Secret. Copy and paste - those to environment variables defined in the `app-config.yaml` file, - `AUTH_GOOGLE_CLIENT_ID` & `AUTH_GOOGLE_CLIENT_SECRET` - -

-
- -
Microsoft -

- -### 1. Open `app-config.yaml` and change it as follows - -_from:_ - -```yaml -auth: - providers: {} -``` - -_to:_ - -```yaml -auth: - providers: - microsoft: - development: - clientId: ${AUTH_MICROSOFT_CLIENT_ID} - clientSecret: ${AUTH_MICROSOFT_CLIENT_SECRET} - tenantId: ${AUTH_MICROSOFT_TENANT_ID} -``` - -### 2. Create a Microsoft App Registration in Microsoft Portal - -- Log into https://portal.azure.com -- Navigate to (Azure Active Directory > App - Registrations)[https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps] -- Create a New Registration -- Add new Redirect URI = `http://localhost:3000` -- Add new Authorised redirect URI = - `http://localhost:7000/api/auth/microsoft/handler/frame` -- Click [Save application] -- Set environment variable `AUTH_MICROSOFT_CLIENT_ID` from - `Application (client) Id` displayed on the directory page -- Set environment variable `AUTH_MICROSOFT_TENANT_ID` from - `Directory (tenant) ID` displayed on the directory page -- Navigate to Certificates & Secrets section and click [Create a new secret] -- Set environment variable `AUTH_MICROSOFT_CLIENT_SECRET` from the `value` field - created. - -

-
- -
Auth0 -

- -### 1. Open `app-config.yaml` and change it as follows - -_from:_ - -```yaml -auth: - providers: {} -``` - -_to:_ - -```yaml -auth: - providers: - auth0: - development: - clientId: ${AUTH_AUTH0_CLIENT_ID} - clientSecret: ${AUTH_AUTH0_CLIENT_SECRET} - domain: ${AUTH_AUTH0_DOMAIN_ID} -``` - -### 2. Create an Auth0 application in the Auth0 management console - -- Log into https://manage.auth0.com/dashboard/ -- Navigate to Applications -- Create a New Application - - Select Single Page Web Application -- Go to Settings tab -- Add new line to Allowed Callback URLs = - `http://localhost:7000/api/auth/auth0/handler/frame` -- Click [Save Changes] -- Set environment variables displayed on the Basic Information page - - `AUTH_AUTH0_CLIENT_ID` from `Client ID` displayed on Auth0 application page - - `AUTH_AUTH0_CLIENT_SECRET` from `Client Secret` displayed on Auth0 - application page - - `AUTH_AUTH0_DOMAIN_ID` from `Domain` displayed on Auth0 application page - -

-
- -### 3. Set environment variables in whatever fashion is easiest for you. I chose to - -add mine to my `.zshrc` profile. - -```zsh -# For macOS Catalina & Z Shell -# ------ simple-backstage-app GitHub -# -# (Change the name of the environment variables based on your auth setup above) -export AUTH_GITHUB_CLIENT_ID=xxx -export AUTH_GITHUB_CLIENT_SECRET=xxx -# export AUTH_GITHUB_ENTERPRISE_INSTANCE_URL=https://github.{MY_BIZ}.com -``` - -### 4. And of course I need to source that file. - -```zsh -# Loading the new variables -% source ~/.zshrc - -# Any other currently opened terminals need to be restarted to pick up the new values -# verify your setup by running env -% env -# should output something like -> ... -> AUTH_GITHUB_CLIENT_ID=xxx -> AUTH_GITHUB_CLIENT_SECRET=xxx -> ... -``` - -### 5. Open and change _root > packages > app > src >_ `App.tsx` to use correct - -authentication provider reference - -```tsx -import { githubAuthApiRef, SignInPage } from '@backstage/core'; -``` - -Modify the imported reference based on the authentication method you selected -above: - -| Auth Provider | Import Name | -| ------------- | ------------------- | -| GitHub | githubAuthApiRef | -| GitLab | gitlabAuthApiRef | -| Google | googleAuthApiRef | -| Microsoft | microsoftAuthApiRef | -| Auth0 | auth0AuthApiRef | - -### 6. In the same file, modify createApp - -Remember to add or modify a `SignInPage` component for `createApp`, using -provider information based on the table above. - -```tsx -const app = createApp({ - apis, - components: { - SignInPage: props => ( - - ), - }, -}); -``` - -After finishing setting up one (or multiple) authentication providers defined -above you can start the backend and frontend as before - -When the browser loads, you should be presented with a login page for GitHub. -Login as usual with your GitHub account. If this is your first time, you will be -asked to authorize and then are redirected to the catalog page if all is well. - -For more information you can clone -[the backstage-auth-example repository](https://github.com/RoadieHQ/backstage-auth-example). -Each authentication setting is set up there on a branch named after the -authentication provider. - -# Where to go from here - -> You're probably eager to write your first custom plugin. Follow this next -> tutorial for an in-depth look at a custom GitHub repository browser plugin. -> [Adding Custom Plugin to Existing Monorepo App](quickstart-app-plugin.md). diff --git a/docs/tutorials/quickstart-app-plugin.md b/docs/tutorials/quickstart-app-plugin.md index f1efcf0745..7dca90c14b 100644 --- a/docs/tutorials/quickstart-app-plugin.md +++ b/docs/tutorials/quickstart-app-plugin.md @@ -35,8 +35,8 @@ title: Adding Custom Plugin to Existing Monorepo App 1. When the process finishes, let's start the backend: `yarn --cwd packages/backend start` 1. If you see errors starting, refer to - [Auth Configuration](https://backstage.io/docs/tutorials/quickstart-app-auth#the-auth-configuration) - for more information on environment variables. + [Auth Configuration](https://backstage.io/docs/auth/) for more information on + environment variables. 1. And now the frontend, from a new terminal window and the root of your project: `yarn start` 1. As usual, a browser window should popup loading the App. diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 6c42614a61..9bb263e79a 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -239,7 +239,6 @@ ], "Tutorials": [ "tutorials/journey", - "tutorials/quickstart-app-auth", "tutorials/quickstart-app-plugin", "tutorials/switching-sqlite-postgres" ], diff --git a/mkdocs.yml b/mkdocs.yml index 574b04f8f7..afc0726ce1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -154,7 +154,6 @@ nav: - Backend: 'api/backend.md' - Tutorials: - Future developer journey: 'tutorials/journey.md' - - Monorepo App Setup With Authentication: 'tutorials/quickstart-app-auth.md' - Adding Custom Plugin to Existing Monorepo App: 'tutorials/quickstart-app-plugin.md' - Switching Backstage from SQLite to PostgreSQL: 'tutorials/switching-sqlite-postgres.md' - Architecture Decision Records (ADRs): diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index 9fc5531dfa..5ea8cdfbbc 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -205,7 +205,7 @@ export default async (cmd: Command) => { return chalk.red('Please enter an ID for the plugin'); } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { return chalk.red( - 'Plugin IDs must be kebab-cased and contain only letters, digits, and dashes.', + 'Plugin IDs must be lowercase and contain only letters, digits, and dashes.', ); } return true; diff --git a/packages/cli/src/commands/remove-plugin/removePlugin.ts b/packages/cli/src/commands/remove-plugin/removePlugin.ts index 81d8322ffa..72d619e0bc 100644 --- a/packages/cli/src/commands/remove-plugin/removePlugin.ts +++ b/packages/cli/src/commands/remove-plugin/removePlugin.ts @@ -189,7 +189,7 @@ export default async () => { return chalk.red('Please enter an ID for the plugin'); } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { return chalk.red( - 'Plugin IDs must be kebab-cased and contain only letters, digits and dashes.', + 'Plugin IDs must be lowercase and contain only letters, digits and dashes.', ); } return true; diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 8a269416a3..0002c25414 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -99,7 +99,7 @@ export default async (cmd: Command): Promise => { return chalk.red('Please enter a name for the app'); } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { return chalk.red( - 'App name must be kebab-cased and contain only letters, digits, and dashes.', + 'App name must be lowercase and contain only letters, digits, and dashes.', ); } return true; @@ -147,9 +147,11 @@ export default async (cmd: Command): Promise => { chalk.green(`🥇 Successfully created ${chalk.cyan(answers.name)}`), ); Task.log(); + Task.section('All set! Now you might want to'); Task.log( - 'See https://backstage.io/docs/tutorials/quickstart-app-auth to know more about enabling auth providers', + ' Set up the software catalog: https://backstage.io/docs/features/software-catalog/configuration', ); + Task.log(' Add authentication: https://backstage.io/docs/auth/'); Task.log(); Task.exit(); } catch (error) { diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index 442e90944e..c40d1385a0 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -62,7 +62,7 @@ techdocs: type: 'local' # Alternatives - 'googleGcs' or 'awsS3'. Read documentation for using alternatives. auth: - # see https://backstage.io/docs/tutorials/quickstart-app-auth to know more about enabling auth providers + # see https://backstage.io/docs/auth/ to learn about auth providers providers: {} scaffolder: From f86ab6d4927b0e71ff4080f77c23cf3b5986b137 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Mon, 24 May 2021 13:29:27 -0600 Subject: [PATCH 15/42] Add relationship cards to create-app template Signed-off-by: Tim Hansen --- .changeset/smart-sloths-drum.md | 12 +++++++ .../components/artist-lookup-component.yaml | 1 + .../templates/default-app/app-config.yaml.hbs | 8 +++-- .../app/src/components/catalog/EntityPage.tsx | 36 +++++++++++++++++++ 4 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 .changeset/smart-sloths-drum.md diff --git a/.changeset/smart-sloths-drum.md b/.changeset/smart-sloths-drum.md new file mode 100644 index 0000000000..c12fbe6734 --- /dev/null +++ b/.changeset/smart-sloths-drum.md @@ -0,0 +1,12 @@ +--- +'@backstage/create-app': patch +--- + +Added newer entity relationship cards to the default `@backstage/create-app` template: + +- `EntityDependsOnComponentsCard` +- `EntityDependsOnResourcesCard` +- `EntityHasResourcesCard` +- `EntityHasSubcomponentsCard` + +The `EntityLinksCard` was also added to the overview page. To apply these to your Backstage application, compare against the updated [EntityPage.tsx](https://github.com/backstage/backstage/blob/371760ca2493c8f63e9b44ecc57cc8488131ba5b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx) diff --git a/packages/catalog-model/examples/components/artist-lookup-component.yaml b/packages/catalog-model/examples/components/artist-lookup-component.yaml index edd9b8fcf9..88fd33ac0d 100644 --- a/packages/catalog-model/examples/components/artist-lookup-component.yaml +++ b/packages/catalog-model/examples/components/artist-lookup-component.yaml @@ -33,3 +33,4 @@ spec: lifecycle: experimental owner: team-a system: artist-engagement-portal + dependsOn: ['resource:artists-db'] diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index 442e90944e..275b3865f9 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -72,7 +72,7 @@ scaffolder: catalog: rules: - - allow: [Component, System, API, Group, User, Template, Location] + - allow: [Component, System, API, Group, User, Resource, Location] locations: # Backstage example components - type: url @@ -86,11 +86,13 @@ catalog: - type: url target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-apis.yaml + # Backstage example resources + - type: url + target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-resources.yaml + # Backstage example organization groups - type: url target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/acme/org.yaml - rules: - - allow: [Group, User] # Backstage example templates - type: url diff --git a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx index a302bb6dd7..4264c4a4ee 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx @@ -26,10 +26,15 @@ import { } from '@backstage/plugin-api-docs'; import { EntityAboutCard, + EntityDependsOnComponentsCard, + EntityDependsOnResourcesCard, EntitySystemDiagramCard, EntityHasComponentsCard, + EntityHasResourcesCard, + EntityHasSubcomponentsCard, EntityHasSystemsCard, EntityLayout, + EntityLinksCard, EntitySwitch, isComponentType, isKind, @@ -78,6 +83,12 @@ const overviewContent = ( + + + + + + ); @@ -102,6 +113,17 @@ const serviceEntityPage = ( + + + + + + + + + + + @@ -118,6 +140,17 @@ const websiteEntityPage = ( {cicdContent} + + + + + + + + + + + @@ -224,6 +257,9 @@ const systemPage = ( + + + From 9836bd292424f8ea9af0309b0a5c5220aba6a25c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 24 May 2021 21:52:04 +0200 Subject: [PATCH 16/42] Add tests for the entity schema including new status field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../catalog-model/src/entity/Entity.test.ts | 629 ++++++++++++++++++ .../src/schema/shared/common.schema.json | 37 +- 2 files changed, 660 insertions(+), 6 deletions(-) create mode 100644 packages/catalog-model/src/entity/Entity.test.ts diff --git a/packages/catalog-model/src/entity/Entity.test.ts b/packages/catalog-model/src/entity/Entity.test.ts new file mode 100644 index 0000000000..9f3bfc6813 --- /dev/null +++ b/packages/catalog-model/src/entity/Entity.test.ts @@ -0,0 +1,629 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 Ajv from 'ajv'; +import { AnyValidateFunction } from 'ajv/dist/core'; +import entitySchema from '../schema/Entity.schema.json'; +import entityMetaSchema from '../schema/EntityMeta.schema.json'; +import commonSchema from '../schema/shared/common.schema.json'; +import { Entity } from './Entity'; + +describe('Entity', () => { + let entity: any; + let validate: AnyValidateFunction; + + beforeEach(() => { + entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + uid: 'e01199ab-08cc-44c2-8e19-5c29ded82521', + etag: 'lsndfkjsndfkjnsdfkjnsd==', + generation: 13, + name: 'test', + namespace: 'ns', + labels: { + 'backstage.io/custom': 'ValueStuff', + }, + annotations: { + 'example.com/bindings': 'are-secret', + }, + tags: ['java', 'data'], + links: [ + { + url: 'https://example.com', + title: 'Website', + icon: 'website', + }, + ], + }, + spec: { + type: 'service', + lifecycle: 'production', + owner: 'me', + }, + relations: [ + { type: 't', target: { kind: 'k', namespace: 'ns', name: 'n' } }, + ], + status: { + items: [ + { + type: 't', + level: 'error', + message: 'm', + error: { name: 'n', message: 'm', code: '1', stack: 's' }, + }, + ], + }, + }; + + const ajv = new Ajv({ allowUnionTypes: true }); + ajv.addSchema([entityMetaSchema, commonSchema], undefined, undefined, true); + validate = ajv.compile(entitySchema); + }); + + function check(data: unknown): () => true { + return () => { + const result = validate(data); + if (result === true) { + return true; + } + + const [error] = validate.errors || []; + if (!error) { + throw new TypeError('Unknown error'); + } + + throw new TypeError( + `${error.dataPath || ''} ${error.message}${ + error.params + ? ` - ${Object.entries(error.params) + .map(([key, val]) => `${key}: ${val}`) + .join(', ')}` + : '' + }`, + ); + }; + } + + it('happy path: accepts valid data', () => { + expect(check(entity)).not.toThrow(); + }); + + // + // apiVersion and kind + // + + it('rejects wrong root type', () => { + expect(check((7 as unknown) as Entity)).toThrow(/object/); + }); + + it('rejects missing apiVersion', () => { + delete entity.apiVersion; + expect(check(entity)).toThrow(/apiVersion/); + }); + + it('rejects bad apiVersion type', () => { + entity.apiVersion = 7; + expect(check(entity)).toThrow(/apiVersion/); + }); + + it('rejects empty apiVersion', () => { + entity.apiVersion = ''; + expect(check(entity)).toThrow(/apiVersion/); + }); + + it('rejects missing kind', () => { + delete entity.kind; + expect(check(entity)).toThrow(/kind/); + }); + + it('rejects bad kind type', () => { + entity.kind = 7; + expect(check(entity)).toThrow(/kind/); + }); + + it('rejects empty kind', () => { + entity.kind = ''; + expect(check(entity)).toThrow(/kind/); + }); + + // + // metadata + // + + it('rejects missing metadata', () => { + delete entity.metadata; + expect(check(entity)).toThrow(/metadata/); + }); + + it('rejects bad metadata type', () => { + entity.metadata = 7; + expect(check(entity)).toThrow(/metadata/); + }); + + it('accepts missing uid', () => { + delete entity.metadata.uid; + expect(check(entity)).not.toThrow(); + }); + + it('rejects bad uid type', () => { + entity.metadata.uid = 7; + expect(check(entity)).toThrow(/uid/); + }); + + it('rejects empty uid', () => { + entity.metadata.uid = ''; + expect(check(entity)).toThrow(/uid/); + }); + + it('accepts missing etag', () => { + delete entity.metadata.etag; + expect(check(entity)).not.toThrow(); + }); + + it('rejects bad etag type', () => { + entity.metadata.etag = 7; + expect(check(entity)).toThrow(/etag/); + }); + + it('rejects empty etag', () => { + entity.metadata.etag = ''; + expect(check(entity)).toThrow(/etag/); + }); + + it('accepts missing generation', () => { + delete entity.metadata.generation; + expect(check(entity)).not.toThrow(); + }); + + it('rejects bad generation type', () => { + entity.metadata.generation = 'a'; + expect(check(entity)).toThrow(/generation/); + }); + + it('rejects zero generation', () => { + entity.metadata.generation = 0; + expect(check(entity)).toThrow(/generation/); + }); + + it('rejects non-integer generation', () => { + entity.metadata.generation = 1.5; + expect(check(entity)).toThrow(/generation/); + }); + + it('rejects missing name', () => { + delete entity.metadata.name; + expect(check(entity)).toThrow(/name/); + }); + + it('rejects bad name type', () => { + entity.metadata.name = 7; + expect(check(entity)).toThrow(/name/); + }); + + it('accepts missing namespace', () => { + delete entity.metadata.namespace; + expect(check(entity)).not.toThrow(); + }); + + it('rejects bad namespace type', () => { + entity.metadata.namespace = 7; + expect(check(entity)).toThrow(/namespace/); + }); + + it('accepts missing description', () => { + delete entity.metadata.description; + expect(check(entity)).not.toThrow(); + }); + + it('rejects bad description type', () => { + entity.metadata.description = 7; + expect(check(entity)).toThrow(/description/); + }); + + it('accepts missing labels', () => { + delete entity.metadata.labels; + expect(check(entity)).not.toThrow(); + }); + + it('accepts empty labels', () => { + entity.metadata.labels = {}; + expect(check(entity)).not.toThrow(); + }); + + it('rejects bad labels type', () => { + entity.metadata.labels = 7; + expect(check(entity)).toThrow(/labels/); + }); + + it('accepts missing annotations', () => { + delete entity.metadata.annotations; + expect(check(entity)).not.toThrow(); + }); + + it('accepts empty annotations object', () => { + entity.metadata.annotations = {}; + expect(check(entity)).not.toThrow(); + }); + + it('rejects bad annotations type', () => { + entity.metadata.annotations = 7; + expect(check(entity)).toThrow(/annotations/); + }); + + it('rejects bad tags type', () => { + entity.metadata.tags = 7; + expect(check(entity)).toThrow(/tags/); + }); + + it('accepts empty tags', () => { + entity.metadata.tags = []; + expect(check(entity)).not.toThrow(); + }); + + it('rejects empty tag', () => { + entity.metadata.tags[0] = ''; + expect(check(entity)).toThrow(/tags/); + }); + + it('rejects bad tag type', () => { + entity.metadata.tags[0] = 7; + expect(check(entity)).toThrow(/tags/); + }); + + it('accepts missing links', () => { + delete entity.metadata.links; + expect(check(entity)).not.toThrow(); + }); + + it('accepts empty links', () => { + entity.metadata.links = []; + expect(check(entity)).not.toThrow(); + }); + + it('rejects empty links.url', () => { + entity.metadata.links[0].url = ''; + expect(check(entity)).toThrow(/links/); + }); + + it('rejects missing links.url', () => { + delete entity.metadata.links[0].url; + expect(check(entity)).toThrow(/links/); + }); + + it('rejects bad links.url type', () => { + entity.metadata.links[0].url = 7; + expect(check(entity)).toThrow(/links/); + }); + + it('rejects empty links.title', () => { + entity.metadata.links[0].title = ''; + expect(check(entity)).toThrow(/links/); + }); + + it('accepts missing links.title', () => { + delete entity.metadata.links[0].title; + expect(check(entity)).not.toThrow(); + }); + + it('rejects bad links.title type', () => { + entity.metadata.links[0].title = 7; + expect(check(entity)).toThrow(/links/); + }); + + it('rejects empty links.icon', () => { + entity.metadata.links[0].icon = ''; + expect(check(entity)).toThrow(/links/); + }); + + it('accepts missing links.icon', () => { + delete entity.metadata.links[0].icon; + expect(check(entity)).not.toThrow(); + }); + + it('rejects bad links.icon type', () => { + entity.metadata.links[0].icon = 7; + expect(check(entity)).toThrow(/links/); + }); + + it('accepts unknown metadata field', () => { + entity.metadata.unknown = 7; + expect(check(entity)).not.toThrow(); + }); + + // + // spec + // + + it('accepts missing spec', () => { + delete entity.spec; + expect(check(entity)).not.toThrow(); + }); + + it('rejects non-object spec', () => { + entity.spec = 7; + expect(check(entity)).toThrow(/spec/); + }); + + it('accepts unknown spec field', () => { + entity.spec.unknown = 7; + expect(check(entity)).not.toThrow(); + }); + + // + // Relations + // + + it('accepts missing relations', () => { + delete entity.relations; + expect(check(entity)).not.toThrow(); + }); + + it('accepts empty relations', () => { + entity.relations = []; + expect(check(entity)).not.toThrow(); + }); + + it('rejects bad relations type', () => { + entity.relations = 7; + expect(check(entity)).toThrow(/relations/); + }); + + it('rejects missing relations.type', () => { + delete entity.relations[0].type; + expect(check(entity)).toThrow(/relations/); + }); + + it('rejects empty relations.type', () => { + entity.relations[0].type = ''; + expect(check(entity)).toThrow(/relations/); + }); + + it('rejects wrong relations.type type', () => { + entity.relations[0].type = 7; + expect(check(entity)).toThrow(/relations/); + }); + + it('rejects missing relations.target', () => { + delete entity.relations[0].target; + expect(check(entity)).toThrow(/relations/); + }); + + it('rejects empty relations.target', () => { + entity.relations[0].target = ''; + expect(check(entity)).toThrow(/relations/); + }); + + it('rejects wrong relations.target type', () => { + entity.relations[0].target = 7; + expect(check(entity)).toThrow(/relations/); + }); + + it('rejects missing relations.target.kind', () => { + delete entity.relations[0].target.kind; + expect(check(entity)).toThrow(/relations/); + }); + + it('rejects empty relations.target.kind', () => { + entity.relations[0].target.kind = ''; + expect(check(entity)).toThrow(/relations/); + }); + + it('rejects wrong relations.target.kind type', () => { + entity.relations[0].target.kind = 7; + expect(check(entity)).toThrow(/relations/); + }); + + it('rejects missing relations.target.namespace', () => { + delete entity.relations[0].target.namespace; + expect(check(entity)).toThrow(/relations/); + }); + + it('rejects empty relations.target.namespace', () => { + entity.relations[0].target.namespace = ''; + expect(check(entity)).toThrow(/relations/); + }); + + it('rejects wrong relations.target.namespace type', () => { + entity.relations[0].target.namespace = 7; + expect(check(entity)).toThrow(/relations/); + }); + + it('rejects missing relations.target.name', () => { + delete entity.relations[0].target.name; + expect(check(entity)).toThrow(/relations/); + }); + + it('rejects empty relations.target.name', () => { + entity.relations[0].target.name = ''; + expect(check(entity)).toThrow(/relations/); + }); + + it('rejects wrong relations.target.name type', () => { + entity.relations[0].target.name = 7; + expect(check(entity)).toThrow(/relations/); + }); + + it('rejects unknown relation field', () => { + entity.relations[0].unknown = 7; + expect(check(entity)).toThrow(/unknown/); + }); + + // + // Status + // + + it('accepts missing status', () => { + delete entity.status; + expect(check(entity)).not.toThrow(); + }); + + it('accepts empty status', () => { + entity.status = {}; + expect(check(entity)).not.toThrow(); + }); + + it('rejects bad status type', () => { + entity.status = 7; + expect(check(entity)).toThrow(/status/); + }); + + it('accepts missing status.items', () => { + delete entity.status.items; + expect(check(entity)).not.toThrow(); + }); + + it('accepts empty status.items', () => { + entity.status.items = []; + expect(check(entity)).not.toThrow(); + }); + + it('rejects bad status.items type', () => { + entity.status.items = 7; + expect(check(entity)).toThrow(/status/); + }); + + it('rejects bad status.items item type', () => { + entity.status.items[0] = 7; + expect(check(entity)).toThrow(/status/); + }); + + it('rejects missing status.items.type', () => { + delete entity.status.items[0].type; + expect(check(entity)).toThrow(/status/); + }); + + it('rejects empty status.items.type', () => { + entity.status.items[0].type = ''; + expect(check(entity)).toThrow(/status/); + }); + + it('rejects bad status.items.type type', () => { + entity.status.items[0].type = 7; + expect(check(entity)).toThrow(/status/); + }); + + it('rejects missing status.items.level', () => { + delete entity.status.items[0].level; + expect(check(entity)).toThrow(/status/); + }); + + it('rejects empty status.items.level', () => { + entity.status.items[0].level = ''; + expect(check(entity)).toThrow(/status/); + }); + + it('rejects bad status.items.level type', () => { + entity.status.items[0].level = 7; + expect(check(entity)).toThrow(/status/); + }); + + it('rejects bad status.items.level enum', () => { + entity.status.items[0].level = 'unknown'; + expect(check(entity)).toThrow(/status/); + }); + + it('rejects missing status.items.message', () => { + delete entity.status.items[0].message; + expect(check(entity)).toThrow(/status/); + }); + + it('accepts empty status.items.message', () => { + entity.status.items[0].message = ''; + expect(check(entity)).not.toThrow(); + }); + + it('rejects bad status.items.message type', () => { + entity.status.items[0].message = 7; + expect(check(entity)).toThrow(/status/); + }); + + it('accepts missing status.items.error', () => { + delete entity.status.items[0].error; + expect(check(entity)).not.toThrow(); + }); + + it('rejects missing status.items.error.name', () => { + delete entity.status.items[0].error.name; + expect(check(entity)).toThrow(/name/); + }); + + it('rejects empty status.items.error.name', () => { + entity.status.items[0].error.name = ''; + expect(check(entity)).toThrow(/name/); + }); + + it('rejects bad status.items.error.name type', () => { + entity.status.items[0].error.name = 7; + expect(check(entity)).toThrow(/name/); + }); + + it('rejects missing status.items.error.message', () => { + delete entity.status.items[0].error.message; + expect(check(entity)).toThrow(/message/); + }); + + it('accepts empty status.items.error.message', () => { + entity.status.items[0].error.message = ''; + expect(check(entity)).not.toThrow(); + }); + + it('rejects bad status.items.error.message type', () => { + entity.status.items[0].error.message = 7; + expect(check(entity)).toThrow(/message/); + }); + + it('accepts missing status.items.error.code', () => { + delete entity.status.items[0].error.code; + expect(check(entity)).not.toThrow(); + }); + + it('accepts empty status.items.error.code', () => { + entity.status.items[0].error.code = ''; + expect(check(entity)).not.toThrow(); + }); + + it('rejects bad status.items.error.code type', () => { + entity.status.items[0].error.code = 7; + expect(check(entity)).toThrow(/code/); + }); + + it('accepts missing status.items.error.stack', () => { + delete entity.status.items[0].error.stack; + expect(check(entity)).not.toThrow(); + }); + + it('accepts empty status.items.error.stack', () => { + entity.status.items[0].error.stack = ''; + expect(check(entity)).not.toThrow(); + }); + + it('rejects bad status.items.error.stack type', () => { + entity.status.items[0].error.stack = 7; + expect(check(entity)).toThrow(/stack/); + }); + + it('accepts unknown status.items field', () => { + entity.status.items[0].unknown = 7; + expect(check(entity)).not.toThrow(); + }); + + it('accepts unknown status.items.error field', () => { + entity.status.items[0].error.unknown = 7; + expect(check(entity)).not.toThrow(); + }); +}); diff --git a/packages/catalog-model/src/schema/shared/common.schema.json b/packages/catalog-model/src/schema/shared/common.schema.json index efb8b6d1bb..a9082d53e4 100644 --- a/packages/catalog-model/src/schema/shared/common.schema.json +++ b/packages/catalog-model/src/schema/shared/common.schema.json @@ -13,15 +13,18 @@ "properties": { "kind": { "type": "string", - "description": "The kind field of the entity." + "description": "The kind field of the entity.", + "minLength": 1 }, "namespace": { "type": "string", - "description": "The metadata.namespace field of the entity." + "description": "The metadata.namespace field of the entity.", + "minLength": 1 }, "name": { "type": "string", - "description": "The metadata.name field of the entity." + "description": "The metadata.name field of the entity.", + "minLength": 1 } } }, @@ -51,7 +54,10 @@ "additionalProperties": true, "properties": { "items": { - "$ref": "#statusItem" + "type": "array", + "items": { + "$ref": "#statusItem" + } } } }, @@ -90,9 +96,28 @@ "$id": "#error", "type": "object", "description": "A serialized error object.", - "required": [], + "required": ["name", "message"], "additionalProperties": true, - "properties": {} + "properties": { + "name": { + "type": "string", + "examples": ["Error", "InputError"], + "description": "The type name of the error", + "minLength": 1 + }, + "message": { + "type": "string", + "description": "The message of the error" + }, + "code": { + "type": "string", + "description": "An error code associated with the error" + }, + "stack": { + "type": "string", + "description": "An error stack trace" + } + } } } } From fffd1af6c08518d3660af45db4661feea472052b Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Mon, 24 May 2021 17:36:03 -0400 Subject: [PATCH 17/42] null groups are valid Signed-off-by: Ryan Vazquez --- plugins/cost-insights/src/utils/history.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cost-insights/src/utils/history.ts b/plugins/cost-insights/src/utils/history.ts index 7d45cfdad2..9e13db3c7a 100644 --- a/plugins/cost-insights/src/utils/history.ts +++ b/plugins/cost-insights/src/utils/history.ts @@ -23,7 +23,7 @@ import { ConfigContextProps } from '../hooks/useConfig'; const schema = yup .object() .shape({ - group: yup.string(), + group: yup.string().nullable(), project: yup.string().nullable(), }) .required(); From e153e4371bf5c3c4d3da10f7d10aa7466b0d90e8 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Mon, 24 May 2021 17:54:11 -0400 Subject: [PATCH 18/42] update tests Signed-off-by: Ryan Vazquez --- plugins/cost-insights/src/utils/history.test.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/plugins/cost-insights/src/utils/history.test.ts b/plugins/cost-insights/src/utils/history.test.ts index e4310e13c6..961a2ce241 100644 --- a/plugins/cost-insights/src/utils/history.test.ts +++ b/plugins/cost-insights/src/utils/history.test.ts @@ -61,6 +61,7 @@ describe.each` params | expected ${''} | ${{}} ${'?foo=bar'} | ${{}} + ${'?group'} | ${{ group: null }} ${'?project'} | ${{ project: null }} ${'?group=some-group'} | ${{ group: 'some-group' }} ${'?group=some-group&project'} | ${{ group: 'some-group', project: null }} @@ -72,9 +73,3 @@ describe.each` expect(pageFilters).toMatchObject(expected); }); }); - -describe('invalidate', () => { - it("should throw an error if param values don't match schema", async () => { - await expect(validate('?group')).rejects.toThrowError(); - }); -}); From 41c3ec421fd0e7aec2d5be8e4e9ad40d0d7d4d31 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Mon, 24 May 2021 17:56:03 -0400 Subject: [PATCH 19/42] changeset Signed-off-by: Ryan Vazquez --- .changeset/cost-insights-smooth-toes-draw.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cost-insights-smooth-toes-draw.md diff --git a/.changeset/cost-insights-smooth-toes-draw.md b/.changeset/cost-insights-smooth-toes-draw.md new file mode 100644 index 0000000000..e106cbda9c --- /dev/null +++ b/.changeset/cost-insights-smooth-toes-draw.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': patch +--- + +fix for query parameters with null groups From 4915b554d055b7dc28c1117122bb192299d691e6 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Mon, 24 May 2021 18:12:19 -0400 Subject: [PATCH 20/42] test for null groups and project Signed-off-by: Ryan Vazquez --- plugins/cost-insights/src/utils/history.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/cost-insights/src/utils/history.test.ts b/plugins/cost-insights/src/utils/history.test.ts index 961a2ce241..dff6015aca 100644 --- a/plugins/cost-insights/src/utils/history.test.ts +++ b/plugins/cost-insights/src/utils/history.test.ts @@ -63,6 +63,7 @@ describe.each` ${'?foo=bar'} | ${{}} ${'?group'} | ${{ group: null }} ${'?project'} | ${{ project: null }} + ${'?project&group'} | ${{ group: null, project: null }} ${'?group=some-group'} | ${{ group: 'some-group' }} ${'?group=some-group&project'} | ${{ group: 'some-group', project: null }} ${'?group=some-group&project=some-project'} | ${{ group: 'some-group', project: 'some-project' }} From 9d1d4441338a544e13022714b2b6ca0bb3185376 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 May 2021 04:10:03 +0000 Subject: [PATCH 21/42] chore(deps): bump eslint-config-prettier from 6.14.0 to 8.3.0 Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 6.14.0 to 8.3.0. - [Release notes](https://github.com/prettier/eslint-config-prettier/releases) - [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/eslint-config-prettier/compare/v6.14.0...v8.3.0) Signed-off-by: dependabot[bot] --- packages/cli/package.json | 2 +- yarn.lock | 16 ++++------------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 12d8373ba5..fd903332d0 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -65,7 +65,7 @@ "diff": "^5.0.0", "esbuild": "^0.8.56", "eslint": "^7.1.0", - "eslint-config-prettier": "^6.0.0", + "eslint-config-prettier": "^8.3.0", "eslint-formatter-friendly": "^7.0.0", "eslint-plugin-import": "^2.20.2", "eslint-plugin-jest": "^24.1.0", diff --git a/yarn.lock b/yarn.lock index 64aeb6eedc..b5a7ec90c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12425,12 +12425,10 @@ escodegen@^1.14.1, escodegen@^1.9.1: optionalDependencies: source-map "~0.6.1" -eslint-config-prettier@^6.0.0: - version "6.14.0" - resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.14.0.tgz#390e7863a8ae99970981933826476169285b3a27" - integrity sha512-DbVwh0qZhAC7CNDWcq8cBdK6FcVHiMTKmCypOPWeZkp9hJ8xYwTaWSa6bb6cjfi8KOeJy0e9a8Izxyx+O4+gCQ== - dependencies: - get-stdin "^6.0.0" +eslint-config-prettier@^8.3.0: + version "8.3.0" + resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz#f7471b20b6fe8a9a9254cc684454202886a2dd7a" + integrity sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew== eslint-formatter-friendly@^7.0.0: version "7.0.0" @@ -13928,11 +13926,6 @@ get-stdin@^4.0.1: resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= -get-stdin@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" - integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== - get-stdin@^8.0.0: version "8.0.0" resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" @@ -18740,7 +18733,6 @@ minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a" integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ== dependencies: - encoding "^0.1.12" minipass "^3.1.0" minipass-sized "^1.0.3" minizlib "^2.0.0" From 4b678c92dab6ebe9d3ec85d9b68ce084a7c13c67 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 May 2021 04:13:04 +0000 Subject: [PATCH 22/42] chore(deps): bump express-session from 1.17.1 to 1.17.2 Bumps [express-session](https://github.com/expressjs/session) from 1.17.1 to 1.17.2. - [Release notes](https://github.com/expressjs/session/releases) - [Changelog](https://github.com/expressjs/session/blob/master/HISTORY.md) - [Commits](https://github.com/expressjs/session/compare/v1.17.1...v1.17.2) Signed-off-by: dependabot[bot] --- yarn.lock | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/yarn.lock b/yarn.lock index 64aeb6eedc..f19bff6940 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10384,7 +10384,7 @@ cookie@0.4.0: resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== -cookie@^0.4.1, cookie@~0.4.1: +cookie@0.4.1, cookie@^0.4.1, cookie@~0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== @@ -12941,17 +12941,17 @@ express-promise-router@^4.1.0: methods "^1.0.0" express-session@^1.17.1: - version "1.17.1" - resolved "https://registry.npmjs.org/express-session/-/express-session-1.17.1.tgz#36ecbc7034566d38c8509885c044d461c11bf357" - integrity sha512-UbHwgqjxQZJiWRTMyhvWGvjBQduGCSBDhhZXYenziMFjxst5rMV+aJZ6hKPHZnPyHGsrqRICxtX8jtEbm/z36Q== + version "1.17.2" + resolved "https://registry.npmjs.org/express-session/-/express-session-1.17.2.tgz#397020374f9bf7997f891b85ea338767b30d0efd" + integrity sha512-mPcYcLA0lvh7D4Oqr5aNJFMtBMKPLl++OKKxkHzZ0U0oDq1rpKBnkR5f5vCHR26VeArlTOEF9td4x5IjICksRQ== dependencies: - cookie "0.4.0" + cookie "0.4.1" cookie-signature "1.0.6" debug "2.6.9" depd "~2.0.0" on-headers "~1.0.2" parseurl "~1.3.3" - safe-buffer "5.2.0" + safe-buffer "5.2.1" uid-safe "~2.1.5" express-xml-bodyparser@^0.3.0: @@ -18740,7 +18740,6 @@ minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a" integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ== dependencies: - encoding "^0.1.12" minipass "^3.1.0" minipass-sized "^1.0.3" minizlib "^2.0.0" @@ -23395,10 +23394,10 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.0, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: - version "5.2.0" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" - integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== safe-identifier@^0.4.2: version "0.4.2" From 8a7799d850d54a0e2bf64375527a2e42056102cd Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 25 May 2021 13:32:20 +0200 Subject: [PATCH 23/42] Fix images on github org data page Signed-off-by: Oliver Sand --- docs/{ => assets}/integrations/github/email.png | Bin .../integrations/github/permissions.png | Bin docs/integrations/github/org.md | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) rename docs/{ => assets}/integrations/github/email.png (100%) rename docs/{ => assets}/integrations/github/permissions.png (100%) diff --git a/docs/integrations/github/email.png b/docs/assets/integrations/github/email.png similarity index 100% rename from docs/integrations/github/email.png rename to docs/assets/integrations/github/email.png diff --git a/docs/integrations/github/permissions.png b/docs/assets/integrations/github/permissions.png similarity index 100% rename from docs/integrations/github/permissions.png rename to docs/assets/integrations/github/permissions.png diff --git a/docs/integrations/github/org.md b/docs/integrations/github/org.md index 5ee650e29c..7fbc0ae792 100644 --- a/docs/integrations/github/org.md +++ b/docs/integrations/github/org.md @@ -57,11 +57,11 @@ If Backstage is configured to use GitHub Apps authentication you must grant `Read-Only` access for `Members` under `Organization` in order to ingest users correctly. You can modify the app's permissions under the organization settings, `https://github.com/organizations/{ORG}/settings/apps/{APP_NAME}/permissions`. -![permissions](./permissions.png) +![permissions](../../assets/integrations/github/permissions.png) **Please note that when you change permissions, the app owner will get an email that must be approved first before the changes are applied.** -![email](./email.png) +![email](../../assets/integrations/github/email.png) Locations point out the specific org(s) you want to import. The `type` of these locations must be `github-org`, and the `target` must point to the exact URL of From d88ae5ff29baac5a2e989a5450a86ac2a644490d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 May 2021 04:14:22 +0000 Subject: [PATCH 24/42] chore(deps-dev): bump @changesets/cli from 2.14.1 to 2.16.0 Bumps [@changesets/cli](https://github.com/changesets/changesets) from 2.14.1 to 2.16.0. - [Release notes](https://github.com/changesets/changesets/releases) - [Changelog](https://github.com/atlassian/changesets/blob/main/docs/modifying-changelog-format.md) - [Commits](https://github.com/changesets/changesets/compare/@changesets/cli@2.14.1...@changesets/cli@2.16.0) Signed-off-by: dependabot[bot] --- yarn.lock | 164 +++++++++++++++++++++++++++--------------------------- 1 file changed, 82 insertions(+), 82 deletions(-) diff --git a/yarn.lock b/yarn.lock index f19bff6940..394c110f46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1689,16 +1689,16 @@ resolved "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-5.0.0.tgz#3ba791f37b90e7f6170d252b63aacfcae943c039" integrity sha512-WmKrB/575EJCzbeSJR3YQ5sET5FaizeljLRw1382qVUeGqzuWBgIS+AF5a0FO51uQTrDpoRgvuHC2IWVsgwkkA== -"@changesets/apply-release-plan@^4.2.0": - version "4.2.0" - resolved "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-4.2.0.tgz#f1005815f27c3238f66507e90c6ae14d8fc62b41" - integrity sha512-/vt6UwgQldhOw93Gb8llI5OuYGlJt2+U45AfcXsoxzl8gZzCmChGm3vUaQJYbmtL8TbL8OOVXHRIKJJidMNPKw== +"@changesets/apply-release-plan@^5.0.0": + version "5.0.0" + resolved "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-5.0.0.tgz#11bf168acecbf4cfa2b0e6425160bac5ceeec1c3" + integrity sha512-SE+5nPNSKUyUociPnAvnjYSVF+diciEhX9ZHSqKWMlydswCDjiaq9gz67qwWCmwgEgEOz0TS7VrQBoOlzbitvA== dependencies: "@babel/runtime" "^7.10.4" - "@changesets/config" "^1.5.0" + "@changesets/config" "^1.6.0" "@changesets/get-version-range-type" "^0.3.2" - "@changesets/git" "^1.0.5" - "@changesets/types" "^3.3.0" + "@changesets/git" "^1.1.1" + "@changesets/types" "^4.0.0" "@manypkg/get-packages" "^1.0.1" detect-indent "^6.0.0" fs-extra "^7.0.1" @@ -1708,36 +1708,36 @@ resolve-from "^5.0.0" semver "^5.4.1" -"@changesets/assemble-release-plan@^4.0.0", "@changesets/assemble-release-plan@^4.1.0": - version "4.1.0" - resolved "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-4.1.0.tgz#091e5e768dfe51835937e71d1ebaca1c9d6de55b" - integrity sha512-dMqe2L5Pn4UGTW89kOuuCuZD3pQFZj1Sxv92ZW4S98sXGsxcb2PdW+PeHbQ7tawkCYCOvzhXxAlN4OdF2DlDKQ== +"@changesets/assemble-release-plan@^5.0.0": + version "5.0.0" + resolved "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-5.0.0.tgz#3e57405e5c375e2d933f62e74d1874915e60cd61" + integrity sha512-LElDXTCBUkPSmdXlCisoUWw2paX48snatBmw/hKnGiSvnyZqdTIylLojAGQWG0/vOO9v3s/DvJ4hdagIquxJjg== dependencies: "@babel/runtime" "^7.10.4" "@changesets/errors" "^0.1.4" - "@changesets/get-dependents-graph" "^1.2.0" - "@changesets/types" "^3.3.0" + "@changesets/get-dependents-graph" "^1.2.1" + "@changesets/types" "^4.0.0" "@manypkg/get-packages" "^1.0.1" semver "^5.4.1" "@changesets/cli@^2.14.0": - version "2.14.1" - resolved "https://registry.npmjs.org/@changesets/cli/-/cli-2.14.1.tgz#ef6ba9f69e4dd5f6cde4220e032200e3039a52f8" - integrity sha512-ydU2ZUP/s7nHQmz8TrliT+kE2dJUpZYKh8MBHrkV6suchCBhT4DmoP4VDF9M4215r18iPbMpE1TjskzXGXB8eQ== + version "2.16.0" + resolved "https://registry.npmjs.org/@changesets/cli/-/cli-2.16.0.tgz#9f794005d0503efba5e348b929821a1732fd0f0d" + integrity sha512-VFkXSyyk/WRjjUoBI7g7cDy09qBjPbBQOloPMEshTzMo/NY9muWHl2yB/FSSkV/6PxGimPtJ7aEJPYfk8HCfXw== dependencies: "@babel/runtime" "^7.10.4" - "@changesets/apply-release-plan" "^4.2.0" - "@changesets/assemble-release-plan" "^4.1.0" - "@changesets/config" "^1.5.0" + "@changesets/apply-release-plan" "^5.0.0" + "@changesets/assemble-release-plan" "^5.0.0" + "@changesets/config" "^1.6.0" "@changesets/errors" "^0.1.4" - "@changesets/get-dependents-graph" "^1.2.0" - "@changesets/get-release-plan" "^2.0.1" - "@changesets/git" "^1.1.0" + "@changesets/get-dependents-graph" "^1.2.1" + "@changesets/get-release-plan" "^3.0.0" + "@changesets/git" "^1.1.1" "@changesets/logger" "^0.0.5" - "@changesets/pre" "^1.0.4" - "@changesets/read" "^0.4.6" - "@changesets/types" "^3.3.0" - "@changesets/write" "^0.1.3" + "@changesets/pre" "^1.0.6" + "@changesets/read" "^0.4.7" + "@changesets/types" "^4.0.0" + "@changesets/write" "^0.1.4" "@manypkg/get-packages" "^1.0.1" "@types/semver" "^6.0.0" boxen "^1.3.0" @@ -1756,15 +1756,15 @@ term-size "^2.1.0" tty-table "^2.8.10" -"@changesets/config@^1.2.0", "@changesets/config@^1.5.0": - version "1.5.0" - resolved "https://registry.npmjs.org/@changesets/config/-/config-1.5.0.tgz#6d58b01e45916318d3f39e9cde86a5d69dc58ed8" - integrity sha512-Bl9nLVYcwFCpd9jpzcOsExZk1NuTYX20D2YWHCdS1xll3W0yOdSUlWLUCCfugN1l3+yTR6iDW6q9o6vpCevWvA== +"@changesets/config@^1.6.0": + version "1.6.0" + resolved "https://registry.npmjs.org/@changesets/config/-/config-1.6.0.tgz#2cd9426b9d4212534d2b31c51de43280b76d3df4" + integrity sha512-vMY/OpMFSDC2crDKb5Nq2kMX9hozcXL4dY5Rr+a1JQ044Rz+jqjJPpdTP2yQ+j7qmeGcUTvwjJoEMeekYwfqhg== dependencies: "@changesets/errors" "^0.1.4" - "@changesets/get-dependents-graph" "^1.2.0" + "@changesets/get-dependents-graph" "^1.2.1" "@changesets/logger" "^0.0.5" - "@changesets/types" "^3.3.0" + "@changesets/types" "^4.0.0" "@manypkg/get-packages" "^1.0.1" fs-extra "^7.0.1" micromatch "^4.0.2" @@ -1776,28 +1776,28 @@ dependencies: extendable-error "^0.1.5" -"@changesets/get-dependents-graph@^1.2.0": - version "1.2.0" - resolved "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-1.2.0.tgz#6986927a5fec60bc6fcc76f966efae2ac30c05ed" - integrity sha512-96NInEKpEZH8KvmXyh42PynXVAdq3kQ9VjAeswHtJ3umUCeTF42b/KVXaov+5P1RNnaUVtRuEwzs4syGuowDTw== +"@changesets/get-dependents-graph@^1.2.1": + version "1.2.1" + resolved "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-1.2.1.tgz#462908693dc3a354622e43f85a764b74b5bb53af" + integrity sha512-vJOibo9SkqhVbgfq5AHIlQ7tzkYQIXh3tPAnlNLy2bPZsU+SByd74GaxHYWt1zOBlncU25WKrIM6J7XBB+GVUg== dependencies: - "@changesets/types" "^3.3.0" + "@changesets/types" "^4.0.0" "@manypkg/get-packages" "^1.0.1" chalk "^2.1.0" fs-extra "^7.0.1" semver "^5.4.1" -"@changesets/get-release-plan@^2.0.1": - version "2.0.1" - resolved "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-2.0.1.tgz#b95d8f1a3cc719ff4b42b9b9aae72458d8787c13" - integrity sha512-+x5N9/Iaka+c0Kq7+3JsboMNyffKYlWPmdm+VeafDcMwJFhBDkxm84qaCJ93ydmnzQOTig6gYVqw0k8BbHExyQ== +"@changesets/get-release-plan@^3.0.0": + version "3.0.0" + resolved "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-3.0.0.tgz#55efc01db2e24bd7a88e703956eb2f6c4a79054f" + integrity sha512-7VLiqpcWZyjwIXYgkubBC/9cdwqUJEhLMRT9/Y9+ctHqrpsXmJg15QQPTOh3HT9yGN5fJPL1WwuZkc1HXUhK0g== dependencies: "@babel/runtime" "^7.10.4" - "@changesets/assemble-release-plan" "^4.0.0" - "@changesets/config" "^1.2.0" - "@changesets/pre" "^1.0.4" - "@changesets/read" "^0.4.6" - "@changesets/types" "^3.1.0" + "@changesets/assemble-release-plan" "^5.0.0" + "@changesets/config" "^1.6.0" + "@changesets/pre" "^1.0.6" + "@changesets/read" "^0.4.7" + "@changesets/types" "^4.0.0" "@manypkg/get-packages" "^1.0.1" "@changesets/get-version-range-type@^0.3.2": @@ -1805,14 +1805,14 @@ resolved "https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.3.2.tgz#8131a99035edd11aa7a44c341cbb05e668618c67" integrity sha512-SVqwYs5pULYjYT4op21F2pVbcrca4qA/bAA3FmFXKMN7Y+HcO8sbZUTx3TAy2VXulP2FACd1aC7f2nTuqSPbqg== -"@changesets/git@^1.0.5", "@changesets/git@^1.1.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@changesets/git/-/git-1.1.0.tgz#ecaa8058ac87b450c09da0e74b68e6854cd0742c" - integrity sha512-f/2rQynT+JiAL/V0V/GQdXhLkcb86ELg3UwH3fQO4wVdfUbE9NHIHq9ohJdH1Ymh0Lv48F/b38aWZ5v2sKiF3w== +"@changesets/git@^1.1.1": + version "1.1.1" + resolved "https://registry.npmjs.org/@changesets/git/-/git-1.1.1.tgz#f444d3ff3604acb6949560656c9ef330485a5fa3" + integrity sha512-Z12TcKwgU33YE3r76cyU+X81RchOXljDZ5s3G2u0Zd+ODyrwlDb91IO55+6R0Ha6ouPz8ioont0gA70c1RFngg== dependencies: "@babel/runtime" "^7.10.4" "@changesets/errors" "^0.1.4" - "@changesets/types" "^3.1.1" + "@changesets/types" "^4.0.0" "@manypkg/get-packages" "^1.0.1" is-subdir "^1.1.1" spawndamnit "^2.0.0" @@ -1824,54 +1824,54 @@ dependencies: chalk "^2.1.0" -"@changesets/parse@^0.3.6": - version "0.3.7" - resolved "https://registry.npmjs.org/@changesets/parse/-/parse-0.3.7.tgz#1368136e2b83d5cff11b4d383a3032723530db99" - integrity sha512-8yqKulslq/7V2VRBsJqPgjnZMoehYqhJm5lEOXJPZ2rcuSdyj8+p/2vq2vRDBJT2m0rP+C9G8DujsGYQIFZezw== +"@changesets/parse@^0.3.8": + version "0.3.8" + resolved "https://registry.npmjs.org/@changesets/parse/-/parse-0.3.8.tgz#0bb244eccb35cb301168f85684bb03389c59341d" + integrity sha512-0S7Dc7XbMOKamBtd48vVuWL2aFZyaglw6lJsXNddn9forFf8oMKMmdyJ/HQPyeEChDDOhDF1/ya7m/zpt4Dk4w== dependencies: - "@changesets/types" "^3.0.0" + "@changesets/types" "^4.0.0" js-yaml "^3.13.1" -"@changesets/pre@^1.0.4": - version "1.0.5" - resolved "https://registry.npmjs.org/@changesets/pre/-/pre-1.0.5.tgz#91e5e3b31b4a85ce37de72f6511a786f62f29b51" - integrity sha512-p43aAQY3aijhDnBLCriPao5YArlRjD4mSHRJq9PsBhljVLWqQQXcn6seSd77d+bD1tATLhB8tQ2eYoxMtMydXQ== +"@changesets/pre@^1.0.6": + version "1.0.6" + resolved "https://registry.npmjs.org/@changesets/pre/-/pre-1.0.6.tgz#45700cf18274b35b2296000befe7fe4df8ff046f" + integrity sha512-ZwFFQLjhTmA4hj8+Cf9pm6nD9Tp+AiBz1wJLaGum4Ae1fPXMwDnJfHknFUTytqZBlC0gHkiGSj6QkUuetWvckg== dependencies: - "@babel/runtime" "^7.4.4" + "@babel/runtime" "^7.10.4" "@changesets/errors" "^0.1.4" - "@changesets/types" "^3.0.0" + "@changesets/types" "^4.0.0" "@manypkg/get-packages" "^1.0.1" fs-extra "^7.0.1" -"@changesets/read@^0.4.6": - version "0.4.6" - resolved "https://registry.npmjs.org/@changesets/read/-/read-0.4.6.tgz#1c03e709a870a070fc95490ffa696297d23458f7" - integrity sha512-rOd8dsF/Lgyy2SYlDalb3Ts/meDI2AcKPXYhSXIW3k6+ZLlj6Pt+nmgV5Ut8euyH7loibklNTDemfvMffF4xig== +"@changesets/read@^0.4.7": + version "0.4.7" + resolved "https://registry.npmjs.org/@changesets/read/-/read-0.4.7.tgz#5a32ae7092330fba31eaec4c83321bb936605766" + integrity sha512-E70QrYQpSCMF0nC0dlPU7i6A9zht+8zkQczrKMbOUwDVrfidcvgojxfuJSQbzptYSb9OKYh8GOLd+bsq9+uO9Q== dependencies: - "@babel/runtime" "^7.4.4" - "@changesets/git" "^1.0.5" + "@babel/runtime" "^7.10.4" + "@changesets/git" "^1.1.1" "@changesets/logger" "^0.0.5" - "@changesets/parse" "^0.3.6" - "@changesets/types" "^3.0.0" + "@changesets/parse" "^0.3.8" + "@changesets/types" "^4.0.0" chalk "^2.1.0" fs-extra "^7.0.1" p-filter "^2.1.0" -"@changesets/types@^3.0.0", "@changesets/types@^3.1.0", "@changesets/types@^3.1.1", "@changesets/types@^3.3.0": - version "3.3.0" - resolved "https://registry.npmjs.org/@changesets/types/-/types-3.3.0.tgz#04cd8184b2d2da760667bd89bf9b930938dbd96e" - integrity sha512-rJamRo+OD/MQekImfIk07JZwYSB18iU6fYL8xOg0gfAiTh1a1+OlR1fPIxm55I7RsWw812is2YcPPwXdIewrhA== +"@changesets/types@^4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@changesets/types/-/types-4.0.0.tgz#635f804546b0a96ecc0ca3f26403a6782a3dc938" + integrity sha512-whLmPx2wgJRoOtxVZop+DJ71z1gTSkij7osiHgN+pe//FiE6bb4ffvBBb0rACs2cUPfAkWxgSPzqkECgKS1jvQ== -"@changesets/write@^0.1.3": - version "0.1.3" - resolved "https://registry.npmjs.org/@changesets/write/-/write-0.1.3.tgz#00ae575af50274773d7493e77fb96838a08ad8ad" - integrity sha512-q79rbwlVmTNKP9O6XxcMDj81CEOn/kQHbTFdRleW0tFUv98S1EyEAE9vLPPzO6WnQipHnaozxB1zMhHy0aQn8Q== +"@changesets/write@^0.1.4": + version "0.1.4" + resolved "https://registry.npmjs.org/@changesets/write/-/write-0.1.4.tgz#5828ecc70c48d0e8696c5f13fe06b730cdfde6f2" + integrity sha512-uco+vS3mo2JqflLciIU707har+6AEFOeP8pgu3vVC1M2WcKukQgR1KylHFqZJxKQWahf8mQnuUSbgR4yJQuhmA== dependencies: - "@babel/runtime" "^7.4.4" - "@changesets/types" "^3.0.0" + "@babel/runtime" "^7.10.4" + "@changesets/types" "^4.0.0" fs-extra "^7.0.1" human-id "^1.0.2" - prettier "^1.18.2" + prettier "^1.19.1" "@cnakazawa/watch@^1.0.3": version "1.0.4" @@ -21411,7 +21411,7 @@ prepend-http@^2.0.0: resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= -prettier@^1.18.2, prettier@^1.19.1: +prettier@^1.19.1: version "1.19.1" resolved "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== From fd30ad189310f6f3e2dfcaf4cb6f7af6d0512a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 26 May 2021 09:33:28 +0200 Subject: [PATCH 25/42] bump browserslist at least partially to address security warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- yarn.lock | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/yarn.lock b/yarn.lock index f19bff6940..8b4b31c972 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9035,15 +9035,15 @@ browserslist@4.14.2: node-releases "^1.1.61" browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.15.0: - version "4.15.0" - resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.15.0.tgz#3d48bbca6a3f378e86102ffd017d9a03f122bdb0" - integrity sha512-IJ1iysdMkGmjjYeRlDU8PQejVwxvVO5QOfXH7ylW31GO6LwNRSmm/SgRXtNsEXqMLl2e+2H5eEJ7sfynF8TCaQ== + version "4.16.6" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2" + integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ== dependencies: - caniuse-lite "^1.0.30001164" - colorette "^1.2.1" - electron-to-chromium "^1.3.612" + caniuse-lite "^1.0.30001219" + colorette "^1.2.2" + electron-to-chromium "^1.3.723" escalade "^3.1.1" - node-releases "^1.1.67" + node-releases "^1.1.71" bs-logger@0.x: version "0.2.6" @@ -9411,10 +9411,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001164: - version "1.0.30001228" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz" - integrity sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001219: + version "1.0.30001230" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001230.tgz#8135c57459854b2240b57a4a6786044bdc5a9f71" + integrity sha512-5yBd5nWCBS+jWKTcHOzXwo5xzcj4ePE/yjtkZyUV1BTUmrBaA9MRGC+e7mxnqXSA90CmCA8L3eKLaSUkt099IQ== canvas@^2.6.1: version "2.7.0" @@ -12025,10 +12025,10 @@ electron-to-chromium@^1.3.564: resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.685.tgz#f636d17c9c232c925f8bbfbff4f86303da091ff9" integrity sha512-C3oFZNkJ8lz85ADqr3hzpjBc2ciejMRN2SCd/D0hwcqpr6MGxfdN/j89VN6l+ERTuCUvhg0VYsf40Q4qTz4bhQ== -electron-to-chromium@^1.3.612: - version "1.3.620" - resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.620.tgz#c6f36a7e398acc9d7d12743a6f58d536fbc58700" - integrity sha512-YbgWXUR2Mu+Fp6rm3GZ5YJdNo8SgZKLUTNSl2PNvdOcM8OIz07jRJnRkIaV9vdszFv9UUuGChh19w9qSuoLJgw== +electron-to-chromium@^1.3.723: + version "1.3.739" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.739.tgz#f07756aa92cabd5a6eec6f491525a64fe62f98b9" + integrity sha512-+LPJVRsN7hGZ9EIUUiWCpO7l4E3qBYHNadazlucBfsXBbccDFNKUBAgzE68FnkWGJPwD/AfKhSzL+G+Iqb8A4A== elegant-spinner@^1.0.1: version "1.0.1" @@ -19421,10 +19421,10 @@ node-releases@^1.1.61: resolved "https://registry.npmjs.org/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb" integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg== -node-releases@^1.1.67: - version "1.1.67" - resolved "https://registry.npmjs.org/node-releases/-/node-releases-1.1.67.tgz#28ebfcccd0baa6aad8e8d4d8fe4cbc49ae239c12" - integrity sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg== +node-releases@^1.1.71: + version "1.1.72" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-1.1.72.tgz#14802ab6b1039a79a0c7d662b610a5bbd76eacbe" + integrity sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw== node-request-interceptor@^0.3.5: version "0.3.5" From 7ab5bfe684fed608141d260b8bf1491859b7dd88 Mon Sep 17 00:00:00 2001 From: Rob Long Date: Wed, 14 Apr 2021 11:00:18 +0100 Subject: [PATCH 26/42] Add support for fullHeight variant Signed-off-by: Rob Long --- .changeset/tidy-garlics-decide.md | 5 +++ .../src/components/AboutCard/AboutCard.tsx | 31 ++++++++++++++++--- 2 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 .changeset/tidy-garlics-decide.md diff --git a/.changeset/tidy-garlics-decide.md b/.changeset/tidy-garlics-decide.md new file mode 100644 index 0000000000..c011bf84cf --- /dev/null +++ b/.changeset/tidy-garlics-decide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Add support for fullHeight variant to the AboutCard diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index e0cd0c2160..2bcf2a5453 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -23,6 +23,7 @@ import { import { HeaderIconLinkRow, IconLinkVerticalProps, + InfoCardVariants, useApi, } from '@backstage/core'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; @@ -50,15 +51,23 @@ const useStyles = makeStyles({ height: 'calc(100% - 10px)', // for pages without content header marginBottom: '10px', }, + fullHeightCard: { + display: 'flex', + flexDirection: 'column', + height: '100%', + }, gridItemCardContent: { flex: 1, }, + fullHeightCardContent: { + flex: 1, + }, }); type AboutCardProps = { /** @deprecated The entity is now grabbed from context instead */ entity?: Entity; - variant?: 'gridItem'; + variant?: InfoCardVariants; }; export function AboutCard({ variant }: AboutCardProps) { @@ -103,8 +112,22 @@ export function AboutCard({ variant }: AboutCardProps) { href: 'api', }; + let cardClass = ''; + if (variant === 'gridItem') { + cardClass = classes.gridItemCard; + } else if (variant === 'fullHeight') { + cardClass = classes.fullHeightCard; + } + + let cardContentClass = ''; + if (variant === 'gridItem') { + cardContentClass = classes.gridItemCardContent; + } else if (variant === 'fullHeight') { + cardContentClass = classes.fullHeightCardContent; + } + return ( - + - + From 704875e26dc6c305c179ca04e4b645406eeeec87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 25 May 2021 22:01:07 +0200 Subject: [PATCH 27/42] Refactor entity validation in catalog-model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/twelve-bobcats-grow.md | 14 + .github/styles/vocab.txt | 190 +++++++------- packages/catalog-model/api-report.md | 64 +++-- packages/catalog-model/package.json | 1 + packages/catalog-model/src/entity/Entity.ts | 5 +- .../src/entity/EntityEnvelope.ts | 52 ++++ packages/catalog-model/src/entity/index.ts | 1 + .../src/kinds/ApiEntityV1alpha1.ts | 13 +- .../src/kinds/ComponentEntityV1alpha1.ts | 13 +- .../src/kinds/DomainEntityV1alpha1.ts | 13 +- .../src/kinds/GroupEntityV1alpha1.ts | 13 +- .../src/kinds/LocationEntityV1alpha1.ts | 13 +- .../src/kinds/ResourceEntityV1alpha1.ts | 13 +- .../src/kinds/SystemEntityV1alpha1.ts | 13 +- .../src/kinds/TemplateEntityV1alpha1.ts | 13 +- .../src/kinds/TemplateEntityV1beta2.ts | 15 +- .../src/kinds/UserEntityV1alpha1.ts | 13 +- packages/catalog-model/src/kinds/index.ts | 3 +- packages/catalog-model/src/kinds/util.ts | 66 +---- .../src/schema/Entity.schema.json | 2 +- .../src/schema/EntityEnvelope.schema.json | 61 +++++ packages/catalog-model/src/validation/ajv.ts | 137 ++++++++++ .../entityEnvelopeSchemaValidator.test.ts | 129 ++++++++++ .../entityEnvelopeSchemaValidator.ts | 58 +++++ .../entityKindSchemaValidator.test.ts | 110 ++++++++ .../validation/entityKindSchemaValidator.ts | 82 ++++++ .../entitySchemaValidator.test.ts} | 243 ++++++++---------- .../src/validation/entitySchemaValidator.ts | 55 ++++ .../catalog-model/src/validation/index.ts | 3 + .../DefaultCatalogProcessingOrchestrator.ts | 3 + 30 files changed, 982 insertions(+), 429 deletions(-) create mode 100644 .changeset/twelve-bobcats-grow.md create mode 100644 packages/catalog-model/src/entity/EntityEnvelope.ts create mode 100644 packages/catalog-model/src/schema/EntityEnvelope.schema.json create mode 100644 packages/catalog-model/src/validation/ajv.ts create mode 100644 packages/catalog-model/src/validation/entityEnvelopeSchemaValidator.test.ts create mode 100644 packages/catalog-model/src/validation/entityEnvelopeSchemaValidator.ts create mode 100644 packages/catalog-model/src/validation/entityKindSchemaValidator.test.ts create mode 100644 packages/catalog-model/src/validation/entityKindSchemaValidator.ts rename packages/catalog-model/src/{entity/Entity.test.ts => validation/entitySchemaValidator.test.ts} (65%) create mode 100644 packages/catalog-model/src/validation/entitySchemaValidator.ts diff --git a/.changeset/twelve-bobcats-grow.md b/.changeset/twelve-bobcats-grow.md new file mode 100644 index 0000000000..302af3c60e --- /dev/null +++ b/.changeset/twelve-bobcats-grow.md @@ -0,0 +1,14 @@ +--- +'@backstage/catalog-model': minor +--- + +Breaking changes: + +- The long-deprecated `schemaValidator` export is finally removed. + +Additions: + +- The `EntityEnvelope` type, which is a supertype of `Entity`. +- The `entityEnvelopeSchemaValidator` function, which returns a validator for an `EntityEnvelope` or its subtypes, based on a JSON schema. +- The `entitySchemaValidator` function, which returns a validator for an `Entity` or its subtypes, based on a JSON schema. +- The `entityKindSchemaValidator` function, which returns a specialized validator for custom `Entity` kinds, based on a JSON schema. diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 7840b331e1..c367fa0a4c 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -1,133 +1,58 @@ -Apdex -Api -automations -Autoscaling -Avro -Bigtable -Billett -Bitrise -Blackbox -Chai -Changesets -Chanwit -Cloudformation -Cobertura -Codecov -Codehilite -Config -Datadog -Debounce -Discoverability -Dockerfile -dockerfiles -Dockerize -Docusaurus -Env -Expedia -Fargate -Figma -Firekube -Fiverr -GitHub -GitLab -Grafana -GraphQL -Hackathons -Helidon -Heroku -hoc -Hostname -Iain -JaCoCo -JavaScript -Jira -Kaewkasi -kubernetes -Knex -Leasot -Lerna -LocalStack -Luxon -Minikube -Minio -Mkdocs -Monorepo -microservices -microservice -Namespaces -OAuth -Okta -Onboarding -Platformize -Podman -Preprarer -Protobuf -Proxying -Readme -readonly -rebase -Recharts -Redash -Repo -Rollbar -Rollup -Routable -Scaffolder -Serverless -Sinon -Snyk -Splunk -Spotifiers -Spotify -Superfences -Talkdesk -Telenor -Templater -Templaters -todo -Tolerations -WWW -Weaveworks -Webpack -XML -Zalando -Zhou abc accessors +Apdex api +Api apis args asciidoc async +automations autoscaling +Autoscaling +Avro backrub +Bigtable +Billett +Bitrise +Blackbox bool boolean builtins +Chai changeset changesets +Changesets chanwit +Chanwit ci cisphobia cissexist classname cli cloudbuild +Cloudformation cncf +Cobertura codeblocks +Codecov codehilite +Codehilite codeowners composability composable config +Config configmaps configs const cookiecutter css +Datadog dataflow deadnaming debounce +Debounce declaratively deps destructured @@ -135,60 +60,98 @@ dev devops devs discoverability +Discoverability dls docgen +Dockerfile +dockerfiles +Dockerize dockerode +Docusaurus env +Env esbuild eslint etag +Expedia facto failover +Fargate +Figma +Firekube +Fiverr gitbeaker +GitHub +GitLab +Grafana graphql +GraphQL graphviz +Hackathons haproxy +Helidon +Heroku +hoc horizontalpodautoscalers +Hostname hotspots html http https +Iain img incentivised inlined inlinehilite interop +JaCoCo +JavaScript +Jira jq js json jsonnet jsx +Kaewkasi +Knex kubectl kubernetes +kubernetes learnings +Leasot lerna +Lerna +LocalStack lockfile lunr +Luxon magiclink mailto maintainership makefile md memcache +microservice +microservices microsite middleware minikube +Minikube +Minio misconfiguration misconfigured misgendering mkdocs +Mkdocs monorepo +Monorepo monorepos msw mysql namespace namespaced namespaces +Namespaces namespacing neuro newrelic @@ -200,43 +163,67 @@ noop npm nvarchar nvm +OAuth octokit oidc +Okta onboarding +Onboarding pagerduty pageview parallelization plantuml +Platformize +Podman postgres postpack pre prebaked preconfigured prepack +Preprarer productional +Protobuf proxying +Proxying pygments pymdownx rankdir readme +Readme +readonly +rebase +Recharts +Redash replicasets repo +Repo repos rerender rollbar +Rollbar +Rollup routable +Routable rst rsync ruleset sam scaffolded scaffolder +Scaffolder seb semlas semver +Serverless +Sinon +Snyk sourcemaps sparklines +Splunk +Spotifiers spotify +Spotify sqlite squidfunk src @@ -247,17 +234,25 @@ subkey subroutes subtree superfences +Superfences superset +supertype talkdesk +Talkdesk tasklist techdocs +Telenor templated templater +Templater templaters +Templaters theia thumbsup toc +todo tolerations +Tolerations toolchain toolsets tooltip @@ -273,11 +268,18 @@ untracked upvote url utils +validator validators varchar +Weaveworks +Webpack winston www +WWW +XML xyz yaml +Zalando +Zhou zoomable zsh diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index 7b5542fcd6..688f5ad249 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -18,9 +18,9 @@ export const analyzeLocationSchema: yup.ObjectSchema<{ // @public (undocumented) interface ApiEntityV1alpha1 extends Entity { // (undocumented) - apiVersion: typeof API_VERSION[number]; + apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; // (undocumented) - kind: typeof KIND; + kind: 'API'; // (undocumented) spec: { type: string; @@ -54,9 +54,9 @@ export function compareEntityToRef(entity: Entity, ref: EntityRef | EntityName, // @public (undocumented) interface ComponentEntityV1alpha1 extends Entity { // (undocumented) - apiVersion: typeof API_VERSION_2[number]; + apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; // (undocumented) - kind: typeof KIND_2; + kind: 'Component'; // (undocumented) spec: { type: string; @@ -87,9 +87,9 @@ export class DefaultNamespaceEntityPolicy implements EntityPolicy { // @public (undocumented) interface DomainEntityV1alpha1 extends Entity { // (undocumented) - apiVersion: typeof API_VERSION_3[number]; + apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; // (undocumented) - kind: typeof KIND_3; + kind: 'Domain'; // (undocumented) spec: { owner: string; @@ -107,7 +107,7 @@ export const domainEntityV1alpha1Validator: KindValidator; export const EDIT_URL_ANNOTATION = "backstage.io/edit-url"; // @public -export type Entity = { +export type Entity = EntityEnvelope & { apiVersion: string; kind: string; metadata: EntityMeta; @@ -122,9 +122,25 @@ export const ENTITY_DEFAULT_NAMESPACE = "default"; // @public export const ENTITY_META_GENERATED_FIELDS: readonly ["uid", "etag", "generation"]; +// @public +export type EntityEnvelope = { + apiVersion: string; + kind: string; + metadata: { + name: string; + namespace?: string; + }; +}; + +// @public +export function entityEnvelopeSchemaValidator(schema?: unknown): (data: unknown) => T; + // @public export function entityHasChanges(previous: Entity, next: Entity): boolean; +// @public +export function entityKindSchemaValidator(schema: unknown): (data: unknown) => T | false; + // @public export type EntityLink = { url: string; @@ -184,6 +200,9 @@ export type EntityRelationSpec = { target: EntityName; }; +// @public +export function entitySchemaValidator(schema?: unknown): (data: unknown) => T; + // @public export class FieldFormatEntityPolicy implements EntityPolicy { constructor(validators?: Validators); @@ -212,9 +231,9 @@ export function getEntitySourceLocation(entity: Entity): { // @public (undocumented) interface GroupEntityV1alpha1 extends Entity { // (undocumented) - apiVersion: typeof API_VERSION_4[number]; + apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; // (undocumented) - kind: typeof KIND_4; + kind: 'Group'; // (undocumented) spec: { type: string; @@ -279,9 +298,9 @@ export const LOCATION_ANNOTATION = "backstage.io/managed-by-location"; // @public (undocumented) interface LocationEntityV1alpha1 extends Entity { // (undocumented) - apiVersion: typeof API_VERSION_5[number]; + apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; // (undocumented) - kind: typeof KIND_5; + kind: 'Location'; // (undocumented) spec: { type?: string; @@ -405,9 +424,9 @@ export const RELATION_PROVIDES_API = "providesApi"; // @public (undocumented) interface ResourceEntityV1alpha1 extends Entity { // (undocumented) - apiVersion: typeof API_VERSION_6[number]; + apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; // (undocumented) - kind: typeof KIND_6; + kind: 'Resource'; // (undocumented) spec: { type: string; @@ -424,9 +443,6 @@ export { ResourceEntityV1alpha1 } // @public (undocumented) export const resourceEntityV1alpha1Validator: KindValidator; -// @public @deprecated (undocumented) -export function schemaValidator(kind: string, apiVersion: readonly string[], schema: yup.Schema): KindValidator; - // @public export class SchemaValidEntityPolicy implements EntityPolicy { // (undocumented) @@ -459,9 +475,9 @@ export function stringifyLocationReference(ref: { // @public (undocumented) interface SystemEntityV1alpha1 extends Entity { // (undocumented) - apiVersion: typeof API_VERSION_7[number]; + apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; // (undocumented) - kind: typeof KIND_7; + kind: 'System'; // (undocumented) spec: { owner: string; @@ -479,9 +495,9 @@ export const systemEntityV1alpha1Validator: KindValidator; // @public (undocumented) interface TemplateEntityV1alpha1 extends Entity { // (undocumented) - apiVersion: typeof API_VERSION_8[number]; + apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; // (undocumented) - kind: typeof KIND_8; + kind: 'Template'; // (undocumented) spec: { type: string; @@ -502,9 +518,9 @@ export const templateEntityV1alpha1Validator: KindValidator; // @public (undocumented) export interface TemplateEntityV1beta2 extends Entity { // (undocumented) - apiVersion: typeof API_VERSION_9[number]; + apiVersion: 'backstage.io/v1beta2'; // (undocumented) - kind: typeof KIND_9; + kind: 'Template'; // (undocumented) metadata: EntityMeta & { title?: string; @@ -548,9 +564,9 @@ export type UNSTABLE_EntityStatusLevel = 'info' | 'warning' | 'error'; // @public (undocumented) interface UserEntityV1alpha1 extends Entity { // (undocumented) - apiVersion: typeof API_VERSION_10[number]; + apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; // (undocumented) - kind: typeof KIND_10; + kind: 'User'; // (undocumented) spec: { profile?: { diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 97ce0e6568..ffb94cb0f8 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -35,6 +35,7 @@ "@types/yup": "^0.29.8", "ajv": "^7.0.3", "json-schema": "^0.3.0", + "typescript-json-schema": "^0.49.0", "lodash": "^4.17.15", "uuid": "^8.0.0", "yup": "^0.29.3" diff --git a/packages/catalog-model/src/entity/Entity.ts b/packages/catalog-model/src/entity/Entity.ts index 3b5a0621bb..2fdf1a93c6 100644 --- a/packages/catalog-model/src/entity/Entity.ts +++ b/packages/catalog-model/src/entity/Entity.ts @@ -16,14 +16,15 @@ import { JsonObject } from '@backstage/config'; import { EntityName } from '../types'; +import { EntityEnvelope } from './EntityEnvelope'; import { UNSTABLE_EntityStatus } from './EntityStatus'; /** - * The format envelope that's common to all versions/kinds of entity. + * The parts of the format that's common to all versions/kinds of entity. * * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/ */ -export type Entity = { +export type Entity = EntityEnvelope & { /** * The version of specification format for this particular entity that * this is written against. diff --git a/packages/catalog-model/src/entity/EntityEnvelope.ts b/packages/catalog-model/src/entity/EntityEnvelope.ts new file mode 100644 index 0000000000..631a8873c7 --- /dev/null +++ b/packages/catalog-model/src/entity/EntityEnvelope.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +/** + * The envelope skeleton parts of an entity - whatever is necessary to be able + * to give it a ref and pass to further validation / policy checking. + * + * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/ + */ +export type EntityEnvelope = { + /** + * The version of specification format for this particular entity that + * this is written against. + */ + apiVersion: string; + + /** + * The high level entity type being described. + */ + kind: string; + + /** + * Metadata related to the entity. + */ + metadata: { + /** + * The name of the entity. + * + * Must be unique within the catalog at any given point in time, for any + * given namespace + kind pair. + */ + name: string; + + /** + * The namespace that the entity belongs to. + */ + namespace?: string; + }; +}; diff --git a/packages/catalog-model/src/entity/index.ts b/packages/catalog-model/src/entity/index.ts index 2ccd205d85..ae2c0bf503 100644 --- a/packages/catalog-model/src/entity/index.ts +++ b/packages/catalog-model/src/entity/index.ts @@ -27,6 +27,7 @@ export type { EntityRelation, EntityRelationSpec, } from './Entity'; +export type { EntityEnvelope } from './EntityEnvelope'; export type { UNSTABLE_EntityStatus, UNSTABLE_EntityStatusItem, diff --git a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts index 5432cafdeb..37d5a4fba0 100644 --- a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts @@ -16,17 +16,11 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/API.v1alpha1.schema.json'; -import entitySchema from '../schema/Entity.schema.json'; -import entityMetaSchema from '../schema/EntityMeta.schema.json'; -import commonSchema from '../schema/shared/common.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; -const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; -const KIND = 'API' as const; - export interface ApiEntityV1alpha1 extends Entity { - apiVersion: typeof API_VERSION[number]; - kind: typeof KIND; + apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; + kind: 'API'; spec: { type: string; lifecycle: string; @@ -37,8 +31,5 @@ export interface ApiEntityV1alpha1 extends Entity { } export const apiEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( - KIND, - API_VERSION, schema, - [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts index 489aa8b1bd..6ef45fda6e 100644 --- a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts @@ -16,17 +16,11 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Component.v1alpha1.schema.json'; -import entitySchema from '../schema/Entity.schema.json'; -import entityMetaSchema from '../schema/EntityMeta.schema.json'; -import commonSchema from '../schema/shared/common.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; -const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; -const KIND = 'Component' as const; - export interface ComponentEntityV1alpha1 extends Entity { - apiVersion: typeof API_VERSION[number]; - kind: typeof KIND; + apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; + kind: 'Component'; spec: { type: string; lifecycle: string; @@ -40,8 +34,5 @@ export interface ComponentEntityV1alpha1 extends Entity { } export const componentEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( - KIND, - API_VERSION, schema, - [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts index 7aab35e367..f23c330a87 100644 --- a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts @@ -16,25 +16,16 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Domain.v1alpha1.schema.json'; -import entitySchema from '../schema/Entity.schema.json'; -import entityMetaSchema from '../schema/EntityMeta.schema.json'; -import commonSchema from '../schema/shared/common.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; -const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; -const KIND = 'Domain' as const; - export interface DomainEntityV1alpha1 extends Entity { - apiVersion: typeof API_VERSION[number]; - kind: typeof KIND; + apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; + kind: 'Domain'; spec: { owner: string; }; } export const domainEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( - KIND, - API_VERSION, schema, - [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts index 74ca4f221a..62a6edbc5e 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts @@ -16,17 +16,11 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Group.v1alpha1.schema.json'; -import entitySchema from '../schema/Entity.schema.json'; -import entityMetaSchema from '../schema/EntityMeta.schema.json'; -import commonSchema from '../schema/shared/common.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; -const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; -const KIND = 'Group' as const; - export interface GroupEntityV1alpha1 extends Entity { - apiVersion: typeof API_VERSION[number]; - kind: typeof KIND; + apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; + kind: 'Group'; spec: { type: string; profile?: { @@ -41,8 +35,5 @@ export interface GroupEntityV1alpha1 extends Entity { } export const groupEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( - KIND, - API_VERSION, schema, - [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts index fb452b6ac7..dc79ff1921 100644 --- a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts @@ -16,17 +16,11 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Location.v1alpha1.schema.json'; -import entitySchema from '../schema/Entity.schema.json'; -import entityMetaSchema from '../schema/EntityMeta.schema.json'; -import commonSchema from '../schema/shared/common.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; -const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; -const KIND = 'Location' as const; - export interface LocationEntityV1alpha1 extends Entity { - apiVersion: typeof API_VERSION[number]; - kind: typeof KIND; + apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; + kind: 'Location'; spec: { type?: string; target?: string; @@ -35,8 +29,5 @@ export interface LocationEntityV1alpha1 extends Entity { } export const locationEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( - KIND, - API_VERSION, schema, - [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts index fd71500f40..4c79209c9c 100644 --- a/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts @@ -16,17 +16,11 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Resource.v1alpha1.schema.json'; -import entitySchema from '../schema/Entity.schema.json'; -import entityMetaSchema from '../schema/EntityMeta.schema.json'; -import commonSchema from '../schema/shared/common.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; -const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; -const KIND = 'Resource' as const; - export interface ResourceEntityV1alpha1 extends Entity { - apiVersion: typeof API_VERSION[number]; - kind: typeof KIND; + apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; + kind: 'Resource'; spec: { type: string; owner: string; @@ -36,8 +30,5 @@ export interface ResourceEntityV1alpha1 extends Entity { } export const resourceEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( - KIND, - API_VERSION, schema, - [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts index 1ee19466f0..41203083a5 100644 --- a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts @@ -16,17 +16,11 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/System.v1alpha1.schema.json'; -import entitySchema from '../schema/Entity.schema.json'; -import entityMetaSchema from '../schema/EntityMeta.schema.json'; -import commonSchema from '../schema/shared/common.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; -const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; -const KIND = 'System' as const; - export interface SystemEntityV1alpha1 extends Entity { - apiVersion: typeof API_VERSION[number]; - kind: typeof KIND; + apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; + kind: 'System'; spec: { owner: string; domain?: string; @@ -34,8 +28,5 @@ export interface SystemEntityV1alpha1 extends Entity { } export const systemEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( - KIND, - API_VERSION, schema, - [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts b/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts index 64721e3739..0600c58278 100644 --- a/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts @@ -16,18 +16,12 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Template.v1alpha1.schema.json'; -import entitySchema from '../schema/Entity.schema.json'; -import entityMetaSchema from '../schema/EntityMeta.schema.json'; -import commonSchema from '../schema/shared/common.schema.json'; import type { JSONSchema } from '../types'; import { ajvCompiledJsonSchemaValidator } from './util'; -const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; -const KIND = 'Template' as const; - export interface TemplateEntityV1alpha1 extends Entity { - apiVersion: typeof API_VERSION[number]; - kind: typeof KIND; + apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; + kind: 'Template'; spec: { type: string; templater: string; @@ -38,8 +32,5 @@ export interface TemplateEntityV1alpha1 extends Entity { } export const templateEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( - KIND, - API_VERSION, schema, - [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts index 7a100de4bb..98b953c9e0 100644 --- a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts +++ b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts @@ -14,20 +14,14 @@ * limitations under the License. */ +import { JsonObject } from '@backstage/config'; import type { Entity, EntityMeta } from '../entity/Entity'; import schema from '../schema/kinds/Template.v1beta2.schema.json'; -import entitySchema from '../schema/Entity.schema.json'; -import entityMetaSchema from '../schema/EntityMeta.schema.json'; -import commonSchema from '../schema/shared/common.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; -import { JsonObject } from '@backstage/config'; - -const API_VERSION = ['backstage.io/v1beta2'] as const; -const KIND = 'Template' as const; export interface TemplateEntityV1beta2 extends Entity { - apiVersion: typeof API_VERSION[number]; - kind: typeof KIND; + apiVersion: 'backstage.io/v1beta2'; + kind: 'Template'; metadata: EntityMeta & { title?: string; }; @@ -46,8 +40,5 @@ export interface TemplateEntityV1beta2 extends Entity { } export const templateEntityV1beta2Validator = ajvCompiledJsonSchemaValidator( - KIND, - API_VERSION, schema, - [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts b/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts index a8700a496e..d73fa7aaf3 100644 --- a/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts @@ -16,17 +16,11 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/User.v1alpha1.schema.json'; -import entitySchema from '../schema/Entity.schema.json'; -import entityMetaSchema from '../schema/EntityMeta.schema.json'; -import commonSchema from '../schema/shared/common.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; -const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; -const KIND = 'User' as const; - export interface UserEntityV1alpha1 extends Entity { - apiVersion: typeof API_VERSION[number]; - kind: typeof KIND; + apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; + kind: 'User'; spec: { profile?: { displayName?: string; @@ -38,8 +32,5 @@ export interface UserEntityV1alpha1 extends Entity { } export const userEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( - KIND, - API_VERSION, schema, - [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/index.ts b/packages/catalog-model/src/kinds/index.ts index 4ae2db483a..e36575f51d 100644 --- a/packages/catalog-model/src/kinds/index.ts +++ b/packages/catalog-model/src/kinds/index.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -export { schemaValidator } from './util'; -export type { KindValidator } from './types'; export { apiEntityV1alpha1Validator } from './ApiEntityV1alpha1'; export type { ApiEntityV1alpha1 as ApiEntity, @@ -59,6 +57,7 @@ export type { } from './TemplateEntityV1alpha1'; export { templateEntityV1beta2Validator } from './TemplateEntityV1beta2'; export type { TemplateEntityV1beta2 } from './TemplateEntityV1beta2'; +export type { KindValidator } from './types'; export { userEntityV1alpha1Validator } from './UserEntityV1alpha1'; export type { UserEntityV1alpha1 as UserEntity, diff --git a/packages/catalog-model/src/kinds/util.ts b/packages/catalog-model/src/kinds/util.ts index 1e9e7b7521..a907df7f8a 100644 --- a/packages/catalog-model/src/kinds/util.ts +++ b/packages/catalog-model/src/kinds/util.ts @@ -14,66 +14,18 @@ * limitations under the License. */ -import Ajv, { AnySchema } from 'ajv'; -import * as yup from 'yup'; +import { entityKindSchemaValidator } from '../validation'; import { KindValidator } from './types'; -/** - * @deprecated We no longer use yup for the catalog model. This utility method will be removed. - */ -export function schemaValidator( - kind: string, - apiVersion: readonly string[], - schema: yup.Schema, -): KindValidator { +// TODO(freben): Left here as a compatibility helper. It would be nicer to +// just export the inner validator directly. However, all of the already +// exported kind validators have the `KindValidator` signature which is +// different. So let's postpone that change until a later time. +export function ajvCompiledJsonSchemaValidator(schema: unknown): KindValidator { + const validator = entityKindSchemaValidator(schema); return { - async check(envelope) { - if (kind !== envelope.kind || !apiVersion.includes(envelope.apiVersion)) { - return false; - } - await schema.validate(envelope, { strict: true }); - return true; - }, - }; -} - -export function ajvCompiledJsonSchemaValidator( - kind: string, - apiVersion: readonly string[], - schema: AnySchema, - extraSchemas?: AnySchema[], -): KindValidator { - const ajv = new Ajv({ allowUnionTypes: true }); - if (extraSchemas) { - ajv.addSchema(extraSchemas, undefined, undefined, true); - } - const validate = ajv.compile(schema); - - return { - async check(envelope) { - if (kind !== envelope.kind || !apiVersion.includes(envelope.apiVersion)) { - return false; - } - - const result = validate(envelope); - if (result === true) { - return true; - } - - const [error] = validate.errors || []; - if (!error) { - throw new TypeError(`Malformed ${kind}, Unknown error`); - } - - throw new TypeError( - `Malformed ${kind}, ${error.dataPath || ''} ${error.message}${ - error.params - ? ` - ${Object.entries(error.params) - .map(([key, val]) => `${key}: ${val}`) - .join(', ')}` - : '' - }`, - ); + async check(data) { + return validator(data) === data; }, }; } diff --git a/packages/catalog-model/src/schema/Entity.schema.json b/packages/catalog-model/src/schema/Entity.schema.json index b8c43dda9e..90eb9dd5c6 100644 --- a/packages/catalog-model/src/schema/Entity.schema.json +++ b/packages/catalog-model/src/schema/Entity.schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema", "$id": "Entity", - "description": "The format envelope that's common to all versions/kinds of entity.", + "description": "The parts of the format that's common to all versions/kinds of entity.", "examples": [ { "apiVersion": "backstage.io/v1alpha1", diff --git a/packages/catalog-model/src/schema/EntityEnvelope.schema.json b/packages/catalog-model/src/schema/EntityEnvelope.schema.json new file mode 100644 index 0000000000..7e64039c75 --- /dev/null +++ b/packages/catalog-model/src/schema/EntityEnvelope.schema.json @@ -0,0 +1,61 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "EntityEnvelope", + "description": "The envelope skeleton parts of an entity - whatever is necessary to be able to give it a ref and pass to further validation / policy checking.", + "examples": [ + { + "apiVersion": "backstage.io/v1alpha1", + "kind": "Component", + "metadata": { + "name": "LoremService" + } + } + ], + "type": "object", + "required": ["apiVersion", "kind", "metadata"], + "additionalProperties": true, + "properties": { + "apiVersion": { + "type": "string", + "description": "The version of specification format for this particular entity that this is written against.", + "minLength": 1, + "examples": ["backstage.io/v1alpha1", "my-company.net/v1", "1.0"] + }, + "kind": { + "type": "string", + "description": "The high level entity type being described.", + "minLength": 1, + "examples": [ + "API", + "Component", + "Domain", + "Group", + "Location", + "Resource", + "System", + "Template", + "User" + ] + }, + "metadata": { + "type": "object", + "required": ["name"], + "additionalProperties": true, + "properties": { + "name": { + "type": "string", + "description": "The name of the entity. Must be unique within the catalog at any given point in time, for any given namespace + kind pair.", + "examples": ["metadata-proxy"], + "minLength": 1 + }, + "namespace": { + "type": "string", + "description": "The namespace that the entity belongs to.", + "default": "default", + "examples": ["default", "admin"], + "minLength": 1 + } + } + } + } +} diff --git a/packages/catalog-model/src/validation/ajv.ts b/packages/catalog-model/src/validation/ajv.ts new file mode 100644 index 0000000000..81f17ebab7 --- /dev/null +++ b/packages/catalog-model/src/validation/ajv.ts @@ -0,0 +1,137 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 Ajv, { Schema, ValidateFunction } from 'ajv'; +import entitySchema from '../schema/Entity.schema.json'; +import entityEnvelopeSchema from '../schema/EntityEnvelope.schema.json'; +import entityMetaSchema from '../schema/EntityMeta.schema.json'; +import commonSchema from '../schema/shared/common.schema.json'; + +// A local cache of compiled schemas, to avoid duplicate work +const compiledSchemaCache = new Map>(); + +// The core schemas that others can depend on +const refDependencyCandidates = [ + entityEnvelopeSchema, + entitySchema, + entityMetaSchema, + commonSchema, +]; + +export function throwAjvError( + errors: ValidateFunction['errors'], +): never { + if (!errors?.length) { + throw new TypeError('Unknown error'); + } + + const error = errors[0]; + throw new TypeError( + `${error.dataPath || ''} ${error.message}${ + error.params + ? ` - ${Object.entries(error.params) + .map(([key, val]) => `${key}: ${val}`) + .join(', ')}` + : '' + }`, + ); +} + +// Compiles the given schema, and makes sure to also grab any core dependencies +// that it depends on +export function compileAjvSchema( + schema: Schema, + options: { disableCache?: boolean } = {}, +): ValidateFunction { + const disableCache = options?.disableCache ?? false; + + if (!disableCache) { + const cached = compiledSchemaCache.get(schema); + if (cached) { + return cached; + } + } + + const extraSchemas = getExtraSchemas(schema); + const ajv = new Ajv({ + allowUnionTypes: true, + allErrors: true, + validateSchema: true, + }); + if (extraSchemas.length) { + ajv.addSchema(extraSchemas, undefined, undefined, true); + } + const compiled = ajv.compile(schema); + + if (!disableCache) { + compiledSchemaCache.set(schema, compiled); + } + + return compiled; +} + +// Find refs in the given schema and recursively in all known schemas it +// targets, collecting that list of schemas as we go +function getExtraSchemas(schema: Schema): Schema[] { + if (typeof schema !== 'object') { + return []; + } + + const seen = new Set(); + if (schema.$id) { + seen.add(schema.$id); + } + + const selected = new Array(); + + const todo: Schema[] = [schema]; + while (todo.length) { + const current = todo.pop()!; + + for (const ref of getAllRefs(current)) { + if (!seen.has(ref)) { + seen.add(ref); + + const match = refDependencyCandidates.find(c => c.$id === ref); + if (match) { + selected.push(match); + todo.push(match); + } + } + } + } + + return selected; +} + +// Naively step through the entire schema looking for "$ref": "x" pairs. The +// resulting iterator may contain duplicates. Ignores fragments, i.e. for a ref +// of "a#b", it will just yield "a". +function* getAllRefs(schema: Schema): Iterable { + const todo: any[] = [schema]; + while (todo.length) { + const current = todo.pop()!; + if (typeof current === 'object' && current) { + for (const [key, value] of Object.entries(current)) { + if (key === '$ref' && typeof value === 'string') { + yield value.split('#')[0]; + } else { + todo.push(value); + } + } + } + } +} diff --git a/packages/catalog-model/src/validation/entityEnvelopeSchemaValidator.test.ts b/packages/catalog-model/src/validation/entityEnvelopeSchemaValidator.test.ts new file mode 100644 index 0000000000..7c6613936c --- /dev/null +++ b/packages/catalog-model/src/validation/entityEnvelopeSchemaValidator.test.ts @@ -0,0 +1,129 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { entityEnvelopeSchemaValidator } from './entityEnvelopeSchemaValidator'; + +describe('entityEnvelopeSchemaValidator', () => { + const validator = entityEnvelopeSchemaValidator(); + let entity: any; + + beforeEach(() => { + entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test', + namespace: 'ns', + }, + }; + }); + + it('happy path: accepts valid data', () => { + expect(() => validator(entity)).not.toThrow(); + }); + + // + // apiVersion and kind + // + + it('rejects wrong root type', () => { + expect(() => validator(7)).toThrow(/object/); + }); + + it('accepts unknown root fields', () => { + entity.blah = 7; + expect(() => validator(entity)).not.toThrow(); + }); + + it('rejects missing apiVersion', () => { + delete entity.apiVersion; + expect(() => validator(entity)).toThrow(/apiVersion/); + }); + + it('rejects bad apiVersion type', () => { + entity.apiVersion = 7; + expect(() => validator(entity)).toThrow(/apiVersion/); + }); + + it('rejects empty apiVersion', () => { + entity.apiVersion = ''; + expect(() => validator(entity)).toThrow(/apiVersion/); + }); + + it('rejects missing kind', () => { + delete entity.kind; + expect(() => validator(entity)).toThrow(/kind/); + }); + + it('rejects bad kind type', () => { + entity.kind = 7; + expect(() => validator(entity)).toThrow(/kind/); + }); + + it('rejects empty kind', () => { + entity.kind = ''; + expect(() => validator(entity)).toThrow(/kind/); + }); + + // + // metadata + // + + it('rejects missing metadata', () => { + delete entity.metadata; + expect(() => validator(entity)).toThrow(/metadata/); + }); + + it('rejects bad metadata type', () => { + entity.metadata = 7; + expect(() => validator(entity)).toThrow(/metadata/); + }); + + it('rejects missing name', () => { + delete entity.metadata.name; + expect(() => validator(entity)).toThrow(/name/); + }); + + it('rejects empty name', () => { + entity.metadata.name = ''; + expect(() => validator(entity)).toThrow(/name/); + }); + + it('rejects bad name type', () => { + entity.metadata.name = 7; + expect(() => validator(entity)).toThrow(/name/); + }); + + it('accepts missing namespace', () => { + delete entity.metadata.namespace; + expect(() => validator(entity)).not.toThrow(); + }); + + it('rejects empty namespace', () => { + entity.metadata.namespace = ''; + expect(() => validator(entity)).toThrow(/namespace/); + }); + + it('rejects bad namespace type', () => { + entity.metadata.namespace = 7; + expect(() => validator(entity)).toThrow(/namespace/); + }); + + it('accepts unknown metadata fields', () => { + entity.metadata.blah = 7; + expect(() => validator(entity)).not.toThrow(); + }); +}); diff --git a/packages/catalog-model/src/validation/entityEnvelopeSchemaValidator.ts b/packages/catalog-model/src/validation/entityEnvelopeSchemaValidator.ts new file mode 100644 index 0000000000..52ae00e399 --- /dev/null +++ b/packages/catalog-model/src/validation/entityEnvelopeSchemaValidator.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { Schema } from 'ajv'; +import { EntityEnvelope } from '../entity/EntityEnvelope'; +import entityEnvelopeSchema from '../schema/EntityEnvelope.schema.json'; +import { compileAjvSchema, throwAjvError } from './ajv'; + +/** + * Creates a validation function that takes some arbitrary data, and either + * returns that data cast to an {@link EntityEnvelope} (or the given subtype) + * if it matches that schema, or throws a {@link TypeError} describing the + * errors. + * + * Note that this validator is only meant for applying the base schema checks; + * it does not take custom policies or additional processor based validation + * into account. + * + * By default, the plain `EntityEnvelope` schema is used. If you pass in your + * own, it may contain `$ref` references to the following, which are resolved + * automatically for you: + * + * - EntityEnvelope + * - Entity + * - EntityMeta + * - common# + * + * @see https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema + */ +export function entityEnvelopeSchemaValidator< + T extends EntityEnvelope = EntityEnvelope +>(schema?: unknown): (data: unknown) => T { + const validate = compileAjvSchema( + schema ? (schema as Schema) : entityEnvelopeSchema, + ); + + return data => { + const result = validate(data); + if (result === true) { + return data as T; + } + + throw throwAjvError(validate.errors); + }; +} diff --git a/packages/catalog-model/src/validation/entityKindSchemaValidator.test.ts b/packages/catalog-model/src/validation/entityKindSchemaValidator.test.ts new file mode 100644 index 0000000000..4b258aed14 --- /dev/null +++ b/packages/catalog-model/src/validation/entityKindSchemaValidator.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { entityKindSchemaValidator } from './entityKindSchemaValidator'; +import componentSchema from '../schema/kinds/Component.v1alpha1.schema.json'; + +describe('entityKindSchemaValidator', () => { + const validator = entityKindSchemaValidator(componentSchema); + let entity: any; + + beforeEach(() => { + entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + uid: 'e01199ab-08cc-44c2-8e19-5c29ded82521', + etag: 'lsndfkjsndfkjnsdfkjnsd==', + generation: 13, + name: 'test', + namespace: 'ns', + labels: { + 'backstage.io/custom': 'ValueStuff', + }, + annotations: { + 'example.com/bindings': 'are-secret', + }, + tags: ['java', 'data'], + links: [ + { + url: 'https://example.com', + title: 'Website', + icon: 'website', + }, + ], + }, + spec: { + type: 'service', + lifecycle: 'production', + owner: 'me', + }, + relations: [ + { type: 't', target: { kind: 'k', namespace: 'ns', name: 'n' } }, + ], + status: { + items: [ + { + type: 't', + level: 'error', + message: 'm', + error: { name: 'n', message: 'm', code: '1', stack: 's' }, + }, + ], + }, + }; + }); + + it('works in the happy path', () => { + expect(validator(entity)).toBe(entity); + }); + + it('nicely rejects an unknown kind', () => { + entity.kind = 'Unknown'; + expect(validator(entity)).toBe(false); + }); + + it('nicely rejects an unknown apiVersion', () => { + entity.apiVersion = 'backstage.io/v1alpha7'; + expect(validator(entity)).toBe(false); + }); + + it('nicely rejects when both kind and apiVersion mismatch', () => { + entity.apiVersion = 'backstage.io/v1alpha7'; + entity.kind = 'Unknown'; + expect(validator(entity)).toBe(false); + }); + + it('rejects when the kind is actually breaking other rules than enum', () => { + entity.kind = 7; + expect(() => validator(entity)).toThrow(/kind/); + }); + + it('rejects when the apiVersion is actually breaking other rules than enum', () => { + entity.apiVersion = 7; + expect(() => validator(entity)).toThrow(/apiVersion/); + }); + + it('rejects nicely when there is both a nice mismatch and a fatal error', () => { + entity.kind = 'Unknown'; + entity.metadata = 7; + expect(validator(entity)).toBe(false); + }); + + it('rejects on errors in other parts of the schema', () => { + entity.spec = 7; + expect(() => validator(entity)).toThrow(/spec/); + }); +}); diff --git a/packages/catalog-model/src/validation/entityKindSchemaValidator.ts b/packages/catalog-model/src/validation/entityKindSchemaValidator.ts new file mode 100644 index 0000000000..c722687f9e --- /dev/null +++ b/packages/catalog-model/src/validation/entityKindSchemaValidator.ts @@ -0,0 +1,82 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { Schema } from 'ajv'; +import { Entity } from '../entity'; +import { compileAjvSchema, throwAjvError } from './ajv'; + +/** + * Creates a validation function that takes some arbitrary data, and either + * returns that data cast to a `T` if it matches that schema, or `false` if the + * schema apiVersion/kind didn't apply to that data, or throws a + * {@link TypeError} describing actual errors. + * + * This validator is highly specialized, in that it has special treatment of + * the `kind` and `apiVersion` root keys. This only works if your schema has + * their rule set to `"enum"`: + * + *
+ *   "apiVersion": {
+ *      "enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"]
+ *   },
+ *   "kind": {
+ *     "enum": ["Group"]
+ *   },
+ * 
+ * + * In the above example, the created validator will return `false` if and only + * if the kind and/or apiVersion mismatch. + * + * Note that this validator is only meant for applying the base schema checks; + * it does not take custom policies or additional processor based validation + * into account. + * + * The given schema may contain `$ref` references to the following, which are + * resolved automatically for you: + * + * - EntityEnvelope + * - Entity + * - EntityMeta + * - common# + * + * @see https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema + */ +export function entityKindSchemaValidator( + schema: unknown, +): (data: unknown) => T | false { + const validate = compileAjvSchema(schema as Schema); + + return data => { + const result = validate(data); + if (result === true) { + return data as T; + } + + // Only in the case where kind and/or apiVersion have enum mismatches AND + // have NO other errors, we call it a soft error. + const softCandidates = validate.errors?.filter(e => + ['/kind', '/apiVersion'].includes(e.dataPath), + ); + if ( + softCandidates?.length && + softCandidates.every(e => e.keyword === 'enum') + ) { + return false; + } + + throw throwAjvError(validate.errors); + }; +} diff --git a/packages/catalog-model/src/entity/Entity.test.ts b/packages/catalog-model/src/validation/entitySchemaValidator.test.ts similarity index 65% rename from packages/catalog-model/src/entity/Entity.test.ts rename to packages/catalog-model/src/validation/entitySchemaValidator.test.ts index 9f3bfc6813..6ab2744c0f 100644 --- a/packages/catalog-model/src/entity/Entity.test.ts +++ b/packages/catalog-model/src/validation/entitySchemaValidator.test.ts @@ -14,16 +14,11 @@ * limitations under the License. */ -import Ajv from 'ajv'; -import { AnyValidateFunction } from 'ajv/dist/core'; -import entitySchema from '../schema/Entity.schema.json'; -import entityMetaSchema from '../schema/EntityMeta.schema.json'; -import commonSchema from '../schema/shared/common.schema.json'; -import { Entity } from './Entity'; +import { entitySchemaValidator } from './entitySchemaValidator'; -describe('Entity', () => { +describe('entitySchemaValidator', () => { + const validator = entitySchemaValidator(); let entity: any; - let validate: AnyValidateFunction; beforeEach(() => { entity = { @@ -69,38 +64,10 @@ describe('Entity', () => { ], }, }; - - const ajv = new Ajv({ allowUnionTypes: true }); - ajv.addSchema([entityMetaSchema, commonSchema], undefined, undefined, true); - validate = ajv.compile(entitySchema); }); - function check(data: unknown): () => true { - return () => { - const result = validate(data); - if (result === true) { - return true; - } - - const [error] = validate.errors || []; - if (!error) { - throw new TypeError('Unknown error'); - } - - throw new TypeError( - `${error.dataPath || ''} ${error.message}${ - error.params - ? ` - ${Object.entries(error.params) - .map(([key, val]) => `${key}: ${val}`) - .join(', ')}` - : '' - }`, - ); - }; - } - it('happy path: accepts valid data', () => { - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); // @@ -108,37 +75,37 @@ describe('Entity', () => { // it('rejects wrong root type', () => { - expect(check((7 as unknown) as Entity)).toThrow(/object/); + expect(() => validator(7)).toThrow(/object/); }); it('rejects missing apiVersion', () => { delete entity.apiVersion; - expect(check(entity)).toThrow(/apiVersion/); + expect(() => validator(entity)).toThrow(/apiVersion/); }); it('rejects bad apiVersion type', () => { entity.apiVersion = 7; - expect(check(entity)).toThrow(/apiVersion/); + expect(() => validator(entity)).toThrow(/apiVersion/); }); it('rejects empty apiVersion', () => { entity.apiVersion = ''; - expect(check(entity)).toThrow(/apiVersion/); + expect(() => validator(entity)).toThrow(/apiVersion/); }); it('rejects missing kind', () => { delete entity.kind; - expect(check(entity)).toThrow(/kind/); + expect(() => validator(entity)).toThrow(/kind/); }); it('rejects bad kind type', () => { entity.kind = 7; - expect(check(entity)).toThrow(/kind/); + expect(() => validator(entity)).toThrow(/kind/); }); it('rejects empty kind', () => { entity.kind = ''; - expect(check(entity)).toThrow(/kind/); + expect(() => validator(entity)).toThrow(/kind/); }); // @@ -147,202 +114,202 @@ describe('Entity', () => { it('rejects missing metadata', () => { delete entity.metadata; - expect(check(entity)).toThrow(/metadata/); + expect(() => validator(entity)).toThrow(/metadata/); }); it('rejects bad metadata type', () => { entity.metadata = 7; - expect(check(entity)).toThrow(/metadata/); + expect(() => validator(entity)).toThrow(/metadata/); }); it('accepts missing uid', () => { delete entity.metadata.uid; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('rejects bad uid type', () => { entity.metadata.uid = 7; - expect(check(entity)).toThrow(/uid/); + expect(() => validator(entity)).toThrow(/uid/); }); it('rejects empty uid', () => { entity.metadata.uid = ''; - expect(check(entity)).toThrow(/uid/); + expect(() => validator(entity)).toThrow(/uid/); }); it('accepts missing etag', () => { delete entity.metadata.etag; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('rejects bad etag type', () => { entity.metadata.etag = 7; - expect(check(entity)).toThrow(/etag/); + expect(() => validator(entity)).toThrow(/etag/); }); it('rejects empty etag', () => { entity.metadata.etag = ''; - expect(check(entity)).toThrow(/etag/); + expect(() => validator(entity)).toThrow(/etag/); }); it('accepts missing generation', () => { delete entity.metadata.generation; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('rejects bad generation type', () => { entity.metadata.generation = 'a'; - expect(check(entity)).toThrow(/generation/); + expect(() => validator(entity)).toThrow(/generation/); }); it('rejects zero generation', () => { entity.metadata.generation = 0; - expect(check(entity)).toThrow(/generation/); + expect(() => validator(entity)).toThrow(/generation/); }); it('rejects non-integer generation', () => { entity.metadata.generation = 1.5; - expect(check(entity)).toThrow(/generation/); + expect(() => validator(entity)).toThrow(/generation/); }); it('rejects missing name', () => { delete entity.metadata.name; - expect(check(entity)).toThrow(/name/); + expect(() => validator(entity)).toThrow(/name/); }); it('rejects bad name type', () => { entity.metadata.name = 7; - expect(check(entity)).toThrow(/name/); + expect(() => validator(entity)).toThrow(/name/); }); it('accepts missing namespace', () => { delete entity.metadata.namespace; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('rejects bad namespace type', () => { entity.metadata.namespace = 7; - expect(check(entity)).toThrow(/namespace/); + expect(() => validator(entity)).toThrow(/namespace/); }); it('accepts missing description', () => { delete entity.metadata.description; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('rejects bad description type', () => { entity.metadata.description = 7; - expect(check(entity)).toThrow(/description/); + expect(() => validator(entity)).toThrow(/description/); }); it('accepts missing labels', () => { delete entity.metadata.labels; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('accepts empty labels', () => { entity.metadata.labels = {}; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('rejects bad labels type', () => { entity.metadata.labels = 7; - expect(check(entity)).toThrow(/labels/); + expect(() => validator(entity)).toThrow(/labels/); }); it('accepts missing annotations', () => { delete entity.metadata.annotations; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('accepts empty annotations object', () => { entity.metadata.annotations = {}; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('rejects bad annotations type', () => { entity.metadata.annotations = 7; - expect(check(entity)).toThrow(/annotations/); + expect(() => validator(entity)).toThrow(/annotations/); }); it('rejects bad tags type', () => { entity.metadata.tags = 7; - expect(check(entity)).toThrow(/tags/); + expect(() => validator(entity)).toThrow(/tags/); }); it('accepts empty tags', () => { entity.metadata.tags = []; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('rejects empty tag', () => { entity.metadata.tags[0] = ''; - expect(check(entity)).toThrow(/tags/); + expect(() => validator(entity)).toThrow(/tags/); }); it('rejects bad tag type', () => { entity.metadata.tags[0] = 7; - expect(check(entity)).toThrow(/tags/); + expect(() => validator(entity)).toThrow(/tags/); }); it('accepts missing links', () => { delete entity.metadata.links; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('accepts empty links', () => { entity.metadata.links = []; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('rejects empty links.url', () => { entity.metadata.links[0].url = ''; - expect(check(entity)).toThrow(/links/); + expect(() => validator(entity)).toThrow(/links/); }); it('rejects missing links.url', () => { delete entity.metadata.links[0].url; - expect(check(entity)).toThrow(/links/); + expect(() => validator(entity)).toThrow(/links/); }); it('rejects bad links.url type', () => { entity.metadata.links[0].url = 7; - expect(check(entity)).toThrow(/links/); + expect(() => validator(entity)).toThrow(/links/); }); it('rejects empty links.title', () => { entity.metadata.links[0].title = ''; - expect(check(entity)).toThrow(/links/); + expect(() => validator(entity)).toThrow(/links/); }); it('accepts missing links.title', () => { delete entity.metadata.links[0].title; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('rejects bad links.title type', () => { entity.metadata.links[0].title = 7; - expect(check(entity)).toThrow(/links/); + expect(() => validator(entity)).toThrow(/links/); }); it('rejects empty links.icon', () => { entity.metadata.links[0].icon = ''; - expect(check(entity)).toThrow(/links/); + expect(() => validator(entity)).toThrow(/links/); }); it('accepts missing links.icon', () => { delete entity.metadata.links[0].icon; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('rejects bad links.icon type', () => { entity.metadata.links[0].icon = 7; - expect(check(entity)).toThrow(/links/); + expect(() => validator(entity)).toThrow(/links/); }); it('accepts unknown metadata field', () => { entity.metadata.unknown = 7; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); // @@ -351,17 +318,17 @@ describe('Entity', () => { it('accepts missing spec', () => { delete entity.spec; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('rejects non-object spec', () => { entity.spec = 7; - expect(check(entity)).toThrow(/spec/); + expect(() => validator(entity)).toThrow(/spec/); }); it('accepts unknown spec field', () => { entity.spec.unknown = 7; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); // @@ -370,97 +337,97 @@ describe('Entity', () => { it('accepts missing relations', () => { delete entity.relations; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('accepts empty relations', () => { entity.relations = []; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('rejects bad relations type', () => { entity.relations = 7; - expect(check(entity)).toThrow(/relations/); + expect(() => validator(entity)).toThrow(/relations/); }); it('rejects missing relations.type', () => { delete entity.relations[0].type; - expect(check(entity)).toThrow(/relations/); + expect(() => validator(entity)).toThrow(/relations/); }); it('rejects empty relations.type', () => { entity.relations[0].type = ''; - expect(check(entity)).toThrow(/relations/); + expect(() => validator(entity)).toThrow(/relations/); }); it('rejects wrong relations.type type', () => { entity.relations[0].type = 7; - expect(check(entity)).toThrow(/relations/); + expect(() => validator(entity)).toThrow(/relations/); }); it('rejects missing relations.target', () => { delete entity.relations[0].target; - expect(check(entity)).toThrow(/relations/); + expect(() => validator(entity)).toThrow(/relations/); }); it('rejects empty relations.target', () => { entity.relations[0].target = ''; - expect(check(entity)).toThrow(/relations/); + expect(() => validator(entity)).toThrow(/relations/); }); it('rejects wrong relations.target type', () => { entity.relations[0].target = 7; - expect(check(entity)).toThrow(/relations/); + expect(() => validator(entity)).toThrow(/relations/); }); it('rejects missing relations.target.kind', () => { delete entity.relations[0].target.kind; - expect(check(entity)).toThrow(/relations/); + expect(() => validator(entity)).toThrow(/relations/); }); it('rejects empty relations.target.kind', () => { entity.relations[0].target.kind = ''; - expect(check(entity)).toThrow(/relations/); + expect(() => validator(entity)).toThrow(/relations/); }); it('rejects wrong relations.target.kind type', () => { entity.relations[0].target.kind = 7; - expect(check(entity)).toThrow(/relations/); + expect(() => validator(entity)).toThrow(/relations/); }); it('rejects missing relations.target.namespace', () => { delete entity.relations[0].target.namespace; - expect(check(entity)).toThrow(/relations/); + expect(() => validator(entity)).toThrow(/relations/); }); it('rejects empty relations.target.namespace', () => { entity.relations[0].target.namespace = ''; - expect(check(entity)).toThrow(/relations/); + expect(() => validator(entity)).toThrow(/relations/); }); it('rejects wrong relations.target.namespace type', () => { entity.relations[0].target.namespace = 7; - expect(check(entity)).toThrow(/relations/); + expect(() => validator(entity)).toThrow(/relations/); }); it('rejects missing relations.target.name', () => { delete entity.relations[0].target.name; - expect(check(entity)).toThrow(/relations/); + expect(() => validator(entity)).toThrow(/relations/); }); it('rejects empty relations.target.name', () => { entity.relations[0].target.name = ''; - expect(check(entity)).toThrow(/relations/); + expect(() => validator(entity)).toThrow(/relations/); }); it('rejects wrong relations.target.name type', () => { entity.relations[0].target.name = 7; - expect(check(entity)).toThrow(/relations/); + expect(() => validator(entity)).toThrow(/relations/); }); it('rejects unknown relation field', () => { entity.relations[0].unknown = 7; - expect(check(entity)).toThrow(/unknown/); + expect(() => validator(entity)).toThrow(/unknown/); }); // @@ -469,161 +436,161 @@ describe('Entity', () => { it('accepts missing status', () => { delete entity.status; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('accepts empty status', () => { entity.status = {}; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('rejects bad status type', () => { entity.status = 7; - expect(check(entity)).toThrow(/status/); + expect(() => validator(entity)).toThrow(/status/); }); it('accepts missing status.items', () => { delete entity.status.items; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('accepts empty status.items', () => { entity.status.items = []; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('rejects bad status.items type', () => { entity.status.items = 7; - expect(check(entity)).toThrow(/status/); + expect(() => validator(entity)).toThrow(/status/); }); it('rejects bad status.items item type', () => { entity.status.items[0] = 7; - expect(check(entity)).toThrow(/status/); + expect(() => validator(entity)).toThrow(/status/); }); it('rejects missing status.items.type', () => { delete entity.status.items[0].type; - expect(check(entity)).toThrow(/status/); + expect(() => validator(entity)).toThrow(/status/); }); it('rejects empty status.items.type', () => { entity.status.items[0].type = ''; - expect(check(entity)).toThrow(/status/); + expect(() => validator(entity)).toThrow(/status/); }); it('rejects bad status.items.type type', () => { entity.status.items[0].type = 7; - expect(check(entity)).toThrow(/status/); + expect(() => validator(entity)).toThrow(/status/); }); it('rejects missing status.items.level', () => { delete entity.status.items[0].level; - expect(check(entity)).toThrow(/status/); + expect(() => validator(entity)).toThrow(/status/); }); it('rejects empty status.items.level', () => { entity.status.items[0].level = ''; - expect(check(entity)).toThrow(/status/); + expect(() => validator(entity)).toThrow(/status/); }); it('rejects bad status.items.level type', () => { entity.status.items[0].level = 7; - expect(check(entity)).toThrow(/status/); + expect(() => validator(entity)).toThrow(/status/); }); it('rejects bad status.items.level enum', () => { entity.status.items[0].level = 'unknown'; - expect(check(entity)).toThrow(/status/); + expect(() => validator(entity)).toThrow(/status/); }); it('rejects missing status.items.message', () => { delete entity.status.items[0].message; - expect(check(entity)).toThrow(/status/); + expect(() => validator(entity)).toThrow(/status/); }); it('accepts empty status.items.message', () => { entity.status.items[0].message = ''; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('rejects bad status.items.message type', () => { entity.status.items[0].message = 7; - expect(check(entity)).toThrow(/status/); + expect(() => validator(entity)).toThrow(/status/); }); it('accepts missing status.items.error', () => { delete entity.status.items[0].error; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('rejects missing status.items.error.name', () => { delete entity.status.items[0].error.name; - expect(check(entity)).toThrow(/name/); + expect(() => validator(entity)).toThrow(/name/); }); it('rejects empty status.items.error.name', () => { entity.status.items[0].error.name = ''; - expect(check(entity)).toThrow(/name/); + expect(() => validator(entity)).toThrow(/name/); }); it('rejects bad status.items.error.name type', () => { entity.status.items[0].error.name = 7; - expect(check(entity)).toThrow(/name/); + expect(() => validator(entity)).toThrow(/name/); }); it('rejects missing status.items.error.message', () => { delete entity.status.items[0].error.message; - expect(check(entity)).toThrow(/message/); + expect(() => validator(entity)).toThrow(/message/); }); it('accepts empty status.items.error.message', () => { entity.status.items[0].error.message = ''; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('rejects bad status.items.error.message type', () => { entity.status.items[0].error.message = 7; - expect(check(entity)).toThrow(/message/); + expect(() => validator(entity)).toThrow(/message/); }); it('accepts missing status.items.error.code', () => { delete entity.status.items[0].error.code; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('accepts empty status.items.error.code', () => { entity.status.items[0].error.code = ''; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('rejects bad status.items.error.code type', () => { entity.status.items[0].error.code = 7; - expect(check(entity)).toThrow(/code/); + expect(() => validator(entity)).toThrow(/code/); }); it('accepts missing status.items.error.stack', () => { delete entity.status.items[0].error.stack; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('accepts empty status.items.error.stack', () => { entity.status.items[0].error.stack = ''; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('rejects bad status.items.error.stack type', () => { entity.status.items[0].error.stack = 7; - expect(check(entity)).toThrow(/stack/); + expect(() => validator(entity)).toThrow(/stack/); }); it('accepts unknown status.items field', () => { entity.status.items[0].unknown = 7; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); it('accepts unknown status.items.error field', () => { entity.status.items[0].error.unknown = 7; - expect(check(entity)).not.toThrow(); + expect(() => validator(entity)).not.toThrow(); }); }); diff --git a/packages/catalog-model/src/validation/entitySchemaValidator.ts b/packages/catalog-model/src/validation/entitySchemaValidator.ts new file mode 100644 index 0000000000..8a30f09d31 --- /dev/null +++ b/packages/catalog-model/src/validation/entitySchemaValidator.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { Schema } from 'ajv'; +import { Entity } from '../entity/Entity'; +import entitySchema from '../schema/Entity.schema.json'; +import { compileAjvSchema, throwAjvError } from './ajv'; + +/** + * Creates a validation function that takes some arbitrary data, and either + * returns that data cast to an {@link Entity} (or the given subtype) if it + * matches that schema, or throws a {@link TypeError} describing the errors. + * + * Note that this validator is only meant for applying the base schema checks; + * it does not take custom policies or additional processor based validation + * into account. + * + * By default, the plain `Entity` schema is used. If you pass in your own, it + * may contain `$ref` references to the following, which are resolved + * automatically for you: + * + * - EntityEnvelope + * - Entity + * - EntityMeta + * - common# + * + * @see https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema + */ +export function entitySchemaValidator( + schema?: unknown, +): (data: unknown) => T { + const validate = compileAjvSchema(schema ? (schema as Schema) : entitySchema); + + return data => { + const result = validate(data); + if (result === true) { + return data as T; + } + + throw throwAjvError(validate.errors); + }; +} diff --git a/packages/catalog-model/src/validation/index.ts b/packages/catalog-model/src/validation/index.ts index d679a5323c..bdf812b4ad 100644 --- a/packages/catalog-model/src/validation/index.ts +++ b/packages/catalog-model/src/validation/index.ts @@ -15,6 +15,9 @@ */ export { CommonValidatorFunctions } from './CommonValidatorFunctions'; +export { entityEnvelopeSchemaValidator } from './entityEnvelopeSchemaValidator'; +export { entityKindSchemaValidator } from './entityKindSchemaValidator'; +export { entitySchemaValidator } from './entitySchemaValidator'; export { KubernetesValidatorFunctions } from './KubernetesValidatorFunctions'; export { makeValidator } from './makeValidator'; export type { Validators } from './types'; diff --git a/plugins/catalog-backend/src/next/DefaultCatalogProcessingOrchestrator.ts b/plugins/catalog-backend/src/next/DefaultCatalogProcessingOrchestrator.ts index 9b2d6550d1..c2c001943c 100644 --- a/plugins/catalog-backend/src/next/DefaultCatalogProcessingOrchestrator.ts +++ b/plugins/catalog-backend/src/next/DefaultCatalogProcessingOrchestrator.ts @@ -277,6 +277,9 @@ function createEmitter(logger: Logger, parentEntity: Entity) { return; } if (i.type === 'entity') { + // TODO(freben): Perform the most basic validation here + // (apiVersion, kind, metadata, metadata.name, metadata.namespace, spec) + const originLocation = getEntityOriginLocationRef(parentEntity); deferredEntities.push({ From 35e73aab14c865d33c7671431a6f755199951acb Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Wed, 26 May 2021 14:10:23 +0200 Subject: [PATCH 28/42] Fix wrong yaml indention in docs Signed-off-by: Oliver Sand --- docs/features/software-templates/writing-templates.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 4f9323c6e9..086fb8124f 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -222,7 +222,7 @@ You can see it in the above full example which is a separate step and it looks a little like this: ```yaml - - title: Choose a location + - title: Choose a location required: - repoUrl properties: From b517c7f9b785bf65ca28dd0120601dac18736b58 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Wed, 26 May 2021 14:17:27 +0200 Subject: [PATCH 29/42] Remove indention Signed-off-by: Oliver Sand --- .../software-templates/writing-templates.md | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 086fb8124f..f58db3ae91 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -222,17 +222,17 @@ You can see it in the above full example which is a separate step and it looks a little like this: ```yaml - - title: Choose a location - required: - - repoUrl - properties: - repoUrl: - title: Repository Location - type: string - ui:field: RepoUrlPicker - ui:options: - allowedHosts: - - github.com +- title: Choose a location + required: + - repoUrl + properties: + repoUrl: + title: Repository Location + type: string + ui:field: RepoUrlPicker + ui:options: + allowedHosts: + - github.com ``` The `allowedHosts` part should be set to where you wish to enable this template From 6fd3350a1d38eabae1e2872b4a55672edb478e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 26 May 2021 14:59:17 +0200 Subject: [PATCH 30/42] cache on strings instead MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/catalog-model/src/validation/ajv.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/catalog-model/src/validation/ajv.ts b/packages/catalog-model/src/validation/ajv.ts index 81f17ebab7..02d53fcd15 100644 --- a/packages/catalog-model/src/validation/ajv.ts +++ b/packages/catalog-model/src/validation/ajv.ts @@ -20,8 +20,9 @@ import entityEnvelopeSchema from '../schema/EntityEnvelope.schema.json'; import entityMetaSchema from '../schema/EntityMeta.schema.json'; import commonSchema from '../schema/shared/common.schema.json'; -// A local cache of compiled schemas, to avoid duplicate work -const compiledSchemaCache = new Map>(); +// A local cache of compiled schemas, to avoid duplicate work. +// The keys are JSON stringified versions of the schema +const compiledSchemaCache = new Map>(); // The core schemas that others can depend on const refDependencyCandidates = [ @@ -57,9 +58,10 @@ export function compileAjvSchema( options: { disableCache?: boolean } = {}, ): ValidateFunction { const disableCache = options?.disableCache ?? false; + const cacheKey = disableCache ? '' : JSON.stringify(schema); if (!disableCache) { - const cached = compiledSchemaCache.get(schema); + const cached = compiledSchemaCache.get(cacheKey); if (cached) { return cached; } @@ -77,7 +79,7 @@ export function compileAjvSchema( const compiled = ajv.compile(schema); if (!disableCache) { - compiledSchemaCache.set(schema, compiled); + compiledSchemaCache.set(cacheKey, compiled); } return compiled; From 260aaa6843860c63b150a26780328f6a9586328d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 26 May 2021 15:55:25 +0200 Subject: [PATCH 31/42] bump all @gitbeaker deps + changelogs Signed-off-by: Patrik Oldsberg --- .changeset/tough-melons-ring.md | 7 +++++++ .changeset/tricky-swans-refuse.md | 5 +++++ .../default-app/packages/backend/package.json.hbs | 2 +- plugins/scaffolder-backend/package.json | 2 +- 4 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 .changeset/tough-melons-ring.md create mode 100644 .changeset/tricky-swans-refuse.md diff --git a/.changeset/tough-melons-ring.md b/.changeset/tough-melons-ring.md new file mode 100644 index 0000000000..629c9549d8 --- /dev/null +++ b/.changeset/tough-melons-ring.md @@ -0,0 +1,7 @@ +--- +'@backstage/create-app': patch +--- + +Bumped the `@gitbeaker` dependencies to `29.x`. + +To apply this change to an existing app, update all `@gitbeaker/*` dependencies in your `package.json`s to point to `^29.2.0`. Then run `yarn install` at the root of your project. diff --git a/.changeset/tricky-swans-refuse.md b/.changeset/tricky-swans-refuse.md new file mode 100644 index 0000000000..94e58a31d7 --- /dev/null +++ b/.changeset/tricky-swans-refuse.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Bump `@gitbeaker` dependencies to `29.x`. diff --git a/packages/create-app/templates/default-app/packages/backend/package.json.hbs b/packages/create-app/templates/default-app/packages/backend/package.json.hbs index e2b5b04a60..a174f495c0 100644 --- a/packages/create-app/templates/default-app/packages/backend/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/backend/package.json.hbs @@ -28,7 +28,7 @@ "@backstage/plugin-proxy-backend": "^{{version '@backstage/plugin-proxy-backend'}}", "@backstage/plugin-scaffolder-backend": "^{{version '@backstage/plugin-scaffolder-backend'}}", "@backstage/plugin-techdocs-backend": "^{{version '@backstage/plugin-techdocs-backend'}}", - "@gitbeaker/node": "^28.0.2", + "@gitbeaker/node": "^29.2.0", "@octokit/rest": "^18.5.3", "dockerode": "^3.2.1", "express": "^4.17.1", diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 80d7b58276..66f6fd0687 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -35,7 +35,7 @@ "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", "@backstage/integration": "^0.5.3", - "@gitbeaker/core": "^28.0.2", + "@gitbeaker/core": "^29.2.0", "@gitbeaker/node": "^29.2.0", "@octokit/rest": "^18.5.3", "@types/express": "^4.17.6", From 3a181cff1042363cb0beab4d77e8ece402e0ffa6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 26 May 2021 16:50:13 +0200 Subject: [PATCH 32/42] added changeset for webpack-node-externals bump Signed-off-by: Patrik Oldsberg --- .changeset/twelve-planets-glow.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/twelve-planets-glow.md diff --git a/.changeset/twelve-planets-glow.md b/.changeset/twelve-planets-glow.md new file mode 100644 index 0000000000..d5470bec78 --- /dev/null +++ b/.changeset/twelve-planets-glow.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Bump webpack-node-externals from `2.5.2` to `3.0.0`. From dda070442ed3b01f2a89153752f6466bb86eb77c Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 21 May 2021 17:23:54 +0200 Subject: [PATCH 33/42] Making note of cache stores in documentation. Signed-off-by: Eric Peterson --- .github/styles/vocab.txt | 1 + docs/overview/architecture-overview.md | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index c367fa0a4c..b8f028a2ae 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -113,6 +113,7 @@ json jsonnet jsx Kaewkasi +Keyv Knex kubectl kubernetes diff --git a/docs/overview/architecture-overview.md b/docs/overview/architecture-overview.md index 316f066614..1098a3f5f9 100644 --- a/docs/overview/architecture-overview.md +++ b/docs/overview/architecture-overview.md @@ -182,6 +182,21 @@ work but [aren't tested as fully](https://github.com/backstage/backstage/issues/2460) yet. +## Cache + +The Backstage backend and its builtin plugins are also able to leverage cache +stores as a means of improving performance or reliability. Similar to how +databases are supported, plugins receive logically separated cache connections, +which are powered by [Keyv](https://github.com/lukechilds/keyv) under the hood. + +At this time of writing, Backstage can be configured to use one of two cache +stores: memory, which is mainly used for local testing, and memcache, which is +is a cache store better suited for production deployment. The right cache store +for your Backstage instance will depend on your own run-time constraints and +those specified by the plugins you're running. + +Contributions supporting other cache stores are welcome! + ## Containerization The example Backstage architecture shown above would Dockerize into three From 5bc7b25184e61f045e1dfa79ea509362394a811f Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 21 May 2021 17:24:33 +0200 Subject: [PATCH 34/42] Update default example config to use memory cache store. Signed-off-by: Eric Peterson --- app-config.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app-config.yaml b/app-config.yaml index b1c915f85a..9874ce37ac 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -29,6 +29,8 @@ backend: database: client: sqlite3 connection: ':memory:' + cache: + store: memory cors: origin: http://localhost:3000 methods: [GET, POST, PUT, DELETE] From 70c345df29690c605158a26d60ab2a6bbcfc57c4 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 21 May 2021 17:25:26 +0200 Subject: [PATCH 35/42] Updating create-app so that cache store can be selected/configured. Signed-off-by: Eric Peterson --- packages/create-app/src/createApp.ts | 9 +++++++++ .../create-app/templates/default-app/app-config.yaml.hbs | 9 +++++++++ .../templates/default-app/packages/backend/src/index.ts | 5 ++++- .../templates/default-app/packages/backend/src/types.ts | 2 ++ 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 8a269416a3..e5bf323399 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -112,10 +112,19 @@ export default async (cmd: Command): Promise => { // @ts-ignore choices: ['SQLite', 'PostgreSQL'], }, + { + type: 'list', + name: 'cacheType', + message: chalk.blue('Select cache store for the backend [required]'), + // @ts-ignore + choices: ['in-memory', 'memcache'], + }, ]; const answers: Answers = await inquirer.prompt(questions); answers.dbTypePG = answers.dbType === 'PostgreSQL'; answers.dbTypeSqlite = answers.dbType === 'SQLite'; + answers.cacheTypeMemory = answers.cacheType === 'in-memory'; + answers.cacheTypeMemcache = answers.cacheType === 'memcache'; const templateDir = paths.resolveOwn('templates/default-app'); const tempDir = resolvePath(os.tmpdir(), answers.name); diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index 275b3865f9..7a8eb8f13c 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -34,6 +34,15 @@ backend: #ca: # if you have a CA file and want to verify it you can uncomment this section # $file: /ca/server.crt {{/if}} + {{#if cacheTypeMemory}} + cache: + store: memory + {{/if}} + {{#if cacheTypeMemcache}} + cache: + store: memcache + connection: ${MEMCACHE_USER}:${MEMCACHE_PASSWORD}@${MEMCACHE_HOST}:$MEMCACHE_PORT} + {{/if}} # workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir integrations: diff --git a/packages/create-app/templates/default-app/packages/backend/src/index.ts b/packages/create-app/templates/default-app/packages/backend/src/index.ts index 80dc6230a6..aebd034aae 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/index.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/index.ts @@ -13,6 +13,7 @@ import { getRootLogger, useHotMemoize, notFoundHandler, + CacheManager, SingleConnectionDatabaseManager, SingleHostDiscovery, UrlReaders, @@ -34,11 +35,13 @@ function makeCreateEnv(config: Config) { root.info(`Created UrlReader ${reader}`); const databaseManager = SingleConnectionDatabaseManager.fromConfig(config); + const cacheManager = CacheManager.fromConfig(config); return (plugin: string): PluginEnvironment => { const logger = root.child({ type: 'plugin', plugin }); const database = databaseManager.forPlugin(plugin); - return { logger, database, config, reader, discovery }; + const cache = cacheManager.forPlugin(plugin); + return { logger, database, cache, config, reader, discovery }; }; } diff --git a/packages/create-app/templates/default-app/packages/backend/src/types.ts b/packages/create-app/templates/default-app/packages/backend/src/types.ts index 757a0e5acf..e5bfff2e1f 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/types.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/types.ts @@ -1,6 +1,7 @@ import { Logger } from 'winston'; import { Config } from '@backstage/config'; import { + PluginCacheManager, PluginDatabaseManager, PluginEndpointDiscovery, UrlReader, @@ -9,6 +10,7 @@ import { export type PluginEnvironment = { logger: Logger; database: PluginDatabaseManager; + cache: PluginCacheManager; config: Config; reader: UrlReader discovery: PluginEndpointDiscovery; From 6c4bd674c0ececcb6b8d8356a82ec9e1a8700441 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 21 May 2021 17:38:17 +0200 Subject: [PATCH 36/42] Changeset. Signed-off-by: Eric Peterson --- .changeset/cache-me-if-you-can.md | 82 +++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 .changeset/cache-me-if-you-can.md diff --git a/.changeset/cache-me-if-you-can.md b/.changeset/cache-me-if-you-can.md new file mode 100644 index 0000000000..4322d1158f --- /dev/null +++ b/.changeset/cache-me-if-you-can.md @@ -0,0 +1,82 @@ +--- +'@backstage/create-app': patch +--- + +Cache management has been added to the Backstage backend. + +To apply this change to an existing app, make the following changes: + +```diff +// packages/backend/src/types.ts + +import { Logger } from 'winston'; +import { Config } from '@backstage/config'; +import { ++ PluginCacheManager, + PluginDatabaseManager, + PluginEndpointDiscovery, + UrlReader, +} from '@backstage/backend-common'; + +export type PluginEnvironment = { + logger: Logger; + database: PluginDatabaseManager; ++ cache: PluginCacheManager; + config: Config; + reader: UrlReader + discovery: PluginEndpointDiscovery; +}; +``` + +```diff +// packages/backend/src/index.ts + +import Router from 'express-promise-router'; +import { + createServiceBuilder, + loadBackendConfig, + getRootLogger, + useHotMemoize, + notFoundHandler, ++ CacheManager, + SingleConnectionDatabaseManager, + SingleHostDiscovery, + UrlReaders, +} from '@backstage/backend-common'; +import { Config } from '@backstage/config'; + +function makeCreateEnv(config: Config) { + const root = getRootLogger(); + const reader = UrlReaders.default({ logger: root, config }); + const discovery = SingleHostDiscovery.fromConfig(config); + + root.info(`Created UrlReader ${reader}`); + + const databaseManager = SingleConnectionDatabaseManager.fromConfig(config); ++ const cacheManager = CacheManager.fromConfig(config); + + return (plugin: string): PluginEnvironment => { + const logger = root.child({ type: 'plugin', plugin }); + const database = databaseManager.forPlugin(plugin); +- return { logger, database, config, reader, discovery }; ++ const cache = cacheManager.forPlugin(plugin); ++ return { logger, database, cache, config, reader, discovery }; + }; +} +``` + +To configure a cache store, add a `backend.cache` key to your app-config.yaml. + +```diff +// app-config.yaml + +backend: + baseUrl: http://localhost:7000 + listen: + port: 7000 + database: + client: sqlite3 + connection: ':memory:' ++ cache: ++ store: memory +``` From 85d1398d1cdc25db2735d5101c0883bba71fc075 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 22 May 2021 20:31:03 +0200 Subject: [PATCH 37/42] Update CLI E2E test Signed-off-by: Eric Peterson --- packages/e2e-test/src/commands/run.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index ebf4e6c835..885b0bbb0e 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -209,6 +209,9 @@ async function createApp( } child.stdin?.write(`\n`); + await waitFor(() => stdout.includes('Select cache store for the backend')); + child.stdin?.write(`\n`); + print('Waiting for app create script to be done'); await waitForExit(child); From e6ce2ef9a1f6c162d7e20a4410492e3e0af78128 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 24 May 2021 18:13:48 +0200 Subject: [PATCH 38/42] Grammar, clarity, etc. Signed-off-by: Eric Peterson --- docs/overview/architecture-overview.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/overview/architecture-overview.md b/docs/overview/architecture-overview.md index 1098a3f5f9..af46a8c830 100644 --- a/docs/overview/architecture-overview.md +++ b/docs/overview/architecture-overview.md @@ -190,10 +190,10 @@ databases are supported, plugins receive logically separated cache connections, which are powered by [Keyv](https://github.com/lukechilds/keyv) under the hood. At this time of writing, Backstage can be configured to use one of two cache -stores: memory, which is mainly used for local testing, and memcache, which is -is a cache store better suited for production deployment. The right cache store -for your Backstage instance will depend on your own run-time constraints and -those specified by the plugins you're running. +stores: memory, which is mainly used for local testing, and memcache, which is a +cache store better suited for production deployment. The right cache store for +your Backstage instance will depend on your own run-time constraints and those +required of the plugins you're running. Contributions supporting other cache stores are welcome! From e2780c7e10ce215f9a0d677e37a94dbda357cad3 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 24 May 2021 21:43:20 +0200 Subject: [PATCH 39/42] Just default to in-memory instead. Signed-off-by: Eric Peterson --- packages/create-app/src/createApp.ts | 9 --------- .../create-app/templates/default-app/app-config.yaml.hbs | 7 ------- packages/e2e-test/src/commands/run.ts | 3 --- 3 files changed, 19 deletions(-) diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index e5bf323399..8a269416a3 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -112,19 +112,10 @@ export default async (cmd: Command): Promise => { // @ts-ignore choices: ['SQLite', 'PostgreSQL'], }, - { - type: 'list', - name: 'cacheType', - message: chalk.blue('Select cache store for the backend [required]'), - // @ts-ignore - choices: ['in-memory', 'memcache'], - }, ]; const answers: Answers = await inquirer.prompt(questions); answers.dbTypePG = answers.dbType === 'PostgreSQL'; answers.dbTypeSqlite = answers.dbType === 'SQLite'; - answers.cacheTypeMemory = answers.cacheType === 'in-memory'; - answers.cacheTypeMemcache = answers.cacheType === 'memcache'; const templateDir = paths.resolveOwn('templates/default-app'); const tempDir = resolvePath(os.tmpdir(), answers.name); diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index 7a8eb8f13c..59cfd09753 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -34,15 +34,8 @@ backend: #ca: # if you have a CA file and want to verify it you can uncomment this section # $file: /ca/server.crt {{/if}} - {{#if cacheTypeMemory}} cache: store: memory - {{/if}} - {{#if cacheTypeMemcache}} - cache: - store: memcache - connection: ${MEMCACHE_USER}:${MEMCACHE_PASSWORD}@${MEMCACHE_HOST}:$MEMCACHE_PORT} - {{/if}} # workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir integrations: diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index 885b0bbb0e..ebf4e6c835 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -209,9 +209,6 @@ async function createApp( } child.stdin?.write(`\n`); - await waitFor(() => stdout.includes('Select cache store for the backend')); - child.stdin?.write(`\n`); - print('Waiting for app create script to be done'); await waitForExit(child); From c112abe6f63de73c711f87bc0b6ee45ea5cda33a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 26 May 2021 18:30:38 +0200 Subject: [PATCH 40/42] cli: update eslint config to match eslint-plugin-prettier 8 Signed-off-by: Patrik Oldsberg --- packages/cli/config/eslint.backend.js | 1 - packages/cli/config/eslint.js | 2 -- 2 files changed, 3 deletions(-) diff --git a/packages/cli/config/eslint.backend.js b/packages/cli/config/eslint.backend.js index e7fd4cb688..e860e426d1 100644 --- a/packages/cli/config/eslint.backend.js +++ b/packages/cli/config/eslint.backend.js @@ -19,7 +19,6 @@ module.exports = { '@spotify/eslint-config-base', '@spotify/eslint-config-typescript', 'prettier', - 'prettier/@typescript-eslint', 'plugin:jest/recommended', 'plugin:monorepo/recommended', ], diff --git a/packages/cli/config/eslint.js b/packages/cli/config/eslint.js index fe45f264b5..6448136769 100644 --- a/packages/cli/config/eslint.js +++ b/packages/cli/config/eslint.js @@ -20,8 +20,6 @@ module.exports = { '@spotify/eslint-config-react', '@spotify/eslint-config-typescript', 'prettier', - 'prettier/react', - 'prettier/@typescript-eslint', 'plugin:jest/recommended', 'plugin:monorepo/recommended', ], From 7a7da51463e8873078eb85f972c6aa2142bb803f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 26 May 2021 18:31:21 +0200 Subject: [PATCH 41/42] changesets: added changeset for eslint-config-prettier bump Signed-off-by: Patrik Oldsberg --- .changeset/silly-mirrors-poke.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/silly-mirrors-poke.md diff --git a/.changeset/silly-mirrors-poke.md b/.changeset/silly-mirrors-poke.md new file mode 100644 index 0000000000..4a6f6bacd2 --- /dev/null +++ b/.changeset/silly-mirrors-poke.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Bumped `eslint-config-prettier` to `8.x`. From 438a512ebb91228b53d591cca8900973163404da Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 26 May 2021 22:25:38 +0200 Subject: [PATCH 42/42] config-loader: workaround for config schema parsing in typescript 4.3 Signed-off-by: Patrik Oldsberg --- .changeset/selfish-turtles-deliver.md | 5 +++++ .../config-loader/src/lib/schema/collect.ts | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 .changeset/selfish-turtles-deliver.md diff --git a/.changeset/selfish-turtles-deliver.md b/.changeset/selfish-turtles-deliver.md new file mode 100644 index 0000000000..dd3baf4774 --- /dev/null +++ b/.changeset/selfish-turtles-deliver.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +Fixed configuration schema parsing when using TypeScript `4.3`. diff --git a/packages/config-loader/src/lib/schema/collect.ts b/packages/config-loader/src/lib/schema/collect.ts index e12a4c9309..5baada2d6f 100644 --- a/packages/config-loader/src/lib/schema/collect.ts +++ b/packages/config-loader/src/lib/schema/collect.ts @@ -168,6 +168,23 @@ function compileTsSchemas(paths: string[]) { }, [path.split(sep).join('/')], // Unix paths are expected for all OSes here ) as JsonObject | null; + + // This is a workaround for an API change in TypeScript 4.3 where doc comments no + // longer are represented by a single string, but instead an array of objects. + // This isn't handled by typescript-json-schema so we do the conversion here instead. + value = JSON.parse(JSON.stringify(value), (key, prop) => { + if (key === 'visibility' && Array.isArray(prop)) { + const text = prop[0]?.text; + if (!text) { + const propStr = JSON.stringify(prop); + throw new Error( + `Failed conversion of visibility schema, got ${propStr}`, + ); + } + return text; + } + return prop; + }); } catch (error) { if (error.message !== 'type Config not found') { throw error;