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 +``` 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 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/.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. 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/.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`. 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/.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 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/.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/.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/.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`. 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/.github/styles/vocab.txt b/.github/styles/vocab.txt index 7840b331e1..b8f028a2ae 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,99 @@ 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 +Keyv +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 +164,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 +235,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 +269,18 @@ untracked upvote url utils +validator validators varchar +Weaveworks +Webpack winston www +WWW +XML xyz yaml +Zalando +Zhou zoomable zsh 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] 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/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/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 4f9323c6e9..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 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 diff --git a/docs/overview/architecture-overview.md b/docs/overview/architecture-overview.md index 316f066614..af46a8c830 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 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! + ## Containerization The example Backstage architecture shown above would Dockerize into three 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/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 0000000000..de8ca43031 Binary files /dev/null and b/microsite/blog/assets/21-05-20/1000-engineers_v.2.png differ 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 0000000000..a2054a589c Binary files /dev/null and b/microsite/blog/assets/21-05-20/1000-plus-engineers_v.2.png differ diff --git a/microsite/blog/assets/21-05-20/200-engineers_v.2.png b/microsite/blog/assets/21-05-20/200-engineers_v.2.png new file mode 100644 index 0000000000..45153add7d Binary files /dev/null and b/microsite/blog/assets/21-05-20/200-engineers_v.2.png differ diff --git a/microsite/sidebars.json b/microsite/sidebars.json index b8ed72596d..56be7464a5 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -240,7 +240,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 a87acc91c8..e4f4f01e8a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -155,7 +155,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/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/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/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/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/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" + } + } } } } diff --git a/packages/catalog-model/src/validation/ajv.ts b/packages/catalog-model/src/validation/ajv.ts new file mode 100644 index 0000000000..02d53fcd15 --- /dev/null +++ b/packages/catalog-model/src/validation/ajv.ts @@ -0,0 +1,139 @@ +/* + * 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. +// The keys are JSON stringified versions of the schema +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; + const cacheKey = disableCache ? '' : JSON.stringify(schema); + + if (!disableCache) { + const cached = compiledSchemaCache.get(cacheKey); + 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(cacheKey, 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/validation/entitySchemaValidator.test.ts b/packages/catalog-model/src/validation/entitySchemaValidator.test.ts new file mode 100644 index 0000000000..6ab2744c0f --- /dev/null +++ b/packages/catalog-model/src/validation/entitySchemaValidator.test.ts @@ -0,0 +1,596 @@ +/* + * 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 { entitySchemaValidator } from './entitySchemaValidator'; + +describe('entitySchemaValidator', () => { + const validator = entitySchemaValidator(); + 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('happy path: accepts valid data', () => { + expect(() => validator(entity)).not.toThrow(); + }); + + // + // apiVersion and kind + // + + it('rejects wrong root type', () => { + expect(() => validator(7)).toThrow(/object/); + }); + + 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('accepts missing uid', () => { + delete entity.metadata.uid; + expect(() => validator(entity)).not.toThrow(); + }); + + it('rejects bad uid type', () => { + entity.metadata.uid = 7; + expect(() => validator(entity)).toThrow(/uid/); + }); + + it('rejects empty uid', () => { + entity.metadata.uid = ''; + expect(() => validator(entity)).toThrow(/uid/); + }); + + it('accepts missing etag', () => { + delete entity.metadata.etag; + expect(() => validator(entity)).not.toThrow(); + }); + + it('rejects bad etag type', () => { + entity.metadata.etag = 7; + expect(() => validator(entity)).toThrow(/etag/); + }); + + it('rejects empty etag', () => { + entity.metadata.etag = ''; + expect(() => validator(entity)).toThrow(/etag/); + }); + + it('accepts missing generation', () => { + delete entity.metadata.generation; + expect(() => validator(entity)).not.toThrow(); + }); + + it('rejects bad generation type', () => { + entity.metadata.generation = 'a'; + expect(() => validator(entity)).toThrow(/generation/); + }); + + it('rejects zero generation', () => { + entity.metadata.generation = 0; + expect(() => validator(entity)).toThrow(/generation/); + }); + + it('rejects non-integer generation', () => { + entity.metadata.generation = 1.5; + expect(() => validator(entity)).toThrow(/generation/); + }); + + it('rejects missing name', () => { + delete 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 bad namespace type', () => { + entity.metadata.namespace = 7; + expect(() => validator(entity)).toThrow(/namespace/); + }); + + it('accepts missing description', () => { + delete entity.metadata.description; + expect(() => validator(entity)).not.toThrow(); + }); + + it('rejects bad description type', () => { + entity.metadata.description = 7; + expect(() => validator(entity)).toThrow(/description/); + }); + + it('accepts missing labels', () => { + delete entity.metadata.labels; + expect(() => validator(entity)).not.toThrow(); + }); + + it('accepts empty labels', () => { + entity.metadata.labels = {}; + expect(() => validator(entity)).not.toThrow(); + }); + + it('rejects bad labels type', () => { + entity.metadata.labels = 7; + expect(() => validator(entity)).toThrow(/labels/); + }); + + it('accepts missing annotations', () => { + delete entity.metadata.annotations; + expect(() => validator(entity)).not.toThrow(); + }); + + it('accepts empty annotations object', () => { + entity.metadata.annotations = {}; + expect(() => validator(entity)).not.toThrow(); + }); + + it('rejects bad annotations type', () => { + entity.metadata.annotations = 7; + expect(() => validator(entity)).toThrow(/annotations/); + }); + + it('rejects bad tags type', () => { + entity.metadata.tags = 7; + expect(() => validator(entity)).toThrow(/tags/); + }); + + it('accepts empty tags', () => { + entity.metadata.tags = []; + expect(() => validator(entity)).not.toThrow(); + }); + + it('rejects empty tag', () => { + entity.metadata.tags[0] = ''; + expect(() => validator(entity)).toThrow(/tags/); + }); + + it('rejects bad tag type', () => { + entity.metadata.tags[0] = 7; + expect(() => validator(entity)).toThrow(/tags/); + }); + + it('accepts missing links', () => { + delete entity.metadata.links; + expect(() => validator(entity)).not.toThrow(); + }); + + it('accepts empty links', () => { + entity.metadata.links = []; + expect(() => validator(entity)).not.toThrow(); + }); + + it('rejects empty links.url', () => { + entity.metadata.links[0].url = ''; + expect(() => validator(entity)).toThrow(/links/); + }); + + it('rejects missing links.url', () => { + delete entity.metadata.links[0].url; + expect(() => validator(entity)).toThrow(/links/); + }); + + it('rejects bad links.url type', () => { + entity.metadata.links[0].url = 7; + expect(() => validator(entity)).toThrow(/links/); + }); + + it('rejects empty links.title', () => { + entity.metadata.links[0].title = ''; + expect(() => validator(entity)).toThrow(/links/); + }); + + it('accepts missing links.title', () => { + delete entity.metadata.links[0].title; + expect(() => validator(entity)).not.toThrow(); + }); + + it('rejects bad links.title type', () => { + entity.metadata.links[0].title = 7; + expect(() => validator(entity)).toThrow(/links/); + }); + + it('rejects empty links.icon', () => { + entity.metadata.links[0].icon = ''; + expect(() => validator(entity)).toThrow(/links/); + }); + + it('accepts missing links.icon', () => { + delete entity.metadata.links[0].icon; + expect(() => validator(entity)).not.toThrow(); + }); + + it('rejects bad links.icon type', () => { + entity.metadata.links[0].icon = 7; + expect(() => validator(entity)).toThrow(/links/); + }); + + it('accepts unknown metadata field', () => { + entity.metadata.unknown = 7; + expect(() => validator(entity)).not.toThrow(); + }); + + // + // spec + // + + it('accepts missing spec', () => { + delete entity.spec; + expect(() => validator(entity)).not.toThrow(); + }); + + it('rejects non-object spec', () => { + entity.spec = 7; + expect(() => validator(entity)).toThrow(/spec/); + }); + + it('accepts unknown spec field', () => { + entity.spec.unknown = 7; + expect(() => validator(entity)).not.toThrow(); + }); + + // + // Relations + // + + it('accepts missing relations', () => { + delete entity.relations; + expect(() => validator(entity)).not.toThrow(); + }); + + it('accepts empty relations', () => { + entity.relations = []; + expect(() => validator(entity)).not.toThrow(); + }); + + it('rejects bad relations type', () => { + entity.relations = 7; + expect(() => validator(entity)).toThrow(/relations/); + }); + + it('rejects missing relations.type', () => { + delete entity.relations[0].type; + expect(() => validator(entity)).toThrow(/relations/); + }); + + it('rejects empty relations.type', () => { + entity.relations[0].type = ''; + expect(() => validator(entity)).toThrow(/relations/); + }); + + it('rejects wrong relations.type type', () => { + entity.relations[0].type = 7; + expect(() => validator(entity)).toThrow(/relations/); + }); + + it('rejects missing relations.target', () => { + delete entity.relations[0].target; + expect(() => validator(entity)).toThrow(/relations/); + }); + + it('rejects empty relations.target', () => { + entity.relations[0].target = ''; + expect(() => validator(entity)).toThrow(/relations/); + }); + + it('rejects wrong relations.target type', () => { + entity.relations[0].target = 7; + expect(() => validator(entity)).toThrow(/relations/); + }); + + it('rejects missing relations.target.kind', () => { + delete entity.relations[0].target.kind; + expect(() => validator(entity)).toThrow(/relations/); + }); + + it('rejects empty relations.target.kind', () => { + entity.relations[0].target.kind = ''; + expect(() => validator(entity)).toThrow(/relations/); + }); + + it('rejects wrong relations.target.kind type', () => { + entity.relations[0].target.kind = 7; + expect(() => validator(entity)).toThrow(/relations/); + }); + + it('rejects missing relations.target.namespace', () => { + delete entity.relations[0].target.namespace; + expect(() => validator(entity)).toThrow(/relations/); + }); + + it('rejects empty relations.target.namespace', () => { + entity.relations[0].target.namespace = ''; + expect(() => validator(entity)).toThrow(/relations/); + }); + + it('rejects wrong relations.target.namespace type', () => { + entity.relations[0].target.namespace = 7; + expect(() => validator(entity)).toThrow(/relations/); + }); + + it('rejects missing relations.target.name', () => { + delete entity.relations[0].target.name; + expect(() => validator(entity)).toThrow(/relations/); + }); + + it('rejects empty relations.target.name', () => { + entity.relations[0].target.name = ''; + expect(() => validator(entity)).toThrow(/relations/); + }); + + it('rejects wrong relations.target.name type', () => { + entity.relations[0].target.name = 7; + expect(() => validator(entity)).toThrow(/relations/); + }); + + it('rejects unknown relation field', () => { + entity.relations[0].unknown = 7; + expect(() => validator(entity)).toThrow(/unknown/); + }); + + // + // Status + // + + it('accepts missing status', () => { + delete entity.status; + expect(() => validator(entity)).not.toThrow(); + }); + + it('accepts empty status', () => { + entity.status = {}; + expect(() => validator(entity)).not.toThrow(); + }); + + it('rejects bad status type', () => { + entity.status = 7; + expect(() => validator(entity)).toThrow(/status/); + }); + + it('accepts missing status.items', () => { + delete entity.status.items; + expect(() => validator(entity)).not.toThrow(); + }); + + it('accepts empty status.items', () => { + entity.status.items = []; + expect(() => validator(entity)).not.toThrow(); + }); + + it('rejects bad status.items type', () => { + entity.status.items = 7; + expect(() => validator(entity)).toThrow(/status/); + }); + + it('rejects bad status.items item type', () => { + entity.status.items[0] = 7; + expect(() => validator(entity)).toThrow(/status/); + }); + + it('rejects missing status.items.type', () => { + delete entity.status.items[0].type; + expect(() => validator(entity)).toThrow(/status/); + }); + + it('rejects empty status.items.type', () => { + entity.status.items[0].type = ''; + expect(() => validator(entity)).toThrow(/status/); + }); + + it('rejects bad status.items.type type', () => { + entity.status.items[0].type = 7; + expect(() => validator(entity)).toThrow(/status/); + }); + + it('rejects missing status.items.level', () => { + delete entity.status.items[0].level; + expect(() => validator(entity)).toThrow(/status/); + }); + + it('rejects empty status.items.level', () => { + entity.status.items[0].level = ''; + expect(() => validator(entity)).toThrow(/status/); + }); + + it('rejects bad status.items.level type', () => { + entity.status.items[0].level = 7; + expect(() => validator(entity)).toThrow(/status/); + }); + + it('rejects bad status.items.level enum', () => { + entity.status.items[0].level = 'unknown'; + expect(() => validator(entity)).toThrow(/status/); + }); + + it('rejects missing status.items.message', () => { + delete entity.status.items[0].message; + expect(() => validator(entity)).toThrow(/status/); + }); + + it('accepts empty status.items.message', () => { + entity.status.items[0].message = ''; + expect(() => validator(entity)).not.toThrow(); + }); + + it('rejects bad status.items.message type', () => { + entity.status.items[0].message = 7; + expect(() => validator(entity)).toThrow(/status/); + }); + + it('accepts missing status.items.error', () => { + delete entity.status.items[0].error; + expect(() => validator(entity)).not.toThrow(); + }); + + it('rejects missing status.items.error.name', () => { + delete entity.status.items[0].error.name; + expect(() => validator(entity)).toThrow(/name/); + }); + + it('rejects empty status.items.error.name', () => { + entity.status.items[0].error.name = ''; + expect(() => validator(entity)).toThrow(/name/); + }); + + it('rejects bad status.items.error.name type', () => { + entity.status.items[0].error.name = 7; + expect(() => validator(entity)).toThrow(/name/); + }); + + it('rejects missing status.items.error.message', () => { + delete entity.status.items[0].error.message; + expect(() => validator(entity)).toThrow(/message/); + }); + + it('accepts empty status.items.error.message', () => { + entity.status.items[0].error.message = ''; + expect(() => validator(entity)).not.toThrow(); + }); + + it('rejects bad status.items.error.message type', () => { + entity.status.items[0].error.message = 7; + expect(() => validator(entity)).toThrow(/message/); + }); + + it('accepts missing status.items.error.code', () => { + delete entity.status.items[0].error.code; + expect(() => validator(entity)).not.toThrow(); + }); + + it('accepts empty status.items.error.code', () => { + entity.status.items[0].error.code = ''; + expect(() => validator(entity)).not.toThrow(); + }); + + it('rejects bad status.items.error.code type', () => { + entity.status.items[0].error.code = 7; + expect(() => validator(entity)).toThrow(/code/); + }); + + it('accepts missing status.items.error.stack', () => { + delete entity.status.items[0].error.stack; + expect(() => validator(entity)).not.toThrow(); + }); + + it('accepts empty status.items.error.stack', () => { + entity.status.items[0].error.stack = ''; + expect(() => validator(entity)).not.toThrow(); + }); + + it('rejects bad status.items.error.stack type', () => { + entity.status.items[0].error.stack = 7; + expect(() => validator(entity)).toThrow(/stack/); + }); + + it('accepts unknown status.items field', () => { + entity.status.items[0].unknown = 7; + expect(() => validator(entity)).not.toThrow(); + }); + + it('accepts unknown status.items.error field', () => { + entity.status.items[0].error.unknown = 7; + 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/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', ], diff --git a/packages/cli/package.json b/packages/cli/package.json index 12d8373ba5..fed457eaeb 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", @@ -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/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/config-loader/src/lib/schema/collect.ts b/packages/config-loader/src/lib/schema/collect.ts index 611413931e..5baada2d6f 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); @@ -166,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; diff --git a/packages/core/src/layout/SignInPage/SignInPage.tsx b/packages/core/src/layout/SignInPage/SignInPage.tsx index 6a8dcf454e..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. @@ -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, }); @@ -138,9 +139,7 @@ export const SingleSignInPage = ({ } }; - if (retry) { - login(); - } + login(); }, [onResult, authApi, retry, autoShowPopup]); return showLoginPage ? ( 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..d37cffd680 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,8 @@ backend: #ca: # if you have a CA file and want to verify it you can uncomment this section # $file: /ca/server.crt {{/if}} + cache: + store: memory # workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir integrations: @@ -62,7 +64,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: @@ -72,7 +74,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 +88,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 = ( + + + 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/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; 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({ 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 ( - + - + diff --git a/plugins/cost-insights/src/utils/history.test.ts b/plugins/cost-insights/src/utils/history.test.ts index e4310e13c6..dff6015aca 100644 --- a/plugins/cost-insights/src/utils/history.test.ts +++ b/plugins/cost-insights/src/utils/history.test.ts @@ -61,7 +61,9 @@ describe.each` params | expected ${''} | ${{}} ${'?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' }} @@ -72,9 +74,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(); - }); -}); 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(); 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} + + ), }; } 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(); + }); +}); diff --git a/plugins/jenkins/src/components/Cards/Cards.tsx b/plugins/jenkins/src/components/Cards/Cards.tsx index f19ed608ce..010afa8990 100644 --- a/plugins/jenkins/src/components/Cards/Cards.tsx +++ b/plugins/jenkins/src/components/Cards/Cards.tsx @@ -17,13 +17,14 @@ import { InfoCard, InfoCardVariants, StructuredMetadataTable, + WarningPanel, } from '@backstage/core'; import { LinearProgress, Link, makeStyles, Theme } from '@material-ui/core'; 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({ @@ -75,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, @@ -83,11 +101,22 @@ 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 5a6f8b9f08..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,6 +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<{ + message: string; + errorType: ErrorType; + }>(); const restartBuild = async (buildName: string) => { try { @@ -48,7 +57,10 @@ export function useBuilds(projectName: string, branch?: string) { return build || []; } catch (e) { - errorApi.post(e); + const errorType = e.notFound + ? ErrorType.NOT_FOUND + : ErrorType.CONNECTION_ERROR; + setError({ message: e.message, errorType }); throw e; } }, [api, errorApi, projectName, branch]); @@ -61,6 +73,7 @@ export function useBuilds(projectName: string, branch?: string) { builds, projectName, total, + error, }, { builds, diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index c135f97ec9..dc68c26c4f 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -35,8 +35,8 @@ "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", "@backstage/integration": "^0.5.3", - "@gitbeaker/core": "^28.0.2", - "@gitbeaker/node": "^28.0.2", + "@gitbeaker/core": "^29.2.0", + "@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 64aeb6eedc..fb5d7f2f33 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" @@ -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" @@ -9035,15 +9054,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 +9430,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" @@ -10384,7 +10403,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== @@ -12025,10 +12044,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" @@ -12425,12 +12444,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" @@ -12941,17 +12958,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: @@ -13928,11 +13945,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" @@ -14349,7 +14361,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== @@ -18740,7 +18752,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" @@ -19422,10 +19433,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" @@ -21412,7 +21423,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== @@ -23395,10 +23406,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" @@ -26572,10 +26583,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"