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`.
-
+
**Please note that when you change permissions, the app owner will get an email
that must be approved first before the changes are applied.**
-
+
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).
-
+
### 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).
-
+
### 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.
-
+
### 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"`:
+ *
+ *