Merge branch 'master' of github.com:splunk/backstage into feature/update-splunk-on-call-invalid-team

This commit is contained in:
Heather Lee
2021-05-26 11:00:08 -07:00
76 changed files with 1487 additions and 981 deletions
+82
View File
@@ -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
```
+5
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core': patch
---
SignInPage: Show login page while pop-up is being displayed when `auto` prop is set.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Bumped `eslint-config-prettier` to `8.x`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-jenkins': patch
---
Show error in Jenkins card for errors exposed by the Jenkins API
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog': patch
---
Add support for fullHeight variant to the AboutCard
+7
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Bump `@gitbeaker` dependencies to `29.x`.
+14
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Bump webpack-node-externals from `2.5.2` to `3.0.0`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-github-deployments': patch
---
Handle deployments without a commit
+97 -94
View File
@@ -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
+2
View File
@@ -29,6 +29,8 @@ backend:
database:
client: sqlite3
connection: ':memory:'
cache:
store: memory
cors:
origin: http://localhost:3000
methods: [GET, POST, PUT, DELETE]
@@ -167,4 +167,4 @@ Once it's deployed, after going through the AAD authentication flow, Backstage s
<!-- links -->
[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

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

@@ -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
+2 -2
View File
@@ -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
+15
View File
@@ -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
-371
View File
@@ -1,371 +0,0 @@
---
id: quickstart-app-auth
title: Monorepo App Setup With Authentication
---
###### January 8th 2021 - @backstage/create-app - v0.4.5
<br />
> 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.
<details><summary>GitHub</summary>
<p>
### 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`
</p>
</details>
<details><summary>GitLab</summary>
<p>
### 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`
</p>
</details>
<details><summary>Google</summary>
<p>
### 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`
</p>
</details>
<details><summary>Microsoft</summary>
<p>
### 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.
</p>
</details>
<details><summary>Auth0</summary>
<p>
### 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
</p>
</details>
### 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 => (
<SignInPage
{...props}
auto
provider={{
id: 'github-auth-provider',
title: 'GitHub',
message: 'Simple Backstage Application Login',
apiRef: githubAuthApiRef,
}}
/>
),
},
});
```
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).
+2 -2
View File
@@ -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.
@@ -72,7 +72,7 @@ Within todays complex development environments, there are barriers both big a
After talking with companies who have already adopted Backstage, weve 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 youre growing).
![200 engineers](assets/21-05-20/200-engineers.png)
![200 engineers](assets/21-05-20/200-engineers_v.2.png)
<br>
### My org has ~200 engineers (and is growing fast)
@@ -93,7 +93,7 @@ Youre 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 doesnt 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 @@ Youre 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 Spotifys 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
Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

-1
View File
@@ -239,7 +239,6 @@
],
"Tutorials": [
"tutorials/journey",
"tutorials/quickstart-app-auth",
"tutorials/quickstart-app-plugin",
"tutorials/switching-sqlite-postgres"
],
-1
View File
@@ -154,7 +154,6 @@ nav:
- Backend: 'api/backend.md'
- Tutorials:
- Future developer journey: 'tutorials/journey.md'
- Monorepo App Setup With Authentication: 'tutorials/quickstart-app-auth.md'
- Adding Custom Plugin to Existing Monorepo App: 'tutorials/quickstart-app-plugin.md'
- Switching Backstage from SQLite to PostgreSQL: 'tutorials/switching-sqlite-postgres.md'
- Architecture Decision Records (ADRs):
+1 -1
View File
@@ -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",
+40 -24
View File
@@ -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<T extends EntityEnvelope = EntityEnvelope>(schema?: unknown): (data: unknown) => T;
// @public
export function entityHasChanges(previous: Entity, next: Entity): boolean;
// @public
export function entityKindSchemaValidator<T extends Entity>(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<T extends Entity = Entity>(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<any>): 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?: {
+1
View File
@@ -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"
+3 -2
View File
@@ -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.
@@ -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;
};
};
@@ -27,6 +27,7 @@ export type {
EntityRelation,
EntityRelationSpec,
} from './Entity';
export type { EntityEnvelope } from './EntityEnvelope';
export type {
UNSTABLE_EntityStatus,
UNSTABLE_EntityStatusItem,
@@ -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],
);
@@ -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],
);
@@ -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],
);
@@ -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],
);
@@ -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],
);
@@ -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],
);
@@ -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],
);
@@ -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],
);
@@ -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],
);
@@ -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],
);
+1 -2
View File
@@ -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,
+9 -57
View File
@@ -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<any>,
): 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 || '<root>'} ${error.message}${
error.params
? ` - ${Object.entries(error.params)
.map(([key, val]) => `${key}: ${val}`)
.join(', ')}`
: ''
}`,
);
async check(data) {
return validator(data) === data;
},
};
}
@@ -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",
@@ -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
}
}
}
}
}
@@ -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<string, ValidateFunction<unknown>>();
// The core schemas that others can depend on
const refDependencyCandidates = [
entityEnvelopeSchema,
entitySchema,
entityMetaSchema,
commonSchema,
];
export function throwAjvError(
errors: ValidateFunction<unknown>['errors'],
): never {
if (!errors?.length) {
throw new TypeError('Unknown error');
}
const error = errors[0];
throw new TypeError(
`${error.dataPath || '<root>'} ${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<unknown> {
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<string>();
if (schema.$id) {
seen.add(schema.$id);
}
const selected = new Array<Schema>();
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<string> {
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);
}
}
}
}
}
@@ -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();
});
});
@@ -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#<id>
*
* @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);
};
}
@@ -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/);
});
});
@@ -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"`:
*
* <pre>
* "apiVersion": {
* "enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"]
* },
* "kind": {
* "enum": ["Group"]
* },
* </pre>
*
* 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#<id>
*
* @see https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema
*/
export function entityKindSchemaValidator<T extends Entity>(
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);
};
}
@@ -14,16 +14,11 @@
* limitations under the License.
*/
import Ajv from 'ajv';
import { AnyValidateFunction } from 'ajv/dist/core';
import entitySchema from '../schema/Entity.schema.json';
import entityMetaSchema from '../schema/EntityMeta.schema.json';
import commonSchema from '../schema/shared/common.schema.json';
import { Entity } from './Entity';
import { entitySchemaValidator } from './entitySchemaValidator';
describe('Entity', () => {
describe('entitySchemaValidator', () => {
const validator = entitySchemaValidator();
let entity: any;
let validate: AnyValidateFunction;
beforeEach(() => {
entity = {
@@ -69,38 +64,10 @@ describe('Entity', () => {
],
},
};
const ajv = new Ajv({ allowUnionTypes: true });
ajv.addSchema([entityMetaSchema, commonSchema], undefined, undefined, true);
validate = ajv.compile(entitySchema);
});
function check(data: unknown): () => true {
return () => {
const result = validate(data);
if (result === true) {
return true;
}
const [error] = validate.errors || [];
if (!error) {
throw new TypeError('Unknown error');
}
throw new TypeError(
`${error.dataPath || '<root>'} ${error.message}${
error.params
? ` - ${Object.entries(error.params)
.map(([key, val]) => `${key}: ${val}`)
.join(', ')}`
: ''
}`,
);
};
}
it('happy path: accepts valid data', () => {
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
//
@@ -108,37 +75,37 @@ describe('Entity', () => {
//
it('rejects wrong root type', () => {
expect(check((7 as unknown) as Entity)).toThrow(/object/);
expect(() => validator(7)).toThrow(/object/);
});
it('rejects missing apiVersion', () => {
delete entity.apiVersion;
expect(check(entity)).toThrow(/apiVersion/);
expect(() => validator(entity)).toThrow(/apiVersion/);
});
it('rejects bad apiVersion type', () => {
entity.apiVersion = 7;
expect(check(entity)).toThrow(/apiVersion/);
expect(() => validator(entity)).toThrow(/apiVersion/);
});
it('rejects empty apiVersion', () => {
entity.apiVersion = '';
expect(check(entity)).toThrow(/apiVersion/);
expect(() => validator(entity)).toThrow(/apiVersion/);
});
it('rejects missing kind', () => {
delete entity.kind;
expect(check(entity)).toThrow(/kind/);
expect(() => validator(entity)).toThrow(/kind/);
});
it('rejects bad kind type', () => {
entity.kind = 7;
expect(check(entity)).toThrow(/kind/);
expect(() => validator(entity)).toThrow(/kind/);
});
it('rejects empty kind', () => {
entity.kind = '';
expect(check(entity)).toThrow(/kind/);
expect(() => validator(entity)).toThrow(/kind/);
});
//
@@ -147,202 +114,202 @@ describe('Entity', () => {
it('rejects missing metadata', () => {
delete entity.metadata;
expect(check(entity)).toThrow(/metadata/);
expect(() => validator(entity)).toThrow(/metadata/);
});
it('rejects bad metadata type', () => {
entity.metadata = 7;
expect(check(entity)).toThrow(/metadata/);
expect(() => validator(entity)).toThrow(/metadata/);
});
it('accepts missing uid', () => {
delete entity.metadata.uid;
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('rejects bad uid type', () => {
entity.metadata.uid = 7;
expect(check(entity)).toThrow(/uid/);
expect(() => validator(entity)).toThrow(/uid/);
});
it('rejects empty uid', () => {
entity.metadata.uid = '';
expect(check(entity)).toThrow(/uid/);
expect(() => validator(entity)).toThrow(/uid/);
});
it('accepts missing etag', () => {
delete entity.metadata.etag;
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('rejects bad etag type', () => {
entity.metadata.etag = 7;
expect(check(entity)).toThrow(/etag/);
expect(() => validator(entity)).toThrow(/etag/);
});
it('rejects empty etag', () => {
entity.metadata.etag = '';
expect(check(entity)).toThrow(/etag/);
expect(() => validator(entity)).toThrow(/etag/);
});
it('accepts missing generation', () => {
delete entity.metadata.generation;
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('rejects bad generation type', () => {
entity.metadata.generation = 'a';
expect(check(entity)).toThrow(/generation/);
expect(() => validator(entity)).toThrow(/generation/);
});
it('rejects zero generation', () => {
entity.metadata.generation = 0;
expect(check(entity)).toThrow(/generation/);
expect(() => validator(entity)).toThrow(/generation/);
});
it('rejects non-integer generation', () => {
entity.metadata.generation = 1.5;
expect(check(entity)).toThrow(/generation/);
expect(() => validator(entity)).toThrow(/generation/);
});
it('rejects missing name', () => {
delete entity.metadata.name;
expect(check(entity)).toThrow(/name/);
expect(() => validator(entity)).toThrow(/name/);
});
it('rejects bad name type', () => {
entity.metadata.name = 7;
expect(check(entity)).toThrow(/name/);
expect(() => validator(entity)).toThrow(/name/);
});
it('accepts missing namespace', () => {
delete entity.metadata.namespace;
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('rejects bad namespace type', () => {
entity.metadata.namespace = 7;
expect(check(entity)).toThrow(/namespace/);
expect(() => validator(entity)).toThrow(/namespace/);
});
it('accepts missing description', () => {
delete entity.metadata.description;
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('rejects bad description type', () => {
entity.metadata.description = 7;
expect(check(entity)).toThrow(/description/);
expect(() => validator(entity)).toThrow(/description/);
});
it('accepts missing labels', () => {
delete entity.metadata.labels;
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('accepts empty labels', () => {
entity.metadata.labels = {};
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('rejects bad labels type', () => {
entity.metadata.labels = 7;
expect(check(entity)).toThrow(/labels/);
expect(() => validator(entity)).toThrow(/labels/);
});
it('accepts missing annotations', () => {
delete entity.metadata.annotations;
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('accepts empty annotations object', () => {
entity.metadata.annotations = {};
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('rejects bad annotations type', () => {
entity.metadata.annotations = 7;
expect(check(entity)).toThrow(/annotations/);
expect(() => validator(entity)).toThrow(/annotations/);
});
it('rejects bad tags type', () => {
entity.metadata.tags = 7;
expect(check(entity)).toThrow(/tags/);
expect(() => validator(entity)).toThrow(/tags/);
});
it('accepts empty tags', () => {
entity.metadata.tags = [];
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('rejects empty tag', () => {
entity.metadata.tags[0] = '';
expect(check(entity)).toThrow(/tags/);
expect(() => validator(entity)).toThrow(/tags/);
});
it('rejects bad tag type', () => {
entity.metadata.tags[0] = 7;
expect(check(entity)).toThrow(/tags/);
expect(() => validator(entity)).toThrow(/tags/);
});
it('accepts missing links', () => {
delete entity.metadata.links;
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('accepts empty links', () => {
entity.metadata.links = [];
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('rejects empty links.url', () => {
entity.metadata.links[0].url = '';
expect(check(entity)).toThrow(/links/);
expect(() => validator(entity)).toThrow(/links/);
});
it('rejects missing links.url', () => {
delete entity.metadata.links[0].url;
expect(check(entity)).toThrow(/links/);
expect(() => validator(entity)).toThrow(/links/);
});
it('rejects bad links.url type', () => {
entity.metadata.links[0].url = 7;
expect(check(entity)).toThrow(/links/);
expect(() => validator(entity)).toThrow(/links/);
});
it('rejects empty links.title', () => {
entity.metadata.links[0].title = '';
expect(check(entity)).toThrow(/links/);
expect(() => validator(entity)).toThrow(/links/);
});
it('accepts missing links.title', () => {
delete entity.metadata.links[0].title;
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('rejects bad links.title type', () => {
entity.metadata.links[0].title = 7;
expect(check(entity)).toThrow(/links/);
expect(() => validator(entity)).toThrow(/links/);
});
it('rejects empty links.icon', () => {
entity.metadata.links[0].icon = '';
expect(check(entity)).toThrow(/links/);
expect(() => validator(entity)).toThrow(/links/);
});
it('accepts missing links.icon', () => {
delete entity.metadata.links[0].icon;
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('rejects bad links.icon type', () => {
entity.metadata.links[0].icon = 7;
expect(check(entity)).toThrow(/links/);
expect(() => validator(entity)).toThrow(/links/);
});
it('accepts unknown metadata field', () => {
entity.metadata.unknown = 7;
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
//
@@ -351,17 +318,17 @@ describe('Entity', () => {
it('accepts missing spec', () => {
delete entity.spec;
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('rejects non-object spec', () => {
entity.spec = 7;
expect(check(entity)).toThrow(/spec/);
expect(() => validator(entity)).toThrow(/spec/);
});
it('accepts unknown spec field', () => {
entity.spec.unknown = 7;
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
//
@@ -370,97 +337,97 @@ describe('Entity', () => {
it('accepts missing relations', () => {
delete entity.relations;
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('accepts empty relations', () => {
entity.relations = [];
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('rejects bad relations type', () => {
entity.relations = 7;
expect(check(entity)).toThrow(/relations/);
expect(() => validator(entity)).toThrow(/relations/);
});
it('rejects missing relations.type', () => {
delete entity.relations[0].type;
expect(check(entity)).toThrow(/relations/);
expect(() => validator(entity)).toThrow(/relations/);
});
it('rejects empty relations.type', () => {
entity.relations[0].type = '';
expect(check(entity)).toThrow(/relations/);
expect(() => validator(entity)).toThrow(/relations/);
});
it('rejects wrong relations.type type', () => {
entity.relations[0].type = 7;
expect(check(entity)).toThrow(/relations/);
expect(() => validator(entity)).toThrow(/relations/);
});
it('rejects missing relations.target', () => {
delete entity.relations[0].target;
expect(check(entity)).toThrow(/relations/);
expect(() => validator(entity)).toThrow(/relations/);
});
it('rejects empty relations.target', () => {
entity.relations[0].target = '';
expect(check(entity)).toThrow(/relations/);
expect(() => validator(entity)).toThrow(/relations/);
});
it('rejects wrong relations.target type', () => {
entity.relations[0].target = 7;
expect(check(entity)).toThrow(/relations/);
expect(() => validator(entity)).toThrow(/relations/);
});
it('rejects missing relations.target.kind', () => {
delete entity.relations[0].target.kind;
expect(check(entity)).toThrow(/relations/);
expect(() => validator(entity)).toThrow(/relations/);
});
it('rejects empty relations.target.kind', () => {
entity.relations[0].target.kind = '';
expect(check(entity)).toThrow(/relations/);
expect(() => validator(entity)).toThrow(/relations/);
});
it('rejects wrong relations.target.kind type', () => {
entity.relations[0].target.kind = 7;
expect(check(entity)).toThrow(/relations/);
expect(() => validator(entity)).toThrow(/relations/);
});
it('rejects missing relations.target.namespace', () => {
delete entity.relations[0].target.namespace;
expect(check(entity)).toThrow(/relations/);
expect(() => validator(entity)).toThrow(/relations/);
});
it('rejects empty relations.target.namespace', () => {
entity.relations[0].target.namespace = '';
expect(check(entity)).toThrow(/relations/);
expect(() => validator(entity)).toThrow(/relations/);
});
it('rejects wrong relations.target.namespace type', () => {
entity.relations[0].target.namespace = 7;
expect(check(entity)).toThrow(/relations/);
expect(() => validator(entity)).toThrow(/relations/);
});
it('rejects missing relations.target.name', () => {
delete entity.relations[0].target.name;
expect(check(entity)).toThrow(/relations/);
expect(() => validator(entity)).toThrow(/relations/);
});
it('rejects empty relations.target.name', () => {
entity.relations[0].target.name = '';
expect(check(entity)).toThrow(/relations/);
expect(() => validator(entity)).toThrow(/relations/);
});
it('rejects wrong relations.target.name type', () => {
entity.relations[0].target.name = 7;
expect(check(entity)).toThrow(/relations/);
expect(() => validator(entity)).toThrow(/relations/);
});
it('rejects unknown relation field', () => {
entity.relations[0].unknown = 7;
expect(check(entity)).toThrow(/unknown/);
expect(() => validator(entity)).toThrow(/unknown/);
});
//
@@ -469,161 +436,161 @@ describe('Entity', () => {
it('accepts missing status', () => {
delete entity.status;
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('accepts empty status', () => {
entity.status = {};
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('rejects bad status type', () => {
entity.status = 7;
expect(check(entity)).toThrow(/status/);
expect(() => validator(entity)).toThrow(/status/);
});
it('accepts missing status.items', () => {
delete entity.status.items;
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('accepts empty status.items', () => {
entity.status.items = [];
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('rejects bad status.items type', () => {
entity.status.items = 7;
expect(check(entity)).toThrow(/status/);
expect(() => validator(entity)).toThrow(/status/);
});
it('rejects bad status.items item type', () => {
entity.status.items[0] = 7;
expect(check(entity)).toThrow(/status/);
expect(() => validator(entity)).toThrow(/status/);
});
it('rejects missing status.items.type', () => {
delete entity.status.items[0].type;
expect(check(entity)).toThrow(/status/);
expect(() => validator(entity)).toThrow(/status/);
});
it('rejects empty status.items.type', () => {
entity.status.items[0].type = '';
expect(check(entity)).toThrow(/status/);
expect(() => validator(entity)).toThrow(/status/);
});
it('rejects bad status.items.type type', () => {
entity.status.items[0].type = 7;
expect(check(entity)).toThrow(/status/);
expect(() => validator(entity)).toThrow(/status/);
});
it('rejects missing status.items.level', () => {
delete entity.status.items[0].level;
expect(check(entity)).toThrow(/status/);
expect(() => validator(entity)).toThrow(/status/);
});
it('rejects empty status.items.level', () => {
entity.status.items[0].level = '';
expect(check(entity)).toThrow(/status/);
expect(() => validator(entity)).toThrow(/status/);
});
it('rejects bad status.items.level type', () => {
entity.status.items[0].level = 7;
expect(check(entity)).toThrow(/status/);
expect(() => validator(entity)).toThrow(/status/);
});
it('rejects bad status.items.level enum', () => {
entity.status.items[0].level = 'unknown';
expect(check(entity)).toThrow(/status/);
expect(() => validator(entity)).toThrow(/status/);
});
it('rejects missing status.items.message', () => {
delete entity.status.items[0].message;
expect(check(entity)).toThrow(/status/);
expect(() => validator(entity)).toThrow(/status/);
});
it('accepts empty status.items.message', () => {
entity.status.items[0].message = '';
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('rejects bad status.items.message type', () => {
entity.status.items[0].message = 7;
expect(check(entity)).toThrow(/status/);
expect(() => validator(entity)).toThrow(/status/);
});
it('accepts missing status.items.error', () => {
delete entity.status.items[0].error;
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('rejects missing status.items.error.name', () => {
delete entity.status.items[0].error.name;
expect(check(entity)).toThrow(/name/);
expect(() => validator(entity)).toThrow(/name/);
});
it('rejects empty status.items.error.name', () => {
entity.status.items[0].error.name = '';
expect(check(entity)).toThrow(/name/);
expect(() => validator(entity)).toThrow(/name/);
});
it('rejects bad status.items.error.name type', () => {
entity.status.items[0].error.name = 7;
expect(check(entity)).toThrow(/name/);
expect(() => validator(entity)).toThrow(/name/);
});
it('rejects missing status.items.error.message', () => {
delete entity.status.items[0].error.message;
expect(check(entity)).toThrow(/message/);
expect(() => validator(entity)).toThrow(/message/);
});
it('accepts empty status.items.error.message', () => {
entity.status.items[0].error.message = '';
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('rejects bad status.items.error.message type', () => {
entity.status.items[0].error.message = 7;
expect(check(entity)).toThrow(/message/);
expect(() => validator(entity)).toThrow(/message/);
});
it('accepts missing status.items.error.code', () => {
delete entity.status.items[0].error.code;
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('accepts empty status.items.error.code', () => {
entity.status.items[0].error.code = '';
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('rejects bad status.items.error.code type', () => {
entity.status.items[0].error.code = 7;
expect(check(entity)).toThrow(/code/);
expect(() => validator(entity)).toThrow(/code/);
});
it('accepts missing status.items.error.stack', () => {
delete entity.status.items[0].error.stack;
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('accepts empty status.items.error.stack', () => {
entity.status.items[0].error.stack = '';
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('rejects bad status.items.error.stack type', () => {
entity.status.items[0].error.stack = 7;
expect(check(entity)).toThrow(/stack/);
expect(() => validator(entity)).toThrow(/stack/);
});
it('accepts unknown status.items field', () => {
entity.status.items[0].unknown = 7;
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
it('accepts unknown status.items.error field', () => {
entity.status.items[0].error.unknown = 7;
expect(check(entity)).not.toThrow();
expect(() => validator(entity)).not.toThrow();
});
});
@@ -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#<id>
*
* @see https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema
*/
export function entitySchemaValidator<T extends Entity = Entity>(
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);
};
}
@@ -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';
-1
View File
@@ -19,7 +19,6 @@ module.exports = {
'@spotify/eslint-config-base',
'@spotify/eslint-config-typescript',
'prettier',
'prettier/@typescript-eslint',
'plugin:jest/recommended',
'plugin:monorepo/recommended',
],
-2
View File
@@ -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',
],
+2 -2
View File
@@ -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",
@@ -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;
@@ -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;
@@ -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);
@@ -89,7 +89,7 @@ export const SingleSignInPage = ({
const [autoShowPopup, setAutoShowPopup] = useState<boolean>(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<Error>();
// 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 ? (
+4 -2
View File
@@ -99,7 +99,7 @@ export default async (cmd: Command): Promise<void> => {
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<void> => {
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) {
@@ -34,6 +34,8 @@ backend:
#ca: # if you have a CA file and want to verify it you can uncomment this section
# $file: <file-path>/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:
@@ -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",
@@ -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 };
};
}
@@ -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;
@@ -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({
@@ -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 (
<Card className={variant === 'gridItem' ? classes.gridItemCard : ''}>
<Card className={cardClass}>
<CardHeader
title="About"
action={
@@ -124,9 +147,7 @@ export function AboutCard({ variant }: AboutCardProps) {
}
/>
<Divider />
<CardContent
className={variant === 'gridItem' ? classes.gridItemCardContent : ''}
>
<CardContent className={cardContentClass}>
<AboutContent entity={entity} />
</CardContent>
</Card>
+1 -1
View File
@@ -60,7 +60,7 @@ export type GithubDeployment = {
commit: {
abbreviatedOid: string;
commitUrl: string;
};
} | null;
creator: {
login: string;
};
@@ -66,11 +66,12 @@ export function createStatusColumn(): TableColumn<GithubDeployment> {
export function createCommitColumn(): TableColumn<GithubDeployment> {
return {
title: 'Commit',
render: (row: GithubDeployment): JSX.Element => (
<Link to={row.commit.commitUrl} target="_blank" rel="noopener">
{row.commit.abbreviatedOid}
</Link>
),
render: (row: GithubDeployment) =>
row.commit && (
<Link to={row.commit.commitUrl} target="_blank" rel="noopener">
{row.commit.abbreviatedOid}
</Link>
),
};
}
@@ -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('<LatestRunCard />', () => {
const entity = {
apiVersion: 'v1',
kind: 'Component',
metadata: {
name: 'software',
description: 'This is the description',
annotations: { JENKINS_ANNOTATION: 'jenkins' },
},
};
const jenkinsApi: Partial<JenkinsApi> = {
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(
<ApiProvider apis={apis}>
<EntityProvider entity={entity}>
<LatestRunCard branch="master" />
</EntityProvider>
</ApiProvider>,
);
expect(getByText('Completed')).toBeInTheDocument();
});
it('should show the appropriate error in case of a connection error', async () => {
const jenkinsApiWithError: Partial<JenkinsApi> = {
getLastBuild: () => Promise.reject(new Error('Unauthorized')),
};
const apis = ApiRegistry.from([[jenkinsApiRef, jenkinsApiWithError]]);
const { getByText } = await renderInTestApp(
<ApiProvider apis={apis}>
<EntityProvider entity={entity}>
<LatestRunCard branch="master" />
</EntityProvider>
</ApiProvider>,
);
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<JenkinsApi> = {
getLastBuild: () =>
Promise.reject({
notFound: true,
message: 'jenkins-project not found',
}),
};
const apis = ApiRegistry.from([[jenkinsApiRef, jenkinsApiWithError]]);
const { getByText } = await renderInTestApp(
<ApiProvider apis={apis}>
<EntityProvider entity={entity}>
<LatestRunCard branch="master" />
</EntityProvider>
</ApiProvider>,
);
expect(getByText("Error: Can't find Jenkins project")).toBeInTheDocument();
expect(getByText('jenkins-project not found')).toBeInTheDocument();
});
});
+32 -3
View File
@@ -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<Theme>({
@@ -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 <WarningPanel severity="error" title={title} message={message} />;
};
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 (
<InfoCard title={`Latest ${branch} build`} variant={variant}>
<WidgetContent loading={loading} branch={branch} latestRun={latestRun} />
{!error ? (
<WidgetContent
loading={loading}
branch={branch}
latestRun={latestRun}
/>
) : (
<JenkinsApiErrorPanel
message={error.message}
errorType={error.errorType}
/>
)}
</InfoCard>
);
};
+14 -1
View File
@@ -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,
+2 -2
View File
@@ -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",
+137 -125
View File
@@ -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"
@@ -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"
@@ -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==
@@ -19421,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"
@@ -21411,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==
@@ -26571,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"