diff --git a/.changeset/brave-boats-greet.md b/.changeset/brave-boats-greet.md new file mode 100644 index 0000000000..e9d3debc35 --- /dev/null +++ b/.changeset/brave-boats-greet.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-lighthouse': patch +--- + +Strip trailing slash from url when creating a new audit. This change prevents duplicate audits from being displayed in the audit list. diff --git a/.changeset/cost-insights-careless-coins-pretend b/.changeset/cost-insights-careless-coins-pretend new file mode 100644 index 0000000000..6314a7c60f --- /dev/null +++ b/.changeset/cost-insights-careless-coins-pretend @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': patch +--- + +bug(cost-insights): Remove entity count when none present diff --git a/.changeset/eighty-rats-smash.md b/.changeset/eighty-rats-smash.md new file mode 100644 index 0000000000..4d49d0f203 --- /dev/null +++ b/.changeset/eighty-rats-smash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-import': patch +--- + +Improve how URLs are analyzed for add/import diff --git a/.changeset/eleven-badgers-wink.md b/.changeset/eleven-badgers-wink.md new file mode 100644 index 0000000000..a08c11d257 --- /dev/null +++ b/.changeset/eleven-badgers-wink.md @@ -0,0 +1,9 @@ +--- +'@backstage/techdocs-common': patch +'@backstage/plugin-techdocs-backend': patch +--- + +Improve techdocs-common Generator API for it to be used by techdocs-cli. TechDocs generator.run function now takes +an input AND an output directory. Most probably you use techdocs-common via plugin-techdocs-backend, and so there +is no breaking change for you. +But if you use techdocs-common separately, you need to create an output directory and pass into the generator. diff --git a/.changeset/flat-cycles-lay.md b/.changeset/flat-cycles-lay.md new file mode 100644 index 0000000000..2f84b38ffb --- /dev/null +++ b/.changeset/flat-cycles-lay.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes': patch +'@backstage/plugin-kubernetes-backend': patch +--- + +Revamped Kubernetes UI and added error reporting/detection diff --git a/.changeset/flat-walls-burn.md b/.changeset/flat-walls-burn.md new file mode 100644 index 0000000000..370b651a4b --- /dev/null +++ b/.changeset/flat-walls-burn.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': minor +--- + +Build out the `ScmIntegrations` class, as well as the individual `*Integration` classes diff --git a/.changeset/friendly-masks-dress.md b/.changeset/friendly-masks-dress.md new file mode 100644 index 0000000000..31405f88eb --- /dev/null +++ b/.changeset/friendly-masks-dress.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': patch +--- + +Export the `schemaValidator` helper function. diff --git a/.changeset/friendly-rats-wonder.md b/.changeset/friendly-rats-wonder.md new file mode 100644 index 0000000000..5a5b30f98a --- /dev/null +++ b/.changeset/friendly-rats-wonder.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-jenkins': patch +--- + +Handle missing ObjectMetadataAction in Jenkins API diff --git a/.changeset/generic-catalog-import-descriptions.md b/.changeset/generic-catalog-import-descriptions.md new file mode 100644 index 0000000000..8f28ce4afd --- /dev/null +++ b/.changeset/generic-catalog-import-descriptions.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-import': patch +--- + +Add more generic descriptions for the catalog-import form. diff --git a/.changeset/grumpy-trains-juggle.md b/.changeset/grumpy-trains-juggle.md new file mode 100644 index 0000000000..91ba37b72a --- /dev/null +++ b/.changeset/grumpy-trains-juggle.md @@ -0,0 +1,5 @@ +--- +'@backstage/techdocs-common': patch +--- + +Fix bug where binary files (`png`, etc.) could not load when using AWS or GCS publisher. diff --git a/.changeset/heavy-owls-float.md b/.changeset/heavy-owls-float.md new file mode 100644 index 0000000000..274876e723 --- /dev/null +++ b/.changeset/heavy-owls-float.md @@ -0,0 +1,12 @@ +--- +'@backstage/techdocs-common': patch +'@backstage/plugin-techdocs': patch +--- + +Google Cloud authentication in TechDocs has been improved. + +1. `techdocs.publisher.googleGcs.credentials` is now optional. If it is missing, `GOOGLE_APPLICATION_CREDENTIALS` + environment variable (and some other methods) will be used to authenticate. + Read more here https://cloud.google.com/docs/authentication/production + +2. `techdocs.publisher.googleGcs.projectId` is no longer used. You can remove it from your `app-config.yaml`. diff --git a/.changeset/rare-paws-listen.md b/.changeset/rare-paws-listen.md new file mode 100644 index 0000000000..1946cd7f2f --- /dev/null +++ b/.changeset/rare-paws-listen.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-import': patch +--- + +Remove dependency to `@backstage/plugin-catalog-backend`. diff --git a/.changeset/red-baboons-rhyme.md b/.changeset/red-baboons-rhyme.md new file mode 100644 index 0000000000..dcf924e912 --- /dev/null +++ b/.changeset/red-baboons-rhyme.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Enable catalog table actions for all location types. + +The edit button has had support for other providers for a while and there is +no specific reason the View in GitHub cannot work for all locations. This +change also replaces the GitHub icon with the OpenInNew icon. diff --git a/.changeset/rich-games-yawn.md b/.changeset/rich-games-yawn.md new file mode 100644 index 0000000000..1106a8b3de --- /dev/null +++ b/.changeset/rich-games-yawn.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': patch +--- + +Fix issue where `SidebarItem` with `onClick` and without `to` renders an inaccessible div. It now renders a button. diff --git a/.changeset/short-badgers-collect.md b/.changeset/short-badgers-collect.md new file mode 100644 index 0000000000..25f5a4044f --- /dev/null +++ b/.changeset/short-badgers-collect.md @@ -0,0 +1,5 @@ +--- +'@backstage/techdocs-common': patch +--- + +AWS SDK version bump for TechDocs. diff --git a/.changeset/sixty-ants-give.md b/.changeset/sixty-ants-give.md new file mode 100644 index 0000000000..f5cb46bdb7 --- /dev/null +++ b/.changeset/sixty-ants-give.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +AWS SDK version bump for Catalog Backend. diff --git a/.changeset/smooth-pigs-deny.md b/.changeset/smooth-pigs-deny.md new file mode 100644 index 0000000000..702c6ab2de --- /dev/null +++ b/.changeset/smooth-pigs-deny.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Allow blank certificates and support logout URLs in the SAML provider. diff --git a/.changeset/spicy-feet-sparkle.md b/.changeset/spicy-feet-sparkle.md new file mode 100644 index 0000000000..dcc8a7c5fb --- /dev/null +++ b/.changeset/spicy-feet-sparkle.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added experimental `create-github-app` command. diff --git a/.changeset/stale-cougars-wink.md b/.changeset/stale-cougars-wink.md new file mode 100644 index 0000000000..306d92ef8f --- /dev/null +++ b/.changeset/stale-cougars-wink.md @@ -0,0 +1,10 @@ +--- +'@backstage/techdocs-common': patch +'@backstage/plugin-techdocs': patch +--- + +AWS S3 authentication in TechDocs has been improved. + +1. `techdocs.publisher.awsS3.bucketName` is now the only required config. `techdocs.publisher.awsS3.credentials` and `techdocs.publisher.awsS3.region` are optional. + +2. If `techdocs.publisher.awsS3.credentials` and `techdocs.publisher.awsS3.region` are missing, the AWS environment variables `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_REGION` will be used. There are more better ways of setting up AWS authentication. Read the guide at https://backstage.io/docs/features/techdocs/using-cloud-storage diff --git a/.changeset/twelve-gorillas-give.md b/.changeset/twelve-gorillas-give.md new file mode 100644 index 0000000000..ed13fc773c --- /dev/null +++ b/.changeset/twelve-gorillas-give.md @@ -0,0 +1,6 @@ +--- +'@backstage/catalog-model': patch +'@backstage/plugin-catalog-backend': patch +--- + +Add subcomponentOf to Component kind to represent subsystems of larger components. diff --git a/.changeset/wild-dolls-rest.md b/.changeset/wild-dolls-rest.md new file mode 100644 index 0000000000..33298dbb52 --- /dev/null +++ b/.changeset/wild-dolls-rest.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Export all preparers and publishers properly diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 2281fcd168..f4c7137d23 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -8,6 +8,8 @@ apis args asciidoc async +Autoscaling +autoscaling Avro backrub Balachandran @@ -75,21 +77,19 @@ Firekube Fiverr freben Fredrik -github GitHub -gitlab GitLab Grafana -graphql +GraphQL graphviz Gustavsson Hackathons haproxy Henneke -heroku Heroku horizontalpodautoscalers Hostname +html http https Iain @@ -98,8 +98,7 @@ incentivised inlined inlinehilite interop -javascript -Javascript +JavaScript jq js json @@ -142,7 +141,6 @@ noop npm nvarchar nvm -oauth OAuth oidc Okta @@ -233,6 +231,7 @@ transpiled transpilation Tuite ui +unmanaged untracked upvote url diff --git a/.github/workflows/microsite-build-check.yml b/.github/workflows/microsite-build-check.yml index 8a2fa99ed2..118ba942bb 100644 --- a/.github/workflows/microsite-build-check.yml +++ b/.github/workflows/microsite-build-check.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - node-version: [12.x, 14.x] + node-version: [14.x] env: CI: true diff --git a/.github/workflows/microsite-with-storybook-deploy.yml b/.github/workflows/microsite-with-storybook-deploy.yml index 22fd48789a..cf571ef4e8 100644 --- a/.github/workflows/microsite-with-storybook-deploy.yml +++ b/.github/workflows/microsite-with-storybook-deploy.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: - node-version: [12.x] + node-version: [14.x] env: CI: true @@ -55,7 +55,7 @@ jobs: run: ls microsite/build/backstage && ls microsite/build/backstage/storybook - name: Deploy both microsite and storybook to gh-pages - uses: JamesIves/github-pages-deploy-action@3.4.2 + uses: JamesIves/github-pages-deploy-action@3.7.1 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BRANCH: gh-pages diff --git a/.github/workflows/techdocs-project-board.yml b/.github/workflows/techdocs-project-board.yml index 679abe6536..99daba61da 100644 --- a/.github/workflows/techdocs-project-board.yml +++ b/.github/workflows/techdocs-project-board.yml @@ -9,17 +9,17 @@ on: pull_request: types: [opened, reopened, labeled, edited] -env: - MY_GITHUB_TOKEN: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} - jobs: assign_issue_or_pr_to_project: runs-on: ubuntu-latest name: Triage + env: + MY_GITHUB_TOKEN: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} steps: - name: Assign new issue to Incoming based on its title. uses: srggrs/assign-one-project-github-action@1.2.0 if: | + env.MY_GITHUB_TOKEN != null && contains(github.event.issue.title, 'TechDocs') || contains(github.event.issue.title, 'techdocs') || contains(github.event.issue.title, 'Techdocs') @@ -30,6 +30,7 @@ jobs: - name: Assign new issue to Incoming based on its label. uses: srggrs/assign-one-project-github-action@1.2.0 if: | + env.MY_GITHUB_TOKEN != null && contains(github.event.issue.labels.*.name, 'docs-like-code') with: project: 'https://github.com/orgs/backstage/projects/1' @@ -38,6 +39,7 @@ jobs: - name: Assign new PR to Incoming based on its title. uses: srggrs/assign-one-project-github-action@1.2.0 if: | + env.MY_GITHUB_TOKEN != null && contains(github.event.pull_request.title, 'TechDocs') || contains(github.event.pull_request.title, 'techdocs') || contains(github.event.pull_request.title, 'Techdocs') @@ -48,6 +50,7 @@ jobs: - name: Assign new PR to Incoming based on its label. uses: srggrs/assign-one-project-github-action@1.2.0 if: | + env.MY_GITHUB_TOKEN != null && contains(github.event.pull_request.labels.*.name, 'docs-like-code') with: project: 'https://github.com/orgs/backstage/projects/1' diff --git a/app-config.yaml b/app-config.yaml index 48869cc0a0..d2100801ae 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -213,6 +213,7 @@ scaffolder: $env: BITBUCKET_USERNAME token: $env: BITBUCKET_TOKEN + auth: environment: development ### Providing an auth.session.secret will enable session support in the auth-backend diff --git a/docs/architecture-decisions/adr000-template.md b/docs/architecture-decisions/adr000-template.md new file mode 100644 index 0000000000..783db8e4bc --- /dev/null +++ b/docs/architecture-decisions/adr000-template.md @@ -0,0 +1,32 @@ +--- +id: adrs-adr000 +title: ADR000: [TITLE] +description: Architecture Decision Record (ADR) for [TITLE] [DESCRIPTION] +--- + +| Created | Status | +| ---------- | ------ | +| YYYY-MM-DD | Open | + +# ADR000: [title] + + + +## Status + + + +## Context + + + +## Decision + + + +## Consequences + + + + diff --git a/docs/architecture-decisions/index.md b/docs/architecture-decisions/index.md index 3211f37550..f852170b1e 100644 --- a/docs/architecture-decisions/index.md +++ b/docs/architecture-decisions/index.md @@ -18,8 +18,9 @@ Records should be stored under the `architecture-decisions` directory. ### Creating an ADR -- Copy `0000-template.md` to `docs/architecture-decisions/0000-my-decision.md` - (my-decision should be descriptive. Do not assign an ADR number.) +- Copy `docs/architecture-decisions/adr000-template.md` to + `docs/architecture-decisions/adr000-my-decision.md` (my-decision should be + descriptive. Do not assign an ADR number.) - Fill in the ADR following the guidelines in the template - Submit a pull request - Address and integrate feedback from the community diff --git a/docs/assets/software-catalog/software-model-core-entities.drawio.svg b/docs/assets/software-catalog/software-model-core-entities.drawio.svg new file mode 100644 index 0000000000..2260e5502e --- /dev/null +++ b/docs/assets/software-catalog/software-model-core-entities.drawio.svg @@ -0,0 +1,3 @@ + + +
dependsOn
dependsOn
Resource
(e.g. SQL Database, S3 bucket, ...)
Resource...
consumesAPI
consumesAPI
API
(e.g. OpenAPI, gRPC API, Avro, Dataset, ...)
API...
providesAPI
providesAPI
Component
(e.g. backend service, data pipeline ...)
Component...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/assets/software-catalog/software-model-core-entities.png b/docs/assets/software-catalog/software-model-core-entities.png deleted file mode 100644 index 60cb283802..0000000000 Binary files a/docs/assets/software-catalog/software-model-core-entities.png and /dev/null differ diff --git a/docs/assets/software-catalog/software-model-entities.drawio.svg b/docs/assets/software-catalog/software-model-entities.drawio.svg new file mode 100644 index 0000000000..7b8b88f224 --- /dev/null +++ b/docs/assets/software-catalog/software-model-entities.drawio.svg @@ -0,0 +1,3 @@ + + +
Domain
Domain
partOf
partOf
System
System
dependsOn
dependsOn
partOf
partOf
Resource
(e.g. SQL Database, S3 bucket, ...)
Resource...
consumesAPI
consumesAPI
API
(e.g. OpenAPI, gRPC API, Avro, Dataset, ...)
API...
providesAPI
providesAPI
partOf
partOf
Component
(e.g. backend service, data pipeline ...)
Component...
partOf
partOf
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/auth/oauth.md b/docs/auth/oauth.md index 0655175b58..4209b81279 100644 --- a/docs/auth/oauth.md +++ b/docs/auth/oauth.md @@ -1,9 +1,8 @@ --- id: oauth title: OAuth and OpenID Connect -description: This section describes how Backstage allows plugins to request -OAuth Access Tokens and OpenID Connect ID Tokens on behalf of the user, to be -used for auth to various third party APIs +# prettier-ignore +description: This section describes how Backstage allows plugins to request OAuth Access Tokens and OpenID Connect ID Tokens on behalf of the user, to be used for auth to various third party APIs --- This section describes how Backstage allows plugins to request OAuth Access diff --git a/docs/dls/figma.md b/docs/dls/figma.md index c5a33d15c2..21a05d2818 100644 --- a/docs/dls/figma.md +++ b/docs/dls/figma.md @@ -1,8 +1,8 @@ --- id: figma title: Figma -description: Documentation on using Figma to build your own plugins for -Backstage +# prettier-ignore +description: Documentation on using Figma to build your own plugins for Backstage --- We have a [Figma component library](https://www.figma.com/@backstage) that you diff --git a/docs/features/kubernetes/index.md b/docs/features/kubernetes/index.md new file mode 100644 index 0000000000..c468fdb116 --- /dev/null +++ b/docs/features/kubernetes/index.md @@ -0,0 +1,127 @@ +--- +id: overview +title: Kubernetes +sidebar_label: Overview +description: Monitoring Kubernetes based services with the service catalog +--- + +Kubernetes in Backstage is a way to monitor your service's current status when +it is deployed on Kubernetes. + +## Configuration + +Example: + +```yaml +kubernetes: + serviceLocatorMethod: 'multiTenant' + clusterLocatorMethods: + - 'config' + clusters: + - url: http://127.0.0.1:9999 + name: minikube + authProvider: 'serviceAccount' + serviceAccountToken: + $env: K8S_MINIKUBE_TOKEN + - url: http://127.0.0.2:9999 + name: gke-cluster-1 + authProvider: 'google' +``` + +### serviceLocatorMethod + +This configures how to determine which clusters a component is running in. + +Currently, the only valid value is: + +- `multiTenant` - This configuration assumes that all components run on all the + provided clusters. + +### clusterLocatorMethods + +This is an array used to determine where to retrieve cluster configuration from. + +Currently, the only valid cluster locator method is: + +- `config` - This cluster locator method will read cluster information from your + app-config (see below). + +### clusters + +Used by the `config` cluster locator method to construct Kubernetes clients. + +### clusters.\*.url + +The base URL to the Kubernetes control plane. Can be found by using the +"Kubernetes master" result from running the `kubectl cluster-info` command. + +### clusters.\*.name + +A name to represent this cluster, this must be unique within the `clusters` +array. Users will see this value in the Service Catalog Kubernetes plugin. + +### clusters.\*.authProvider + +This determines how the Kubernetes client authenticates with the Kubernetes +cluster. Valid values are: + +| Value | Description | +| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `serviceAccount` | This will use a Kubernetes [service account](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/) to access the Kubernetes API. When this is used the `serviceAccountToken` field should also be set. | +| `google` | This will use a user's Google auth token from the [Google auth plugin](https://backstage.io/docs/auth/) to access the Kubernetes API. | + +### clusters.\*.serviceAccount (optional) + +The service account token to be used when using the `serviceAccount` auth +provider. + +## Role Based Access Control + +The current RBAC permissions required are read-only cluster wide, for the +following objects: + +- pods +- services +- configmaps +- deployments +- replicasets +- horizontalpodautoscalers +- ingresses + +## Surfacing your Kubernetes components as part of an entity + +There are two ways to surface your Kubernetes components as part of an entity. +The label selector takes precedence over the annotation/service id. + +### Common `backstage.io/kubernetes-id` label + +#### Adding the entity annotation + +In order for Backstage to detect that an entity has Kubernetes components, the +following annotation should be added to the entity's `catalog-info.yaml`: + +```yaml +annotations: + 'backstage.io/kubernetes-id': dice-roller +``` + +#### Labeling Kubernetes components + +In order for Kubernetes components to show up in the service catalog as a part +of an entity, Kubernetes components themselves can have the following label: + +```yaml +'backstage.io/kubernetes-id': +``` + +### Label selector query annotation + +You can write your own custom label selector query that Backstage will use to +lookup the objects (similar to `kubectl --selector="your query here"`). Review +the +[labels and selectors Kubernetes documentation](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) +for more info. + +```yaml +'backstage.io/kubernetes-label-selector': 'app=my-app,component=front-end' +``` diff --git a/docs/features/software-catalog/configuration.md b/docs/features/software-catalog/configuration.md index d10fb29e30..d5ad240dde 100644 --- a/docs/features/software-catalog/configuration.md +++ b/docs/features/software-catalog/configuration.md @@ -60,7 +60,7 @@ data from. Each entry is a structure with up to four elements: and raw. If it is not supplied, anonymous access will be used. - `apiBaseUrl` (optional): If you want to communicate using the APIv3 method with this provider, specify the base URL for its endpoint here, with no - trailing slash. Specifically when the target is github, you can leave it out + trailing slash. Specifically when the target is GitHub, you can leave it out to be inferred automatically. For a GitHub Enterprise installation, it is commonly at `https://api.` or `https:///api/v3`. - `rawBaseUrl` (optional): If you want to communicate using the raw HTTP method diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 763acd2261..77feef45e2 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -60,7 +60,7 @@ software catalog API. }, "spec": { "lifecycle": "production", - "owner": "artist-relations@example.com", + "owner": "artist-relations-team", "type": "website" } } @@ -84,7 +84,7 @@ metadata: spec: type: website lifecycle: production - owner: artist-relations@example.com + owner: artist-relations-team ``` The root fields `apiVersion`, `kind`, `metadata`, and `spec` are part of the @@ -268,7 +268,7 @@ identical in use to Their purpose is mainly, but not limited, to reference into external systems. This could for example be a reference to the git ref the entity was ingested -from, to monitoring and logging systems, to pagerduty schedules, etc. Users may +from, to monitoring and logging systems, to PagerDuty schedules, etc. Users may add these to descriptor YAML files, but in addition to this automated systems may also add annotations, either during ingestion into the catalog, or at a later time. @@ -381,7 +381,8 @@ metadata: spec: type: website lifecycle: production - owner: artist-relations@example.com + owner: artist-relations-team + system: artist-engagement-portal providesApis: - artist-api ``` @@ -427,8 +428,8 @@ The current set of well-known and common values for this field is: ### `spec.owner` [required] -The owner of the component, e.g. `artist-relations@example.com`. This field is -required. +An [entity reference](#string-references) to the owner of the component, e.g. +`artist-relations-team`. This field is required. In Backstage, the owner of a component is the singular entity (commonly a team) that bears ultimate responsibility for the component, and has the authority and @@ -440,25 +441,45 @@ not to be used by automated processes to for example assign authorization in runtime systems. There may be others that also develop or otherwise touch the component, but there will always be one ultimate owner. -Apart from being a string, the software catalog leaves the format of this field -open to implementers to choose. Most commonly, it is set to the ID or email of a -group of people in an organizational structure. +| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | +| ------------------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------- | +| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownerOf`, and reverse `ownedBy`](well-known-relations.md#ownedby-and-ownerof) | + +### `spec.system` [optional] + +An [entity reference](#string-references) to the system that the component +belongs to, e.g. `artist-engagement-portal`. This field is optional. + +| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | +| --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | +| [`System`](#kind-system) (default) | Same as this entity, typically `default` | [`partOf`, and reverse `hasPart`](well-known-relations.md#partof-and-haspart) | + +### `spec.subcomponentOf` [optional] + +An [entity reference](#string-references) to another component of which the +component is a part, e.g. `spotify-ios-app`. This field is optional. + +| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | +| ---------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | +| [`Component`](#kind-component) (default) | Same as this entity, typically `default` | [`partOf`, and reverse `hasPart`](well-known-relations.md#partof-and-haspart) | ### `spec.providesApis` [optional] -Links APIs that are provided by the component, e.g. `artist-api`. This field is -optional. +An array of [entity references](#string-references) to the APIs that are +provided by the component, e.g. `artist-api`. This field is optional. -The software catalog expects a list of one or more strings that references the -names of other entities of the `kind` `API`. +| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | +| --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------- | +| [`API`](#kind-api) (default) | Same as this entity, typically `default` | [`providesApi`, and reverse `apiProvidedBy`](well-known-relations.md#providesapi-and-apiprovidedby) | ### `spec.consumesApis` [optional] -Links APIs that are consumed by the component, e.g. `artist-api`. This field is -optional. +An array of [entity references](#string-references) to the APIs that are +consumed by the component, e.g. `artist-api`. This field is optional. -The software catalog expects a list of one or more strings that references the -names of other entities of the `kind` `API`. +| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | +| --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------- | +| [`API`](#kind-api) (default) | Same as this entity, typically `default` | [`consumesApi`, and reverse `apiConsumedBy`](well-known-relations.md#consumesapi-and-apiconsumedby) | ## Kind: Template @@ -597,7 +618,8 @@ metadata: spec: type: openapi lifecycle: production - owner: artist-relations@example.com + owner: artist-relations-team + system: artist-engagement-portal definition: | openapi: "3.0.0" info: @@ -663,8 +685,8 @@ The current set of well-known and common values for this field is: ### `spec.owner` [required] -The owner of the API, e.g. `artist-relations@example.com`. This field is -required. +An [entity reference](#string-references) to the owner of the component, e.g. +`artist-relations-team`. This field is required. In Backstage, the owner of an API is the singular entity (commonly a team) that bears ultimate responsibility for the API, and has the authority and capability @@ -676,9 +698,18 @@ processes to for example assign authorization in runtime systems. There may be others that also develop or otherwise touch the API, but there will always be one ultimate owner. -Apart from being a string, the software catalog leaves the format of this field -open to implementers to choose. Most commonly, it is set to the ID or email of a -group of people in an organizational structure. +| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | +| ------------------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------- | +| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownerOf`, and reverse `ownedBy`](well-known-relations.md#ownedby-and-ownerof) | + +### `spec.system` [optional] + +An [entity reference](#string-references) to the system that the API belongs to, +e.g. `artist-engagement-portal`. This field is optional. + +| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | +| --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | +| [`System`](#kind-system) (default) | Same as this entity, typically `default` | [`partOf`, and reverse `hasPart`](well-known-relations.md#partof-and-haspart) | ### `spec.definition` [required] @@ -751,11 +782,11 @@ parent; the catalog supports multi-root hierarchies. Groups may however not have more than one parent. This field is an -[entity reference](https://backstage.io/docs/features/software-catalog/references), -with the default kind `Group` and the default namespace equal to the same -namespace as the user. Only `Group` entities may be referenced. Most commonly, -this field points to a group in the same namespace, so in those cases it is -sufficient to enter only the `metadata.name` field of that group. +[entity reference](https://backstage.io/docs/features/software-catalog/references). + +| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | +| --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------- | +| [`Group`](#kind-group) (default) | Same as this entity, typically `default` | [`childOf`, and reverse `parentOf`](well-known-relations.md#parentof-and-childof) | ### `spec.children` [required] @@ -765,11 +796,11 @@ no child groups. The items are not guaranteed to be ordered in any particular way. The entries of this array are -[entity references](https://backstage.io/docs/features/software-catalog/references), -with the default kind `Group` and the default namespace equal to the same -namespace as the user. Only `Group` entities may be referenced. Most commonly, -these entries point to groups in the same namespace, so in those cases it is -sufficient to enter only the `metadata.name` field of those groups. +[entity references](https://backstage.io/docs/features/software-catalog/references). + +| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | +| --------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------- | +| [`Group`](#kind-group) (default) | Same as this entity, typically `default` | [`hasMember`, and reverse `memberOf`](well-known-relations.md#memberof-and-hasmember) | ## Kind: User @@ -825,23 +856,202 @@ user is not member of any groups. The items are not guaranteed to be ordered in any particular way. The entries of this array are -[entity references](https://backstage.io/docs/features/software-catalog/references), -with the default kind `Group` and the default namespace equal to the same -namespace as the user. Only `Group` entities may be referenced. Most commonly, -these entries point to groups in the same namespace, so in those cases it is -sufficient to enter only the `metadata.name` field of those groups. +[entity references](https://backstage.io/docs/features/software-catalog/references). + +| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | +| --------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------- | +| [`Group`](#kind-group) (default) | Same as this entity, typically `default` | [`memberOf`, and reverse `hasMember`](well-known-relations.md#memberof-and-hasmember) | ## Kind: Resource -This kind is not yet defined, but is reserved [for future use](system-model.md). +Describes the following entity kind: + +| Field | Value | +| ------------ | ----------------------- | +| `apiVersion` | `backstage.io/v1alpha1` | +| `kind` | `Resource` | + +A resource describes the infrastructure a system needs to operate, like BigTable +databases, Pub/Sub topics, S3 buckets or CDNs. Modelling them together with +components and systems allows to visualize resource footprint, and create +tooling around them. + +Descriptor files for this kind may look as follows. + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: Resource +metadata: + name: artists-db + description: Stores artist details +spec: + type: database + owner: artist-relations-team + system: artist-engagement-portal +``` + +In addition to the [common envelope metadata](#common-to-all-kinds-the-metadata) +shape, this kind has the following structure. + +### `apiVersion` and `kind` [required] + +Exactly equal to `backstage.io/v1alpha1` and `Resource`, respectively. + +### `spec.owner` [required] + +An [entity reference](#string-references) to the owner of the resource, e.g. +`artist-relations-team`. This field is required. + +In Backstage, the owner of a resource is the singular entity (commonly a team) +that bears ultimate responsibility for the resource, and has the authority and +capability to develop and maintain it. They will be the point of contact if +something goes wrong, or if features are to be requested. The main purpose of +this field is for display purposes in Backstage, so that people looking at +catalog items can get an understanding of to whom this resource belongs. It is +not to be used by automated processes to for example assign authorization in +runtime systems. There may be others that also manage or otherwise touch the +resource, but there will always be one ultimate owner. + +| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | +| ------------------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------- | +| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownerOf`, and reverse `ownedBy`](well-known-relations.md#ownedby-and-ownerof) | + +### `spec.type` [required] + +The type of resource as a string, e.g. `database`. This field is required. There +is currently no enforced set of values for this field, so it is left up to the +adopting organization to choose a nomenclature that matches the resources used +in their tech stack. + +Some common values for this field could be: + +- `database` +- `s3-bucket` +- `cluster` + +### `spec.system` [optional] + +An [entity reference](#string-references) to the system that the resource +belongs to, e.g. `artist-engagement-portal`. This field is optional. + +| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | +| --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | +| [`System`](#kind-system) (default) | Same as this entity, typically `default` | [`partOf`, and reverse `hasPart`](well-known-relations.md#partof-and-haspart) | ## Kind: System -This kind is not yet defined, but is reserved [for future use](system-model.md). +Describes the following entity kind: + +| Field | Value | +| ------------ | ----------------------- | +| `apiVersion` | `backstage.io/v1alpha1` | +| `kind` | `System` | + +A system is a collection of resources and components. The system may expose or +consume one or several APIs. It is viewed as abstraction level that provides +potential consumers insights into exposed features without needing a too +detailed view into the details of all components. This also gives the owning +team the possibility to decide about published artifacts and APIs. + +Descriptor files for this kind may look as follows. + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: System +metadata: + name: artist-engagement-portal + description: Handy tools to keep artists in the loop +spec: + owner: artist-relations-team + domain: artists + providesApis: + - artist-api +``` + +In addition to the [common envelope metadata](#common-to-all-kinds-the-metadata) +shape, this kind has the following structure. + +### `apiVersion` and `kind` [required] + +Exactly equal to `backstage.io/v1alpha1` and `System`, respectively. + +### `spec.owner` [required] + +An [entity reference](#string-references) to the owner of the system, e.g. +`artist-relations-team`. This field is required. + +In Backstage, the owner of a system is the singular entity (commonly a team) +that bears ultimate responsibility for the system, and has the authority and +capability to develop and maintain it. They will be the point of contact if +something goes wrong, or if features are to be requested. The main purpose of +this field is for display purposes in Backstage, so that people looking at +catalog items can get an understanding of to whom this system belongs. It is not +to be used by automated processes to for example assign authorization in runtime +systems. There may be others that also develop or otherwise touch the system, +but there will always be one ultimate owner. + +| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | +| ------------------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------- | +| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownerOf`, and reverse `ownedBy`](well-known-relations.md#ownedby-and-ownerof) | + +### `spec.domain` [optional] + +An [entity reference](#string-references) to the domain that the system belongs +to, e.g. `artists`. This field is optional. + +| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | +| --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | +| [`Domain`](#kind-domain) (default) | Same as this entity, typically `default` | [`partOf`, and reverse `hasPart`](well-known-relations.md#partof-and-haspart) | ## Kind: Domain -This kind is not yet defined, but is reserved [for future use](system-model.md). +Describes the following entity kind: + +| Field | Value | +| ------------ | ----------------------- | +| `apiVersion` | `backstage.io/v1alpha1` | +| `kind` | `Domain` | + +A Domain groups a collection of systems that share terminology, domain models, +business purpose, or documentation, i.e. form a bounded context. + +Descriptor files for this kind may look as follows. + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: Domain +metadata: + name: artists + description: Everything about artists +spec: + owner: artist-relations-team +``` + +In addition to the [common envelope metadata](#common-to-all-kinds-the-metadata) +shape, this kind has the following structure. + +### `apiVersion` and `kind` [required] + +Exactly equal to `backstage.io/v1alpha1` and `Domain`, respectively. + +### `spec.owner` [required] + +An [entity reference](#string-references) to the owner of the domain, e.g. +`artist-relations-team`. This field is required. + +In Backstage, the owner of a domain is the singular entity (commonly a team) +that bears ultimate responsibility for the domain, and has the authority and +capability to develop and maintain it. They will be the point of contact if +something goes wrong, or if features are to be requested. The main purpose of +this field is for display purposes in Backstage, so that people looking at +catalog items can get an understanding of to whom this domain belongs. It is not +to be used by automated processes to for example assign authorization in runtime +systems. There may be others that also develop or otherwise touch the domain, +but there will always be one ultimate owner. + +| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | +| ------------------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------- | +| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownerOf`, and reverse `ownedBy`](well-known-relations.md#ownedby-and-ownerof) | ## Kind: Location diff --git a/docs/features/software-catalog/system-model.md b/docs/features/software-catalog/system-model.md index d797e87499..f0ead20664 100644 --- a/docs/features/software-catalog/system-model.md +++ b/docs/features/software-catalog/system-model.md @@ -23,7 +23,7 @@ We model software in the Backstage catalogue using these three core entities - **Resources** are physical or virtual infrastructure needed to operate a component -![](../../assets/software-catalog/software-model-core-entities.png) +![](../../assets/software-catalog/software-model-core-entities.drawio.svg) ### Component @@ -73,6 +73,8 @@ these entities using the following (optional) concepts: function - **Domains** relate entities and systems to part of the business +![](../../assets/software-catalog/software-model-entities.drawio.svg) + ### System With increasing complexity in software, systems form an important abstraction @@ -107,10 +109,6 @@ product or use-case, share the same entity types in their APIs, and integrate well with each other. Other domains could be “Content Ingestion”, “Ads” or “Search”. -## Current status - -Backstage currently supports Components and APIs. - ## Links - [Original RFC](https://github.com/backstage/backstage/issues/390) diff --git a/docs/features/software-catalog/well-known-relations.md b/docs/features/software-catalog/well-known-relations.md index 54f7833d1b..6fb7ae2fea 100644 --- a/docs/features/software-catalog/well-known-relations.md +++ b/docs/features/software-catalog/well-known-relations.md @@ -48,11 +48,10 @@ where present. ### `providesApi` and `apiProvidedBy` A relation with an [API](descriptor-format.md#kind-api) entity, typically from a -[Component](descriptor-format.md#kind-component) or -[System](descriptor-format.md#kind-system). +[Component](descriptor-format.md#kind-component). -These relations express that a component or system exposes an API - meaning that -it hosts callable endpoints from which you can consume that API. +These relations express that a component exposes an API - meaning that it hosts +callable endpoints from which you can consume that API. This relation is commonly generated based on `spec.providesApis` of the component or system in question. @@ -60,11 +59,10 @@ component or system in question. ### `consumesApi` and `apiConsumedBy` A relation with an [API](descriptor-format.md#kind-api) entity, typically from a -[Component](descriptor-format.md#kind-component) or -[System](descriptor-format.md#kind-system). +[Component](descriptor-format.md#kind-component). -These relations express that a component or system consumes an API - meaning -that it depends on endpoints of the API. +These relations express that a component consumes an API - meaning that it +depends on endpoints of the API. This relation is commonly generated based on `spec.consumesApis` of the component or system in question. @@ -91,3 +89,18 @@ A membership relation, typically for [Users](descriptor-format.md#kind-user) in [Groups](descriptor-format.md#kind-group). This relation is commonly based on `spec.memberOf`. + +### `partOf` and `hasPart` + +A relation with a [Domain](descriptor-format.md#kind-domain), +[System](descriptor-format.md#kind-system) or +[Component](descriptor-format.md#kind-component) entity, typically from a +[Component](descriptor-format.md#kind-component), +[API](descriptor-format.md#kind-api), or +[System](descriptor-format.md#kind-system). + +These relations express that a component belongs to a larger component; a +component, API or resource belongs to a system; or that a system is grouped +under a domain. + +This relation is commonly based on `spec.system` or `spec.domain`. diff --git a/docs/features/software-templates/index.md b/docs/features/software-templates/index.md index 3ebb426466..1034ffef0f 100644 --- a/docs/features/software-templates/index.md +++ b/docs/features/software-templates/index.md @@ -2,8 +2,8 @@ id: software-templates-index title: Backstage Software Templates sidebar_label: Overview -description: The Software Templates part of Backstage is a tool that can help -you create Components inside Backstage +# prettier-ignore +description: The Software Templates part of Backstage is a tool that can help you create Components inside Backstage --- The Software Templates part of Backstage is a tool that can help you create diff --git a/docs/features/techdocs/README.md b/docs/features/techdocs/README.md index 82db4e15ff..c43d6d978d 100644 --- a/docs/features/techdocs/README.md +++ b/docs/features/techdocs/README.md @@ -2,8 +2,8 @@ id: techdocs-overview title: TechDocs Documentation sidebar_label: Overview -description: TechDocs is Spotify’s homegrown docs-like-code solution built -directly into Backstage +# prettier-ignore +description: TechDocs is Spotify’s homegrown docs-like-code solution built directly into Backstage --- ## What is it? diff --git a/docs/features/techdocs/concepts.md b/docs/features/techdocs/concepts.md index 26254a7e12..408f092ebd 100644 --- a/docs/features/techdocs/concepts.md +++ b/docs/features/techdocs/concepts.md @@ -1,8 +1,8 @@ --- id: concepts title: Concepts -description: Documentation on concepts that are introduced with -Spotify's docs-like-code solution in Backstage +# prettier-ignore +description: Documentation on concepts that are introduced with Spotify's docs-like-code solution in Backstage --- This page describes concepts that are introduced with Spotify's docs-like-code diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index 7a0a4dbbbf..1580abe69e 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -1,8 +1,8 @@ --- id: configuration title: TechDocs Configuration Options -description: - Reference documentation for configuring TechDocs using app-config.yaml +# prettier-ignore +description: Reference documentation for configuring TechDocs using app-config.yaml --- Using the `app-config.yaml` in the Backstage app, you can configure TechDocs @@ -54,28 +54,34 @@ techdocs: # Required when techdocs.publisher.type is set to 'googleGcs'. Skip otherwise. googleGcs: - # An API key is required to write to a storage bucket. + # (Required) Cloud Storage Bucket Name + bucketName: 'techdocs-storage' + + # (Optional) An API key is required to write to a storage bucket. + # If missing, GOOGLE_APPLICATION_CREDENTIALS environment variable will be used. + # https://cloud.google.com/docs/authentication/production credentials: $file: '/path/to/google_application_credentials.json' - # Your GCP Project ID where the Cloud Storage Bucket is hosted. - projectId: 'gcp-project-id' - - # Cloud Storage Bucket Name - bucketName: 'techdocs-storage' - # Required when techdocs.publisher.type is set to 'awsS3'. Skip otherwise. awsS3: - # An API key is required to write to a storage bucket. + # (Required) AWS S3 Bucket Name + bucketName: 'techdocs-storage' + + # (Optional) An API key is required to write to a storage bucket. + # If not set, environment variables or aws config file will be used to authenticate. + # https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-environment.html + # https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-shared.html credentials: accessKeyId: $env: TECHDOCS_AWSS3_ACCESS_KEY_ID_CREDENTIAL secretAccessKey: $env: TECHDOCS_AWSS3_SECRET_ACCESS_KEY_CREDENTIAL - region: - $env: AWSS3_REGION - # AWS S3 Bucket Name - bucketName: 'techdocs-storage' + # (Optional) AWS Region of the bucket. + # If not set, AWS_REGION environment variable or aws config file will be used. + # https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-region.html + region: + $env: AWS_REGION ``` diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md index c05346ca4a..21206dede5 100644 --- a/docs/features/techdocs/using-cloud-storage.md +++ b/docs/features/techdocs/using-cloud-storage.md @@ -30,20 +30,35 @@ techdocs: type: 'googleGcs' ``` -**2. GCP (Google Cloud Platform) Project** +**2. Create a GCS Bucket** -Create or choose a dedicated GCP project. Set -`techdocs.publisher.googleGcs.projectId` to the project ID. +Create a dedicated Google Cloud Storage bucket for TechDocs sites. +techdocs-backend will publish documentation to this bucket. TechDocs will fetch +files from here to serve documentation in Backstage. Note that the bucket names +are globally unique. + +Set the config `techdocs.publisher.googleGcs.bucketName` in your +`app-config.yaml` to the name of the bucket you just created. ```yaml techdocs: publisher: type: 'googleGcs' - googleGcs: - projectId: 'gcp-project-id' + googleGcs: + bucketName: 'name-of-techdocs-storage-bucket' ``` -**3. Service account API key** +**3a. (Recommended) Authentication using environment variable** + +The GCS Node.js client will automatically use the environment variable +`GOOGLE_APPLICATION_CREDENTIALS` to authenticate with Google Cloud. It might +already be set in Compute Engine, Google Kubernetes Engine, etc. Read +https://cloud.google.com/docs/authentication/production for more details. + +**3b. Authentication using app-config.yaml** + +If you do not prefer (3a) and optionally like to use a service account, you can +follow these steps. Create a new Service Account and a key associated with it. In roles of the service account, use "Storage Admin". @@ -65,41 +80,32 @@ techdocs: publisher: type: 'googleGcs' googleGcs: - projectId: 'gcp-project-id' + bucketName: 'name-of-techdocs-storage-bucket' credentials: $file: '/path/to/google_application_credentials.json' ``` -**4. GCS Bucket** - -Create a dedicated bucket for TechDocs sites. techdocs-backend will publish -documentation to this bucket. TechDocs will fetch files from here to serve -documentation in Backstage. - -Set the name of the bucket to `techdocs.publisher.googleGcs.bucketName`. +Note: If you are finding it difficult to make the file +`google_application_credentials.json` available on a server, you could use the +file's content and set as an environment variable. And then use ```yaml techdocs: publisher: type: 'googleGcs' googleGcs: - projectId: 'gcp-project-id' - credentials: - $file: '/path/to/google_application_credentials.json' bucketName: 'name-of-techdocs-storage-bucket' + credentials: + $env: GOOGLE_APPLICATION_CREDENTIALS ``` -**5. That's it!** +**4. That's it!** Your Backstage app is now ready to use Google Cloud Storage for TechDocs, to -store the static generated documentation files. +store and read the static generated documentation files. ## Configuring AWS S3 Bucket with TechDocs -Follow the -[official AWS S3 documentation](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html) -for the latest instructions on the following steps involving AWS S3. - **1. Set `techdocs.publisher.type` config in your `app-config.yaml`** Set `techdocs.publisher.type` to `'awsS3'`. @@ -110,43 +116,17 @@ techdocs: type: 'awsS3' ``` -**2. AWS Policies** +**2. Create an S3 Bucket** -AWS Policies lets you **control access** to Amazon Web Services (AWS) products -and resources. -Here we will use a user policy **and** a bucket policy to show you the different -possibilities you have but you can use only one. +Create a dedicated AWS S3 bucket for the storage of TechDocs sites. +[Refer to the official documentation](https://docs.aws.amazon.com/AmazonS3/latest/user-guide/create-bucket.html). -AWS S3 +TechDocs will publish documentation to this bucket and will fetch files from +here to serve documentation in Backstage. Note that the bucket names are +globally unique. -This is an example of how you can manage your policies: - -a. Admin user creates a **bucket policy** granting a set of permissions to our -TechDocs user. - -b. Admin user attaches a **user policy** to the TechDocs user granting -additional permissions. - -c. TechDocs User then tries permissions granted via both the **bucket** policy -and the **user** policy. - -**2.1 Creation** - -**2.1.1 Create an Admin user** (if you don't have one yet) - -Create an **administrator user** account `ADMIN_USER` and grant it administrator -privileges by attaching a user policy giving the account **full access**. -Note down the Admin User credentials and IAM User Sign-In URL as you will need -to use this information in the next step. - -**2.1.2 Create an AWS S3 Bucket** - -Using the credentials of your Admin User `ADMIN_USER`, and the special IAM user -sign-in URL, create a dedicated **bucket** for TechDocs sites. techdocs-backend -will publish documentation to this bucket. TechDocs will fetch files from here -to serve documentation in Backstage. - -Set the name of the bucket to `techdocs.publisher.awsS3.bucketName`. +Set the config `techdocs.publisher.awsS3.bucketName` in your `app-config.yaml` +to the name of the bucket you just created. ```yaml techdocs: @@ -156,112 +136,62 @@ techdocs: bucketName: 'name-of-techdocs-storage-bucket' ``` -**2.1.3 Create the `TechDocs` user** +**3a. (Recommended) Setup authentication the AWS way, using environment +variables** -This user will be used to interact with your bucket, it will only have -permissions to **get - put** objects. +You should follow the +[AWS security best practices guide for authentication](https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html). -In the IAM console, do the following: +If the environment variables -- Create a new user, `TechDocs` -- Note down the TechDocs User credentials -- Note down the Amazon Resource Name (ARN) for the TechDocs user. In the IAM - console, select the TechDocs user, and you can find the user ARN in the - Summary tab. +- `AWS_ACCESS_KEY_ID` +- `AWS_SECRET_ACCESS_KEY` +- `AWS_REGION` -**2.2 Attach policies** +are set and can be used to access the bucket you created in step 2, they will be +used by the AWS SDK v3 Node.js client for authentication. +[Refer to the official documentation.](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-environment.html) -Remember that you can use Bucket policy **or** User policy. -Just make sure that you grant all the permissions to the TechDocs user: -`3:PutObject`, `s3:GetObject`, `s3:ListBucket` and `s3:GetBucketLocation`. +If the environment variables are missing, the AWS SDK tries to read the +`~/.aws/credentials` file for credentials. +[Refer to the official documentation.](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-shared.html) -**2.2.1 Create the bucket policy** +Note that the region of the bucket has to be set for the AWS SDK to work. +[See this](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-region.html). -You now have to attach the following policy to your bucket in the Permission -section: +**3b. Authentication using app-config.yaml** -```json -{ - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "statement1", - "Effect": "Allow", - "Principal": { - "AWS": "arn:aws:iam::YOUR_ACCOUNT_ID:user/TechDocs" - }, - "Action": ["s3:GetBucketLocation", "s3:ListBucket"], - "Resource": ["arn:aws:s3:::name-of-techdocs-storage-bucket"] - }, - { - "Sid": "statement2", - "Effect": "Allow", - "Principal": { - "AWS": "arn:aws:iam::YOUR_ACCOUNT_ID:user/TechDocs" - }, - "Action": ["s3:GetObject"], - "Resource": ["arn:aws:s3:::name-of-techdocs-storage-bucket/*"] - } - ] -} -``` - -- The first statement grants **TechDocs User** the bucket operation permissions - `s3:GetBucketLocation` and `s3:ListBucket` which are permissions required by - the console. -- The second statement grants the `s3:GetObject` permission. - (**NOTE :** if you do not use the user policy defined below you must also add - the `s3:PutObject` permission to allow the TechDocs user to add objects.) - -**2.2.2 Create the user policy** - -Create an inline policy for the TechDocs user by using the following policy: - -```json -{ - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "PermissionForObjectOperations", - "Effect": "Allow", - "Action": ["s3:PutObject"], - "Resource": ["arn:aws:s3:::name-of-techdocs-storage-bucket/*"] - } - ] -} -``` - -See more details in the section -[Working with Inline Policies](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_manage.html). - -Now you need to fill in the environment variables with the `TechDocs` User -credentials. You can also specify a region if you want to accesses the resources -in a specific region. Otherwise no region will be selected by default. - -```properties -TECHDOCS_AWSS3_ACCESS_KEY_ID_CREDENTIAL="TECHDOCS_ACCESS_KEY_ID" -TECHDOCS_AWSS3_SECRET_ACCESS_KEY_CREDENTIAL="TECHDOCS_SECRET_ACCESS_KEY" -AWSS3_REGION="" // Optional -``` - -Make it available in your Backstage server and/or your local development server -and set it in the app config techdocs.publisher.awsS3. +AWS credentials and region can be provided to the AWS SDK via `app-config.yaml`. +If the configs below are present, they will be used over existing `AWS_*` +environment variables and the `~/.aws/credentials` config file. ```yaml techdocs: publisher: type: 'awsS3' awsS3: + bucketName: 'name-of-techdocs-storage-bucket' + region: + $env: AWS_REGION credentials: accessKeyId: - $env: TECHDOCS_AWSS3_ACCESS_KEY_ID_CREDENTIAL + $env: AWS_ACCESS_KEY_ID secretAccessKey: - $env: TECHDOCS_AWSS3_SECRET_ACCESS_KEY_CREDENTIAL - region: - $env: AWSS3_REGION + $env: AWS_SECRET_ACCESS_KEY ``` -**3. That's it!** +Refer to the +[official AWS documentation for obtaining the credentials](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-your-credentials.html). -Your Backstage app is now ready to use AWS S3 for TechDocs, to store the static -generated documentation files. +Note: If you are using Amazon EC2 instance to deploy Backstage, you do not need +to obtain the access keys separately. They can be made available in the +environment automatically by defining appropriate IAM role with access to the +bucket. Read more +[here](https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html#use-roles). + +**4. That's it!** + +Your Backstage app is now ready to use AWS S3 for TechDocs, to store and read +the static generated documentation files. When you start the backend of the app, +you should be able to see +`techdocs info Successfully connected to the AWS S3 bucket` in the logs. diff --git a/docs/getting-started/deployment-other.md b/docs/getting-started/deployment-other.md index cfd2e903d9..55582ee77a 100644 --- a/docs/getting-started/deployment-other.md +++ b/docs/getting-started/deployment-other.md @@ -16,7 +16,7 @@ $ yarn docker-build $ docker run --rm -it -p 7000:7000 -e APP_ENV=production -e NODE_ENV=development example-backend:latest ``` -Then open http://localhost/ on your browser. +Then open http://localhost:7000 on your browser. ## Heroku diff --git a/docs/getting-started/development-environment.md b/docs/getting-started/development-environment.md index c5275114ce..b5ff3885b3 100644 --- a/docs/getting-started/development-environment.md +++ b/docs/getting-started/development-environment.md @@ -1,8 +1,8 @@ --- id: development-environment title: Development Environment -description: Documentation on how to get set up for doing development on -the Backstage repository +# prettier-ignore +description: Documentation on how to get set up for doing development on the Backstage repository --- This section describes how to get set up for doing development on the Backstage diff --git a/docs/overview/adopting.md b/docs/overview/adopting.md index 0d404d5cb4..152be21a2c 100644 --- a/docs/overview/adopting.md +++ b/docs/overview/adopting.md @@ -1,8 +1,8 @@ --- id: adopting title: Strategies for adopting -description: Documentation on some general best practices that have been key -to Backstage's success inside Spotify +# prettier-ignore +description: Documentation on some general best practices that have been key to Backstage's success inside Spotify --- This document outlines some general best practices that have been key to diff --git a/docs/overview/background.md b/docs/overview/background.md index aca46614c2..0c22396e72 100644 --- a/docs/overview/background.md +++ b/docs/overview/background.md @@ -1,8 +1,8 @@ --- id: background title: The Spotify Story -description: Backstage was born out of necessity at Spotify. We found that as we grew, our -infrastructure was becoming more fragmented, our engineers less productive. +# prettier-ignore +description: Backstage was born out of necessity at Spotify. We found that as we grew, our infrastructure was becoming more fragmented, our engineers less productive. --- Backstage was born out of necessity at Spotify. We found that as we grew, our diff --git a/docs/overview/roadmap.md b/docs/overview/roadmap.md index b0c031689d..6ed4a3272d 100644 --- a/docs/overview/roadmap.md +++ b/docs/overview/roadmap.md @@ -8,9 +8,9 @@ description: Roadmap of Backstage Project > Backstage is currently under rapid development. This means that you can expect > APIs and features to evolve. It is also recommended that teams who adopt -> Backstage today upgrade their installation as new -> [releases](https://github.com/backstage/backstage/releases) become available, -> as Backwards compatibility is not yet guaranteed. +> Backstage today [upgrade their installation](../cli/commands.md#versionsbump) +> as new [releases](https://github.com/backstage/backstage/releases) become +> available, as Backwards compatibility is not yet guaranteed. ## Phases diff --git a/docs/overview/stability-index.md b/docs/overview/stability-index.md index c53193f063..3304913f87 100644 --- a/docs/overview/stability-index.md +++ b/docs/overview/stability-index.md @@ -1,9 +1,8 @@ --- id: stability-index title: Stability Index -description: - An overview of the commitment to stability for different parts of the - Backstage codebase. +# prettier-ignore +description: An overview of the commitment to stability for different parts of the Backstage codebase. --- ## Overview @@ -291,7 +290,7 @@ Stability: `1`. There are plans to rework parts of the Processor interface. ### `catalog-graphql` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/catalog-graphql/) -Provides the catalog schema and resolvers for the graphql backend. +Provides the catalog schema and resolvers for the GraphQL backend. Stability: `0`. Under heavy development and subject to change. diff --git a/docs/overview/vision.md b/docs/overview/vision.md index c17e2b17ba..f6d6af90dd 100644 --- a/docs/overview/vision.md +++ b/docs/overview/vision.md @@ -1,8 +1,8 @@ --- id: vision title: Vision -description: Goal is to provide engineers with the best developer experience in -the world +# prettier-ignore +description: Goal is to provide engineers with the best developer experience in the world --- Our goal is to provide engineers with the best developer experience in the diff --git a/docs/overview/what-is-backstage.md b/docs/overview/what-is-backstage.md index b414e62853..ec824a1e11 100644 --- a/docs/overview/what-is-backstage.md +++ b/docs/overview/what-is-backstage.md @@ -1,8 +1,8 @@ --- id: what-is-backstage title: What is Backstage? -description: Backstage is an open platform for building developer portals. -Powered by a centralized service catalog, Backstage restores order to your microservices and infrastructure +# prettier-ignore +description: Backstage is an open platform for building developer portals. Powered by a centralized service catalog, Backstage restores order to your microservices and infrastructure --- ![service-catalog](https://backstage.io/blog/assets/6/header.png) diff --git a/docs/plugins/call-existing-api.md b/docs/plugins/call-existing-api.md index 75e054d2b9..5dbf7988c9 100644 --- a/docs/plugins/call-existing-api.md +++ b/docs/plugins/call-existing-api.md @@ -1,8 +1,8 @@ --- id: call-existing-api title: Call Existing API -description: Describes the various options that Backstage frontend plugins have, -in communicating with service APIs that already exist +# prettier-ignore +description: Describes the various options that Backstage frontend plugins have, in communicating with service APIs that already exist --- This article describes the various options that Backstage frontend plugins have, diff --git a/docs/plugins/publishing.md b/docs/plugins/publishing.md index efea02ee23..06fae0533e 100644 --- a/docs/plugins/publishing.md +++ b/docs/plugins/publishing.md @@ -7,7 +7,7 @@ description: Documentation on Publishing npm packages ## npm npm packages are published through CI/CD in the -[.github/workflows/master.yml](https://github.com/backstage/backstage/blob/master/.github/workflows/master.yml) +[`.github/workflows/master.yml`](https://github.com/backstage/backstage/blob/master/.github/workflows/master.yml) workflow. Every commit that is merged to master will be checked for new versions of all public packages, and any new versions will automatically be published to npm. diff --git a/docs/reference/utility-apis/ErrorApi.md b/docs/reference/utility-apis/ErrorApi.md index 93f4f9cd48..9bba0c76c6 100644 --- a/docs/reference/utility-apis/ErrorApi.md +++ b/docs/reference/utility-apis/ErrorApi.md @@ -29,7 +29,7 @@ These types are part of the API declaration, but may not be unique to this API. ### Error -Mirrors the javascript Error class, for the purpose of providing documentation +Mirrors the JavaScript Error class, for the purpose of providing documentation and optional fields.
diff --git a/docs/support/project-structure.md b/docs/support/project-structure.md
index a30288e85e..bb2c02ec76 100644
--- a/docs/support/project-structure.md
+++ b/docs/support/project-structure.md
@@ -1,8 +1,8 @@
 ---
 id: project-structure
 title: Backstage Project Structure
-description:
-  Introduction to files and folders in the Backstage Project repository
+# prettier-ignore
+description: Introduction to files and folders in the Backstage Project repository
 ---
 
 Backstage is a complex project, and the GitHub repository contains many
diff --git a/docs/tutorials/quickstart-app-plugin.md b/docs/tutorials/quickstart-app-plugin.md
index 0f8d9bb84b..045062fdcb 100644
--- a/docs/tutorials/quickstart-app-plugin.md
+++ b/docs/tutorials/quickstart-app-plugin.md
@@ -59,7 +59,7 @@ import GitHubIcon from '@material-ui/icons/GitHub';
 ```
 
 Simple! The App will reload with your changes automatically. You should now see
-a github icon displayed in the sidebar. Clicking that will link to our new
+a GitHub icon displayed in the sidebar. Clicking that will link to our new
 plugin. And now, the API fun begins.
 
 # The Identity
diff --git a/microsite/blog/2020-09-08-announcing-tech-docs.md b/microsite/blog/2020-09-08-announcing-tech-docs.md
index f09c73fd83..98fbad57c5 100644
--- a/microsite/blog/2020-09-08-announcing-tech-docs.md
+++ b/microsite/blog/2020-09-08-announcing-tech-docs.md
@@ -6,7 +6,7 @@ authorURL: https://github.com/garyniemen
 
 Since we [open sourced Backstage](https://backstage.io/blog/2020/03/16/announcing-backstage), one of the most requested features has been for a technical documentation plugin. Well, good news. The first open source version of TechDocs is here. Now let’s start collaborating and making it better, together.
 
-
 
 
diff --git a/microsite/blog/2020-10-22-cost-insights-plugin.md b/microsite/blog/2020-10-22-cost-insights-plugin.md
index c7ae9c492d..d265698694 100644
--- a/microsite/blog/2020-10-22-cost-insights-plugin.md
+++ b/microsite/blog/2020-10-22-cost-insights-plugin.md
@@ -6,7 +6,7 @@ authorURL: https://twitter.com/janisa_a
 
 How did Spotify save millions on cloud costs within a matter of months?? We made cost optimization just another part of the daily development process. Our newly open sourced [Cost Insights plugin](https://github.com/backstage/backstage/tree/master/plugins/cost-insights) makes a team’s cloud costs visible — and actionable — right inside Backstage. So engineers can see the impact of their cloud usage (down to a product and resource level) and make optimizations wherever and whenever it makes sense. By managing cloud costs from the ground up, you can make smarter decisions that let you continue to build and scale quickly, without wasting resources.
 
-
+
 
 Are we turning engineers into accountants? Nope, we’re just letting engineers do what they do best, in the place that feels natural to them: inside Backstage.
 
diff --git a/microsite/blog/2021-01-12-new-backstage-feature-kubernetes-for-service-owners.md b/microsite/blog/2021-01-12-new-backstage-feature-kubernetes-for-service-owners.md
new file mode 100644
index 0000000000..b26806fecd
--- /dev/null
+++ b/microsite/blog/2021-01-12-new-backstage-feature-kubernetes-for-service-owners.md
@@ -0,0 +1,72 @@
+---
+title: New Backstage feature: Kubernetes for Service owners
+author: Matthew Clarke, Spotify
+authorURL: https://github.com/mclarke47
+---
+
+![Animation of Kubernetes and cloud provider icons becoming the Backstage logo](/blog/assets/21-01-12/backstage-k8s-1-hero.gif)
+
+TLDR; We’re rethinking the Kubernetes developer experience with a new feature: a Kubernetes monitoring tool that’s designed around the needs of service owners, not cluster admins. Now developers can easily check the health of their services no matter how or where those services are deployed — whether it’s on a local host for testing or in production on dozens of clusters around the world.
+
+And since Backstage uses the native Kubernetes API, the feature works with whichever cloud provider (AWS, Azure, GCP, etc.) or managed service (OpenShift, IBM Cloud, GKE, etc.) you already use.
+
+
+
+## The missing link between K8s and your service
+
+A core feature of Backstage is its service catalog, which aggregates information about software systems together inside a single tool, with a consistent, familiar UI.
+
+By navigating to a service’s overview page in Backstage, you can see everything you need to know about the service: what it does, its APIs and technical documentation, CI/CD progress — and now detailed information about its presence on Kubernetes clusters.
+
+## No more context switching
+
+Kubernetes in Backstage can be configured to search multiple clusters for your services. It will then aggregate them together into a single view. So if you deploy to multiple clusters you will no longer need to switch kubectl contexts to understand the current state of your service.
+
+![List of deployments in Backstage Kubernetes plugin](/blog/assets/21-01-12/backstage-k8s-2-deployments.png)
+
+## Automatic error reporting
+
+Instead of trying different kubectl commands to figure out where an error occurred, Backstage will automatically find and highlight errors in Kubernetes resources that are affecting your service. So you can spend time fixing errors, not hunting for them.
+
+![Error reporting screen in Backstage Kubernetes plugin](/blog/assets/21-01-12/backstage-k8s-3-error-reporting.png)
+
+## Autoscaling limits at a glance
+
+Backstage also shows you how close your service is to its autoscaling limit. Coming up to a period of high load? Now you will be able to see how your horizontal autoscaling is dealing with it across multiple clusters.
+
+![Autoscaling limits screen in Backstage Kubernetes plugin](/blog/assets/21-01-12/backstage-k8s-4-autoscaling-limits.png)
+
+![Autoscaling limits screen in Backstage Kubernetes plugin](/blog/assets/21-01-12/backstage-k8s-5-autoscaling-limits.png)
+
+## Pick a cloud, any Cloud
+
+Since Backstage communicates directly with the Kubernetes API, it’s cloud agnostic — it doesn’t matter how or where you’re running Kubernetes. You’ll always get the same familiar view of your deployments, whether you’re:
+
+- Deploying to clusters on AWS, Azure, GCP, or another cloud provider
+- Using an unmanaged or managed Kubernetes service (like OpenShift, etc.)
+- Migrating from one cloud provider or service to another
+- Testing on a single local machine or deploying to a dozen clusters in production
+
+In short: local or global, single or multi-cloud, managed or unmanaged — Backstage always provides a seamless Kubernetes experience for your service owners’ day-to-day development needs.
+
+## Rethinking the developer experience
+
+The philosophy behind Backstage is simple: improve developer experience by reducing infrastructure complexity. As popular and widespread as Kubernetes has become, all of the tools to date have been geared toward the needs of cluster admins. These tools add unnecessary complexity to the workflows of the typical developer building, testing, and deploying services.
+
+We believe Backstage Kubernetes gives developers back control of their services by providing a more focused and consistent experience. Backstage provides a single standard for developers to monitor their Kubernetes deployments, regardless of the underlying cloud infrastructure.
+
+## Future iterations
+
+The current focus of Kubernetes in Backstage is Deployments/ReplicaSets/Pods — but we know that not everyone utilizes these.
+
+As we continue to grow and develop Kubernetes in Backstage with the community, we hope to offer support for Kubernetes resources beyond Deployments and Custom Resource Definitions. You can browse or add open issues for the plugin [here].
+
+## Getting started
+
+We made the Kubernetes plugin a core feature of Backstage. Like Software Templates (scaffolder) and TechDocs, the k8s-plugin is installed with the core app. When you update the app to the latest version and go to the Kubernetes tab of any service, you will be asked to provide your cloud provider credentials. To learn more, including details on configuration and surfacing your Kubernetes components as part of an entity, [read the docs].
+
+To contribute or get more information on Kubernetes in Backstage, [join the discussion on Discord]!
+
+[here]: https://github.com/backstage/backstage/issues?q=is%3Aissue+is%3Aopen+kubernetes+label%3Ak8s-plugin
+[read the docs]: https://backstage.io/docs/features/kubernetes/overview
+[join the discussion on discord]: https://discord.gg/MUpMjP2
diff --git a/microsite/blog/assets/21-01-12/backstage-k8s-1-hero.gif b/microsite/blog/assets/21-01-12/backstage-k8s-1-hero.gif
new file mode 100644
index 0000000000..7af1a5a305
Binary files /dev/null and b/microsite/blog/assets/21-01-12/backstage-k8s-1-hero.gif differ
diff --git a/microsite/blog/assets/21-01-12/backstage-k8s-2-deployments.png b/microsite/blog/assets/21-01-12/backstage-k8s-2-deployments.png
new file mode 100644
index 0000000000..4a9f2b06fa
Binary files /dev/null and b/microsite/blog/assets/21-01-12/backstage-k8s-2-deployments.png differ
diff --git a/microsite/blog/assets/21-01-12/backstage-k8s-3-error-reporting.png b/microsite/blog/assets/21-01-12/backstage-k8s-3-error-reporting.png
new file mode 100644
index 0000000000..29002369d3
Binary files /dev/null and b/microsite/blog/assets/21-01-12/backstage-k8s-3-error-reporting.png differ
diff --git a/microsite/blog/assets/21-01-12/backstage-k8s-4-autoscaling-limits.png b/microsite/blog/assets/21-01-12/backstage-k8s-4-autoscaling-limits.png
new file mode 100644
index 0000000000..7333fc7076
Binary files /dev/null and b/microsite/blog/assets/21-01-12/backstage-k8s-4-autoscaling-limits.png differ
diff --git a/microsite/blog/assets/21-01-12/backstage-k8s-5-autoscaling-limits.png b/microsite/blog/assets/21-01-12/backstage-k8s-5-autoscaling-limits.png
new file mode 100644
index 0000000000..c9be936787
Binary files /dev/null and b/microsite/blog/assets/21-01-12/backstage-k8s-5-autoscaling-limits.png differ
diff --git a/microsite/package.json b/microsite/package.json
index 975bec3149..cb82e6ec85 100644
--- a/microsite/package.json
+++ b/microsite/package.json
@@ -15,7 +15,7 @@
   },
   "devDependencies": {
     "@spotify/prettier-config": "^9.0.0",
-    "docusaurus": "^2.0.0-alpha.378053ac5",
+    "docusaurus": "^2.0.0-alpha.70",
     "js-yaml": "^4.0.0",
     "prettier": "^2.2.1"
   },
diff --git a/microsite/pages/en/demos.js b/microsite/pages/en/demos.js
index c26fc9a8e8..47ea1ee9ee 100644
--- a/microsite/pages/en/demos.js
+++ b/microsite/pages/en/demos.js
@@ -100,9 +100,9 @@ const Background = props => {
               width="560"
               height="315"
               src="https://www.youtube.com/embed/YLAd5hdXR_Q"
-              frameborder="0"
+              frameBorder="0"
               allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
-              allowfullscreen
+              allowFullScreen
             >
           
         
@@ -135,9 +135,9 @@ const Background = props => {
               width="560"
               height="315"
               src="https://www.youtube.com/embed/mOLCgdPw1iA"
-              frameborder="0"
+              frameBorder="0"
               allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
-              allowfullscreen
+              allowFullScreen
             >
           
         
diff --git a/microsite/pages/en/index.js b/microsite/pages/en/index.js
index 395aba8cd0..d572605171 100644
--- a/microsite/pages/en/index.js
+++ b/microsite/pages/en/index.js
@@ -379,6 +379,72 @@ class Index extends React.Component {
           
         
 
+        
+          
+            
+              
+
+              Backstage Kubernetes
+              
+                Manage your services, not clusters
+              
+            
+
+            
+              
+              
+                Kubernetes made just for service owners
+              
+              
+                Backstage features the first Kubernetes monitoring tool designed
+                around the needs of service owners, not cluster admins
+              
+            
+
+            
+              
+              
+                Your service at a glance
+              
+              
+                Get all your service's deployments in one, aggregated view — no
+                more digging through cluster logs in a CLI, no more combing
+                through lists of services you don't own
+              
+            
+
+            
+              
+              Pick a cloud, any cloud
+              
+                Since Backstage uses the Kubernetes API, it's cloud agnostic —
+                so it works no matter which cloud provide or managed Kubernetes
+                service you use, and even works in multi-cloud orgs
+              
+            
+
+            
+              
+              Any K8s, one UI
+              
+                Now you don't have to switch dashboards when you move from local
+                testing to production, or from one cloud provider to another
+              
+            
+          
+        
+        
+          Learn more about the K8s plugin
+          
+            Read
+          
+        
+
         
           
             
diff --git a/microsite/sidebars.json b/microsite/sidebars.json
index b31f0ccdf3..3d6417cc65 100644
--- a/microsite/sidebars.json
+++ b/microsite/sidebars.json
@@ -36,6 +36,11 @@
     ],
     "CLI": ["cli/index", "cli/commands"],
     "Core Features": [
+      {
+        "type": "subcategory",
+        "label": "Kubernetes",
+        "ids": ["features/kubernetes/overview"]
+      },
       {
         "type": "subcategory",
         "label": "Software Catalog",
@@ -50,6 +55,7 @@
           "features/software-catalog/well-known-relations",
           "features/software-catalog/extending-the-model",
           "features/software-catalog/external-integrations",
+          "features/software-catalog/kubernetes-in-backstage",
           "features/software-catalog/software-catalog-api"
         ]
       },
diff --git a/microsite/static/animations/backstage-kubernetes-icon-1.gif b/microsite/static/animations/backstage-kubernetes-icon-1.gif
new file mode 100644
index 0000000000..a7a653a3a3
Binary files /dev/null and b/microsite/static/animations/backstage-kubernetes-icon-1.gif differ
diff --git a/microsite/yarn.lock b/microsite/yarn.lock
index 356585cf3e..898fd69ad2 100644
--- a/microsite/yarn.lock
+++ b/microsite/yarn.lock
@@ -9,55 +9,54 @@
   dependencies:
     "@babel/highlight" "^7.0.0"
 
-"@babel/code-frame@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a"
-  integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==
+"@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.11":
+  version "7.12.11"
+  resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f"
+  integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==
   dependencies:
     "@babel/highlight" "^7.10.4"
 
 "@babel/compat-data@^7.12.5", "@babel/compat-data@^7.12.7":
   version "7.12.7"
-  resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.12.7.tgz#9329b4782a7d6bbd7eef57e11addf91ee3ef1e41"
+  resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.12.7.tgz#9329b4782a7d6bbd7eef57e11addf91ee3ef1e41"
   integrity sha512-YaxPMGs/XIWtYqrdEOZOCPsVWfEoriXopnsz3/i7apYPXQ3698UFhS6dVT1KN5qOsWmVgw/FOrmQgpRaZayGsw==
 
 "@babel/core@^7.12.3":
-  version "7.12.9"
-  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.9.tgz#fd450c4ec10cdbb980e2928b7aa7a28484593fc8"
-  integrity sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ==
+  version "7.12.10"
+  resolved "https://registry.npmjs.org/@babel/core/-/core-7.12.10.tgz#b79a2e1b9f70ed3d84bbfb6d8c4ef825f606bccd"
+  integrity sha512-eTAlQKq65zHfkHZV0sIVODCPGVgoo1HdBlbSLi9CqOzuZanMv2ihzY+4paiKr1mH+XmYESMAmJ/dpZ68eN6d8w==
   dependencies:
     "@babel/code-frame" "^7.10.4"
-    "@babel/generator" "^7.12.5"
+    "@babel/generator" "^7.12.10"
     "@babel/helper-module-transforms" "^7.12.1"
     "@babel/helpers" "^7.12.5"
-    "@babel/parser" "^7.12.7"
+    "@babel/parser" "^7.12.10"
     "@babel/template" "^7.12.7"
-    "@babel/traverse" "^7.12.9"
-    "@babel/types" "^7.12.7"
+    "@babel/traverse" "^7.12.10"
+    "@babel/types" "^7.12.10"
     convert-source-map "^1.7.0"
     debug "^4.1.0"
     gensync "^1.0.0-beta.1"
     json5 "^2.1.2"
     lodash "^4.17.19"
-    resolve "^1.3.2"
     semver "^5.4.1"
     source-map "^0.5.0"
 
-"@babel/generator@^7.12.5":
-  version "7.12.5"
-  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.5.tgz#a2c50de5c8b6d708ab95be5e6053936c1884a4de"
-  integrity sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==
+"@babel/generator@^7.12.10", "@babel/generator@^7.12.11":
+  version "7.12.11"
+  resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz#98a7df7b8c358c9a37ab07a24056853016aba3af"
+  integrity sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==
   dependencies:
-    "@babel/types" "^7.12.5"
+    "@babel/types" "^7.12.11"
     jsesc "^2.5.1"
     source-map "^0.5.0"
 
-"@babel/helper-annotate-as-pure@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3"
-  integrity sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==
+"@babel/helper-annotate-as-pure@^7.10.4", "@babel/helper-annotate-as-pure@^7.12.10":
+  version "7.12.10"
+  resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz#54ab9b000e60a93644ce17b3f37d313aaf1d115d"
+  integrity sha512-XplmVbC1n+KY6jL8/fgLVXXUauDIB+lD5+GsQEh6F6GBF1dq1qy4DP4yXWzDKcoqXB3X58t61e85Fitoww4JVQ==
   dependencies:
-    "@babel/types" "^7.10.4"
+    "@babel/types" "^7.12.10"
 
 "@babel/helper-builder-binary-assignment-operator-visitor@^7.10.4":
   version "7.10.4"
@@ -67,26 +66,9 @@
     "@babel/helper-explode-assignable-expression" "^7.10.4"
     "@babel/types" "^7.10.4"
 
-"@babel/helper-builder-react-jsx-experimental@^7.12.4":
-  version "7.12.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.12.4.tgz#55fc1ead5242caa0ca2875dcb8eed6d311e50f48"
-  integrity sha512-AjEa0jrQqNk7eDQOo0pTfUOwQBMF+xVqrausQwT9/rTKy0g04ggFNaJpaE09IQMn9yExluigWMJcj0WC7bq+Og==
-  dependencies:
-    "@babel/helper-annotate-as-pure" "^7.10.4"
-    "@babel/helper-module-imports" "^7.12.1"
-    "@babel/types" "^7.12.1"
-
-"@babel/helper-builder-react-jsx@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.10.4.tgz#8095cddbff858e6fa9c326daee54a2f2732c1d5d"
-  integrity sha512-5nPcIZ7+KKDxT1427oBivl9V9YTal7qk0diccnh7RrcgrT/pGFOjgGw1dgryyx1GvHEpXVfoDF6Ak3rTiWh8Rg==
-  dependencies:
-    "@babel/helper-annotate-as-pure" "^7.10.4"
-    "@babel/types" "^7.10.4"
-
 "@babel/helper-compilation-targets@^7.12.5":
   version "7.12.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.5.tgz#cb470c76198db6a24e9dbc8987275631e5d29831"
+  resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.5.tgz#cb470c76198db6a24e9dbc8987275631e5d29831"
   integrity sha512-+qH6NrscMolUlzOYngSBMIOQpKUGPPsc61Bu5W10mg84LxZ7cmvnBHzARKbDoFxVvqqAbj6Tg6N7bSrWSPXMyw==
   dependencies:
     "@babel/compat-data" "^7.12.5"
@@ -96,7 +78,7 @@
 
 "@babel/helper-create-class-features-plugin@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz#3c45998f431edd4a9214c5f1d3ad1448a6137f6e"
+  resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz#3c45998f431edd4a9214c5f1d3ad1448a6137f6e"
   integrity sha512-hkL++rWeta/OVOBTRJc9a5Azh5mt5WgZUGAKMD8JM141YsE08K//bp1unBBieO6rUKkIPyUE0USQ30jAy3Sk1w==
   dependencies:
     "@babel/helper-function-name" "^7.10.4"
@@ -105,18 +87,9 @@
     "@babel/helper-replace-supers" "^7.12.1"
     "@babel/helper-split-export-declaration" "^7.10.4"
 
-"@babel/helper-create-regexp-features-plugin@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz#fdd60d88524659a0b6959c0579925e425714f3b8"
-  integrity sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g==
-  dependencies:
-    "@babel/helper-annotate-as-pure" "^7.10.4"
-    "@babel/helper-regex" "^7.10.4"
-    regexpu-core "^4.7.0"
-
 "@babel/helper-create-regexp-features-plugin@^7.12.1":
   version "7.12.7"
-  resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.7.tgz#2084172e95443fa0a09214ba1bb328f9aea1278f"
+  resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.7.tgz#2084172e95443fa0a09214ba1bb328f9aea1278f"
   integrity sha512-idnutvQPdpbduutvi3JVfEgcVIHooQnhvhx0Nk9isOINOIGYkZea1Pk2JlJRiUnMefrlvr0vkByATBY/mB4vjQ==
   dependencies:
     "@babel/helper-annotate-as-pure" "^7.10.4"
@@ -132,27 +105,27 @@
     lodash "^4.17.19"
 
 "@babel/helper-explode-assignable-expression@^7.10.4":
-  version "7.11.4"
-  resolved "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.11.4.tgz#2d8e3470252cc17aba917ede7803d4a7a276a41b"
-  integrity sha512-ux9hm3zR4WV1Y3xXxXkdG/0gxF9nvI0YVmKVhvK9AfMoaQkemL3sJpXw+Xbz65azo8qJiEz2XVDUpK3KYhH3ZQ==
+  version "7.12.1"
+  resolved "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.12.1.tgz#8006a466695c4ad86a2a5f2fb15b5f2c31ad5633"
+  integrity sha512-dmUwH8XmlrUpVqgtZ737tK88v07l840z9j3OEhCLwKTkjlvKpfqXVIZ0wpK3aeOxspwGrf/5AP5qLx4rO3w5rA==
   dependencies:
-    "@babel/types" "^7.10.4"
+    "@babel/types" "^7.12.1"
 
-"@babel/helper-function-name@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a"
-  integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==
+"@babel/helper-function-name@^7.10.4", "@babel/helper-function-name@^7.12.11":
+  version "7.12.11"
+  resolved "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz#1fd7738aee5dcf53c3ecff24f1da9c511ec47b42"
+  integrity sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA==
   dependencies:
-    "@babel/helper-get-function-arity" "^7.10.4"
-    "@babel/template" "^7.10.4"
-    "@babel/types" "^7.10.4"
+    "@babel/helper-get-function-arity" "^7.12.10"
+    "@babel/template" "^7.12.7"
+    "@babel/types" "^7.12.11"
 
-"@babel/helper-get-function-arity@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2"
-  integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==
+"@babel/helper-get-function-arity@^7.12.10":
+  version "7.12.10"
+  resolved "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz#b158817a3165b5faa2047825dfa61970ddcc16cf"
+  integrity sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==
   dependencies:
-    "@babel/types" "^7.10.4"
+    "@babel/types" "^7.12.10"
 
 "@babel/helper-hoist-variables@^7.10.4":
   version "7.10.4"
@@ -161,23 +134,23 @@
   dependencies:
     "@babel/types" "^7.10.4"
 
-"@babel/helper-member-expression-to-functions@^7.12.1":
+"@babel/helper-member-expression-to-functions@^7.12.1", "@babel/helper-member-expression-to-functions@^7.12.7":
   version "7.12.7"
-  resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz#aa77bd0396ec8114e5e30787efa78599d874a855"
+  resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz#aa77bd0396ec8114e5e30787efa78599d874a855"
   integrity sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==
   dependencies:
     "@babel/types" "^7.12.7"
 
 "@babel/helper-module-imports@^7.12.1", "@babel/helper-module-imports@^7.12.5":
   version "7.12.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz#1bfc0229f794988f76ed0a4d4e90860850b54dfb"
+  resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz#1bfc0229f794988f76ed0a4d4e90860850b54dfb"
   integrity sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA==
   dependencies:
     "@babel/types" "^7.12.5"
 
 "@babel/helper-module-transforms@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz#7954fec71f5b32c48e4b303b437c34453fd7247c"
+  resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz#7954fec71f5b32c48e4b303b437c34453fd7247c"
   integrity sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w==
   dependencies:
     "@babel/helper-module-imports" "^7.12.1"
@@ -190,28 +163,21 @@
     "@babel/types" "^7.12.1"
     lodash "^4.17.19"
 
-"@babel/helper-optimise-call-expression@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673"
-  integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==
+"@babel/helper-optimise-call-expression@^7.10.4", "@babel/helper-optimise-call-expression@^7.12.10":
+  version "7.12.10"
+  resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz#94ca4e306ee11a7dd6e9f42823e2ac6b49881e2d"
+  integrity sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==
   dependencies:
-    "@babel/types" "^7.10.4"
+    "@babel/types" "^7.12.10"
 
 "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
   version "7.10.4"
   resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375"
   integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==
 
-"@babel/helper-regex@^7.10.4":
-  version "7.10.5"
-  resolved "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.10.5.tgz#32dfbb79899073c415557053a19bd055aae50ae0"
-  integrity sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg==
-  dependencies:
-    lodash "^4.17.19"
-
 "@babel/helper-remap-async-to-generator@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz#8c4dbbf916314f6047dc05e6a2217074238347fd"
+  resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz#8c4dbbf916314f6047dc05e6a2217074238347fd"
   integrity sha512-9d0KQCRM8clMPcDwo8SevNs+/9a8yWVVmaE80FGJcEP8N1qToREmWEGnBn8BUlJhYRFz6fqxeRL1sl5Ogsed7A==
   dependencies:
     "@babel/helper-annotate-as-pure" "^7.10.4"
@@ -219,50 +185,50 @@
     "@babel/types" "^7.12.1"
 
 "@babel/helper-replace-supers@^7.12.1":
-  version "7.12.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz#f009a17543bbbbce16b06206ae73b63d3fca68d9"
-  integrity sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA==
+  version "7.12.11"
+  resolved "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz#ea511658fc66c7908f923106dd88e08d1997d60d"
+  integrity sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==
   dependencies:
-    "@babel/helper-member-expression-to-functions" "^7.12.1"
-    "@babel/helper-optimise-call-expression" "^7.10.4"
-    "@babel/traverse" "^7.12.5"
-    "@babel/types" "^7.12.5"
+    "@babel/helper-member-expression-to-functions" "^7.12.7"
+    "@babel/helper-optimise-call-expression" "^7.12.10"
+    "@babel/traverse" "^7.12.10"
+    "@babel/types" "^7.12.11"
 
 "@babel/helper-simple-access@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz#32427e5aa61547d38eb1e6eaf5fd1426fdad9136"
+  resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz#32427e5aa61547d38eb1e6eaf5fd1426fdad9136"
   integrity sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA==
   dependencies:
     "@babel/types" "^7.12.1"
 
 "@babel/helper-skip-transparent-expression-wrappers@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz#462dc63a7e435ade8468385c63d2b84cce4b3cbf"
+  resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz#462dc63a7e435ade8468385c63d2b84cce4b3cbf"
   integrity sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA==
   dependencies:
     "@babel/types" "^7.12.1"
 
-"@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0":
-  version "7.11.0"
-  resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f"
-  integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==
+"@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0", "@babel/helper-split-export-declaration@^7.12.11":
+  version "7.12.11"
+  resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz#1b4cc424458643c47d37022223da33d76ea4603a"
+  integrity sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g==
   dependencies:
-    "@babel/types" "^7.11.0"
+    "@babel/types" "^7.12.11"
 
-"@babel/helper-validator-identifier@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2"
-  integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==
+"@babel/helper-validator-identifier@^7.10.4", "@babel/helper-validator-identifier@^7.12.11":
+  version "7.12.11"
+  resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
+  integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==
 
-"@babel/helper-validator-option@^7.12.1":
-  version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.1.tgz#175567380c3e77d60ff98a54bb015fe78f2178d9"
-  integrity sha512-YpJabsXlJVWP0USHjnC/AQDTLlZERbON577YUVO/wLpqyj6HAtVYnWaQaN0iUN+1/tWn3c+uKKXjRut5115Y2A==
+"@babel/helper-validator-option@^7.12.1", "@babel/helper-validator-option@^7.12.11":
+  version "7.12.11"
+  resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.11.tgz#d66cb8b7a3e7fe4c6962b32020a131ecf0847f4f"
+  integrity sha512-TBFCyj939mFSdeX7U7DDj32WtzYY7fDcalgq8v3fBZMNOJQNn7nOYzMaUCiPxPYfCup69mtIpqlKgMZLvQ8Xhw==
 
 "@babel/helper-wrap-function@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz#8a6f701eab0ff39f765b5a1cfef409990e624b87"
-  integrity sha512-6py45WvEF0MhiLrdxtRjKjufwLL1/ob2qDJgg5JgNdojBAZSAKnAjkyOCNug6n+OBl4VW76XjvgSFTdaMcW0Ug==
+  version "7.12.3"
+  resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.12.3.tgz#3332339fc4d1fbbf1c27d7958c27d34708e990d9"
+  integrity sha512-Cvb8IuJDln3rs6tzjW3Y8UeelAOdnpB8xtQ4sme2MSZ9wOxrbThporC0y/EtE16VAtoyEfLM404Xr1e0OOp+ow==
   dependencies:
     "@babel/helper-function-name" "^7.10.4"
     "@babel/template" "^7.10.4"
@@ -271,7 +237,7 @@
 
 "@babel/helpers@^7.12.5":
   version "7.12.5"
-  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.12.5.tgz#1a1ba4a768d9b58310eda516c449913fe647116e"
+  resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.5.tgz#1a1ba4a768d9b58310eda516c449913fe647116e"
   integrity sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA==
   dependencies:
     "@babel/template" "^7.10.4"
@@ -287,20 +253,15 @@
     chalk "^2.0.0"
     js-tokens "^4.0.0"
 
-"@babel/parser@^7.10.4":
-  version "7.11.5"
-  resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037"
-  integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==
-
-"@babel/parser@^7.12.7":
-  version "7.12.7"
-  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.7.tgz#fee7b39fe809d0e73e5b25eecaf5780ef3d73056"
-  integrity sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==
+"@babel/parser@^7.12.10", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7":
+  version "7.12.11"
+  resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz#9ce3595bcd74bc5c466905e86c535b8b25011e79"
+  integrity sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==
 
 "@babel/plugin-proposal-async-generator-functions@^7.12.1":
-  version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.1.tgz#dc6c1170e27d8aca99ff65f4925bd06b1c90550e"
-  integrity sha512-d+/o30tJxFxrA1lhzJqiUcEJdI6jKlNregCv5bASeGf2Q4MXmnwH7viDo7nhx1/ohf09oaH8j1GVYG/e3Yqk6A==
+  version "7.12.12"
+  resolved "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.12.tgz#04b8f24fd4532008ab4e79f788468fd5a8476566"
+  integrity sha512-nrz9y0a4xmUrRq51bYkWJIO5SBZyG2ys2qinHsN0zHDHVsUaModrkpyWWWXfGqYQmOL3x9sQIcTNN/pBGpo09A==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
     "@babel/helper-remap-async-to-generator" "^7.12.1"
@@ -308,7 +269,7 @@
 
 "@babel/plugin-proposal-class-properties@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.1.tgz#a082ff541f2a29a4821065b8add9346c0c16e5de"
+  resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.1.tgz#a082ff541f2a29a4821065b8add9346c0c16e5de"
   integrity sha512-cKp3dlQsFsEs5CWKnN7BnSHOd0EOW8EKpEjkoz1pO2E5KzIDNV9Ros1b0CnmbVgAGXJubOYVBOGCT1OmJwOI7w==
   dependencies:
     "@babel/helper-create-class-features-plugin" "^7.12.1"
@@ -316,7 +277,7 @@
 
 "@babel/plugin-proposal-dynamic-import@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.1.tgz#43eb5c2a3487ecd98c5c8ea8b5fdb69a2749b2dc"
+  resolved "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.1.tgz#43eb5c2a3487ecd98c5c8ea8b5fdb69a2749b2dc"
   integrity sha512-a4rhUSZFuq5W8/OO8H7BL5zspjnc1FLd9hlOxIK/f7qG4a0qsqk8uvF/ywgBA8/OmjsapjpvaEOYItfGG1qIvQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
@@ -324,7 +285,7 @@
 
 "@babel/plugin-proposal-export-namespace-from@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.12.1.tgz#8b9b8f376b2d88f5dd774e4d24a5cc2e3679b6d4"
+  resolved "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.12.1.tgz#8b9b8f376b2d88f5dd774e4d24a5cc2e3679b6d4"
   integrity sha512-6CThGf0irEkzujYS5LQcjBx8j/4aQGiVv7J9+2f7pGfxqyKh3WnmVJYW3hdrQjyksErMGBPQrCnHfOtna+WLbw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
@@ -332,7 +293,7 @@
 
 "@babel/plugin-proposal-json-strings@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.12.1.tgz#d45423b517714eedd5621a9dfdc03fa9f4eb241c"
+  resolved "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.12.1.tgz#d45423b517714eedd5621a9dfdc03fa9f4eb241c"
   integrity sha512-GoLDUi6U9ZLzlSda2Df++VSqDJg3CG+dR0+iWsv6XRw1rEq+zwt4DirM9yrxW6XWaTpmai1cWJLMfM8qQJf+yw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
@@ -340,7 +301,7 @@
 
 "@babel/plugin-proposal-logical-assignment-operators@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.12.1.tgz#f2c490d36e1b3c9659241034a5d2cd50263a2751"
+  resolved "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.12.1.tgz#f2c490d36e1b3c9659241034a5d2cd50263a2751"
   integrity sha512-k8ZmVv0JU+4gcUGeCDZOGd0lCIamU/sMtIiX3UWnUc5yzgq6YUGyEolNYD+MLYKfSzgECPcqetVcJP9Afe/aCA==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
@@ -348,7 +309,7 @@
 
 "@babel/plugin-proposal-nullish-coalescing-operator@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.12.1.tgz#3ed4fff31c015e7f3f1467f190dbe545cd7b046c"
+  resolved "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.12.1.tgz#3ed4fff31c015e7f3f1467f190dbe545cd7b046c"
   integrity sha512-nZY0ESiaQDI1y96+jk6VxMOaL4LPo/QDHBqL+SF3/vl6dHkTwHlOI8L4ZwuRBHgakRBw5zsVylel7QPbbGuYgg==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
@@ -356,7 +317,7 @@
 
 "@babel/plugin-proposal-numeric-separator@^7.12.7":
   version "7.12.7"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.7.tgz#8bf253de8139099fea193b297d23a9d406ef056b"
+  resolved "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.7.tgz#8bf253de8139099fea193b297d23a9d406ef056b"
   integrity sha512-8c+uy0qmnRTeukiGsjLGy6uVs/TFjJchGXUeBqlG4VWYOdJWkhhVPdQ3uHwbmalfJwv2JsV0qffXP4asRfL2SQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
@@ -364,7 +325,7 @@
 
 "@babel/plugin-proposal-object-rest-spread@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz#def9bd03cea0f9b72283dac0ec22d289c7691069"
+  resolved "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz#def9bd03cea0f9b72283dac0ec22d289c7691069"
   integrity sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
@@ -373,7 +334,7 @@
 
 "@babel/plugin-proposal-optional-catch-binding@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.12.1.tgz#ccc2421af64d3aae50b558a71cede929a5ab2942"
+  resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.12.1.tgz#ccc2421af64d3aae50b558a71cede929a5ab2942"
   integrity sha512-hFvIjgprh9mMw5v42sJWLI1lzU5L2sznP805zeT6rySVRA0Y18StRhDqhSxlap0oVgItRsB6WSROp4YnJTJz0g==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
@@ -381,7 +342,7 @@
 
 "@babel/plugin-proposal-optional-chaining@^7.12.7":
   version "7.12.7"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.7.tgz#e02f0ea1b5dc59d401ec16fb824679f683d3303c"
+  resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.7.tgz#e02f0ea1b5dc59d401ec16fb824679f683d3303c"
   integrity sha512-4ovylXZ0PWmwoOvhU2vhnzVNnm88/Sm9nx7V8BPgMvAzn5zDou3/Awy0EjglyubVHasJj+XCEkr/r1X3P5elCA==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
@@ -390,28 +351,20 @@
 
 "@babel/plugin-proposal-private-methods@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.12.1.tgz#86814f6e7a21374c980c10d38b4493e703f4a389"
+  resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.12.1.tgz#86814f6e7a21374c980c10d38b4493e703f4a389"
   integrity sha512-mwZ1phvH7/NHK6Kf8LP7MYDogGV+DKB1mryFOEwx5EBNQrosvIczzZFTUmWaeujd5xT6G1ELYWUz3CutMhjE1w==
   dependencies:
     "@babel/helper-create-class-features-plugin" "^7.12.1"
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-proposal-unicode-property-regex@^7.12.1":
+"@babel/plugin-proposal-unicode-property-regex@^7.12.1", "@babel/plugin-proposal-unicode-property-regex@^7.4.4":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.1.tgz#2a183958d417765b9eae334f47758e5d6a82e072"
+  resolved "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.1.tgz#2a183958d417765b9eae334f47758e5d6a82e072"
   integrity sha512-MYq+l+PvHuw/rKUz1at/vb6nCnQ2gmJBNaM62z0OgH7B2W1D9pvkpYtlti9bGtizNIU1K3zm4bZF9F91efVY0w==
   dependencies:
     "@babel/helper-create-regexp-features-plugin" "^7.12.1"
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-proposal-unicode-property-regex@^7.4.4":
-  version "7.10.4"
-  resolved "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz#4483cda53041ce3413b7fe2f00022665ddfaa75d"
-  integrity sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA==
-  dependencies:
-    "@babel/helper-create-regexp-features-plugin" "^7.10.4"
-    "@babel/helper-plugin-utils" "^7.10.4"
-
 "@babel/plugin-syntax-async-generators@^7.8.0":
   version "7.8.4"
   resolved "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
@@ -421,7 +374,7 @@
 
 "@babel/plugin-syntax-class-properties@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz#bcb297c5366e79bebadef509549cd93b04f19978"
+  resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz#bcb297c5366e79bebadef509549cd93b04f19978"
   integrity sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
@@ -449,7 +402,7 @@
 
 "@babel/plugin-syntax-jsx@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz#9d9d357cc818aa7ae7935917c1257f67677a0926"
+  resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz#9d9d357cc818aa7ae7935917c1257f67677a0926"
   integrity sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
@@ -498,21 +451,21 @@
 
 "@babel/plugin-syntax-top-level-await@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz#dd6c0b357ac1bb142d98537450a319625d13d2a0"
+  resolved "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz#dd6c0b357ac1bb142d98537450a319625d13d2a0"
   integrity sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
 "@babel/plugin-transform-arrow-functions@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.12.1.tgz#8083ffc86ac8e777fbe24b5967c4b2521f3cb2b3"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.12.1.tgz#8083ffc86ac8e777fbe24b5967c4b2521f3cb2b3"
   integrity sha512-5QB50qyN44fzzz4/qxDPQMBCTHgxg3n0xRBLJUmBlLoU/sFvxVWGZF/ZUfMVDQuJUKXaBhbupxIzIfZ6Fwk/0A==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
 "@babel/plugin-transform-async-to-generator@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.12.1.tgz#3849a49cc2a22e9743cbd6b52926d30337229af1"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.12.1.tgz#3849a49cc2a22e9743cbd6b52926d30337229af1"
   integrity sha512-SDtqoEcarK1DFlRJ1hHRY5HvJUj5kX4qmtpMAm2QnhOlyuMC4TMdCRgW6WXpv93rZeYNeLP22y8Aq2dbcDRM1A==
   dependencies:
     "@babel/helper-module-imports" "^7.12.1"
@@ -521,21 +474,21 @@
 
 "@babel/plugin-transform-block-scoped-functions@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.1.tgz#f2a1a365bde2b7112e0a6ded9067fdd7c07905d9"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.1.tgz#f2a1a365bde2b7112e0a6ded9067fdd7c07905d9"
   integrity sha512-5OpxfuYnSgPalRpo8EWGPzIYf0lHBWORCkj5M0oLBwHdlux9Ri36QqGW3/LR13RSVOAoUUMzoPI/jpE4ABcHoA==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-block-scoping@^7.12.1":
-  version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.1.tgz#f0ee727874b42a208a48a586b84c3d222c2bbef1"
-  integrity sha512-zJyAC9sZdE60r1nVQHblcfCj29Dh2Y0DOvlMkcqSo0ckqjiCwNiUezUKw+RjOCwGfpLRwnAeQ2XlLpsnGkvv9w==
+"@babel/plugin-transform-block-scoping@^7.12.11":
+  version "7.12.12"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.12.tgz#d93a567a152c22aea3b1929bb118d1d0a175cdca"
+  integrity sha512-VOEPQ/ExOVqbukuP7BYJtI5ZxxsmegTwzZ04j1aF0dkSypGo9XpDHuOrABsJu+ie+penpSJheDJ11x1BEZNiyQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
 "@babel/plugin-transform-classes@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.12.1.tgz#65e650fcaddd3d88ddce67c0f834a3d436a32db6"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.12.1.tgz#65e650fcaddd3d88ddce67c0f834a3d436a32db6"
   integrity sha512-/74xkA7bVdzQTBeSUhLLJgYIcxw/dpEpCdRDiHgPJ3Mv6uC11UhjpOhl72CgqbBCmt1qtssCyB2xnJm1+PFjog==
   dependencies:
     "@babel/helper-annotate-as-pure" "^7.10.4"
@@ -549,44 +502,36 @@
 
 "@babel/plugin-transform-computed-properties@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.12.1.tgz#d68cf6c9b7f838a8a4144badbe97541ea0904852"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.12.1.tgz#d68cf6c9b7f838a8a4144badbe97541ea0904852"
   integrity sha512-vVUOYpPWB7BkgUWPo4C44mUQHpTZXakEqFjbv8rQMg7TC6S6ZhGZ3otQcRH6u7+adSlE5i0sp63eMC/XGffrzg==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
 "@babel/plugin-transform-destructuring@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.12.1.tgz#b9a570fe0d0a8d460116413cb4f97e8e08b2f847"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.12.1.tgz#b9a570fe0d0a8d460116413cb4f97e8e08b2f847"
   integrity sha512-fRMYFKuzi/rSiYb2uRLiUENJOKq4Gnl+6qOv5f8z0TZXg3llUwUhsNNwrwaT/6dUhJTzNpBr+CUvEWBtfNY1cw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-dotall-regex@^7.12.1":
+"@babel/plugin-transform-dotall-regex@^7.12.1", "@babel/plugin-transform-dotall-regex@^7.4.4":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.1.tgz#a1d16c14862817b6409c0a678d6f9373ca9cd975"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.1.tgz#a1d16c14862817b6409c0a678d6f9373ca9cd975"
   integrity sha512-B2pXeRKoLszfEW7J4Hg9LoFaWEbr/kzo3teWHmtFCszjRNa/b40f9mfeqZsIDLLt/FjwQ6pz/Gdlwy85xNckBA==
   dependencies:
     "@babel/helper-create-regexp-features-plugin" "^7.12.1"
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-dotall-regex@^7.4.4":
-  version "7.10.4"
-  resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz#469c2062105c1eb6a040eaf4fac4b488078395ee"
-  integrity sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA==
-  dependencies:
-    "@babel/helper-create-regexp-features-plugin" "^7.10.4"
-    "@babel/helper-plugin-utils" "^7.10.4"
-
 "@babel/plugin-transform-duplicate-keys@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.1.tgz#745661baba295ac06e686822797a69fbaa2ca228"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.1.tgz#745661baba295ac06e686822797a69fbaa2ca228"
   integrity sha512-iRght0T0HztAb/CazveUpUQrZY+aGKKaWXMJ4uf9YJtqxSUe09j3wteztCUDRHs+SRAL7yMuFqUsLoAKKzgXjw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
 "@babel/plugin-transform-exponentiation-operator@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.1.tgz#b0f2ed356ba1be1428ecaf128ff8a24f02830ae0"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.1.tgz#b0f2ed356ba1be1428ecaf128ff8a24f02830ae0"
   integrity sha512-7tqwy2bv48q+c1EHbXK0Zx3KXd2RVQp6OC7PbwFNt/dPTAV3Lu5sWtWuAj8owr5wqtWnqHfl2/mJlUmqkChKug==
   dependencies:
     "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.4"
@@ -594,14 +539,14 @@
 
 "@babel/plugin-transform-for-of@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.12.1.tgz#07640f28867ed16f9511c99c888291f560921cfa"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.12.1.tgz#07640f28867ed16f9511c99c888291f560921cfa"
   integrity sha512-Zaeq10naAsuHo7heQvyV0ptj4dlZJwZgNAtBYBnu5nNKJoW62m0zKcIEyVECrUKErkUkg6ajMy4ZfnVZciSBhg==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
 "@babel/plugin-transform-function-name@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.1.tgz#2ec76258c70fe08c6d7da154003a480620eba667"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.1.tgz#2ec76258c70fe08c6d7da154003a480620eba667"
   integrity sha512-JF3UgJUILoFrFMEnOJLJkRHSk6LUSXLmEFsA23aR2O5CSLUxbeUX1IZ1YQ7Sn0aXb601Ncwjx73a+FVqgcljVw==
   dependencies:
     "@babel/helper-function-name" "^7.10.4"
@@ -609,21 +554,21 @@
 
 "@babel/plugin-transform-literals@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.1.tgz#d73b803a26b37017ddf9d3bb8f4dc58bfb806f57"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.1.tgz#d73b803a26b37017ddf9d3bb8f4dc58bfb806f57"
   integrity sha512-+PxVGA+2Ag6uGgL0A5f+9rklOnnMccwEBzwYFL3EUaKuiyVnUipyXncFcfjSkbimLrODoqki1U9XxZzTvfN7IQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
 "@babel/plugin-transform-member-expression-literals@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.1.tgz#496038602daf1514a64d43d8e17cbb2755e0c3ad"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.1.tgz#496038602daf1514a64d43d8e17cbb2755e0c3ad"
   integrity sha512-1sxePl6z9ad0gFMB9KqmYofk34flq62aqMt9NqliS/7hPEpURUCMbyHXrMPlo282iY7nAvUB1aQd5mg79UD9Jg==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
 "@babel/plugin-transform-modules-amd@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.12.1.tgz#3154300b026185666eebb0c0ed7f8415fefcf6f9"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.12.1.tgz#3154300b026185666eebb0c0ed7f8415fefcf6f9"
   integrity sha512-tDW8hMkzad5oDtzsB70HIQQRBiTKrhfgwC/KkJeGsaNFTdWhKNt/BiE8c5yj19XiGyrxpbkOfH87qkNg1YGlOQ==
   dependencies:
     "@babel/helper-module-transforms" "^7.12.1"
@@ -632,7 +577,7 @@
 
 "@babel/plugin-transform-modules-commonjs@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.12.1.tgz#fa403124542636c786cf9b460a0ffbb48a86e648"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.12.1.tgz#fa403124542636c786cf9b460a0ffbb48a86e648"
   integrity sha512-dY789wq6l0uLY8py9c1B48V8mVL5gZh/+PQ5ZPrylPYsnAvnEMjqsUXkuoDVPeVK+0VyGar+D08107LzDQ6pag==
   dependencies:
     "@babel/helper-module-transforms" "^7.12.1"
@@ -642,7 +587,7 @@
 
 "@babel/plugin-transform-modules-systemjs@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.1.tgz#663fea620d593c93f214a464cd399bf6dc683086"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.1.tgz#663fea620d593c93f214a464cd399bf6dc683086"
   integrity sha512-Hn7cVvOavVh8yvW6fLwveFqSnd7rbQN3zJvoPNyNaQSvgfKmDBO9U1YL9+PCXGRlZD9tNdWTy5ACKqMuzyn32Q==
   dependencies:
     "@babel/helper-hoist-variables" "^7.10.4"
@@ -653,7 +598,7 @@
 
 "@babel/plugin-transform-modules-umd@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.12.1.tgz#eb5a218d6b1c68f3d6217b8fa2cc82fec6547902"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.12.1.tgz#eb5a218d6b1c68f3d6217b8fa2cc82fec6547902"
   integrity sha512-aEIubCS0KHKM0zUos5fIoQm+AZUMt1ZvMpqz0/H5qAQ7vWylr9+PLYurT+Ic7ID/bKLd4q8hDovaG3Zch2uz5Q==
   dependencies:
     "@babel/helper-module-transforms" "^7.12.1"
@@ -661,21 +606,21 @@
 
 "@babel/plugin-transform-named-capturing-groups-regex@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.1.tgz#b407f5c96be0d9f5f88467497fa82b30ac3e8753"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.1.tgz#b407f5c96be0d9f5f88467497fa82b30ac3e8753"
   integrity sha512-tB43uQ62RHcoDp9v2Nsf+dSM8sbNodbEicbQNA53zHz8pWUhsgHSJCGpt7daXxRydjb0KnfmB+ChXOv3oADp1Q==
   dependencies:
     "@babel/helper-create-regexp-features-plugin" "^7.12.1"
 
 "@babel/plugin-transform-new-target@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.1.tgz#80073f02ee1bb2d365c3416490e085c95759dec0"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.1.tgz#80073f02ee1bb2d365c3416490e085c95759dec0"
   integrity sha512-+eW/VLcUL5L9IvJH7rT1sT0CzkdUTvPrXC2PXTn/7z7tXLBuKvezYbGdxD5WMRoyvyaujOq2fWoKl869heKjhw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
 "@babel/plugin-transform-object-super@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.1.tgz#4ea08696b8d2e65841d0c7706482b048bed1066e"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.1.tgz#4ea08696b8d2e65841d0c7706482b048bed1066e"
   integrity sha512-AvypiGJH9hsquNUn+RXVcBdeE3KHPZexWRdimhuV59cSoOt5kFBmqlByorAeUlGG2CJWd0U+4ZtNKga/TB0cAw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
@@ -683,61 +628,46 @@
 
 "@babel/plugin-transform-parameters@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.12.1.tgz#d2e963b038771650c922eff593799c96d853255d"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.12.1.tgz#d2e963b038771650c922eff593799c96d853255d"
   integrity sha512-xq9C5EQhdPK23ZeCdMxl8bbRnAgHFrw5EOC3KJUsSylZqdkCaFEXxGSBuTSObOpiiHHNyb82es8M1QYgfQGfNg==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
 "@babel/plugin-transform-property-literals@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.1.tgz#41bc81200d730abb4456ab8b3fbd5537b59adecd"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.1.tgz#41bc81200d730abb4456ab8b3fbd5537b59adecd"
   integrity sha512-6MTCR/mZ1MQS+AwZLplX4cEySjCpnIF26ToWo942nqn8hXSm7McaHQNeGx/pt7suI1TWOWMfa/NgBhiqSnX0cQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
 "@babel/plugin-transform-react-display-name@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.12.1.tgz#1cbcd0c3b1d6648c55374a22fc9b6b7e5341c00d"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.12.1.tgz#1cbcd0c3b1d6648c55374a22fc9b6b7e5341c00d"
   integrity sha512-cAzB+UzBIrekfYxyLlFqf/OagTvHLcVBb5vpouzkYkBclRPraiygVnafvAoipErZLI8ANv8Ecn6E/m5qPXD26w==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
 "@babel/plugin-transform-react-jsx-development@^7.12.7":
-  version "7.12.7"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.7.tgz#4c2a647de79c7e2b16bfe4540677ba3121e82a08"
-  integrity sha512-Rs3ETtMtR3VLXFeYRChle5SsP/P9Jp/6dsewBQfokDSzKJThlsuFcnzLTDRALiUmTC48ej19YD9uN1mupEeEDg==
+  version "7.12.12"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.12.tgz#bccca33108fe99d95d7f9e82046bfe762e71f4e7"
+  integrity sha512-i1AxnKxHeMxUaWVXQOSIco4tvVvvCxMSfeBMnMM06mpaJt3g+MpxYQQrDfojUQldP1xxraPSJYSMEljoWM/dCg==
   dependencies:
-    "@babel/helper-builder-react-jsx-experimental" "^7.12.4"
-    "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/plugin-syntax-jsx" "^7.12.1"
-
-"@babel/plugin-transform-react-jsx-self@^7.12.1":
-  version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.12.1.tgz#ef43cbca2a14f1bd17807dbe4376ff89d714cf28"
-  integrity sha512-FbpL0ieNWiiBB5tCldX17EtXgmzeEZjFrix72rQYeq9X6nUK38HCaxexzVQrZWXanxKJPKVVIU37gFjEQYkPkA==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.10.4"
-
-"@babel/plugin-transform-react-jsx-source@^7.12.1":
-  version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.12.1.tgz#d07de6863f468da0809edcf79a1aa8ce2a82a26b"
-  integrity sha512-keQ5kBfjJNRc6zZN1/nVHCd6LLIHq4aUKcVnvE/2l+ZZROSbqoiGFRtT5t3Is89XJxBQaP7NLZX2jgGHdZvvFQ==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.10.4"
-
-"@babel/plugin-transform-react-jsx@^7.12.7":
-  version "7.12.7"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.7.tgz#8b14d45f6eccd41b7f924bcb65c021e9f0a06f7f"
-  integrity sha512-YFlTi6MEsclFAPIDNZYiCRbneg1MFGao9pPG9uD5htwE0vDbPaMUMeYd6itWjw7K4kro4UbdQf3ljmFl9y48dQ==
-  dependencies:
-    "@babel/helper-builder-react-jsx" "^7.10.4"
-    "@babel/helper-builder-react-jsx-experimental" "^7.12.4"
+    "@babel/plugin-transform-react-jsx" "^7.12.12"
+
+"@babel/plugin-transform-react-jsx@^7.12.10", "@babel/plugin-transform-react-jsx@^7.12.12":
+  version "7.12.12"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.12.tgz#b0da51ffe5f34b9a900e9f1f5fb814f9e512d25e"
+  integrity sha512-JDWGuzGNWscYcq8oJVCtSE61a5+XAOos+V0HrxnDieUus4UMnBEosDnY1VJqU5iZ4pA04QY7l0+JvHL1hZEfsw==
+  dependencies:
+    "@babel/helper-annotate-as-pure" "^7.12.10"
+    "@babel/helper-module-imports" "^7.12.5"
     "@babel/helper-plugin-utils" "^7.10.4"
     "@babel/plugin-syntax-jsx" "^7.12.1"
+    "@babel/types" "^7.12.12"
 
 "@babel/plugin-transform-react-pure-annotations@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.12.1.tgz#05d46f0ab4d1339ac59adf20a1462c91b37a1a42"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.12.1.tgz#05d46f0ab4d1339ac59adf20a1462c91b37a1a42"
   integrity sha512-RqeaHiwZtphSIUZ5I85PEH19LOSzxfuEazoY7/pWASCAIBuATQzpSVD+eT6MebeeZT2F4eSL0u4vw6n4Nm0Mjg==
   dependencies:
     "@babel/helper-annotate-as-pure" "^7.10.4"
@@ -745,28 +675,28 @@
 
 "@babel/plugin-transform-regenerator@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.1.tgz#5f0a28d842f6462281f06a964e88ba8d7ab49753"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.1.tgz#5f0a28d842f6462281f06a964e88ba8d7ab49753"
   integrity sha512-gYrHqs5itw6i4PflFX3OdBPMQdPbF4bj2REIUxlMRUFk0/ZOAIpDFuViuxPjUL7YC8UPnf+XG7/utJvqXdPKng==
   dependencies:
     regenerator-transform "^0.14.2"
 
 "@babel/plugin-transform-reserved-words@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.12.1.tgz#6fdfc8cc7edcc42b36a7c12188c6787c873adcd8"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.12.1.tgz#6fdfc8cc7edcc42b36a7c12188c6787c873adcd8"
   integrity sha512-pOnUfhyPKvZpVyBHhSBoX8vfA09b7r00Pmm1sH+29ae2hMTKVmSp4Ztsr8KBKjLjx17H0eJqaRC3bR2iThM54A==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
 "@babel/plugin-transform-shorthand-properties@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.1.tgz#0bf9cac5550fce0cfdf043420f661d645fdc75e3"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.1.tgz#0bf9cac5550fce0cfdf043420f661d645fdc75e3"
   integrity sha512-GFZS3c/MhX1OusqB1MZ1ct2xRzX5ppQh2JU1h2Pnfk88HtFTM+TWQqJNfwkmxtPQtb/s1tk87oENfXJlx7rSDw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
 "@babel/plugin-transform-spread@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.12.1.tgz#527f9f311be4ec7fdc2b79bb89f7bf884b3e1e1e"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.12.1.tgz#527f9f311be4ec7fdc2b79bb89f7bf884b3e1e1e"
   integrity sha512-vuLp8CP0BE18zVYjsEBZ5xoCecMK6LBMMxYzJnh01rxQRvhNhH1csMMmBfNo5tGpGO+NhdSNW2mzIvBu3K1fng==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
@@ -774,35 +704,35 @@
 
 "@babel/plugin-transform-sticky-regex@^7.12.7":
   version "7.12.7"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.7.tgz#560224613ab23987453948ed21d0b0b193fa7fad"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.7.tgz#560224613ab23987453948ed21d0b0b193fa7fad"
   integrity sha512-VEiqZL5N/QvDbdjfYQBhruN0HYjSPjC4XkeqW4ny/jNtH9gcbgaqBIXYEZCNnESMAGs0/K/R7oFGMhOyu/eIxg==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
 "@babel/plugin-transform-template-literals@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.12.1.tgz#b43ece6ed9a79c0c71119f576d299ef09d942843"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.12.1.tgz#b43ece6ed9a79c0c71119f576d299ef09d942843"
   integrity sha512-b4Zx3KHi+taXB1dVRBhVJtEPi9h1THCeKmae2qP0YdUHIFhVjtpqqNfxeVAa1xeHVhAy4SbHxEwx5cltAu5apw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-typeof-symbol@^7.12.1":
-  version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.1.tgz#9ca6be343d42512fbc2e68236a82ae64bc7af78a"
-  integrity sha512-EPGgpGy+O5Kg5pJFNDKuxt9RdmTgj5sgrus2XVeMp/ZIbOESadgILUbm50SNpghOh3/6yrbsH+NB5+WJTmsA7Q==
+"@babel/plugin-transform-typeof-symbol@^7.12.10":
+  version "7.12.10"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.10.tgz#de01c4c8f96580bd00f183072b0d0ecdcf0dec4b"
+  integrity sha512-JQ6H8Rnsogh//ijxspCjc21YPd3VLVoYtAwv3zQmqAt8YGYUtdo5usNhdl4b9/Vir2kPFZl6n1h0PfUz4hJhaA==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
 "@babel/plugin-transform-unicode-escapes@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.1.tgz#5232b9f81ccb07070b7c3c36c67a1b78f1845709"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.1.tgz#5232b9f81ccb07070b7c3c36c67a1b78f1845709"
   integrity sha512-I8gNHJLIc7GdApm7wkVnStWssPNbSRMPtgHdmH3sRM1zopz09UWPS4x5V4n1yz/MIWTVnJ9sp6IkuXdWM4w+2Q==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
 "@babel/plugin-transform-unicode-regex@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.1.tgz#cc9661f61390db5c65e3febaccefd5c6ac3faecb"
+  resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.1.tgz#cc9661f61390db5c65e3febaccefd5c6ac3faecb"
   integrity sha512-SqH4ClNngh/zGwHZOOQMTD+e8FGWexILV+ePMyiDJttAWRh5dhDL8rcl5lSgU3Huiq6Zn6pWTMvdPAb21Dwdyg==
   dependencies:
     "@babel/helper-create-regexp-features-plugin" "^7.12.1"
@@ -810,22 +740,22 @@
 
 "@babel/polyfill@^7.12.1":
   version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.12.1.tgz#1f2d6371d1261bbd961f3c5d5909150e12d0bd96"
+  resolved "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.12.1.tgz#1f2d6371d1261bbd961f3c5d5909150e12d0bd96"
   integrity sha512-X0pi0V6gxLi6lFZpGmeNa4zxtwEmCs42isWLNjZZDE0Y8yVfgu0T2OAHlzBbdYlqbW/YXVvoBHpATEM+goCj8g==
   dependencies:
     core-js "^2.6.5"
     regenerator-runtime "^0.13.4"
 
 "@babel/preset-env@^7.12.1":
-  version "7.12.7"
-  resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.12.7.tgz#54ea21dbe92caf6f10cb1a0a576adc4ebf094b55"
-  integrity sha512-OnNdfAr1FUQg7ksb7bmbKoby4qFOHw6DKWWUNB9KqnnCldxhxJlP+21dpyaWFmf2h0rTbOkXJtAGevY3XW1eew==
+  version "7.12.11"
+  resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.12.11.tgz#55d5f7981487365c93dbbc84507b1c7215e857f9"
+  integrity sha512-j8Tb+KKIXKYlDBQyIOy4BLxzv1NUOwlHfZ74rvW+Z0Gp4/cI2IMDPBWAgWceGcE7aep9oL/0K9mlzlMGxA8yNw==
   dependencies:
     "@babel/compat-data" "^7.12.7"
     "@babel/helper-compilation-targets" "^7.12.5"
     "@babel/helper-module-imports" "^7.12.5"
     "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/helper-validator-option" "^7.12.1"
+    "@babel/helper-validator-option" "^7.12.11"
     "@babel/plugin-proposal-async-generator-functions" "^7.12.1"
     "@babel/plugin-proposal-class-properties" "^7.12.1"
     "@babel/plugin-proposal-dynamic-import" "^7.12.1"
@@ -854,7 +784,7 @@
     "@babel/plugin-transform-arrow-functions" "^7.12.1"
     "@babel/plugin-transform-async-to-generator" "^7.12.1"
     "@babel/plugin-transform-block-scoped-functions" "^7.12.1"
-    "@babel/plugin-transform-block-scoping" "^7.12.1"
+    "@babel/plugin-transform-block-scoping" "^7.12.11"
     "@babel/plugin-transform-classes" "^7.12.1"
     "@babel/plugin-transform-computed-properties" "^7.12.1"
     "@babel/plugin-transform-destructuring" "^7.12.1"
@@ -880,12 +810,12 @@
     "@babel/plugin-transform-spread" "^7.12.1"
     "@babel/plugin-transform-sticky-regex" "^7.12.7"
     "@babel/plugin-transform-template-literals" "^7.12.1"
-    "@babel/plugin-transform-typeof-symbol" "^7.12.1"
+    "@babel/plugin-transform-typeof-symbol" "^7.12.10"
     "@babel/plugin-transform-unicode-escapes" "^7.12.1"
     "@babel/plugin-transform-unicode-regex" "^7.12.1"
     "@babel/preset-modules" "^0.1.3"
-    "@babel/types" "^7.12.7"
-    core-js-compat "^3.7.0"
+    "@babel/types" "^7.12.11"
+    core-js-compat "^3.8.0"
     semver "^5.5.0"
 
 "@babel/preset-modules@^0.1.3":
@@ -900,22 +830,20 @@
     esutils "^2.0.2"
 
 "@babel/preset-react@^7.12.5":
-  version "7.12.7"
-  resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.12.7.tgz#36d61d83223b07b6ac4ec55cf016abb0f70be83b"
-  integrity sha512-wKeTdnGUP5AEYCYQIMeXMMwU7j+2opxrG0WzuZfxuuW9nhKvvALBjl67653CWamZJVefuJGI219G591RSldrqQ==
+  version "7.12.10"
+  resolved "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.12.10.tgz#4fed65f296cbb0f5fb09de6be8cddc85cc909be9"
+  integrity sha512-vtQNjaHRl4DUpp+t+g4wvTHsLQuye+n0H/wsXIZRn69oz/fvNC7gQ4IK73zGJBaxvHoxElDvnYCthMcT7uzFoQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
     "@babel/plugin-transform-react-display-name" "^7.12.1"
-    "@babel/plugin-transform-react-jsx" "^7.12.7"
+    "@babel/plugin-transform-react-jsx" "^7.12.10"
     "@babel/plugin-transform-react-jsx-development" "^7.12.7"
-    "@babel/plugin-transform-react-jsx-self" "^7.12.1"
-    "@babel/plugin-transform-react-jsx-source" "^7.12.1"
     "@babel/plugin-transform-react-pure-annotations" "^7.12.1"
 
 "@babel/register@^7.12.1":
-  version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.12.1.tgz#cdb087bdfc4f7241c03231f22e15d211acf21438"
-  integrity sha512-XWcmseMIncOjoydKZnWvWi0/5CUCD+ZYKhRwgYlWOrA8fGZ/FjuLRpqtIhLOVD/fvR1b9DQHtZPn68VvhpYf+Q==
+  version "7.12.10"
+  resolved "https://registry.npmjs.org/@babel/register/-/register-7.12.10.tgz#19b87143f17128af4dbe7af54c735663b3999f60"
+  integrity sha512-EvX/BvMMJRAA3jZgILWgbsrHwBQvllC5T8B29McyME8DvkdOxk4ujESfrMvME8IHSDvWXrmMXxPvA/lx2gqPLQ==
   dependencies:
     find-cache-dir "^2.0.0"
     lodash "^4.17.19"
@@ -924,51 +852,42 @@
     source-map-support "^0.5.16"
 
 "@babel/runtime@^7.8.4":
-  version "7.11.2"
-  resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
-  integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
+  version "7.12.5"
+  resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
+  integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==
   dependencies:
     regenerator-runtime "^0.13.4"
 
-"@babel/template@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
-  integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==
-  dependencies:
-    "@babel/code-frame" "^7.10.4"
-    "@babel/parser" "^7.10.4"
-    "@babel/types" "^7.10.4"
-
-"@babel/template@^7.12.7":
+"@babel/template@^7.10.4", "@babel/template@^7.12.7":
   version "7.12.7"
-  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc"
+  resolved "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc"
   integrity sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==
   dependencies:
     "@babel/code-frame" "^7.10.4"
     "@babel/parser" "^7.12.7"
     "@babel/types" "^7.12.7"
 
-"@babel/traverse@^7.10.4", "@babel/traverse@^7.12.1", "@babel/traverse@^7.12.5", "@babel/traverse@^7.12.9":
-  version "7.12.9"
-  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.9.tgz#fad26c972eabbc11350e0b695978de6cc8e8596f"
-  integrity sha512-iX9ajqnLdoU1s1nHt36JDI9KG4k+vmI8WgjK5d+aDTwQbL2fUnzedNedssA645Ede3PM2ma1n8Q4h2ohwXgMXw==
+"@babel/traverse@^7.10.4", "@babel/traverse@^7.12.1", "@babel/traverse@^7.12.10", "@babel/traverse@^7.12.5":
+  version "7.12.12"
+  resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.12.tgz#d0cd87892704edd8da002d674bc811ce64743376"
+  integrity sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w==
   dependencies:
-    "@babel/code-frame" "^7.10.4"
-    "@babel/generator" "^7.12.5"
-    "@babel/helper-function-name" "^7.10.4"
-    "@babel/helper-split-export-declaration" "^7.11.0"
-    "@babel/parser" "^7.12.7"
-    "@babel/types" "^7.12.7"
+    "@babel/code-frame" "^7.12.11"
+    "@babel/generator" "^7.12.11"
+    "@babel/helper-function-name" "^7.12.11"
+    "@babel/helper-split-export-declaration" "^7.12.11"
+    "@babel/parser" "^7.12.11"
+    "@babel/types" "^7.12.12"
     debug "^4.1.0"
     globals "^11.1.0"
     lodash "^4.17.19"
 
-"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.12.1", "@babel/types@^7.12.5", "@babel/types@^7.12.6", "@babel/types@^7.12.7", "@babel/types@^7.4.4":
-  version "7.12.7"
-  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.7.tgz#6039ff1e242640a29452c9ae572162ec9a8f5d13"
-  integrity sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==
+"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.12", "@babel/types@^7.12.5", "@babel/types@^7.12.6", "@babel/types@^7.12.7", "@babel/types@^7.4.4":
+  version "7.12.12"
+  resolved "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz#4608a6ec313abbd87afa55004d373ad04a96c299"
+  integrity sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==
   dependencies:
-    "@babel/helper-validator-identifier" "^7.10.4"
+    "@babel/helper-validator-identifier" "^7.12.11"
     lodash "^4.17.19"
     to-fast-properties "^2.0.0"
 
@@ -996,21 +915,16 @@
   integrity sha512-In1q0tIiqTYKAGe3KOHDcFDdZRFISyQeSeipeTHGfki23ebHRZcjxvqj5SSdBkw65D4VpSREMi0s9i5iJiMcTw==
 
 "@types/cheerio@^0.22.8":
-  version "0.22.21"
-  resolved "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.21.tgz#5e37887de309ba11b2e19a6e14cad7874b31a8a3"
-  integrity sha512-aGI3DfswwqgKPiEOTaiHV2ZPC9KEhprpgEbJnv0fZl3SGX0cGgEva1126dGrMC6AJM6v/aihlUgJn9M5DbDZ/Q==
+  version "0.22.23"
+  resolved "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.23.tgz#74bcfee9c5ee53f619711dca953a89fe5cfa4eb4"
+  integrity sha512-QfHLujVMlGqcS/ePSf3Oe5hK3H8wi/yN2JYuxSB1U10VvW1fO3K8C+mURQesFYS1Hn7lspOsTT75SKq/XtydQg==
   dependencies:
     "@types/node" "*"
 
-"@types/color-name@^1.1.1":
-  version "1.1.1"
-  resolved "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
-  integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
-
 "@types/node@*":
-  version "14.6.4"
-  resolved "https://registry.npmjs.org/@types/node/-/node-14.6.4.tgz#a145cc0bb14ef9c4777361b7bbafa5cf8e3acb5a"
-  integrity sha512-Wk7nG1JSaMfMpoMJDKUsWYugliB2Vy55pdjLpmLixeyMi7HizW2I/9QoxsPCkXl3dO+ZOVqPumKaDUv5zJu2uQ==
+  version "14.14.20"
+  resolved "https://registry.npmjs.org/@types/node/-/node-14.14.20.tgz#f7974863edd21d1f8a494a73e8e2b3658615c340"
+  integrity sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A==
 
 "@types/q@^1.5.1":
   version "1.5.4"
@@ -1031,9 +945,9 @@ address@1.1.2, address@^1.0.1:
   integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA==
 
 ajv@^6.12.3:
-  version "6.12.4"
-  resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234"
-  integrity sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==
+  version "6.12.6"
+  resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
+  integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
   dependencies:
     fast-deep-equal "^3.1.1"
     fast-json-stable-stringify "^2.0.0"
@@ -1085,11 +999,10 @@ ansi-styles@^3.2.1:
     color-convert "^1.9.0"
 
 ansi-styles@^4.1.0:
-  version "4.2.1"
-  resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
-  integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==
+  version "4.3.0"
+  resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+  integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
   dependencies:
-    "@types/color-name" "^1.1.1"
     color-convert "^2.0.1"
 
 ansi-wrap@0.1.0:
@@ -1106,9 +1019,9 @@ anymatch@^2.0.0:
     normalize-path "^2.1.1"
 
 arch@^2.1.0:
-  version "2.1.2"
-  resolved "https://registry.npmjs.org/arch/-/arch-2.1.2.tgz#0c52bbe7344bb4fa260c443d2cbad9c00ff2f0bf"
-  integrity sha512-NTBIIbAfkJeIletyABbVtdPgeKfDafR+1mZV/AyyfC1UkVkp9iUjV+wwmqtUgphHYajbI86jejBJp5e+jkGTiQ==
+  version "2.2.0"
+  resolved "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11"
+  integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==
 
 archive-type@^4.0.0:
   version "4.0.0"
@@ -1212,7 +1125,7 @@ asynckit@^0.4.0:
 
 at-least-node@^1.0.0:
   version "1.0.0"
-  resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
+  resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
   integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
 
 atob@^2.1.2:
@@ -1221,9 +1134,9 @@ atob@^2.1.2:
   integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
 
 autolinker@^3.11.0:
-  version "3.14.1"
-  resolved "https://registry.npmjs.org/autolinker/-/autolinker-3.14.1.tgz#6ae4b812b6eaf42d4d68138b9e67757cbf2bc1e4"
-  integrity sha512-yvsRHIaY51EYDml6MGlbqyJGfl4n7zezGYf+R7gvM8c5LNpRGc4SISkvgAswSS8SWxk/OrGCylKV9mJyVstz7w==
+  version "3.14.2"
+  resolved "https://registry.npmjs.org/autolinker/-/autolinker-3.14.2.tgz#71856274eb768fb7149039e24d3a2be2f5c55a63"
+  integrity sha512-VO66nXUCZFxTq7fVHAaiAkZNXRQ1l3IFi6D5P7DLoyIEAn2E8g7TWbyEgLlz1uW74LfWmu1A17IPWuPQyGuNVg==
   dependencies:
     tslib "^1.9.3"
 
@@ -1253,9 +1166,9 @@ aws-sign2@~0.7.0:
   integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
 
 aws4@^1.8.0:
-  version "1.10.1"
-  resolved "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz#e1e82e4f3e999e2cfd61b161280d16a111f86428"
-  integrity sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==
+  version "1.11.0"
+  resolved "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
+  integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
 
 babel-code-frame@^6.22.0:
   version "6.26.0"
@@ -1283,10 +1196,10 @@ balanced-match@^1.0.0:
   resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
   integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
 
-base64-js@^1.0.2:
-  version "1.3.1"
-  resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
-  integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
+base64-js@^1.3.1:
+  version "1.5.1"
+  resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
 
 base@^0.11.1:
   version "0.11.2"
@@ -1445,24 +1358,14 @@ browserslist@4.7.0:
     electron-to-chromium "^1.3.247"
     node-releases "^1.1.29"
 
-browserslist@^4.0.0, browserslist@^4.12.0:
-  version "4.14.1"
-  resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.14.1.tgz#cb2b490ba881d45dc3039078c7ed04411eaf3fa3"
-  integrity sha512-zyBTIHydW37pnb63c7fHFXUG6EcqWOqoMdDx6cdyaDFriZ20EoVxcE95S54N+heRqY8m8IUgB5zYta/gCwSaaA==
+browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.16.0:
+  version "4.16.0"
+  resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.16.0.tgz#410277627500be3cb28a1bfe037586fbedf9488b"
+  integrity sha512-/j6k8R0p3nxOC6kx5JGAxsnhc9ixaWJfYc+TNTzxg6+ARaESAvQGV7h0uNOB4t+pLQJZWzcrMxXOxjgsCj3dqQ==
   dependencies:
-    caniuse-lite "^1.0.30001124"
-    electron-to-chromium "^1.3.562"
-    escalade "^3.0.2"
-    node-releases "^1.1.60"
-
-browserslist@^4.14.5, browserslist@^4.14.7:
-  version "4.15.0"
-  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.15.0.tgz#3d48bbca6a3f378e86102ffd017d9a03f122bdb0"
-  integrity sha512-IJ1iysdMkGmjjYeRlDU8PQejVwxvVO5QOfXH7ylW31GO6LwNRSmm/SgRXtNsEXqMLl2e+2H5eEJ7sfynF8TCaQ==
-  dependencies:
-    caniuse-lite "^1.0.30001164"
+    caniuse-lite "^1.0.30001165"
     colorette "^1.2.1"
-    electron-to-chromium "^1.3.612"
+    electron-to-chromium "^1.3.621"
     escalade "^3.1.1"
     node-releases "^1.1.67"
 
@@ -1495,12 +1398,12 @@ buffer-from@^1.0.0:
   integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
 
 buffer@^5.2.1:
-  version "5.6.0"
-  resolved "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786"
-  integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==
+  version "5.7.1"
+  resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
+  integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
   dependencies:
-    base64-js "^1.0.2"
-    ieee754 "^1.1.4"
+    base64-js "^1.3.1"
+    ieee754 "^1.1.13"
 
 bytes@1:
   version "1.0.0"
@@ -1540,6 +1443,14 @@ cacheable-request@^2.1.1:
     normalize-url "2.0.1"
     responselike "1.0.2"
 
+call-bind@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz#24127054bb3f9bdcb4b1fb82418186072f77b8ce"
+  integrity sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==
+  dependencies:
+    function-bind "^1.1.1"
+    get-intrinsic "^1.0.0"
+
 call-me-maybe@^1.0.1:
   version "1.0.1"
   resolved "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
@@ -1587,15 +1498,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.30000989, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001124:
-  version "1.0.30001124"
-  resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001124.tgz#5d9998190258e11630d674fc50ea8e579ae0ced2"
-  integrity sha512-zQW8V3CdND7GHRH6rxm6s59Ww4g/qGWTheoboW9nfeMg7sUoopIfKCcNZUjwYRCOrvereh3kwDpZj4VLQ7zGtA==
-
-caniuse-lite@^1.0.30001164:
-  version "1.0.30001164"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001164.tgz#5bbfd64ca605d43132f13cc7fdabb17c3036bfdc"
-  integrity sha512-G+A/tkf4bu0dSp9+duNiXc7bGds35DioCyC6vgK2m/rjA4Krpy5WeZgZyfH2f0wj2kI6yAWWucyap6oOwmY1mg==
+caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000989, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001165:
+  version "1.0.30001173"
+  resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001173.tgz#3c47bbe3cd6d7a9eda7f50ac016d158005569f56"
+  integrity sha512-R3aqmjrICdGCTAnSXtNyvWYMK3YtV5jwudbq0T7nN9k4kmE4CBuwPqyJ+KBzepSTh0huivV2gLbSMEzTTmfeYw==
 
 caseless@~0.12.0:
   version "0.12.0"
@@ -1775,21 +1681,21 @@ color-name@^1.0.0, color-name@~1.1.4:
   resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
-color-string@^1.5.2:
-  version "1.5.3"
-  resolved "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc"
-  integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==
+color-string@^1.5.4:
+  version "1.5.4"
+  resolved "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz#dd51cd25cfee953d138fe4002372cc3d0e504cb6"
+  integrity sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==
   dependencies:
     color-name "^1.0.0"
     simple-swizzle "^0.2.2"
 
 color@^3.0.0:
-  version "3.1.2"
-  resolved "https://registry.npmjs.org/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10"
-  integrity sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==
+  version "3.1.3"
+  resolved "https://registry.npmjs.org/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e"
+  integrity sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==
   dependencies:
     color-convert "^1.9.1"
-    color-string "^1.5.2"
+    color-string "^1.5.4"
 
 colorette@^1.2.1:
   version "1.2.1"
@@ -1902,18 +1808,18 @@ copy-descriptor@^0.1.0:
   resolved "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
   integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
 
-core-js-compat@^3.7.0:
-  version "3.8.0"
-  resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.8.0.tgz#3248c6826f4006793bd637db608bca6e4cd688b1"
-  integrity sha512-o9QKelQSxQMYWHXc/Gc4L8bx/4F7TTraE5rhuN8I7mKBt5dBIUpXpIR3omv70ebr8ST5R3PqbDQr+ZI3+Tt1FQ==
+core-js-compat@^3.8.0:
+  version "3.8.2"
+  resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.8.2.tgz#3717f51f6c3d2ebba8cbf27619b57160029d1d4c"
+  integrity sha512-LO8uL9lOIyRRrQmZxHZFl1RV+ZbcsAkFWTktn5SmH40WgLtSNYN4m4W2v9ONT147PxBY/XrRhrWq8TlvObyUjQ==
   dependencies:
-    browserslist "^4.14.7"
+    browserslist "^4.16.0"
     semver "7.0.0"
 
 core-js@^2.6.5:
-  version "2.6.11"
-  resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
-  integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==
+  version "2.6.12"
+  resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
+  integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
 
 core-util-is@1.0.2, core-util-is@~1.0.0:
   version "1.0.2"
@@ -2005,12 +1911,12 @@ css-tree@1.0.0-alpha.37:
     mdn-data "2.0.4"
     source-map "^0.6.1"
 
-css-tree@1.0.0-alpha.39:
-  version "1.0.0-alpha.39"
-  resolved "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.39.tgz#2bff3ffe1bb3f776cf7eefd91ee5cba77a149eeb"
-  integrity sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA==
+css-tree@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.npmjs.org/css-tree/-/css-tree-1.1.2.tgz#9ae393b5dafd7dae8a622475caec78d3d8fbd7b5"
+  integrity sha512-wCoWush5Aeo48GLhfHPbmvZs59Z+M7k5+B1xDnXbdWNcEF423DoFdqSWE0PM5aNk5nI5cp1q7ms36zGApY/sKQ==
   dependencies:
-    mdn-data "2.0.6"
+    mdn-data "2.0.14"
     source-map "^0.6.1"
 
 css-what@2.1:
@@ -2019,9 +1925,9 @@ css-what@2.1:
   integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
 
 css-what@^3.2.1:
-  version "3.3.0"
-  resolved "https://registry.npmjs.org/css-what/-/css-what-3.3.0.tgz#10fec696a9ece2e591ac772d759aacabac38cd39"
-  integrity sha512-pv9JPyatiPaQ6pf4OvD/dbfm0o5LviWmwxNWzblYf/1u9QZd0ihV+PMwy5jdQWQ3349kZmKEx9WXuSka2dM4cg==
+  version "3.4.2"
+  resolved "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4"
+  integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==
 
 cssesc@^3.0.0:
   version "3.0.0"
@@ -2097,11 +2003,11 @@ cssnano@^4.1.10:
     postcss "^7.0.0"
 
 csso@^4.0.2:
-  version "4.0.3"
-  resolved "https://registry.npmjs.org/csso/-/csso-4.0.3.tgz#0d9985dc852c7cc2b2cacfbbe1079014d1a8e903"
-  integrity sha512-NL3spysxUkcrOgnpsT4Xdl2aiEiBG6bXswAABQVHcMrfjjBisFOKwLDOmf4wf32aPdcJws1zds2B0Rg+jqMyHQ==
+  version "4.2.0"
+  resolved "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529"
+  integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==
   dependencies:
-    css-tree "1.0.0-alpha.39"
+    css-tree "^1.1.2"
 
 currently-unhandled@^0.4.1:
   version "0.4.1"
@@ -2124,24 +2030,17 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0:
   dependencies:
     ms "2.0.0"
 
-debug@4.1.0:
-  version "4.1.0"
-  resolved "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz#373687bffa678b38b1cd91f861b63850035ddc87"
-  integrity sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==
+debug@4.3.1, debug@^4.1.0:
+  version "4.3.1"
+  resolved "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
+  integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
   dependencies:
-    ms "^2.1.1"
+    ms "2.1.2"
 
 debug@^3.1.0, debug@^3.1.1, debug@^3.2.5:
-  version "3.2.6"
-  resolved "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
-  integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
-  dependencies:
-    ms "^2.1.1"
-
-debug@^4.1.0:
-  version "4.1.1"
-  resolved "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
-  integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
+  version "3.2.7"
+  resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
+  integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
   dependencies:
     ms "^2.1.1"
 
@@ -2220,7 +2119,7 @@ deep-is@^0.1.3:
   resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
   integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
 
-define-properties@^1.1.2, define-properties@^1.1.3:
+define-properties@^1.1.3:
   version "1.1.3"
   resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
   integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
@@ -2290,9 +2189,9 @@ dir-glob@2.0.0:
     arrify "^1.0.1"
     path-type "^3.0.0"
 
-docusaurus@^2.0.0-alpha.378053ac5:
+docusaurus@^2.0.0-alpha.70:
   version "2.0.0-alpha.378053ac5"
-  resolved "https://registry.yarnpkg.com/docusaurus/-/docusaurus-2.0.0-alpha.378053ac5.tgz#9ca31969ef6eb8958692948ae1fd1d4e0f452f44"
+  resolved "https://registry.npmjs.org/docusaurus/-/docusaurus-2.0.0-alpha.378053ac5.tgz#9ca31969ef6eb8958692948ae1fd1d4e0f452f44"
   integrity sha512-+NM1NrJKYcmHYiMQ/4b5ew9QDbLW3QI/ii2lCs/gpEZhtU0FVnl1RLC3FUO41xgOhth3A8M5G5ChzlPuNzxjug==
   dependencies:
     "@babel/core" "^7.12.3"
@@ -2364,9 +2263,9 @@ domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1:
   integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
 
 domelementtype@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d"
-  integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==
+  version "2.1.0"
+  resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e"
+  integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==
 
 domhandler@^2.3.0:
   version "2.4.2"
@@ -2392,9 +2291,9 @@ domutils@^1.5.1, domutils@^1.7.0:
     domelementtype "1"
 
 dot-prop@^5.2.0:
-  version "5.2.0"
-  resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb"
-  integrity sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==
+  version "5.3.0"
+  resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88"
+  integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==
   dependencies:
     is-obj "^2.0.0"
 
@@ -2456,15 +2355,10 @@ ee-first@1.1.1:
   resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
   integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
 
-electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.562:
-  version "1.3.562"
-  resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.562.tgz#79c20277ee1c8d0173a22af00e38433b752bc70f"
-  integrity sha512-WhRe6liQ2q/w1MZc8mD8INkenHivuHdrr4r5EQHNomy3NJux+incP6M6lDMd0paShP3MD0WGe5R1TWmEClf+Bg==
-
-electron-to-chromium@^1.3.612:
-  version "1.3.614"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.614.tgz#ff359e8d2249e2ce859a4c2bc34c22bd2e2eb0a2"
-  integrity sha512-JMDl46mg4G+n6q/hAJkwy9eMTj5FJjsE+8f/irAGRMLM4yeRVbMuRrdZrbbGGOrGVcZc4vJPjUpEUWNb/fA6hg==
+electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.621:
+  version "1.3.634"
+  resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.634.tgz#82ea400f520f739c4f6ff00c1f7524827a917d25"
+  integrity sha512-QPrWNYeE/A0xRvl/QP3E0nkaEvYUvH3gM04ZWYtIa6QlSpEetRlRI1xvQ7hiMIySHHEV+mwDSX8Kj4YZY6ZQAw==
 
 "emoji-regex@>=6.0.0 <=6.1.1":
   version "6.1.1"
@@ -2494,9 +2388,9 @@ entities@^1.1.1, entities@~1.1.1:
   integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
 
 entities@^2.0.0:
-  version "2.0.3"
-  resolved "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f"
-  integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==
+  version "2.1.0"
+  resolved "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
+  integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
 
 error-ex@^1.2.0, error-ex@^1.3.1:
   version "1.3.2"
@@ -2512,20 +2406,38 @@ error@^7.0.0:
   dependencies:
     string-template "~0.2.1"
 
-es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5:
-  version "1.17.6"
-  resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a"
-  integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==
+es-abstract@^1.17.2:
+  version "1.17.7"
+  resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c"
+  integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==
   dependencies:
     es-to-primitive "^1.2.1"
     function-bind "^1.1.1"
     has "^1.0.3"
     has-symbols "^1.0.1"
-    is-callable "^1.2.0"
-    is-regex "^1.1.0"
-    object-inspect "^1.7.0"
+    is-callable "^1.2.2"
+    is-regex "^1.1.1"
+    object-inspect "^1.8.0"
     object-keys "^1.1.1"
-    object.assign "^4.1.0"
+    object.assign "^4.1.1"
+    string.prototype.trimend "^1.0.1"
+    string.prototype.trimstart "^1.0.1"
+
+es-abstract@^1.18.0-next.1:
+  version "1.18.0-next.1"
+  resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68"
+  integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==
+  dependencies:
+    es-to-primitive "^1.2.1"
+    function-bind "^1.1.1"
+    has "^1.0.3"
+    has-symbols "^1.0.1"
+    is-callable "^1.2.2"
+    is-negative-zero "^2.0.0"
+    is-regex "^1.1.1"
+    object-inspect "^1.8.0"
+    object-keys "^1.1.1"
+    object.assign "^4.1.1"
     string.prototype.trimend "^1.0.1"
     string.prototype.trimstart "^1.0.1"
 
@@ -2538,14 +2450,9 @@ es-to-primitive@^1.2.1:
     is-date-object "^1.0.1"
     is-symbol "^1.0.2"
 
-escalade@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.npmjs.org/escalade/-/escalade-3.0.2.tgz#6a580d70edb87880f22b4c91d0d56078df6962c4"
-  integrity sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ==
-
 escalade@^3.1.1:
   version "3.1.1"
-  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+  resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
   integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
 
 escape-html@~1.0.3:
@@ -2798,7 +2705,7 @@ fd-slicer@~1.1.0:
 
 feed@^4.2.1:
   version "4.2.1"
-  resolved "https://registry.yarnpkg.com/feed/-/feed-4.2.1.tgz#b246ef891051c7dbf088ca203341d9fb0444baee"
+  resolved "https://registry.npmjs.org/feed/-/feed-4.2.1.tgz#b246ef891051c7dbf088ca203341d9fb0444baee"
   integrity sha512-l28KKcK1J/u3iq5dRDmmoB2p7dtBfACC2NqJh4dI2kFptxH0asfjmOfcxqh5Sv8suAlVa73gZJ4REY5RrafVvg==
   dependencies:
     xml-js "^1.6.11"
@@ -3009,7 +2916,7 @@ fs-constants@^1.0.0:
 
 fs-extra@^9.0.1:
   version "9.0.1"
-  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc"
+  resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc"
   integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==
   dependencies:
     at-least-node "^1.0.0"
@@ -3043,9 +2950,18 @@ gaze@^1.1.3:
     globule "^1.0.0"
 
 gensync@^1.0.0-beta.1:
-  version "1.0.0-beta.1"
-  resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
-  integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==
+  version "1.0.0-beta.2"
+  resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
+  integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
+
+get-intrinsic@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.2.tgz#6820da226e50b24894e08859469dc68361545d49"
+  integrity sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==
+  dependencies:
+    function-bind "^1.1.1"
+    has "^1.0.3"
+    has-symbols "^1.0.1"
 
 get-proxy@^2.0.0:
   version "2.1.0"
@@ -3103,7 +3019,7 @@ gifsicle@^4.0.0:
 
 github-slugger@^1.3.0:
   version "1.3.0"
-  resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.3.0.tgz#9bd0a95c5efdfc46005e82a906ef8e2a059124c9"
+  resolved "https://registry.npmjs.org/github-slugger/-/github-slugger-1.3.0.tgz#9bd0a95c5efdfc46005e82a906ef8e2a059124c9"
   integrity sha512-gwJScWVNhFYSRDvURk/8yhcFBee6aFjye2a7Lhb2bUyRulpIoek9p0I9Kt7PT67d/nUlZbFu8L9RLiA0woQN8Q==
   dependencies:
     emoji-regex ">=6.0.0 <=6.1.1"
@@ -3294,7 +3210,7 @@ has-symbol-support-x@^1.4.1:
   resolved "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz#1409f98bc00247da45da67cee0a36f282ff26455"
   integrity sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==
 
-has-symbols@^1.0.0, has-symbols@^1.0.1:
+has-symbols@^1.0.1:
   version "1.0.1"
   resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
   integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
@@ -3350,9 +3266,9 @@ hex-color-regex@^1.1.0:
   integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
 
 highlight.js@^9.16.2:
-  version "9.18.3"
-  resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.3.tgz#a1a0a2028d5e3149e2380f8a865ee8516703d634"
-  integrity sha512-zBZAmhSupHIl5sITeMqIJnYCDfAEc3Gdkqj65wC1lpI468MMQeeQkhcIAvk+RylAkxrCcI9xy9piHiXeQ1BdzQ==
+  version "9.18.5"
+  resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.5.tgz#d18a359867f378c138d6819edfc2a8acd5f29825"
+  integrity sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA==
 
 hosted-git-info@^2.1.4:
   version "2.8.8"
@@ -3414,9 +3330,9 @@ http-errors@~1.7.2:
     toidentifier "1.0.0"
 
 http-parser-js@>=0.5.1:
-  version "0.5.2"
-  resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.2.tgz#da2e31d237b393aae72ace43882dd7e270a8ff77"
-  integrity sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ==
+  version "0.5.3"
+  resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz#01d2709c79d41698bb01d4decc5e9da4e4a033d9"
+  integrity sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==
 
 http-signature@~1.2.0:
   version "1.2.0"
@@ -3434,10 +3350,10 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24:
   dependencies:
     safer-buffer ">= 2.1.2 < 3"
 
-ieee754@^1.1.4:
-  version "1.1.13"
-  resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
-  integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
+ieee754@^1.1.13:
+  version "1.2.1"
+  resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
+  integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
 
 ignore@^3.3.5:
   version "3.3.10"
@@ -3540,9 +3456,9 @@ inherits@2.0.3:
   integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
 
 ini@^1.3.4, ini@^1.3.5:
-  version "1.3.5"
-  resolved "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
-  integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
+  version "1.3.8"
+  resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
+  integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
 
 inquirer@6.5.0:
   version "6.5.0"
@@ -3576,10 +3492,10 @@ into-stream@^3.1.0:
     from2 "^2.1.1"
     p-is-promise "^1.1.0"
 
-ip-regex@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
-  integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=
+ip-regex@^4.1.0:
+  version "4.2.0"
+  resolved "https://registry.npmjs.org/ip-regex/-/ip-regex-4.2.0.tgz#a03f5eb661d9a154e3973a03de8b23dd0ad6892e"
+  integrity sha512-n5cDDeTWWRwK1EBoWwRti+8nP4NbytBBY0pldmnIkq6Z55KNFmWofh4rl9dPZpj+U/nVq7gweR3ylrvMt4YZ5A==
 
 ipaddr.js@1.9.1:
   version "1.9.1"
@@ -3627,10 +3543,10 @@ is-buffer@^1.1.5:
   resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
   integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
 
-is-callable@^1.1.4, is-callable@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb"
-  integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==
+is-callable@^1.1.4, is-callable@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9"
+  integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==
 
 is-color-stop@^1.0.0:
   version "1.1.0"
@@ -3644,6 +3560,13 @@ is-color-stop@^1.0.0:
     rgb-regex "^1.0.1"
     rgba-regex "^1.0.0"
 
+is-core-module@^2.1.0:
+  version "2.2.0"
+  resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a"
+  integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==
+  dependencies:
+    has "^1.0.3"
+
 is-data-descriptor@^0.1.4:
   version "0.1.4"
   resolved "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
@@ -3744,6 +3667,11 @@ is-natural-number@^4.0.1:
   resolved "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8"
   integrity sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=
 
+is-negative-zero@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
+  integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==
+
 is-number@^2.1.0:
   version "2.1.0"
   resolved "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
@@ -3769,9 +3697,9 @@ is-obj@^2.0.0:
   integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==
 
 is-object@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470"
-  integrity sha1-iVJojF7C/9awPsyF52ngKQMINHA=
+  version "1.0.2"
+  resolved "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz#a56552e1c665c9e950b4a025461da87e72f86fcf"
+  integrity sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==
 
 is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
   version "1.1.0"
@@ -3790,7 +3718,7 @@ is-png@^1.0.0:
   resolved "https://registry.npmjs.org/is-png/-/is-png-1.1.0.tgz#d574b12bf275c0350455570b0e5b57ab062077ce"
   integrity sha1-1XSxK/J1wDUEVVcLDltXqwYgd84=
 
-is-regex@^1.1.0:
+is-regex@^1.1.1:
   version "1.1.1"
   resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
   integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==
@@ -3843,7 +3771,7 @@ is-typedarray@~1.0.0:
   resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
   integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
 
-is-url@^1.2.2:
+is-url@^1.2.4:
   version "1.2.4"
   resolved "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52"
   integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==
@@ -3863,14 +3791,14 @@ is-wsl@^1.1.0:
   resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
   integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=
 
-is2@2.0.1:
-  version "2.0.1"
-  resolved "https://registry.npmjs.org/is2/-/is2-2.0.1.tgz#8ac355644840921ce435d94f05d3a94634d3481a"
-  integrity sha512-+WaJvnaA7aJySz2q/8sLjMb2Mw14KTplHmSwcSpZ/fWJPkUmqw3YTzSWbPJ7OAwRvdYTWF2Wg+yYJ1AdP5Z8CA==
+is2@^2.0.6:
+  version "2.0.6"
+  resolved "https://registry.npmjs.org/is2/-/is2-2.0.6.tgz#094f887248b49ba7ce278f8c39f85a70927bb5de"
+  integrity sha512-+Z62OHOjA6k2sUDOKXoZI3EXv7Fb1K52jpTBLbkfx62bcUeSsrTBLhEquCRDKTx0XE5XbHcG/S2vrtE3lnEDsQ==
   dependencies:
     deep-is "^0.1.3"
-    ip-regex "^2.1.0"
-    is-url "^1.2.2"
+    ip-regex "^4.1.0"
+    is-url "^1.2.4"
 
 isarray@1.0.0, isarray@~1.0.0:
   version "1.0.0"
@@ -3928,7 +3856,7 @@ js-tokens@^3.0.2:
 
 js-yaml@^3.13.1, js-yaml@^3.8.1:
   version "3.14.1"
-  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
+  resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
   integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
   dependencies:
     argparse "^1.0.7"
@@ -4002,7 +3930,7 @@ json5@^2.1.2:
 
 jsonfile@^6.0.1:
   version "6.1.0"
-  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
+  resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
   integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
   dependencies:
     universalify "^2.0.0"
@@ -4334,16 +4262,16 @@ math-random@^1.0.1:
   resolved "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c"
   integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==
 
+mdn-data@2.0.14:
+  version "2.0.14"
+  resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
+  integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
+
 mdn-data@2.0.4:
   version "2.0.4"
   resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"
   integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==
 
-mdn-data@2.0.6:
-  version "2.0.6"
-  resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978"
-  integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==
-
 media-typer@0.3.0:
   version "0.3.0"
   resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@@ -4404,17 +4332,17 @@ micromatch@^3.1.10, micromatch@^3.1.4:
     snapdragon "^0.8.1"
     to-regex "^3.0.2"
 
-mime-db@1.44.0, mime-db@^1.28.0:
-  version "1.44.0"
-  resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
-  integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
+mime-db@1.45.0, mime-db@^1.28.0:
+  version "1.45.0"
+  resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea"
+  integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==
 
 mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24:
-  version "2.1.27"
-  resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
-  integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==
+  version "2.1.28"
+  resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd"
+  integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==
   dependencies:
-    mime-db "1.44.0"
+    mime-db "1.45.0"
 
 mime@1.6.0:
   version "1.6.0"
@@ -4468,20 +4396,25 @@ ms@2.1.1:
   resolved "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
   integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
 
-ms@^2.1.1:
+ms@2.1.2:
   version "2.1.2"
   resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
 
+ms@^2.1.1:
+  version "2.1.3"
+  resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
 mute-stream@0.0.7:
   version "0.0.7"
   resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
   integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
 
 nan@^2.12.1:
-  version "2.14.1"
-  resolved "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
-  integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
+  version "2.14.2"
+  resolved "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
+  integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
 
 nanomatch@^1.2.9:
   version "1.2.13"
@@ -4515,15 +4448,10 @@ node-modules-regexp@^1.0.0:
   resolved "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40"
   integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=
 
-node-releases@^1.1.29, node-releases@^1.1.60:
-  version "1.1.60"
-  resolved "https://registry.npmjs.org/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084"
-  integrity sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA==
-
-node-releases@^1.1.67:
-  version "1.1.67"
-  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.67.tgz#28ebfcccd0baa6aad8e8d4d8fe4cbc49ae239c12"
-  integrity sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==
+node-releases@^1.1.29, node-releases@^1.1.67:
+  version "1.1.69"
+  resolved "https://registry.npmjs.org/node-releases/-/node-releases-1.1.69.tgz#3149dbde53b781610cd8b486d62d86e26c3725f6"
+  integrity sha512-DGIjo79VDEyAnRlfSqYTsy+yoHd2IOjJiKUozD2MV2D85Vso6Bug56mb9tT/fY5Urt0iqk01H7x+llAruDR2zA==
 
 normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
   version "2.5.0"
@@ -4612,12 +4540,12 @@ object-copy@^0.1.0:
     define-property "^0.2.5"
     kind-of "^3.0.3"
 
-object-inspect@^1.7.0:
-  version "1.8.0"
-  resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
-  integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==
+object-inspect@^1.8.0:
+  version "1.9.0"
+  resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a"
+  integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==
 
-object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
+object-keys@^1.0.12, object-keys@^1.1.1:
   version "1.1.1"
   resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
   integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
@@ -4629,23 +4557,24 @@ object-visit@^1.0.0:
   dependencies:
     isobject "^3.0.0"
 
-object.assign@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
-  integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
+object.assign@^4.1.0, object.assign@^4.1.1:
+  version "4.1.2"
+  resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
+  integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
   dependencies:
-    define-properties "^1.1.2"
-    function-bind "^1.1.1"
-    has-symbols "^1.0.0"
-    object-keys "^1.0.11"
+    call-bind "^1.0.0"
+    define-properties "^1.1.3"
+    has-symbols "^1.0.1"
+    object-keys "^1.1.1"
 
 object.getownpropertydescriptors@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649"
-  integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==
+  version "2.1.1"
+  resolved "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz#0dfda8d108074d9c563e80490c883b6661091544"
+  integrity sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.0-next.1"
+    es-abstract "^1.18.0-next.1"
 
 object.pick@^1.2.0, object.pick@^1.3.0:
   version "1.3.0"
@@ -4655,13 +4584,13 @@ object.pick@^1.2.0, object.pick@^1.3.0:
     isobject "^3.0.1"
 
 object.values@^1.1.0:
-  version "1.1.1"
-  resolved "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e"
-  integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==
+  version "1.1.2"
+  resolved "https://registry.npmjs.org/object.values/-/object.values-1.1.2.tgz#7a2015e06fcb0f546bd652486ce8583a4731c731"
+  integrity sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.0-next.1"
-    function-bind "^1.1.1"
+    es-abstract "^1.18.0-next.1"
     has "^1.0.3"
 
 on-finished@~2.3.0:
@@ -4961,7 +4890,7 @@ pkg-up@2.0.0:
 
 portfinder@^1.0.28:
   version "1.0.28"
-  resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778"
+  resolved "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778"
   integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==
   dependencies:
     async "^2.6.2"
@@ -4974,9 +4903,9 @@ posix-character-classes@^0.1.0:
   integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
 
 postcss-calc@^7.0.1:
-  version "7.0.4"
-  resolved "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.4.tgz#5e177ddb417341e6d4a193c5d9fd8ada79094f8b"
-  integrity sha512-0I79VRAd1UTkaHzY9w83P39YGO/M3bG7/tNLrHGEunBolfoGM0hSjrGvjoeaj0JE/zIw5GsI2KZ0UwDJqv5hjw==
+  version "7.0.5"
+  resolved "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.5.tgz#f8a6e99f12e619c2ebc23cf6c486fdc15860933e"
+  integrity sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg==
   dependencies:
     postcss "^7.0.27"
     postcss-selector-parser "^6.0.2"
@@ -5211,13 +5140,14 @@ postcss-selector-parser@^3.0.0:
     uniq "^1.0.1"
 
 postcss-selector-parser@^6.0.2:
-  version "6.0.2"
-  resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c"
-  integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==
+  version "6.0.4"
+  resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3"
+  integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==
   dependencies:
     cssesc "^3.0.0"
     indexes-of "^1.0.1"
     uniq "^1.0.1"
+    util-deprecate "^1.0.2"
 
 postcss-svgo@^4.0.2:
   version "4.0.2"
@@ -5249,9 +5179,9 @@ postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0:
   integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
 
 postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.23, postcss@^7.0.27, postcss@^7.0.32:
-  version "7.0.32"
-  resolved "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d"
-  integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==
+  version "7.0.35"
+  resolved "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24"
+  integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==
   dependencies:
     chalk "^2.4.2"
     source-map "^0.6.1"
@@ -5273,9 +5203,9 @@ prettier@^2.2.1:
   integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==
 
 prismjs@^1.22.0:
-  version "1.22.0"
-  resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.22.0.tgz#73c3400afc58a823dd7eed023f8e1ce9fd8977fa"
-  integrity sha512-lLJ/Wt9yy0AiSYBf212kK3mM5L8ycwlyTlSxHBAneXLR0nzFMlZ5y7riFPF3E33zXOF2IH95xdY5jIyZbM9z/w==
+  version "1.23.0"
+  resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33"
+  integrity sha512-c29LVsqOaLbBHuIbsTxaKENh1N2EQBOHaWv7gkHN4dgRbxSREqDnDbtFJYdpPauS4YCplMSNCABQ6Eeor69bAA==
   optionalDependencies:
     clipboard "^2.0.0"
 
@@ -5427,9 +5357,9 @@ react-dev-utils@^9.1.0:
     text-table "0.2.0"
 
 react-dom@^16.8.4:
-  version "16.13.1"
-  resolved "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f"
-  integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==
+  version "16.14.0"
+  resolved "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89"
+  integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==
   dependencies:
     loose-envify "^1.1.0"
     object-assign "^4.1.1"
@@ -5437,9 +5367,9 @@ react-dom@^16.8.4:
     scheduler "^0.19.1"
 
 react-error-overlay@^6.0.3:
-  version "6.0.7"
-  resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108"
-  integrity sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA==
+  version "6.0.8"
+  resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.8.tgz#474ed11d04fc6bda3af643447d85e9127ed6b5de"
+  integrity sha512-HvPuUQnLp5H7TouGq3kzBeioJmXms1wHy9EGjz2OURWBp4qZO6AfGEcnxts1D/CbwPLRAgTMPCEgYhA3sEM4vw==
 
 react-is@^16.8.1:
   version "16.13.1"
@@ -5447,9 +5377,9 @@ react-is@^16.8.1:
   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
 
 react@^16.8.4:
-  version "16.13.1"
-  resolved "https://registry.npmjs.org/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
-  integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==
+  version "16.14.0"
+  resolved "https://registry.npmjs.org/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
+  integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==
   dependencies:
     loose-envify "^1.1.0"
     object-assign "^4.1.1"
@@ -5533,9 +5463,9 @@ regenerate-unicode-properties@^8.2.0:
     regenerate "^1.4.0"
 
 regenerate@^1.4.0:
-  version "1.4.1"
-  resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.1.tgz#cad92ad8e6b591773485fbe05a485caf4f457e6f"
-  integrity sha512-j2+C8+NtXQgEKWk49MMP5P/u2GhnahTtVkRIHr5R5lVRlbKvmQ+oS+A5aLKWp2ma5VkT8sh6v+v4hbH0YHR66A==
+  version "1.4.2"
+  resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
+  integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
 
 regenerator-runtime@^0.13.4:
   version "0.13.7"
@@ -5557,21 +5487,9 @@ regex-not@^1.0.0, regex-not@^1.0.2:
     extend-shallow "^3.0.2"
     safe-regex "^1.1.0"
 
-regexpu-core@^4.7.0:
-  version "4.7.0"
-  resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938"
-  integrity sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ==
-  dependencies:
-    regenerate "^1.4.0"
-    regenerate-unicode-properties "^8.2.0"
-    regjsgen "^0.5.1"
-    regjsparser "^0.6.4"
-    unicode-match-property-ecmascript "^1.0.4"
-    unicode-match-property-value-ecmascript "^1.2.0"
-
 regexpu-core@^4.7.1:
   version "4.7.1"
-  resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6"
+  resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6"
   integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==
   dependencies:
     regenerate "^1.4.0"
@@ -5677,11 +5595,12 @@ resolve-url@^0.2.1:
   resolved "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
   integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
 
-resolve@^1.1.6, resolve@^1.10.0, resolve@^1.3.2:
-  version "1.17.0"
-  resolved "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444"
-  integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==
+resolve@^1.1.6, resolve@^1.10.0:
+  version "1.19.0"
+  resolved "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c"
+  integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==
   dependencies:
+    is-core-module "^2.1.0"
     path-parse "^1.0.6"
 
 responselike@1.0.2:
@@ -5727,9 +5646,9 @@ run-async@^2.2.0:
   integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==
 
 rxjs@^6.4.0:
-  version "6.6.2"
-  resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz#8096a7ac03f2cc4fe5860ef6e572810d9e01c0d2"
-  integrity sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==
+  version "6.6.3"
+  resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552"
+  integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==
   dependencies:
     tslib "^1.9.0"
 
@@ -6030,9 +5949,9 @@ spdx-expression-parse@^3.0.0:
     spdx-license-ids "^3.0.0"
 
 spdx-license-ids@^3.0.0:
-  version "3.0.5"
-  resolved "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654"
-  integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==
+  version "3.0.7"
+  resolved "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz#e9c18a410e5ed7e12442a549fbd8afa767038d65"
+  integrity sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==
 
 split-string@^3.0.1, split-string@^3.0.2:
   version "3.1.0"
@@ -6107,20 +6026,20 @@ string-width@^2.1.0:
     strip-ansi "^4.0.0"
 
 string.prototype.trimend@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"
-  integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==
+  version "1.0.3"
+  resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz#a22bd53cca5c7cf44d7c9d5c732118873d6cd18b"
+  integrity sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.5"
 
 string.prototype.trimstart@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54"
-  integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==
+  version "1.0.3"
+  resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz#9b4cb590e123bb36564401d59824298de50fd5aa"
+  integrity sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.5"
 
 string_decoder@0.10:
   version "0.10.31"
@@ -6273,12 +6192,12 @@ tar-stream@^1.5.2:
     xtend "^4.0.0"
 
 tcp-port-used@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.1.tgz#46061078e2d38c73979a2c2c12b5a674e6689d70"
-  integrity sha512-rwi5xJeU6utXoEIiMvVBMc9eJ2/ofzB+7nLOdnZuFTmNCLqRiQh2sMG9MqCxHU/69VC/Fwp5dV9306Qd54ll1Q==
+  version "1.0.2"
+  resolved "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz#9652b7436eb1f4cfae111c79b558a25769f6faea"
+  integrity sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==
   dependencies:
-    debug "4.1.0"
-    is2 "2.0.1"
+    debug "4.3.1"
+    is2 "^2.0.6"
 
 temp-dir@^1.0.0:
   version "1.0.0"
@@ -6433,9 +6352,9 @@ truncate-html@^1.0.3:
     cheerio "0.22.0"
 
 tslib@^1.9.0, tslib@^1.9.3:
-  version "1.13.0"
-  resolved "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
-  integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
+  version "1.14.1"
+  resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
+  integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
 
 tunnel-agent@^0.6.0:
   version "0.6.0"
@@ -6515,12 +6434,12 @@ uniqs@^2.0.0:
 
 universalify@^1.0.0:
   version "1.0.0"
-  resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d"
+  resolved "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d"
   integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==
 
 universalify@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
+  resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
   integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
 
 unpipe@1.0.0, unpipe@~1.0.0:
@@ -6590,7 +6509,7 @@ use@^3.1.0:
   resolved "https://registry.npmjs.org/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
   integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
 
-util-deprecate@^1.0.1, util-deprecate@~1.0.1:
+util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
   version "1.0.2"
   resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
   integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
diff --git a/mkdocs.yml b/mkdocs.yml
index 109be46a58..29198963e3 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -58,6 +58,8 @@ nav:
           - Using Cloud Storage: 'features/techdocs/using-cloud-storage.md'
           - Troubleshooting: 'features/techdocs/troubleshooting.md'
           - FAQ: 'features/techdocs/FAQ.md'
+      - Kubernetes:
+          - Overview: 'features/kubernetes/index.md'
   - Plugins:
       - Overview: 'plugins/index.md'
       - Existing plugins: 'plugins/existing-plugins.md'
diff --git a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts
index 030c99c151..10d66ac880 100644
--- a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts
+++ b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts
@@ -33,6 +33,7 @@ describe('ComponentV1alpha1Validator', () => {
         type: 'service',
         lifecycle: 'production',
         owner: 'me',
+        subcomponentOf: 'monolith',
         providesApis: ['api-0'],
         consumesApis: ['api-0'],
       },
@@ -103,6 +104,21 @@ describe('ComponentV1alpha1Validator', () => {
     await expect(validator.check(entity)).rejects.toThrow(/owner/);
   });
 
+  it('accepts missing subcomponentOf', async () => {
+    delete (entity as any).spec.subcomponentOf;
+    await expect(validator.check(entity)).resolves.toBe(true);
+  });
+
+  it('rejects wrong subcomponentOf', async () => {
+    (entity as any).spec.subcomponentOf = 7;
+    await expect(validator.check(entity)).rejects.toThrow(/subcomponentOf/);
+  });
+
+  it('rejects empty subcomponentOf', async () => {
+    (entity as any).spec.subcomponentOf = '';
+    await expect(validator.check(entity)).rejects.toThrow(/subcomponentOf/);
+  });
+
   it('accepts missing providesApis', async () => {
     delete (entity as any).spec.providesApis;
     await expect(validator.check(entity)).resolves.toBe(true);
diff --git a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts
index e511dcb24a..97519ad403 100644
--- a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts
+++ b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts
@@ -29,6 +29,7 @@ const schema = yup.object>({
       type: yup.string().required().min(1),
       lifecycle: yup.string().required().min(1),
       owner: yup.string().required().min(1),
+      subcomponentOf: yup.string().notRequired().min(1),
       providesApis: yup.array(yup.string().required()).notRequired(),
       consumesApis: yup.array(yup.string().required()).notRequired(),
     })
@@ -42,6 +43,7 @@ export interface ComponentEntityV1alpha1 extends Entity {
     type: string;
     lifecycle: string;
     owner: string;
+    subcomponentOf?: string;
     providesApis?: string[];
     consumesApis?: string[];
   };
diff --git a/packages/catalog-model/src/kinds/index.ts b/packages/catalog-model/src/kinds/index.ts
index 914d7efea7..e00a49acb5 100644
--- a/packages/catalog-model/src/kinds/index.ts
+++ b/packages/catalog-model/src/kinds/index.ts
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+export { schemaValidator } from './util';
+export type { KindValidator } from './types';
 export { apiEntityV1alpha1Validator } from './ApiEntityV1alpha1';
 export type {
   ApiEntityV1alpha1 as ApiEntity,
@@ -40,7 +42,6 @@ export type {
   TemplateEntityV1alpha1 as TemplateEntity,
   TemplateEntityV1alpha1,
 } from './TemplateEntityV1alpha1';
-export type { KindValidator } from './types';
 export { userEntityV1alpha1Validator } from './UserEntityV1alpha1';
 export type {
   UserEntityV1alpha1 as UserEntity,
diff --git a/packages/catalog-model/src/kinds/relations.ts b/packages/catalog-model/src/kinds/relations.ts
index 3d5d629b9e..78bbc61df2 100644
--- a/packages/catalog-model/src/kinds/relations.ts
+++ b/packages/catalog-model/src/kinds/relations.ts
@@ -55,3 +55,10 @@ export const RELATION_CHILD_OF = 'childOf';
  */
 export const RELATION_MEMBER_OF = 'memberOf';
 export const RELATION_HAS_MEMBER = 'hasMember';
+
+/**
+ * A part/whole relation, typically for components in a system and systems
+ * in a domain.
+ */
+export const RELATION_PART_OF = 'partOf';
+export const RELATION_HAS_PART = 'hasPart';
diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md
index 8b503c6941..adf041b4bb 100644
--- a/packages/cli/CHANGELOG.md
+++ b/packages/cli/CHANGELOG.md
@@ -21,7 +21,7 @@
 
 ### Patch Changes
 
-- 19554f6d6: Added Github Actions for Create React App, and allow better imports of files inside a module when they're exposed using `files` in `package.json`
+- 19554f6d6: Added GitHub Actions for Create React App, and allow better imports of files inside a module when they're exposed using `files` in `package.json`
 - 7d72f9b09: Fix for `app.listen.host` configuration not properly overriding listening host.
 
 ## 0.4.2
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 6ce4ba3715..277f710b5e 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -34,6 +34,7 @@
     "@hot-loader/react-dom": "^16.13.0",
     "@lerna/package-graph": "^3.18.5",
     "@lerna/project": "^3.18.0",
+    "@octokit/request": "^5.2.0",
     "@rollup/plugin-commonjs": "^16.0.0",
     "@rollup/plugin-json": "^4.0.2",
     "@rollup/plugin-node-resolve": "^9.0.0",
@@ -69,6 +70,7 @@
     "eslint-plugin-monorepo": "^0.3.2",
     "eslint-plugin-react": "^7.12.4",
     "eslint-plugin-react-hooks": "^4.0.0",
+    "express": "^4.17.1",
     "fork-ts-checker-webpack-plugin": "^4.0.5",
     "fs-extra": "^9.0.0",
     "handlebars": "^4.7.3",
@@ -118,6 +120,7 @@
     "@backstage/test-utils": "^0.1.6",
     "@backstage/theme": "^0.2.2",
     "@types/diff": "^4.0.2",
+    "@types/express": "^4.17.6",
     "@types/fs-extra": "^9.0.1",
     "@types/html-webpack-plugin": "^3.2.2",
     "@types/http-proxy": "^1.17.4",
diff --git a/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts b/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts
new file mode 100644
index 0000000000..406e563ebc
--- /dev/null
+++ b/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts
@@ -0,0 +1,152 @@
+/*
+ * 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 crypto from 'crypto';
+import openBrowser from 'react-dev-utils/openBrowser';
+import { request } from '@octokit/request';
+import express, { Express, Request, Response } from 'express';
+
+const MANIFEST_DATA = {
+  default_events: ['create', 'delete', 'push', 'repository'],
+  default_permissions: {
+    contents: 'read',
+    metadata: 'read',
+  },
+  name: 'Backstage-',
+  url: 'https://backstage.io',
+  description: 'GitHub App for Backstage',
+  public: false,
+};
+
+const FORM_PAGE = `
+
+  
+    
+ + +
+ + + +`; + +type GithubAppConfig = { + appId: number; + apiUrl: string; + slug?: string; + name?: string; + webhookUrl?: string; + clientId: string; + clientSecret: string; + webhookSecret: string; + privateKey: string; +}; + +export class GithubCreateAppServer { + private baseUrl?: string; + private webhookUrl?: string; + + static async run({ org }: { org: string }): Promise { + const encodedOrg = encodeURIComponent(org); + const actionUrl = `https://github.com/organizations/${encodedOrg}/settings/apps/new`; + const server = new GithubCreateAppServer(actionUrl); + return server.start(); + } + + constructor(private readonly actionUrl: string) { + const webhookId = crypto + .randomBytes(15) + .toString('base64') + .replace(/[\+\/]/g, ''); + + this.webhookUrl = `https://smee.io/${webhookId}`; + } + + private async start(): Promise { + const app = express(); + + app.get('/', this.formHandler); + + const callPromise = new Promise((resolve, reject) => { + app.get('/callback', (req, res) => { + request( + `POST /app-manifests/${encodeURIComponent( + req.query.code as string, + )}/conversions`, + ).then(({ data, url }) => { + // url = https://api.github.com/app-manifests//conversions + const apiUrl = url.replace(/(?:\/[^\/]+){3}$/, ''); + resolve({ + name: data.name, + slug: data.slug, + appId: data.id, + apiUrl, + webhookUrl: this.webhookUrl, + clientId: data.client_id, + clientSecret: data.client_secret, + webhookSecret: data.webhook_secret, + privateKey: data.pem, + }); + res.redirect(302, `${data.html_url}/installations/new`); + }, reject); + }); + }); + + this.baseUrl = await this.listen(app); + + openBrowser(this.baseUrl); + + return callPromise; + } + + private formHandler = (_req: Request, res: Response) => { + const baseUrl = this.baseUrl; + if (!baseUrl) { + throw new Error('baseUrl is not set'); + } + const manifest = { + ...MANIFEST_DATA, + redirect_url: `${baseUrl}/callback`, + hook_attributes: { + url: this.webhookUrl, + }, + }; + const manifestJson = JSON.stringify(manifest).replace(/\"/g, '"'); + + let body = FORM_PAGE; + body = body.replace('MANIFEST_JSON', manifestJson); + body = body.replace('ACTION_URL', this.actionUrl); + + res.setHeader('content-type', 'text/html'); + res.send(body); + }; + + private async listen(app: Express) { + return new Promise((resolve, reject) => { + const listener = app.listen(0, () => { + const info = listener.address(); + if (typeof info !== 'object' || info === null) { + reject(new Error(`Unexpected listener info '${info}'`)); + return; + } + const { port } = info; + resolve(`http://localhost:${port}`); + }); + }); + } +} diff --git a/packages/cli/src/commands/create-github-app/index.ts b/packages/cli/src/commands/create-github-app/index.ts new file mode 100644 index 0000000000..b8d232674e --- /dev/null +++ b/packages/cli/src/commands/create-github-app/index.ts @@ -0,0 +1,34 @@ +/* + * 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 fs from 'fs-extra'; +import chalk from 'chalk'; +import { stringify as stringifyYaml } from 'yaml'; +import { paths } from '../../lib/paths'; +import { GithubCreateAppServer } from './GithubCreateAppServer'; + +// This is an experimental command that at this point does not support GitHub Enterprise +// due to lacking support for creating apps from manifests. +// https://docs.github.com/en/free-pro-team@latest/developers/apps/creating-a-github-app-from-a-manifest +export default async (org: string) => { + const { slug, name, ...config } = await GithubCreateAppServer.run({ org }); + + const fileName = `github-app-${slug}.yaml`; + const content = `# Name: ${name}\n${stringifyYaml(config)}`; + await fs.writeFile(paths.resolveTargetRoot(fileName), content); + console.log(`GitHub App configuration written to ${chalk.cyan(fileName)}`); + // TODO: log instructions on how to use the newly created app configuration. +}; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 9dce57813b..1151e434c8 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -205,6 +205,13 @@ export function registerCommands(program: CommanderStatic) { .command('build-workspace ...') .description('Builds a temporary dist workspace from the provided packages') .action(lazy(() => import('./buildWorkspace').then(m => m.default))); + + program + .command('create-github-app ', { hidden: true }) + .description( + 'Create new GitHub App in your organization. This command is experimental and may change in the future.', + ) + .action(lazy(() => import('./create-github-app').then(m => m.default))); } // Wraps an action function so that it always exits and handles errors diff --git a/packages/core-api/src/apis/definitions/ErrorApi.ts b/packages/core-api/src/apis/definitions/ErrorApi.ts index dfa08130cd..6205f8e058 100644 --- a/packages/core-api/src/apis/definitions/ErrorApi.ts +++ b/packages/core-api/src/apis/definitions/ErrorApi.ts @@ -18,7 +18,7 @@ import { ApiRef, createApiRef } from '../system'; import { Observable } from '../../types'; /** - * Mirrors the javascript Error class, for the purpose of + * Mirrors the JavaScript Error class, for the purpose of * providing documentation and optional fields. */ type Error = { diff --git a/packages/core/src/layout/Sidebar/Items.test.tsx b/packages/core/src/layout/Sidebar/Items.test.tsx new file mode 100644 index 0000000000..099a3477d3 --- /dev/null +++ b/packages/core/src/layout/Sidebar/Items.test.tsx @@ -0,0 +1,58 @@ +/* + * 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 { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import HomeIcon from '@material-ui/icons/Home'; +import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; +import { Sidebar } from './Bar'; +import { SidebarItem } from './Items'; + +async function renderSidebar() { + await renderInTestApp( + + + {}} + text="Create..." + /> + , + ); + userEvent.hover(screen.getByTestId('sidebar-root')); +} + +describe('Items', () => { + beforeEach(async () => { + await renderSidebar(); + }); + + describe('SidebarItem', () => { + it('should render a link when `to` prop provided', async () => { + expect( + await screen.findByRole('link', { name: /home/i }), + ).toBeInTheDocument(); + }); + + it('should render a button when `to` prop is not provided', async () => { + expect( + await screen.findByRole('button', { name: /create/i }), + ).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/core/src/layout/Sidebar/Items.tsx b/packages/core/src/layout/Sidebar/Items.tsx index 929828cee4..e954dcb4f8 100644 --- a/packages/core/src/layout/Sidebar/Items.tsx +++ b/packages/core/src/layout/Sidebar/Items.tsx @@ -53,6 +53,15 @@ const useStyles = makeStyles(theme => { height: 48, cursor: 'pointer', }, + buttonItem: { + background: 'none', + border: 'none', + width: 'auto', + margin: 0, + padding: 0, + textAlign: 'inherit', + font: 'inherit', + }, closed: { width: drawerWidthClosed, justifyContent: 'center', @@ -114,100 +123,102 @@ const useStyles = makeStyles(theme => { }; }); -type SidebarItemProps = { +type SidebarItemBaseProps = { icon: IconComponent; text?: string; - // If 'to' is set the item will act as a nav link with highlight, otherwise it's just a button - to?: string; hasNotifications?: boolean; - onClick?: (ev: React.MouseEvent) => void; children?: ReactNode; }; -export const SidebarItem = forwardRef( - ( - { icon: Icon, text, to, hasNotifications = false, onClick, children }, - ref, - ) => { - const classes = useStyles(); - // XXX (@koroeskohr): unsure this is optimal. But I just really didn't want to have the item component - // depend on the current location, and at least have it being optionally forced to selected. - // Still waiting on a Q answered to fine tune the implementation - const { isOpen } = useContext(SidebarContext); +type SidebarItemButtonProps = SidebarItemBaseProps & { + onClick: (ev: React.MouseEvent) => void; +}; - const itemIcon = ( - - - - ); +type SidebarItemLinkProps = SidebarItemBaseProps & { + to: string; + onClick?: (ev: React.MouseEvent) => void; +}; - const childProps = { - onClick, - className: clsx(classes.root, isOpen ? classes.open : classes.closed), - }; +type SidebarItemProps = SidebarItemButtonProps | SidebarItemLinkProps; - if (!isOpen) { - if (to === undefined) { - return ( -
- {itemIcon} -
- ); - } +function isButtonItem( + props: SidebarItemProps, +): props is SidebarItemButtonProps { + return (props as SidebarItemLinkProps).to === undefined; +} - return ( - - {itemIcon} - - ); - } +export const SidebarItem = forwardRef((props, ref) => { + const { + icon: Icon, + text, + hasNotifications = false, + onClick, + children, + } = props; + const classes = useStyles(); + // XXX (@koroeskohr): unsure this is optimal. But I just really didn't want to have the item component + // depend on the current location, and at least have it being optionally forced to selected. + // Still waiting on a Q answered to fine tune the implementation + const { isOpen } = useContext(SidebarContext); - const content = ( - <> -
- {itemIcon} -
- {text && ( - - {text} - - )} -
{children}
- - ); + const itemIcon = ( + + + + ); - if (to === undefined) { - return ( -
- {content} -
- ); - } + const closedContent = itemIcon; + const openContent = ( + <> +
+ {itemIcon} +
+ {text && ( + + {text} + + )} +
{children}
+ + ); + + const content = isOpen ? openContent : closedContent; + + const childProps = { + onClick, + className: clsx( + classes.root, + isOpen ? classes.open : classes.closed, + isButtonItem(props) && classes.buttonItem, + ), + }; + + if (isButtonItem(props)) { return ( - + ); - }, -); + } + + return ( + + {content} + + ); +}); type SidebarSearchFieldProps = { onSearch: (input: string) => void; diff --git a/packages/integration/src/ScmIntegrations.test.ts b/packages/integration/src/ScmIntegrations.test.ts new file mode 100644 index 0000000000..b43e69eba4 --- /dev/null +++ b/packages/integration/src/ScmIntegrations.test.ts @@ -0,0 +1,76 @@ +/* + * 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 { AzureIntegrationConfig } from './azure'; +import { AzureIntegration } from './azure/AzureIntegration'; +import { BitbucketIntegrationConfig } from './bitbucket'; +import { BitbucketIntegration } from './bitbucket/BitbucketIntegration'; +import { GitHubIntegrationConfig } from './github'; +import { GitHubIntegration } from './github/GitHubIntegration'; +import { GitLabIntegrationConfig } from './gitlab'; +import { GitLabIntegration } from './gitlab/GitLabIntegration'; +import { basicIntegrations } from './helpers'; +import { ScmIntegrations } from './ScmIntegrations'; + +describe('ScmIntegrations', () => { + const azure = new AzureIntegration({ + host: 'azure.local', + } as AzureIntegrationConfig); + + const bitbucket = new BitbucketIntegration({ + host: 'bitbucket.local', + } as BitbucketIntegrationConfig); + + const github = new GitHubIntegration({ + host: 'github.local', + } as GitHubIntegrationConfig); + + const gitlab = new GitLabIntegration({ + host: 'gitlab.local', + } as GitLabIntegrationConfig); + + const i = new ScmIntegrations({ + azure: basicIntegrations([azure], i => i.config.host), + bitbucket: basicIntegrations([bitbucket], i => i.config.host), + github: basicIntegrations([github], i => i.config.host), + gitlab: basicIntegrations([gitlab], i => i.config.host), + }); + + it('can get the specifics', () => { + expect(i.azure.byUrl('https://azure.local')).toBe(azure); + expect(i.bitbucket.byUrl('https://bitbucket.local')).toBe(bitbucket); + expect(i.github.byUrl('https://github.local')).toBe(github); + expect(i.gitlab.byUrl('https://gitlab.local')).toBe(gitlab); + }); + + it('can list', () => { + expect(i.list()).toEqual( + expect.arrayContaining([azure, bitbucket, github, gitlab]), + ); + }); + + it('can select by url and host', () => { + expect(i.byUrl('https://azure.local')).toBe(azure); + expect(i.byUrl('https://bitbucket.local')).toBe(bitbucket); + expect(i.byUrl('https://github.local')).toBe(github); + expect(i.byUrl('https://gitlab.local')).toBe(gitlab); + + expect(i.byHost('azure.local')).toBe(azure); + expect(i.byHost('bitbucket.local')).toBe(bitbucket); + expect(i.byHost('github.local')).toBe(github); + expect(i.byHost('gitlab.local')).toBe(gitlab); + }); +}); diff --git a/packages/integration/src/ScmIntegrations.ts b/packages/integration/src/ScmIntegrations.ts index 515a32502d..102273a03b 100644 --- a/packages/integration/src/ScmIntegrations.ts +++ b/packages/integration/src/ScmIntegrations.ts @@ -21,27 +21,64 @@ import { GitHubIntegration } from './github/GitHubIntegration'; import { GitLabIntegration } from './gitlab/GitLabIntegration'; import { ScmIntegration, - ScmIntegrationPredicateTuple, ScmIntegrationRegistry, + ScmIntegrationsGroup, } from './types'; +type IntegrationsByType = { + azure: ScmIntegrationsGroup; + bitbucket: ScmIntegrationsGroup; + github: ScmIntegrationsGroup; + gitlab: ScmIntegrationsGroup; +}; + export class ScmIntegrations implements ScmIntegrationRegistry { + private readonly byType: IntegrationsByType; + static fromConfig(config: Config): ScmIntegrations { - return new ScmIntegrations([ - ...AzureIntegration.factory({ config }), - ...BitbucketIntegration.factory({ config }), - ...GitHubIntegration.factory({ config }), - ...GitLabIntegration.factory({ config }), - ]); + return new ScmIntegrations({ + azure: AzureIntegration.factory({ config }), + bitbucket: BitbucketIntegration.factory({ config }), + github: GitHubIntegration.factory({ config }), + gitlab: GitLabIntegration.factory({ config }), + }); } - constructor(private readonly integrations: ScmIntegrationPredicateTuple[]) {} + constructor(integrationsByType: IntegrationsByType) { + this.byType = integrationsByType; + } + + get azure(): ScmIntegrationsGroup { + return this.byType.azure; + } + + get bitbucket(): ScmIntegrationsGroup { + return this.byType.bitbucket; + } + + get github(): ScmIntegrationsGroup { + return this.byType.github; + } + + get gitlab(): ScmIntegrationsGroup { + return this.byType.gitlab; + } list(): ScmIntegration[] { - return this.integrations.map(i => i.integration); + return Object.values(this.byType).flatMap( + i => i.list() as ScmIntegration[], + ); } - byUrl(url: string): ScmIntegration | undefined { - return this.integrations.find(i => i.predicate(new URL(url)))?.integration; + byUrl(url: string | URL): ScmIntegration | undefined { + return Object.values(this.byType) + .map(i => i.byUrl(url)) + .find(Boolean); + } + + byHost(host: string): ScmIntegration | undefined { + return Object.values(this.byType) + .map(i => i.byHost(host)) + .find(Boolean); } } diff --git a/packages/integration/src/azure/AzureIntegration.test.ts b/packages/integration/src/azure/AzureIntegration.test.ts index 4a0c32badb..90c098b637 100644 --- a/packages/integration/src/azure/AzureIntegration.test.ts +++ b/packages/integration/src/azure/AzureIntegration.test.ts @@ -31,8 +31,9 @@ describe('AzureIntegration', () => { }, }), }); - expect(integrations.length).toBe(2); // including default - expect(integrations[0].predicate(new URL('https://h.com/a'))).toBe(true); + expect(integrations.list().length).toBe(2); // including default + expect(integrations.list()[0].config.host).toBe('h.com'); + expect(integrations.list()[1].config.host).toBe('dev.azure.com'); }); it('returns the basics', () => { diff --git a/packages/integration/src/azure/AzureIntegration.ts b/packages/integration/src/azure/AzureIntegration.ts index 84dc3800d2..446e6a9480 100644 --- a/packages/integration/src/azure/AzureIntegration.ts +++ b/packages/integration/src/azure/AzureIntegration.ts @@ -14,27 +14,32 @@ * limitations under the License. */ -import { ScmIntegration, ScmIntegrationFactory } from '../types'; +import { basicIntegrations } from '../helpers'; +import { ScmIntegration, ScmIntegrationsFactory } from '../types'; import { AzureIntegrationConfig, readAzureIntegrationConfigs } from './config'; export class AzureIntegration implements ScmIntegration { - static factory: ScmIntegrationFactory = ({ config }) => { + static factory: ScmIntegrationsFactory = ({ config }) => { const configs = readAzureIntegrationConfigs( config.getOptionalConfigArray('integrations.azure') ?? [], ); - return configs.map(integration => ({ - predicate: (url: URL) => url.host === integration.host, - integration: new AzureIntegration(integration), - })); + return basicIntegrations( + configs.map(c => new AzureIntegration(c)), + i => i.config.host, + ); }; - constructor(private readonly config: AzureIntegrationConfig) {} + constructor(private readonly integrationConfig: AzureIntegrationConfig) {} get type(): string { return 'azure'; } get title(): string { - return this.config.host; + return this.integrationConfig.host; + } + + get config(): AzureIntegrationConfig { + return this.integrationConfig; } } diff --git a/packages/integration/src/bitbucket/BitbucketIntegration.test.ts b/packages/integration/src/bitbucket/BitbucketIntegration.test.ts index 87dbac2263..3f130a393c 100644 --- a/packages/integration/src/bitbucket/BitbucketIntegration.test.ts +++ b/packages/integration/src/bitbucket/BitbucketIntegration.test.ts @@ -34,8 +34,9 @@ describe('BitbucketIntegration', () => { }, }), }); - expect(integrations.length).toBe(2); // including default - expect(integrations[0].predicate(new URL('https://h.com/a'))).toBe(true); + expect(integrations.list().length).toBe(2); // including default + expect(integrations.list()[0].config.host).toBe('h.com'); + expect(integrations.list()[1].config.host).toBe('bitbucket.org'); }); it('returns the basics', () => { diff --git a/packages/integration/src/bitbucket/BitbucketIntegration.ts b/packages/integration/src/bitbucket/BitbucketIntegration.ts index b271e2f408..f3e69b946a 100644 --- a/packages/integration/src/bitbucket/BitbucketIntegration.ts +++ b/packages/integration/src/bitbucket/BitbucketIntegration.ts @@ -14,30 +14,37 @@ * limitations under the License. */ -import { ScmIntegration, ScmIntegrationFactory } from '../types'; +import { basicIntegrations } from '../helpers'; +import { ScmIntegration, ScmIntegrationsFactory } from '../types'; import { BitbucketIntegrationConfig, readBitbucketIntegrationConfigs, } from './config'; export class BitbucketIntegration implements ScmIntegration { - static factory: ScmIntegrationFactory = ({ config }) => { + static factory: ScmIntegrationsFactory = ({ + config, + }) => { const configs = readBitbucketIntegrationConfigs( config.getOptionalConfigArray('integrations.bitbucket') ?? [], ); - return configs.map(integration => ({ - predicate: (url: URL) => url.host === integration.host, - integration: new BitbucketIntegration(integration), - })); + return basicIntegrations( + configs.map(c => new BitbucketIntegration(c)), + i => i.config.host, + ); }; - constructor(private readonly config: BitbucketIntegrationConfig) {} + constructor(private readonly integrationConfig: BitbucketIntegrationConfig) {} get type(): string { return 'bitbucket'; } get title(): string { - return this.config.host; + return this.integrationConfig.host; + } + + get config(): BitbucketIntegrationConfig { + return this.integrationConfig; } } diff --git a/packages/integration/src/github/GitHubIntegration.test.ts b/packages/integration/src/github/GitHubIntegration.test.ts index 0c326d81cd..9056517f32 100644 --- a/packages/integration/src/github/GitHubIntegration.test.ts +++ b/packages/integration/src/github/GitHubIntegration.test.ts @@ -33,13 +33,20 @@ describe('GitHubIntegration', () => { }, }), }); - expect(integrations.length).toBe(2); // including default - expect(integrations[0].predicate(new URL('https://h.com/a'))).toBe(true); + expect(integrations.list().length).toBe(2); // including default + expect(integrations.list()[0].config.host).toBe('h.com'); + expect(integrations.list()[1].config.host).toBe('github.com'); }); it('returns the basics', () => { - const integration = new GitHubIntegration({ host: 'h.com' } as any); + const integration = new GitHubIntegration({ + host: 'h.com', + apiBaseUrl: 'a', + rawBaseUrl: 'r', + token: 't', + }); expect(integration.type).toBe('github'); expect(integration.title).toBe('h.com'); + expect(integration.config.host).toBe('h.com'); }); }); diff --git a/packages/integration/src/github/GitHubIntegration.ts b/packages/integration/src/github/GitHubIntegration.ts index 92c5951873..c103597d74 100644 --- a/packages/integration/src/github/GitHubIntegration.ts +++ b/packages/integration/src/github/GitHubIntegration.ts @@ -14,30 +14,35 @@ * limitations under the License. */ -import { ScmIntegration, ScmIntegrationFactory } from '../types'; +import { basicIntegrations } from '../helpers'; +import { ScmIntegration, ScmIntegrationsFactory } from '../types'; import { GitHubIntegrationConfig, readGitHubIntegrationConfigs, } from './config'; export class GitHubIntegration implements ScmIntegration { - static factory: ScmIntegrationFactory = ({ config }) => { + static factory: ScmIntegrationsFactory = ({ config }) => { const configs = readGitHubIntegrationConfigs( config.getOptionalConfigArray('integrations.github') ?? [], ); - return configs.map(integration => ({ - predicate: (url: URL) => url.host === integration.host, - integration: new GitHubIntegration(integration), - })); + return basicIntegrations( + configs.map(c => new GitHubIntegration(c)), + i => i.config.host, + ); }; - constructor(private readonly config: GitHubIntegrationConfig) {} + constructor(private readonly integrationConfig: GitHubIntegrationConfig) {} get type(): string { return 'github'; } get title(): string { - return this.config.host; + return this.integrationConfig.host; + } + + get config(): GitHubIntegrationConfig { + return this.integrationConfig; } } diff --git a/packages/integration/src/gitlab/GitLabIntegration.test.ts b/packages/integration/src/gitlab/GitLabIntegration.test.ts index 260afd23d8..8814e33302 100644 --- a/packages/integration/src/gitlab/GitLabIntegration.test.ts +++ b/packages/integration/src/gitlab/GitLabIntegration.test.ts @@ -31,8 +31,9 @@ describe('GitLabIntegration', () => { }, }), }); - expect(integrations.length).toBe(2); // including default - expect(integrations[0].predicate(new URL('https://h.com/a'))).toBe(true); + expect(integrations.list().length).toBe(2); // including default + expect(integrations.list()[0].config.host).toBe('h.com'); + expect(integrations.list()[1].config.host).toBe('gitlab.com'); }); it('returns the basics', () => { diff --git a/packages/integration/src/gitlab/GitLabIntegration.ts b/packages/integration/src/gitlab/GitLabIntegration.ts index 4d035cb24e..d939917366 100644 --- a/packages/integration/src/gitlab/GitLabIntegration.ts +++ b/packages/integration/src/gitlab/GitLabIntegration.ts @@ -14,30 +14,35 @@ * limitations under the License. */ -import { ScmIntegration, ScmIntegrationFactory } from '../types'; +import { basicIntegrations } from '../helpers'; +import { ScmIntegration, ScmIntegrationsFactory } from '../types'; import { GitLabIntegrationConfig, readGitLabIntegrationConfigs, } from './config'; export class GitLabIntegration implements ScmIntegration { - static factory: ScmIntegrationFactory = ({ config }) => { + static factory: ScmIntegrationsFactory = ({ config }) => { const configs = readGitLabIntegrationConfigs( config.getOptionalConfigArray('integrations.gitlab') ?? [], ); - return configs.map(integration => ({ - predicate: (url: URL) => url.host === integration.host, - integration: new GitLabIntegration(integration), - })); + return basicIntegrations( + configs.map(c => new GitLabIntegration(c)), + i => i.config.host, + ); }; - constructor(private readonly config: GitLabIntegrationConfig) {} + constructor(private readonly integrationConfig: GitLabIntegrationConfig) {} get type(): string { return 'gitlab'; } get title(): string { - return this.config.host; + return this.integrationConfig.host; + } + + get config(): GitLabIntegrationConfig { + return this.integrationConfig; } } diff --git a/packages/integration/src/helpers.ts b/packages/integration/src/helpers.ts index 02393f99e1..cc1c59a238 100644 --- a/packages/integration/src/helpers.ts +++ b/packages/integration/src/helpers.ts @@ -14,9 +14,29 @@ * limitations under the License. */ +import { ScmIntegration, ScmIntegrationsGroup } from './types'; + /** Checks whether the given url is a valid host */ export function isValidHost(url: string): boolean { const check = new URL('http://example.com'); check.host = url; return check.host === url; } + +export function basicIntegrations( + integrations: T[], + getHost: (integration: T) => string, +): ScmIntegrationsGroup { + return { + list(): T[] { + return integrations; + }, + byUrl(url: string | URL): T | undefined { + const parsed = typeof url === 'string' ? new URL(url) : url; + return integrations.find(i => getHost(i) === parsed.hostname); + }, + byHost(host: string): T | undefined { + return integrations.find(i => getHost(i) === host); + }, + }; +} diff --git a/packages/integration/src/types.ts b/packages/integration/src/types.ts index ae9c360980..d8fb7a14e2 100644 --- a/packages/integration/src/types.ts +++ b/packages/integration/src/types.ts @@ -15,11 +15,15 @@ */ import { Config } from '@backstage/config'; +import { AzureIntegration } from './azure/AzureIntegration'; +import { BitbucketIntegration } from './bitbucket/BitbucketIntegration'; +import { GitHubIntegration } from './github/GitHubIntegration'; +import { GitLabIntegration } from './gitlab/GitLabIntegration'; /** * Encapsulates a single SCM integration. */ -export type ScmIntegration = { +export interface ScmIntegration { /** * The type of integration, e.g. "github". */ @@ -30,30 +34,43 @@ export type ScmIntegration = { * differentiate between different integrations. */ title: string; -}; +} /** - * Holds all registered SCM integrations. + * Encapsulates several integrations, that are all of the same type. */ -export type ScmIntegrationRegistry = { +export interface ScmIntegrationsGroup { /** - * Lists all registered integrations. + * Lists all registered integrations of this type. */ - list(): ScmIntegration[]; + list(): T[]; /** - * Fetches an integration by URL. + * Fetches an integration of this type by URL. * - * @param url A URL that matches a registered integration + * @param url A URL that matches a registered integration of this type */ - byUrl(url: string): ScmIntegration | undefined; -}; + byUrl(url: string | URL): T | undefined; -export type ScmIntegrationPredicateTuple = { - predicate: (url: URL) => boolean; - integration: ScmIntegration; -}; + /** + * Fetches an integration of this type by host name. + * + * @param url A host name that matches a registered integration of this type + */ + byHost(host: string): T | undefined; +} -export type ScmIntegrationFactory = (options: { +/** + * Holds all registered SCM integrations, of all types. + */ +export interface ScmIntegrationRegistry + extends ScmIntegrationsGroup { + azure: ScmIntegrationsGroup; + bitbucket: ScmIntegrationsGroup; + github: ScmIntegrationsGroup; + gitlab: ScmIntegrationsGroup; +} + +export type ScmIntegrationsFactory = (options: { config: Config; -}) => ScmIntegrationPredicateTuple[]; +}) => ScmIntegrationsGroup; diff --git a/packages/techdocs-common/__mocks__/aws-sdk.ts b/packages/techdocs-common/__mocks__/@aws-sdk/client-s3.ts similarity index 52% rename from packages/techdocs-common/__mocks__/aws-sdk.ts rename to packages/techdocs-common/__mocks__/@aws-sdk/client-s3.ts index 987b3d2800..2d2581a6cd 100644 --- a/packages/techdocs-common/__mocks__/aws-sdk.ts +++ b/packages/techdocs-common/__mocks__/@aws-sdk/client-s3.ts @@ -13,74 +13,49 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type { S3 as S3Types } from 'aws-sdk'; +import type { S3ClientConfig } from '@aws-sdk/client-s3'; import { EventEmitter } from 'events'; import fs from 'fs'; export class S3 { private readonly options; - constructor(options: S3Types.ClientConfiguration) { + constructor(options: S3ClientConfig) { this.options = options; } headObject({ Key }: { Key: string }) { - return { - promise: () => this.checkFileExists(Key), - }; - } - - getObject({ Key }: { Key: string }) { - return { - promise: () => this.checkFileExists(Key), - createReadStream: () => { - const emitter = new EventEmitter(); - process.nextTick(() => { - if (fs.existsSync(Key)) { - emitter.emit('data', Buffer.from(fs.readFileSync(Key))); - } else { - emitter.emit( - 'error', - new Error(`The file ${Key} doest not exist !`), - ); - } - emitter.emit('end'); - }); - return emitter; - }, - }; - } - - checkFileExists(Key: string) { return new Promise((resolve, reject) => { if (fs.existsSync(Key)) { resolve(''); } else { - reject({ message: 'The object doest not exist !' }); + reject({ message: `The file ${Key} doest not exist.` }); + } + }); + } + + getObject({ Key }: { Key: string }) { + return new Promise((resolve, reject) => { + if (fs.existsSync(Key)) { + const emitter = new EventEmitter(); + process.nextTick(() => { + emitter.emit('data', Buffer.from(fs.readFileSync(Key))); + emitter.emit('end'); + }); + resolve({ + Body: emitter, + }); + } else { + reject({ message: `The file ${Key} doest not exist.` }); } }); } headBucket() { - return new Promise(resolve => { - resolve(''); - }); + return ''; } - upload({ Key }: { Key: string }) { - return { - promise: () => - new Promise((resolve, reject) => { - if (!fs.existsSync(Key)) { - reject(''); - } else { - resolve(''); - } - }), - }; + putObject() { + return ''; } } - -export default { - S3, -}; diff --git a/packages/techdocs-common/__mocks__/@google-cloud/storage.ts b/packages/techdocs-common/__mocks__/@google-cloud/storage.ts index e95cee11d0..b84018c089 100644 --- a/packages/techdocs-common/__mocks__/@google-cloud/storage.ts +++ b/packages/techdocs-common/__mocks__/@google-cloud/storage.ts @@ -14,7 +14,6 @@ * limitations under the License. */ type storageOptions = { - projectId?: string; keyFilename?: string; }; @@ -39,11 +38,9 @@ class Bucket { } export class Storage { - private readonly projectId; private readonly keyFilename; constructor(options: storageOptions) { - this.projectId = options.projectId; this.keyFilename = options.keyFilename; } diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index ab83f9b262..c95f415ad3 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -36,6 +36,7 @@ "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { + "@aws-sdk/client-s3": "^3.1.0", "@backstage/backend-common": "^0.4.2", "@backstage/catalog-model": "^0.6.0", "@backstage/config": "^0.1.2", @@ -43,7 +44,6 @@ "@google-cloud/storage": "^5.6.0", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", - "aws-sdk": "^2.817.0", "cross-fetch": "^3.0.6", "dockerode": "^3.2.1", "express": "^4.17.1", @@ -56,6 +56,7 @@ "winston": "^3.2.1" }, "devDependencies": { + "@aws-sdk/types": "3.1.0", "@backstage/cli": "^0.4.5", "@types/fs-extra": "^9.0.5", "@types/git-url-parse": "^9.0.0", diff --git a/packages/techdocs-common/src/stages/generate/helpers.test.ts b/packages/techdocs-common/src/stages/generate/helpers.test.ts index 48a48986f2..4d62648a7d 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.test.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.test.ts @@ -80,14 +80,14 @@ describe('helpers', () => { const imageName = 'spotify/techdocs'; const args = ['build', '-d', '/result']; const docsDir = os.tmpdir(); - const resultDir = os.tmpdir(); + const outputDir = os.tmpdir(); it('should pull the techdocs docker container', async () => { await runDockerContainer({ imageName, args, docsDir, - resultDir, + outputDir, dockerClient: mockDocker, }); @@ -103,7 +103,7 @@ describe('helpers', () => { imageName, args, docsDir, - resultDir, + outputDir, dockerClient: mockDocker, }); @@ -118,7 +118,7 @@ describe('helpers', () => { }, WorkingDir: '/content', HostConfig: { - Binds: [`${docsDir}:/content`, `${resultDir}:/result`], + Binds: [`${docsDir}:/content`, `${outputDir}:/result`], }, }, ); @@ -129,7 +129,7 @@ describe('helpers', () => { imageName, args, docsDir, - resultDir, + outputDir, dockerClient: mockDocker, }); @@ -151,7 +151,7 @@ describe('helpers', () => { imageName, args, docsDir, - resultDir, + outputDir, dockerClient: mockDocker, }), ).rejects.toThrow(new RegExp(`.+: ${dockerError}`)); diff --git a/packages/techdocs-common/src/stages/generate/helpers.ts b/packages/techdocs-common/src/stages/generate/helpers.ts index 16f5b26632..e396157b92 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.ts @@ -39,7 +39,7 @@ type RunDockerContainerOptions = { args: string[]; logStream?: Writable; docsDir: string; - resultDir: string; + outputDir: string; dockerClient: Docker; createOptions?: Docker.ContainerCreateOptions; }; @@ -56,7 +56,7 @@ export async function runDockerContainer({ args, logStream = new PassThrough(), docsDir, - resultDir, + outputDir, dockerClient, createOptions, }: RunDockerContainerOptions) { @@ -89,7 +89,7 @@ export async function runDockerContainer({ }, WorkingDir: '/content', HostConfig: { - Binds: [`${docsDir}:/content`, `${resultDir}:/result`], + Binds: [`${docsDir}:/content`, `${outputDir}:/result`], }, ...createOptions, }, diff --git a/packages/techdocs-common/src/stages/generate/techdocs.ts b/packages/techdocs-common/src/stages/generate/techdocs.ts index faefd8ac16..507b9da6b8 100644 --- a/packages/techdocs-common/src/stages/generate/techdocs.ts +++ b/packages/techdocs-common/src/stages/generate/techdocs.ts @@ -14,18 +14,12 @@ * limitations under the License. */ -import fs from 'fs-extra'; import path from 'path'; -import os from 'os'; import { Logger } from 'winston'; import { PassThrough } from 'stream'; import { Config } from '@backstage/config'; -import { - GeneratorBase, - GeneratorRunOptions, - GeneratorRunResult, -} from './types'; +import { GeneratorBase, GeneratorRunOptions } from './types'; import { runDockerContainer, runCommand, @@ -64,40 +58,37 @@ export class TechdocsGenerator implements GeneratorBase { } public async run({ - directory, + inputDir, + outputDir, dockerClient, parsedLocationAnnotation, - }: GeneratorRunOptions): Promise { - const tmpdirPath = os.tmpdir(); - // Fixes a problem with macOS returning a path that is a symlink - const tmpdirResolvedPath = fs.realpathSync(tmpdirPath); - const resultDir = fs.mkdtempSync( - path.join(tmpdirResolvedPath, 'techdocs-tmp-'), - ); + }: GeneratorRunOptions): Promise { const [log, logStream] = createStream(); // TODO: In future mkdocs.yml can be mkdocs.yaml. So, use a config variable here to find out // the correct file name. // Do some updates to mkdocs.yml before generating docs e.g. adding repo_url - await patchMkdocsYmlPreBuild( - path.join(directory, 'mkdocs.yml'), - this.logger, - parsedLocationAnnotation, - ); + if (parsedLocationAnnotation) { + await patchMkdocsYmlPreBuild( + path.join(inputDir, 'mkdocs.yml'), + this.logger, + parsedLocationAnnotation, + ); + } try { switch (this.options.runGeneratorIn) { case 'local': await runCommand({ command: 'mkdocs', - args: ['build', '-d', resultDir, '-v'], + args: ['build', '-d', outputDir, '-v'], options: { - cwd: directory, + cwd: inputDir, }, logStream, }); this.logger.info( - `Successfully generated docs from ${directory} into ${resultDir} using local mkdocs`, + `Successfully generated docs from ${inputDir} into ${outputDir} using local mkdocs`, ); break; case 'docker': @@ -105,12 +96,12 @@ export class TechdocsGenerator implements GeneratorBase { imageName: 'spotify/techdocs', args: ['build', '-d', '/result'], logStream, - docsDir: directory, - resultDir, + docsDir: inputDir, + outputDir, dockerClient, }); this.logger.info( - `Successfully generated docs from ${directory} into ${resultDir} using techdocs-container`, + `Successfully generated docs from ${inputDir} into ${outputDir} using techdocs-container`, ); break; default: @@ -120,14 +111,12 @@ export class TechdocsGenerator implements GeneratorBase { } } catch (error) { this.logger.debug( - `Failed to generate docs from ${directory} into ${resultDir}`, + `Failed to generate docs from ${inputDir} into ${outputDir}`, ); this.logger.debug(`Build failed with error: ${log}`); throw new Error( - `Failed to generate docs from ${directory} into ${resultDir} with error ${error.message}`, + `Failed to generate docs from ${inputDir} into ${outputDir} with error ${error.message}`, ); } - - return { resultDir }; } } diff --git a/packages/techdocs-common/src/stages/generate/types.ts b/packages/techdocs-common/src/stages/generate/types.ts index 65e411c3aa..7724107dac 100644 --- a/packages/techdocs-common/src/stages/generate/types.ts +++ b/packages/techdocs-common/src/stages/generate/types.ts @@ -18,32 +18,26 @@ import Docker from 'dockerode'; import { Entity } from '@backstage/catalog-model'; import { ParsedLocationAnnotation } from '../../helpers'; -/** - * The returned directory from the generator which is ready - * to pass to the next stage of the TechDocs which is publishing - */ -export type GeneratorRunResult = { - resultDir: string; -}; - /** * The values that the generator will receive. * - * @param {string} directory The directory of the uncompiled documentation, with the values from the frontend + * @param {string} inputDir The directory of the uncompiled documentation, with the values from the frontend + * @param {string} outputDir Directory to store generated docs in. Usually - a newly created temporary directory. * @param {Docker} dockerClient A docker client to run any generator on top of your directory * @param {ParsedLocationAnnotation} parsedLocationAnnotation backstage.io/techdocs-ref annotation of an entity * @param {Writable} [logStream] A dedicated log stream */ export type GeneratorRunOptions = { - directory: string; + inputDir: string; + outputDir: string; dockerClient: Docker; - parsedLocationAnnotation: ParsedLocationAnnotation; + parsedLocationAnnotation?: ParsedLocationAnnotation; logStream?: Writable; }; export type GeneratorBase = { - // runs the generator with the values and returns the directory to be published - run(opts: GeneratorRunOptions): Promise; + // Runs the generator with the values + run(opts: GeneratorRunOptions): Promise; }; /** diff --git a/packages/techdocs-common/src/stages/prepare/index.ts b/packages/techdocs-common/src/stages/prepare/index.ts index 6c73725a3a..dcb178bafa 100644 --- a/packages/techdocs-common/src/stages/prepare/index.ts +++ b/packages/techdocs-common/src/stages/prepare/index.ts @@ -17,4 +17,4 @@ export { DirectoryPreparer } from './dir'; export { CommonGitPreparer } from './commonGit'; export { UrlPreparer } from './url'; export { Preparers } from './preparers'; -export type { PreparerBuilder, PreparerBase } from './types'; +export type { PreparerBuilder, PreparerBase, RemoteProtocol } from './types'; diff --git a/packages/techdocs-common/src/stages/publish/awsS3.test.ts b/packages/techdocs-common/src/stages/publish/awsS3.test.ts old mode 100644 new mode 100755 index a1d52be85b..c157a7c205 --- a/packages/techdocs-common/src/stages/publish/awsS3.test.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.test.ts @@ -102,12 +102,6 @@ describe('AwsS3Publish', () => { }); it('should fail to publish a directory', async () => { - const wrongPathToGeneratedDirectory = path.join( - 'wrong', - 'path', - 'to', - 'generatedDirectory', - ); const entity = createMockEntity(); const entityRootDir = getEntityRootDir(entity); @@ -124,13 +118,11 @@ describe('AwsS3Publish', () => { await publisher .publish({ entity, - directory: wrongPathToGeneratedDirectory, + directory: '/wrong/path/to/generatedDirectory', }) .catch(error => - expect(error).toEqual( - new Error( - `Unable to upload file(s) to AWS S3. Error Failed to read template directory: ENOENT, no such file or directory '${wrongPathToGeneratedDirectory}'`, - ), + expect(error.message).toContain( + 'Unable to upload file(s) to AWS S3. Error Failed to read template directory', ), ); mockFs.restore(); @@ -180,14 +172,17 @@ describe('AwsS3Publish', () => { it('should return an error if the techdocs_metadata.json file is not present', async () => { const entityNameMock = createMockEntityName(); const entity = createMockEntity(); - const entityRootDir = getEntityRootDir(entity); + const { + metadata: { name, namespace }, + kind, + } = entity; await publisher .fetchTechDocsMetadata(entityNameMock) .catch(error => expect(error).toEqual( new Error( - `TechDocs metadata fetch failed, The file ${entityRootDir}/techdocs_metadata.json doest not exist !`, + `TechDocs metadata fetch failed, The file ${namespace}/${kind}/${name}/techdocs_metadata.json doest not exist.`, ), ), ); diff --git a/packages/techdocs-common/src/stages/publish/awsS3.ts b/packages/techdocs-common/src/stages/publish/awsS3.ts index 15ff33408a..7a21ae6475 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.ts @@ -15,41 +15,72 @@ */ import path from 'path'; import express from 'express'; -import aws from 'aws-sdk'; +import { PutObjectCommandOutput, S3 } from '@aws-sdk/client-s3'; import { Logger } from 'winston'; import { Entity, EntityName } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { getHeadersForFileExtension, getFileTreeRecursively } from './helpers'; import { PublisherBase, PublishRequest } from './types'; -import { ManagedUpload } from 'aws-sdk/clients/s3'; import fs from 'fs-extra'; +import { Readable } from 'stream'; + +const streamToString = (stream: Readable): Promise => { + return new Promise((resolve, reject) => { + try { + const chunks: any[] = []; + stream.on('data', chunk => chunks.push(chunk)); + stream.on('error', reject); + stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))); + } catch (e) { + throw new Error(`Unable to parse the response data, ${e.message}`); + } + }); +}; export class AwsS3Publish implements PublisherBase { static fromConfig(config: Config, logger: Logger): PublisherBase { - let region = null; - let accessKeyId = null; - let secretAccessKey = null; let bucketName = ''; try { - accessKeyId = config.getString( - 'techdocs.publisher.awsS3.credentials.accessKeyId', - ); - secretAccessKey = config.getString( - 'techdocs.publisher.awsS3.credentials.secretAccessKey', - ); - region = config.getOptionalString('techdocs.publisher.awsS3.region'); bucketName = config.getString('techdocs.publisher.awsS3.bucketName'); } catch (error) { throw new Error( "Since techdocs.publisher.type is set to 'awsS3' in your app config, " + - 'credentials and bucketName are required in techdocs.publisher.awsS3 ' + - 'required to authenticate with AWS S3.', + 'techdocs.publisher.awsS3.bucketName is required.', ); } - const storageClient = new aws.S3({ - credentials: { accessKeyId, secretAccessKey }, - ...(region && { region }), + // Credentials is an optional config. If missing, default AWS environment variables + // or AWS shared credentials file at ~/.aws/credentials will be used to authenticate + // https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-environment.html + // https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-shared.html + const credentials = config.getOptionalConfig( + 'techdocs.publisher.awsS3.credentials', + ); + let accessKeyId = undefined; + let secretAccessKey = undefined; + if (credentials) { + accessKeyId = credentials.getOptionalString('accessKeyId'); + secretAccessKey = credentials.getOptionalString('secretAccessKey'); + } + + // AWS Region is an optional config. If missing, default AWS env variable AWS_REGION + // or AWS shared credentials file at ~/.aws/credentials will be used. Any way, AWS SDK v3 client needs + // to have the AWS Region information for it to work. + // https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-region.html + const region = config.getOptionalString('techdocs.publisher.awsS3.region'); + + const storageClient = new S3({ + ...(credentials && + accessKeyId && + secretAccessKey && { + credentials: { + accessKeyId, + secretAccessKey, + }, + }), + ...(region && { + region, + }), }); // Check if the defined bucket exists. Being able to connect means the configuration is good @@ -62,11 +93,12 @@ export class AwsS3Publish implements PublisherBase { if (err) { logger.error( `Could not retrieve metadata about the AWS S3 bucket ${bucketName}. ` + - 'Make sure the AWS project and the bucket exists and the access key located at the path ' + - "techdocs.publisher.awsS3.credentials defined in app config has the role 'Storage Object Creator'. " + - 'Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage', + 'Make sure the bucket exists. Also make sure that authentication is setup either by ' + + 'explicitly defining credentials and region in techdocs.publisher.awsS3 in app config or ' + + 'by using environment variables. Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage', ); - throw new Error(`from AWS client library: ${err.message}`); + logger.error(`from AWS client library: ${err.message}`); + throw new Error(); } else { logger.info( `Successfully connected to the AWS S3 bucket ${bucketName}.`, @@ -79,7 +111,7 @@ export class AwsS3Publish implements PublisherBase { } constructor( - private readonly storageClient: aws.S3, + private readonly storageClient: S3, private readonly bucketName: string, private readonly logger: Logger, ) { @@ -98,7 +130,7 @@ export class AwsS3Publish implements PublisherBase { // So collecting path of only the files is good enough. const allFilesToUpload = await getFileTreeRecursively(directory); - const uploadPromises: Array> = []; + const uploadPromises: Array> = []; for (const filePath of allFilesToUpload) { // Remove the absolute path prefix of the source directory @@ -116,7 +148,7 @@ export class AwsS3Publish implements PublisherBase { Body: fileContent, }; - uploadPromises.push(this.storageClient.upload(params).promise()); + uploadPromises.push(this.storageClient.putObject(params)); } await Promise.all(uploadPromises); this.logger.info( @@ -135,25 +167,27 @@ export class AwsS3Publish implements PublisherBase { return await new Promise((resolve, reject) => { const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`; - const fileStreamChunks: Array = []; this.storageClient .getObject({ Bucket: this.bucketName, Key: `${entityRootDir}/techdocs_metadata.json`, }) - .createReadStream() - .on('error', err => { + .then(async file => { + const techdocsMetadataJson = await streamToString( + file.Body as Readable, + ); + + if (!techdocsMetadataJson) { + throw new Error( + `Unable to parse the techdocs metadata file ${entityRootDir}/techdocs_metadata.json.`, + ); + } + + resolve(techdocsMetadataJson); + }) + .catch(err => { this.logger.error(err.message); reject(new Error(err.message)); - }) - .on('data', chunk => { - fileStreamChunks.push(chunk); - }) - .on('end', () => { - const techdocsMetadataJson = Buffer.concat( - fileStreamChunks, - ).toString(); - resolve(techdocsMetadataJson); }); }); } catch (e) { @@ -174,19 +208,14 @@ export class AwsS3Publish implements PublisherBase { const fileExtension = path.extname(filePath); const responseHeaders = getHeadersForFileExtension(fileExtension); - const fileStreamChunks: Array = []; this.storageClient .getObject({ Bucket: this.bucketName, Key: filePath }) - .createReadStream() - .on('error', err => { - this.logger.warn(err.message); - res.status(404).send(err.message); - }) - .on('data', chunk => { - fileStreamChunks.push(chunk); - }) - .on('end', () => { - const fileContent = Buffer.concat(fileStreamChunks).toString(); + .then(async object => { + const fileContent = await streamToString(object.Body as Readable); + if (!fileContent) { + throw new Error(`Unable to parse the file ${filePath}.`); + } + // Inject response headers for (const [headerKey, headerValue] of Object.entries( responseHeaders, @@ -195,6 +224,10 @@ export class AwsS3Publish implements PublisherBase { } res.send(fileContent); + }) + .catch(err => { + this.logger.warn(err.message); + res.status(404).send(err.message); }); }; } @@ -206,12 +239,10 @@ export class AwsS3Publish implements PublisherBase { async hasDocsBeenGenerated(entity: Entity): Promise { try { const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`; - await this.storageClient - .headObject({ - Bucket: this.bucketName, - Key: `${entityRootDir}/index.html`, - }) - .promise(); + await this.storageClient.headObject({ + Bucket: this.bucketName, + Key: `${entityRootDir}/index.html`, + }); return Promise.resolve(true); } catch (e) { return Promise.resolve(false); diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts b/packages/techdocs-common/src/stages/publish/googleStorage.test.ts index 43375b72d0..f8fb00647c 100644 --- a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts +++ b/packages/techdocs-common/src/stages/publish/googleStorage.test.ts @@ -37,7 +37,7 @@ jest.spyOn(logger, 'info').mockReturnValue(logger); let publisher: PublisherBase; -beforeEach(() => { +beforeEach(async () => { const mockConfig = new ConfigReader({ techdocs: { requestUrl: 'http://localhost:7000', @@ -45,14 +45,13 @@ beforeEach(() => { type: 'googleGcs', googleGcs: { credentials: '{}', - projectId: 'gcp-project-id', bucketName: 'bucketName', }, }, }, }); - publisher = GoogleGCSPublish.fromConfig(mockConfig, logger); + publisher = await GoogleGCSPublish.fromConfig(mockConfig, logger); }); describe('GoogleGCSPublish', () => { diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.ts b/packages/techdocs-common/src/stages/publish/googleStorage.ts index b33e2bfbe0..e6b2c26943 100644 --- a/packages/techdocs-common/src/stages/publish/googleStorage.ts +++ b/packages/techdocs-common/src/stages/publish/googleStorage.ts @@ -27,57 +27,56 @@ import { getHeadersForFileExtension, getFileTreeRecursively } from './helpers'; import { PublisherBase, PublishRequest } from './types'; export class GoogleGCSPublish implements PublisherBase { - static fromConfig(config: Config, logger: Logger): PublisherBase { - let credentials = ''; - let projectId = ''; + static async fromConfig( + config: Config, + logger: Logger, + ): Promise { let bucketName = ''; try { - credentials = config.getString( - 'techdocs.publisher.googleGcs.credentials', - ); - projectId = config.getString('techdocs.publisher.googleGcs.projectId'); bucketName = config.getString('techdocs.publisher.googleGcs.bucketName'); } catch (error) { throw new Error( "Since techdocs.publisher.type is set to 'googleGcs' in your app config, " + - 'credentials, projectId and bucketName are required in techdocs.publisher.googleGcs ' + - 'required to authenticate with Google Cloud Storage.', + 'techdocs.publisher.googleGcs.bucketName is required.', ); } + // Credentials is an optional config. If missing, default GCS environment variables will be used. + // Read more here https://cloud.google.com/docs/authentication/production + const credentials = config.getOptionalString( + 'techdocs.publisher.googleGcs.credentials', + ); let credentialsJson = {}; - try { - credentialsJson = JSON.parse(credentials); - } catch (err) { - throw new Error( - 'Error in parsing techdocs.publisher.googleGcs.credentials config to JSON.', - ); + if (credentials) { + try { + credentialsJson = JSON.parse(credentials); + } catch (err) { + throw new Error( + 'Error in parsing techdocs.publisher.googleGcs.credentials config to JSON.', + ); + } } const storageClient = new Storage({ - credentials: credentialsJson, - projectId: projectId, + ...(credentials && { + credentials: credentialsJson, + }), }); // Check if the defined bucket exists. Being able to connect means the configuration is good // and the storage client will work. - storageClient - .bucket(bucketName) - .getMetadata() - .then(() => { - logger.info( - `Successfully connected to the GCS bucket ${bucketName} in the GCP project ${projectId}.`, - ); - }) - .catch(reason => { - logger.error( - `Could not retrieve metadata about the GCS bucket ${bucketName} in the GCP project ${projectId}. ` + - 'Make sure the GCP project and the bucket exists and the access key located at the path ' + - "techdocs.publisher.googleGcs.credentials defined in app config has the role 'Storage Object Creator'. " + - 'Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage', - ); - throw new Error(`from GCS client library: ${reason.message}`); - }); + try { + await storageClient.bucket(bucketName).getMetadata(); + logger.info(`Successfully connected to the GCS bucket ${bucketName}.`); + } catch (err) { + logger.error( + `Could not retrieve metadata about the GCS bucket ${bucketName}. ` + + 'Make sure the bucket exists. Also make sure that authentication is setup either by explicitly defining ' + + 'techdocs.publisher.googleGcs.credentials in app config or by using environment variables. ' + + 'Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage', + ); + throw new Error(err.message); + } return new GoogleGCSPublish(storageClient, bucketName, logger); } @@ -184,7 +183,7 @@ export class GoogleGCSPublish implements PublisherBase { fileStreamChunks.push(chunk); }) .on('end', () => { - const fileContent = Buffer.concat(fileStreamChunks).toString(); + const fileContent = Buffer.concat(fileStreamChunks); // Inject response headers for (const [headerKey, headerValue] of Object.entries( responseHeaders, diff --git a/packages/techdocs-common/src/stages/publish/publish.test.ts b/packages/techdocs-common/src/stages/publish/publish.test.ts index d7cc257b89..89f2c0ffdd 100644 --- a/packages/techdocs-common/src/stages/publish/publish.test.ts +++ b/packages/techdocs-common/src/stages/publish/publish.test.ts @@ -69,7 +69,6 @@ describe('Publisher', () => { type: 'googleGcs', googleGcs: { credentials: '{}', - projectId: 'gcp-project-id', bucketName: 'bucketName', }, }, diff --git a/packages/techdocs-common/src/stages/publish/publish.ts b/packages/techdocs-common/src/stages/publish/publish.ts index caaf8a4cec..82232c2fd1 100644 --- a/packages/techdocs-common/src/stages/publish/publish.ts +++ b/packages/techdocs-common/src/stages/publish/publish.ts @@ -43,7 +43,7 @@ export class Publisher { switch (publisherType) { case 'googleGcs': logger.info('Creating Google Storage Bucket publisher for TechDocs'); - return GoogleGCSPublish.fromConfig(config, logger); + return await GoogleGCSPublish.fromConfig(config, logger); case 'awsS3': logger.info('Creating AWS S3 Bucket publisher for TechDocs'); return AwsS3Publish.fromConfig(config, logger); diff --git a/plugins/auth-backend/README.md b/plugins/auth-backend/README.md index ee6e84cded..1fea3346a7 100644 --- a/plugins/auth-backend/README.md +++ b/plugins/auth-backend/README.md @@ -10,7 +10,7 @@ to the appropriate provider in the backend. ## Local development Choose your OAuth Providers, replace `x` with actual value and then start backend: -Example for Google Oauth Provider at root directory: +Example for Google OAuth Provider at root directory: ```bash export AUTH_GOOGLE_CLIENT_ID=x @@ -89,8 +89,16 @@ export AUTH_GITLAB_CLIENT_SECRET=x ### Okta +Add a new Okta application using the following URI conventions: + +Login redirect URI's: `http://localhost:7000/api/auth/okta/handler/frame` +Logout redirect URI's: `http://localhost:7000/api/auth/okta/logout` +Initiate login URI's: `http://localhost:7000/api/auth/okta/start` + +Then configure the following environment variables to be used in the `app-config.yaml` file: + ```bash -export AUTH_OKTA_AUDIENCE=x +export AUTH_OKTA_AUDIENCE=https://example.okta.com export AUTH_OKTA_CLIENT_ID=x export AUTH_OKTA_CLIENT_SECRET=x ``` diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts index f7e1da9b96..7c105af524 100644 --- a/plugins/auth-backend/config.d.ts +++ b/plugins/auth-backend/config.d.ts @@ -46,6 +46,7 @@ export interface Config { }; saml?: { entryPoint: string; + logoutUrl?: string; issuer: string; cert?: string; privateKey?: string; diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 4cdee1da0f..45676ac43e 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -129,6 +129,7 @@ export const createSamlProvider: AuthProviderFactory = ({ const opts = { callbackUrl: `${globalConfig.baseUrl}/${providerId}/handler/frame`, entryPoint: config.getString('entryPoint'), + logoutUrl: config.getOptionalString('logoutUrl'), issuer: config.getString('issuer'), cert: config.getOptionalString('cert'), privateCert: config.getOptionalString('privateKey'), @@ -142,5 +143,10 @@ export const createSamlProvider: AuthProviderFactory = ({ appUrl: globalConfig.appUrl, }; + // passport-saml will return an error if the `cert` key is set, and the value is empty. + // Since we read from config (such as environment variables) an empty string should be equal to being unset. + if (!opts.cert) { + delete opts.cert; + } return new SamlAuthProvider(opts); }; diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index bef186f8c3..b60555037c 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -54,7 +54,7 @@ ### Patch Changes -- c6eeefa35: Add support for Github Enterprise in GitHubOrgReaderProcessor so you can properly ingest users of a GHE organization. +- c6eeefa35: Add support for GitHub Enterprise in GitHubOrgReaderProcessor so you can properly ingest users of a GHE organization. - fb386b760: Break the refresh loop into several smaller transactions - 7c3ffc0cd: Support `profile` of groups including `displayName`, `email`, and `picture` in `LdapOrgReaderProcessor`. The source fields for them can be configured in the diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 9fbc16e3d3..8fc9712777 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -29,6 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@aws-sdk/client-organizations": "^3.2.0", "@azure/msal-node": "^1.0.0-alpha.8", "@backstage/backend-common": "^0.4.2", "@backstage/catalog-model": "^0.6.0", @@ -36,7 +37,6 @@ "@octokit/graphql": "^4.5.6", "@types/express": "^4.17.6", "@types/ldapjs": "^1.0.9", - "aws-sdk": "^2.817.0", "codeowners-utils": "^1.0.2", "core-js": "^3.6.5", "cross-fetch": "^3.0.6", diff --git a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts index eb5e634d66..5044a16aea 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts @@ -27,22 +27,18 @@ describe('AwsOrganizationCloudAccountProcessor', () => { afterEach(() => jest.resetAllMocks()); it('generates component entities for accounts', async () => { - listAccounts.mockImplementation(() => { - return { - async promise() { - return { - Accounts: [ - { - Arn: - 'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395', - Name: 'testaccount', - }, - ], - NextToken: undefined, - }; - }, - }; - }); + listAccounts.mockImplementation(() => + Promise.resolve({ + Accounts: [ + { + Arn: + 'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395', + Name: 'testaccount', + }, + ], + NextToken: undefined, + }), + ); await processor.readLocation(location, false, emit); expect(emit).toBeCalledWith({ type: 'entity', @@ -71,27 +67,23 @@ describe('AwsOrganizationCloudAccountProcessor', () => { it('filters out accounts not in specified location target', async () => { const location = { type: 'aws-cloud-accounts', target: 'o-1vl18kc5a3' }; - listAccounts.mockImplementation(() => { - return { - async promise() { - return { - Accounts: [ - { - Arn: - 'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395', - Name: 'testaccount', - }, - { - Arn: - 'arn:aws:organizations::192594491037:account/o-zzzzzzzzz/957140518395', - Name: 'testaccount2', - }, - ], - NextToken: undefined, - }; - }, - }; - }); + listAccounts.mockImplementation(() => + Promise.resolve({ + Accounts: [ + { + Arn: + 'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395', + Name: 'testaccount', + }, + { + Arn: + 'arn:aws:organizations::192594491037:account/o-zzzzzzzzz/957140518395', + Name: 'testaccount2', + }, + ], + NextToken: undefined, + }), + ); await processor.readLocation(location, false, emit); expect(emit).toBeCalledTimes(1); expect(emit).toBeCalledWith({ diff --git a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts index f019f5207b..298fcb82ec 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts @@ -17,8 +17,11 @@ import { ComponentEntityV1alpha1, LocationSpec, } from '@backstage/catalog-model'; -import AWS, { Organizations } from 'aws-sdk'; -import { Account, ListAccountsResponse } from 'aws-sdk/clients/organizations'; +import { + Account, + Organizations, + ListAccountsCommandOutput, +} from '@aws-sdk/client-organizations'; import * as results from './results'; import { CatalogProcessor, CatalogProcessorEmit } from './types'; @@ -38,7 +41,7 @@ const ORGANIZATION_ANNOTATION: string = 'amazonaws.com/organization-id'; export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { organizations: Organizations; constructor() { - this.organizations = new AWS.Organizations({ + this.organizations = new Organizations({ region: AWS_ORGANIZATION_REGION, }); // Only available in us-east-1 } @@ -67,9 +70,9 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { let nextToken = undefined; while (isInitialAttempt || nextToken) { isInitialAttempt = false; - const orgAccounts: ListAccountsResponse = await this.organizations - .listAccounts({ NextToken: nextToken }) - .promise(); + const orgAccounts: ListAccountsCommandOutput = await this.organizations.listAccounts( + { NextToken: nextToken }, + ); if (orgAccounts.Accounts) { awsAccounts = awsAccounts.concat(orgAccounts.Accounts); } diff --git a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts index 94a08c5e73..1d2562c25b 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts @@ -38,6 +38,7 @@ describe('BuiltinKindsEntityProcessor', () => { spec: { type: 'service', owner: 'o', + subcomponentOf: 's', lifecycle: 'l', providesApis: ['b'], consumesApis: ['c'], @@ -46,7 +47,7 @@ describe('BuiltinKindsEntityProcessor', () => { await processor.postProcessEntity(entity, location, emit); - expect(emit).toBeCalledTimes(6); + expect(emit).toBeCalledTimes(8); expect(emit).toBeCalledWith({ type: 'relation', relation: { @@ -95,6 +96,22 @@ describe('BuiltinKindsEntityProcessor', () => { target: { kind: 'API', namespace: 'default', name: 'c' }, }, }); + expect(emit).toBeCalledWith({ + type: 'relation', + relation: { + source: { kind: 'Component', namespace: 'default', name: 's' }, + type: 'hasPart', + target: { kind: 'Component', namespace: 'default', name: 'n' }, + }, + }); + expect(emit).toBeCalledWith({ + type: 'relation', + relation: { + source: { kind: 'Component', namespace: 'default', name: 'n' }, + type: 'partOf', + target: { kind: 'Component', namespace: 'default', name: 's' }, + }, + }); }); it('generates relations for api entities', async () => { diff --git a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts index 6cbec5d655..67e89ac52c 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts @@ -32,6 +32,8 @@ import { RELATION_CONSUMES_API, RELATION_HAS_MEMBER, RELATION_MEMBER_OF, + RELATION_HAS_PART, + RELATION_PART_OF, RELATION_OWNED_BY, RELATION_OWNER_OF, RELATION_PARENT_OF, @@ -115,6 +117,12 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { RELATION_OWNED_BY, RELATION_OWNER_OF, ); + doEmit( + component.spec.subcomponentOf, + { defaultKind: 'Component', defaultNamespace: selfRef.namespace }, + RELATION_PART_OF, + RELATION_HAS_PART, + ); doEmit( component.spec.providesApis, { defaultKind: 'API', defaultNamespace: selfRef.namespace }, diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 495552983f..d308bef7b8 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -33,7 +33,6 @@ "@backstage/catalog-model": "^0.6.0", "@backstage/core": "^0.4.3", "@backstage/plugin-catalog": "^0.2.10", - "@backstage/plugin-catalog-backend": "^0.5.2", "@backstage/integration": "^0.1.5", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts index 0e28042db3..a578f37a96 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.ts @@ -17,7 +17,6 @@ import { Octokit } from '@octokit/rest'; import { DiscoveryApi, OAuthApi, ConfigApi } from '@backstage/core'; import { CatalogImportApi } from './CatalogImportApi'; -import { AnalyzeLocationResponse } from '@backstage/plugin-catalog-backend'; import { PartialEntity } from '../util/types'; import { GitHubIntegrationConfig } from '@backstage/integration'; @@ -61,8 +60,8 @@ export class CatalogImportClient implements CatalogImportApi { ); } - const payload = (await response.json()) as AnalyzeLocationResponse; - return payload.generateEntities.map(x => x.entity); + const payload = await response.json(); + return payload.generateEntities.map((x: any) => x.entity); } async createRepositoryLocation({ diff --git a/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx b/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx index ab5c71c7d3..96b1bf2298 100644 --- a/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx +++ b/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx @@ -33,13 +33,13 @@ import { StructuredMetadataTable, useApi, } from '@backstage/core'; -import parseGitUri from 'git-url-parse'; import { PartialEntity } from '../util/types'; import { generatePath, resolvePath } from 'react-router'; import { entityRoute, entityRouteParams } from '@backstage/plugin-catalog'; import { Entity } from '@backstage/catalog-model'; import { Link as RouterLink } from 'react-router-dom'; import * as YAML from 'yaml'; +import { urlType } from '../util/urls'; const getEntityCatalogPath = ({ entity, @@ -81,7 +81,7 @@ const ComponentConfigDisplay = ({ const onNext = useCallback(async () => { try { setSubmitting(true); - if (!parseGitUri(configFile.location).filepathtype) { + if (urlType(configFile.location) === 'tree') { const result = await submitPrToRepo(configFile); savePRLink(result.link); setSubmitting(false); @@ -100,7 +100,7 @@ const ComponentConfigDisplay = ({ return ( - {!parseGitUri(configFile.location).filepathtype ? ( + {urlType(configFile.location) === 'tree' ? ( Following config object will be submitted in a pull request to the repository{' '} @@ -127,7 +127,7 @@ const ComponentConfigDisplay = ({ )} - {!parseGitUri(configFile.location).filepathtype ? ( + {urlType(configFile.location) === 'tree' ? (
{YAML.stringify(configFile.config)}
) : ( diff --git a/plugins/catalog-import/src/components/ImportComponentForm.tsx b/plugins/catalog-import/src/components/ImportComponentForm.tsx index dab5ccbea2..7434f07b10 100644 --- a/plugins/catalog-import/src/components/ImportComponentForm.tsx +++ b/plugins/catalog-import/src/components/ImportComponentForm.tsx @@ -26,11 +26,11 @@ import { makeStyles } from '@material-ui/core/styles'; import React from 'react'; import { useForm } from 'react-hook-form'; import { useMountedState } from 'react-use'; -import parseGitUri from 'git-url-parse'; import { ComponentIdValidators } from '../util/validate'; import { useGithubRepos } from '../util/useGithubRepos'; import { ConfigSpec } from './ImportComponentPage'; import { catalogApiRef } from '@backstage/plugin-catalog'; +import { urlType } from '../util/urls'; const useStyles = makeStyles(theme => ({ form: { @@ -46,9 +46,14 @@ const useStyles = makeStyles(theme => ({ type Props = { nextStep: () => void; saveConfig: (configFile: ConfigSpec) => void; + repository: string; }; -export const RegisterComponentForm = ({ nextStep, saveConfig }: Props) => { +export const RegisterComponentForm = ({ + nextStep, + saveConfig, + repository, +}: Props) => { const { register, handleSubmit, errors, formState } = useForm({ mode: 'onChange', }); @@ -65,9 +70,9 @@ export const RegisterComponentForm = ({ nextStep, saveConfig }: Props) => { const { componentLocation: target } = formData; try { if (!isMounted()) return; - const type = !parseGitUri(target).filepathtype ? 'repo' : 'file'; + const type = urlType(target); - if (type === 'repo') { + if (type === 'tree') { saveConfig({ type, location: target, @@ -103,7 +108,7 @@ export const RegisterComponentForm = ({ nextStep, saveConfig }: Props) => { name="componentLocation" required margin="normal" - helperText="Enter the full path to the repository in GitHub to start tracking your component." + helperText={`Enter the full path to the repository in ${repository} to start tracking your component.`} inputRef={register({ required: true, validate: ComponentIdValidators, diff --git a/plugins/catalog-import/src/components/ImportComponentPage.tsx b/plugins/catalog-import/src/components/ImportComponentPage.tsx index 9fdb465dd1..7e0d0ff92e 100644 --- a/plugins/catalog-import/src/components/ImportComponentPage.tsx +++ b/plugins/catalog-import/src/components/ImportComponentPage.tsx @@ -24,6 +24,9 @@ import { SupportButton, ContentHeader, RouteRef, + useApi, + configApiRef, + ConfigApi, } from '@backstage/core'; import { RegisterComponentForm } from './ImportComponentForm'; import ImportStepper from './ImportStepper'; @@ -32,11 +35,33 @@ import { ImportFinished } from './ImportFinished'; import { PartialEntity } from '../util/types'; export type ConfigSpec = { - type: 'repo' | 'file'; + type: 'tree' | 'file'; location: string; config: PartialEntity[]; }; +function manifestGenerationAvailable(configApi: ConfigApi): boolean { + return configApi.has('integrations.github'); +} + +function repositories(configApi: ConfigApi): string[] { + const integrations = configApi.getConfig('integrations'); + const repositories = []; + if (integrations.has('github')) { + repositories.push('GitHub'); + } + if (integrations.has('bitbucket')) { + repositories.push('Bitbucket'); + } + if (integrations.has('gitlab')) { + repositories.push('GitLab'); + } + if (integrations.has('azure')) { + repositories.push('Azure'); + } + return repositories; +} + export const ImportComponentPage = ({ catalogRouteRef, }: { @@ -44,7 +69,7 @@ export const ImportComponentPage = ({ }) => { const [activeStep, setActiveStep] = useState(0); const [configFile, setConfigFile] = useState({ - type: 'repo', + type: 'tree', location: '', config: [], }); @@ -53,13 +78,17 @@ export const ImportComponentPage = ({ setActiveStep(step => (options?.reset ? 0 : step + 1)); }; + const configApi = useApi(configApiRef); + const appTitle = configApi.getOptional('app.title') || 'Backstage'; + const repos = repositories(configApi); + const repositoryString = repos.join(', ').replace(/, (\w*)$/, ' or $1'); return (
- + - Start tracking your component in Backstage by adding it to the + Start tracking your component in {appTitle} by adding it to the software catalog. @@ -73,21 +102,28 @@ export const ImportComponentPage = ({ }} > - There are two ways to register an existing component. - - GitHub Repo - - If you already have code in a GitHub repository, enter the full - URL to your repo and a new pull request with a sample Backstage - metadata Entity File (catalog-info.yaml) will be - opened for you. + Ways to register an existing component + + {manifestGenerationAvailable(configApi) && ( + + GitHub Repo + + If you already have code in a GitHub repository, enter the + full URL to your repo and a new pull request with a sample + Backstage metadata Entity File ( + catalog-info.yaml) will be opened for you. + + + )} - GitHub Repo & Entity File + {repos.length === 1 ? `${repos[0]} ` : ''} Repository & + Entity File - If you've already created a Backstage metadata file and put it - in your repo, you can enter the full URL to that Entity File. + If you've already created a {appTitle} metadata file and put it + in your {repositoryString} repository, you can enter the full + URL to that Entity File. @@ -96,11 +132,14 @@ export const ImportComponentPage = ({ ), }, diff --git a/plugins/catalog-import/src/components/ImportFinished.tsx b/plugins/catalog-import/src/components/ImportFinished.tsx index caba678999..9b0f8575a6 100644 --- a/plugins/catalog-import/src/components/ImportFinished.tsx +++ b/plugins/catalog-import/src/components/ImportFinished.tsx @@ -19,7 +19,7 @@ import { Alert } from '@material-ui/lab'; import { Button, Grid, Link } from '@material-ui/core'; type Props = { - type: 'repo' | 'file'; + type: 'tree' | 'file'; nextStep: (options?: { reset: boolean }) => void; PRLink: string; }; @@ -29,13 +29,13 @@ export const ImportFinished = ({ nextStep, PRLink, type }: Props) => { - {type === 'repo' + {type === 'tree' ? 'Pull requests have been successfully opened. You can start again to import more repositories' : 'Entity added to catalog successfully'} - {type === 'repo' ? ( + {type === 'tree' ? ( { const location = findLocationForEntityMeta(rowData.metadata); return { - icon: () => , - tooltip: 'View on GitHub', + icon: () => , + tooltip: 'View', onClick: () => { if (!location) return; window.open(location.target, '_blank'); }, - hidden: location?.type !== 'github', }; }, (rowData: Entity) => { @@ -129,7 +128,6 @@ export const CatalogTable = ({ if (!location) return; window.open(createEditLink(location), '_blank'); }, - hidden: location?.type !== 'github', }; }, (rowData: Entity) => { diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx index 572f2f6ca0..bd3585f739 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx @@ -117,6 +117,7 @@ describe('', () => { ); const subheaderRgx = new RegExp(subheader); expect(rendered.getByText(subheaderRgx)).toBeInTheDocument(); + expect(rendered.queryByText(/sorted by cost/)).not.toBeInTheDocument(); expect( rendered.queryByTestId('.resource-growth-chart-legend'), ).not.toBeInTheDocument(); diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx index 1d15b4f427..291044c977 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx @@ -96,9 +96,10 @@ export const ProductInsightsCard = ({ const entityKey = findAnyKey(entity?.entities); const entities = entityKey ? entity!.entities[entityKey] : []; - const subheader = entityKey - ? `${pluralize(entityKey, entities.length, true)}, sorted by cost` - : null; + const subheader = + entityKey && entities.length + ? `${pluralize(entityKey, entities.length, true)}, sorted by cost` + : null; const headerProps = { classes: classes, action: , diff --git a/plugins/cost-insights/src/utils/mockData.ts b/plugins/cost-insights/src/utils/mockData.ts index 19f9676eea..f84473b84f 100644 --- a/plugins/cost-insights/src/utils/mockData.ts +++ b/plugins/cost-insights/src/utils/mockData.ts @@ -211,16 +211,15 @@ export function trendlineOf(aggregation: DateAggregation[]): Trendline { } export function changeOf(aggregation: DateAggregation[]): ChangeStatistic { - const half = Math.ceil(aggregation.length / 2); - const before = aggregation - .slice(0, half) - .reduce((sum, a) => sum + a.amount, 0); - const after = aggregation - .slice(half, aggregation.length) - .reduce((sum, a) => sum + a.amount, 0); + const firstAmount = aggregation.length ? aggregation[0].amount : 0; + const lastAmount = aggregation.length + ? aggregation[aggregation.length - 1].amount + : 0; + const ratio = + firstAmount !== 0 ? (lastAmount - firstAmount) / firstAmount : 0; return { - ratio: (after - before) / before, - amount: after - before, + ratio: ratio, + amount: lastAmount - firstAmount, }; } @@ -530,7 +529,7 @@ export const SampleBigQueryInsights: Entity = { entities: { dataset: [ { - id: 'entity-a', + id: 'dataset-a', aggregation: [5_000, 10_000], change: { ratio: 1, @@ -539,7 +538,7 @@ export const SampleBigQueryInsights: Entity = { entities: {}, }, { - id: 'entity-b', + id: 'dataset-b', aggregation: [5_000, 10_000], change: { ratio: 1, @@ -548,7 +547,7 @@ export const SampleBigQueryInsights: Entity = { entities: {}, }, { - id: 'entity-c', + id: 'dataset-c', aggregation: [0, 10_000], change: { ratio: 10_000, @@ -600,7 +599,7 @@ export const SampleCloudDataflowInsights: Entity = { }, }, { - id: 'entity-a', + id: 'pipeline-a', aggregation: [60_000, 70_000], change: { ratio: 0.16666666666666666, @@ -639,7 +638,7 @@ export const SampleCloudDataflowInsights: Entity = { }, }, { - id: 'entity-b', + id: 'pipeline-b', aggregation: [12_000, 8_000], change: { ratio: -0.33333, @@ -669,7 +668,7 @@ export const SampleCloudDataflowInsights: Entity = { }, }, { - id: 'entity-c', + id: 'pipeline-c', aggregation: [0, 10_000], change: { ratio: 10_000, @@ -691,7 +690,7 @@ export const SampleCloudStorageInsights: Entity = { entities: { bucket: [ { - id: 'entity-a', + id: 'bucket-a', aggregation: [15_000, 20_000], change: { ratio: 0.333, @@ -730,7 +729,7 @@ export const SampleCloudStorageInsights: Entity = { }, }, { - id: 'entity-b', + id: 'bucket-b', aggregation: [30_000, 25_000], change: { ratio: -0.16666, @@ -769,7 +768,7 @@ export const SampleCloudStorageInsights: Entity = { }, }, { - id: 'entity-c', + id: 'bucket-c', aggregation: [0, 0], change: { ratio: 0, @@ -791,7 +790,7 @@ export const SampleComputeEngineInsights: Entity = { entities: { service: [ { - id: 'entity-a', + id: 'service-a', aggregation: [20_000, 10_000], change: { ratio: -0.5, @@ -850,7 +849,7 @@ export const SampleComputeEngineInsights: Entity = { }, }, { - id: 'entity-b', + id: 'service-b', aggregation: [10_000, 20_000], change: { ratio: 1, @@ -909,7 +908,7 @@ export const SampleComputeEngineInsights: Entity = { }, }, { - id: 'entity-c', + id: 'service-c', aggregation: [0, 10_000], change: { ratio: 10_000, @@ -931,7 +930,7 @@ export const SampleEventsInsights: Entity = { entities: { event: [ { - id: 'entity-a', + id: 'event-a', aggregation: [15_000, 7_000], change: { ratio: -0.53333333333, @@ -970,7 +969,7 @@ export const SampleEventsInsights: Entity = { }, }, { - id: 'entity-b', + id: 'event-b', aggregation: [5_000, 3_000], change: { ratio: -0.4, diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index 2d69b1499d..f2537742f4 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -4,7 +4,7 @@ ### Patch Changes -- 19554f6d6: Added Github Actions for Create React App, and allow better imports of files inside a module when they're exposed using `files` in `package.json` +- 19554f6d6: Added GitHub Actions for Create React App, and allow better imports of files inside a module when they're exposed using `files` in `package.json` - Updated dependencies [d681db2b5] - Updated dependencies [1dc445e89] - Updated dependencies [342270e4d] @@ -94,7 +94,7 @@ ### Minor Changes - 28edd7d29: Create backend plugin through CLI -- d67c529ab: Adds a widget to show recent git workflow runs to the github actions plugin. The default setting is the last 5 runs across all branches but both branch and the number of runs are configurable. +- d67c529ab: Adds a widget to show recent git workflow runs to the GitHub actions plugin. The default setting is the last 5 runs across all branches but both branch and the number of runs are configurable. - 6d97d2d6f: The InfoCard variant `'height100'` is deprecated. Use variant `'gridItem'` instead. When the InfoCard is displayed as a grid item within a grid, you may want items to have the same height for all items. diff --git a/plugins/jenkins/src/api/JenkinsApi.ts b/plugins/jenkins/src/api/JenkinsApi.ts index 8dbde9160d..ed70607c27 100644 --- a/plugins/jenkins/src/api/JenkinsApi.ts +++ b/plugins/jenkins/src/api/JenkinsApi.ts @@ -63,7 +63,7 @@ export class JenkinsApi { return lastBuild; } - extractScmDetailsFromJob(jobDetails: any): any { + extractScmDetailsFromJob(jobDetails: any): any | undefined { const scmInfo = jobDetails.actions .filter( (action: any) => @@ -79,6 +79,10 @@ export class JenkinsApi { }) .pop(); + if (!scmInfo) { + return undefined; + } + const author = jobDetails.actions .filter( (action: any) => @@ -141,7 +145,7 @@ export class JenkinsApi { for (const jobDetails of folder.jobs) { const jobScmInfo = this.extractScmDetailsFromJob(jobDetails); - if (jobDetails.jobs) { + if (jobDetails?.jobs) { // skipping folders inside folders for now } else { for (const buildDetails of jobDetails.builds) { diff --git a/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml b/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml index 50956bd7d3..68ef489170 100644 --- a/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml +++ b/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml @@ -18,6 +18,17 @@ spec: containers: - name: nginx image: nginx:1.14.2 + args: + - bash + - -c + - yes > /dev/null & yes > /dev/null & yes > /dev/null + resources: + requests: + memory: '64Mi' + cpu: '50m' + limits: + memory: '128Mi' + cpu: '50m' ports: - containerPort: 80 @@ -44,14 +55,134 @@ spec: image: nginx:1.14.2 ports: - containerPort: 80 + resources: + requests: + memory: '64Mi' + cpu: '50m' + limits: + memory: '128Mi' + cpu: '500m' - name: side-car image: nginx:1.14.2 ports: - containerPort: 81 + resources: + requests: + memory: '64Mi' + cpu: '50m' + limits: + memory: '128Mi' + cpu: '500m' - name: other-side-car image: nginx:1.14.2 ports: - containerPort: 82 + resources: + requests: + memory: '64Mi' + cpu: '50m' + limits: + memory: '128Mi' + cpu: '500m' + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dice-roller-bad-config + labels: + 'backstage.io/kubernetes-id': dice-roller +spec: + selector: + matchLabels: + app: dice-roller-bad-config + replicas: 2 + template: + metadata: + labels: + app: dice-roller-bad-config + 'backstage.io/kubernetes-id': dice-roller + spec: + containers: + - name: nginx + image: nginx:6000000 + resources: + requests: + memory: '64Mi' + cpu: '50m' + limits: + memory: '128Mi' + cpu: '500m' + ports: + - containerPort: 80 + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dice-roller-big + labels: + 'backstage.io/kubernetes-id': dice-roller +spec: + selector: + matchLabels: + app: dice-roller-big + replicas: 1 + template: + metadata: + labels: + app: dice-roller-big + 'backstage.io/kubernetes-id': dice-roller + spec: + containers: + - name: nginx + image: nginx:1.14.2 + resources: + requests: + memory: '64Mi' + cpu: '100000m' + limits: + memory: '128Mi' + cpu: '100000m' + ports: + - containerPort: 80 + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dice-roller-bad-cm + labels: + 'backstage.io/kubernetes-id': dice-roller +spec: + selector: + matchLabels: + app: dice-roller-bad-cm + replicas: 1 + template: + metadata: + labels: + app: dice-roller-bad-cm + 'backstage.io/kubernetes-id': dice-roller + spec: + containers: + - name: nginx + image: nginx:1.14.2 + env: + - name: SOME_ENV_VAR + valueFrom: + configMapKeyRef: + name: some-cm + key: some-key + resources: + requests: + memory: '64Mi' + cpu: '50m' + limits: + memory: '128Mi' + cpu: '500m' + ports: + - containerPort: 80 --- apiVersion: autoscaling/v1 diff --git a/plugins/kubernetes/assets/emptystate.svg b/plugins/kubernetes/assets/emptystate.svg new file mode 100644 index 0000000000..fa7f19123e --- /dev/null +++ b/plugins/kubernetes/assets/emptystate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 56a61d12a2..f00c846163 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -40,6 +40,7 @@ "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", + "js-yaml": "^4.0.0", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", diff --git a/plugins/kubernetes/src/components/ConfigMaps/ConfigMaps.test.tsx b/plugins/kubernetes/src/components/ConfigMaps/ConfigMaps.test.tsx deleted file mode 100644 index 35b8f2143a..0000000000 --- a/plugins/kubernetes/src/components/ConfigMaps/ConfigMaps.test.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 { render } from '@testing-library/react'; -import { ConfigMaps } from './ConfigMaps'; -import * as configmapFixture from './__fixtures__/configmap.json'; -import { wrapInTestApp } from '@backstage/test-utils'; - -describe('ConfigMaps', () => { - it('should render configmap', async () => { - const { getByText } = render( - wrapInTestApp( - , - ), - ); - - // title - expect(getByText('dice-roller')).toBeInTheDocument(); - expect(getByText('Config Map')).toBeInTheDocument(); - - // values - expect(getByText('Immutable')).toBeInTheDocument(); - expect(getByText('false')).toBeInTheDocument(); - expect(getByText('Data')).toBeInTheDocument(); - expect(getByText('Foo: bar')).toBeInTheDocument(); // TODO wish this wasn't upper case - }); -}); diff --git a/plugins/kubernetes/src/components/ConfigMaps/ConfigMaps.tsx b/plugins/kubernetes/src/components/ConfigMaps/ConfigMaps.tsx deleted file mode 100644 index 5a013eb2c6..0000000000 --- a/plugins/kubernetes/src/components/ConfigMaps/ConfigMaps.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 { Grid } from '@material-ui/core'; -import { V1ConfigMap } from '@kubernetes/client-node'; -import { InfoCard, StructuredMetadataTable } from '@backstage/core'; - -type ConfigMapsProps = { - configMaps: V1ConfigMap[]; - children?: React.ReactNode; -}; - -export const ConfigMaps = ({ configMaps }: ConfigMapsProps) => { - return ( - - {configMaps.map((cm, i) => { - return ( - - -
- -
-
-
- ); - })} -
- ); -}; diff --git a/plugins/kubernetes/src/components/ConfigMaps/__fixtures__/configmap.json b/plugins/kubernetes/src/components/ConfigMaps/__fixtures__/configmap.json deleted file mode 100644 index 77ea844611..0000000000 --- a/plugins/kubernetes/src/components/ConfigMaps/__fixtures__/configmap.json +++ /dev/null @@ -1,46 +0,0 @@ -[ - { - "data": { - "foo": "bar" - }, - "metadata": { - "annotations": { - "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"data\":{\"foo\":\"bar\"},\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"}}\n" - }, - "creationTimestamp": "2020-09-24T11:39:26.000Z", - "labels": { - "backstage.io/kubernetes-id": "dice-roller" - }, - "managedFields": [ - { - "apiVersion": "v1", - "fieldsType": "FieldsV1", - "fieldsV1": { - "f:data": { - ".": {}, - "f:foo": {} - }, - "f:metadata": { - "f:annotations": { - ".": {}, - "f:kubectl.kubernetes.io/last-applied-configuration": {} - }, - "f:labels": { - ".": {}, - "f:backstage.io/kubernetes-id": {} - } - } - }, - "manager": "kubectl", - "operation": "Update", - "time": "2020-09-24T11:39:26.000Z" - } - ], - "name": "dice-roller", - "namespace": "default", - "resourceVersion": "503867", - "selfLink": "/api/v1/namespaces/default/configmaps/dice-roller", - "uid": "e9efe5ee-53b9-4422-aef2-877a03c73d5f" - } - } -] diff --git a/plugins/kubernetes/src/components/DeploymentTables/DeploymentTables.tsx b/plugins/kubernetes/src/components/DeploymentTables/DeploymentTables.tsx deleted file mode 100644 index a009c20d7e..0000000000 --- a/plugins/kubernetes/src/components/DeploymentTables/DeploymentTables.tsx +++ /dev/null @@ -1,220 +0,0 @@ -/* - * 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, { Fragment } from 'react'; -import { Chip, Grid } from '@material-ui/core'; -import { - StatusAborted, - StatusError, - StatusOK, - SubvalueCell, - Table, - TableColumn, -} from '@backstage/core'; -import { - V1ComponentCondition, - V1Deployment, - V1Pod, - V1ReplicaSet, -} from '@kubernetes/client-node'; -import { V1OwnerReference } from '@kubernetes/client-node/dist/gen/model/v1OwnerReference'; -import { DeploymentTriple } from '../../types/types'; - -const renderCondition = (condition: V1ComponentCondition | undefined) => { - if (!condition) { - return ; - } - - const status = condition.status; - - if (status === 'True') { - return ; - } else if (status === 'False') { - return ; - } - return ; -}; - -const columns: TableColumn[] = [ - { - title: 'name', - highlight: true, - width: '20%', - render: (pod: V1Pod) => pod.metadata?.name ?? 'un-named pod', - }, - { - title: 'images', - width: '20%', - render: (pod: V1Pod) => { - const containerStatuses = pod.status?.containerStatuses ?? []; - return containerStatuses.map((cs, i) => { - return ; - }); - }, - }, - { - title: 'phase', - render: (pod: V1Pod) => pod.status?.phase ?? 'unknown', - }, - { - title: 'containers ready', - align: 'center', - render: (pod: V1Pod) => { - const containerStatuses = pod.status?.containerStatuses ?? []; - const containersReady = containerStatuses.filter(cs => cs.ready).length; - - return `${containersReady}/${containerStatuses.length}`; - }, - }, - { - title: 'total restarts', - render: (pod: V1Pod) => { - const containerStatuses = pod.status?.containerStatuses ?? []; - return containerStatuses?.reduce((a, b) => a + b.restartCount, 0); - }, - type: 'numeric', - }, - { - title: 'status', - width: '20%', - render: (pod: V1Pod) => { - const containerStatuses = pod.status?.containerStatuses ?? []; - const errors = containerStatuses.reduce((accum, next) => { - if (next.state === undefined) { - return accum; - } - - const waiting = next.state.waiting; - const terminated = next.state.terminated; - - const renderCell = (reason: string | undefined) => ( - - Container: {next.name}} - subvalue={reason} - /> -
- - ); - - if (waiting) { - accum.push(renderCell(waiting.reason)); - } - - if (terminated) { - accum.push(renderCell(terminated.reason)); - } - - return accum; - }, [] as React.ReactNode[]); - - if (errors.length === 0) { - return OK; - } - - return errors; - }, - }, - { - title: 'Pod Initialized', - align: 'center', - render: (pod: V1Pod) => { - const conditions = pod.status?.conditions ?? []; - return renderCondition(conditions.find(c => c.type === 'Initialized')); - }, - }, - { - title: 'Pod Ready', - align: 'center', - render: (pod: V1Pod) => { - const conditions = pod.status?.conditions ?? []; - return renderCondition(conditions.find(c => c.type === 'Ready')); - }, - }, - { - title: 'Containers Ready', - align: 'center', - render: (pod: V1Pod) => { - const conditions = pod.status?.conditions ?? []; - return renderCondition( - conditions.find(c => c.type === 'ContainersReady'), - ); - }, - }, - { - title: 'Pod Scheduled', - align: 'center', - render: (pod: V1Pod) => { - const conditions = pod.status?.conditions ?? []; - return renderCondition(conditions.find(c => c.type === 'PodScheduled')); - }, - }, -]; - -type DeploymentTablesProps = { - deploymentTriple: DeploymentTriple; - children?: React.ReactNode; -}; - -export const DeploymentTables = ({ - deploymentTriple, -}: DeploymentTablesProps) => { - const isOwnedBy = ( - ownerReferences: V1OwnerReference[], - obj: V1Pod | V1ReplicaSet | V1Deployment, - ): boolean => { - return ownerReferences?.some(or => or.name === obj.metadata?.name); - }; - - return ( - - {deploymentTriple.deployments.map((deployment, i) => ( - - {deploymentTriple.replicaSets - // Filter out replica sets with no replicas - .filter(rs => rs.status && rs.status.replicas > 0) - // Find the replica sets this deployment owns - .filter(rs => - isOwnedBy(rs.metadata?.ownerReferences ?? [], deployment), - ) - .map((rs, j) => { - // Find the pods this replica set owns and render them in the table - const ownedPods = deploymentTriple.pods.filter(pod => - isOwnedBy(pod.metadata?.ownerReferences ?? [], rs), - ); - - return ( - - - - ); - })} - - ))} - - ); -}; diff --git a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.test.tsx b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.test.tsx new file mode 100644 index 0000000000..5bf2c16de2 --- /dev/null +++ b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.test.tsx @@ -0,0 +1,50 @@ +/* + * 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 { render } from '@testing-library/react'; +import * as deployments from './__fixtures__/2-deployments.json'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { DeploymentDrawer } from './DeploymentDrawer'; + +describe('DeploymentDrawer', () => { + it('should render deployment drawer', async () => { + const { getByText, getAllByText } = render( + wrapInTestApp( + , + ), + ); + + expect(getAllByText('dice-roller')).toHaveLength(2); + expect(getAllByText('Deployment')).toHaveLength(2); + expect(getByText('YAML')).toBeInTheDocument(); + expect(getByText('Strategy')).toBeInTheDocument(); + expect(getByText('Rolling Update:')).toBeInTheDocument(); + expect(getByText('Max Surge: 25%')).toBeInTheDocument(); + expect(getByText('Max Unavailable: 25%')).toBeInTheDocument(); + expect(getByText('Type: RollingUpdate')).toBeInTheDocument(); + expect(getByText('Min Ready Seconds')).toBeInTheDocument(); + expect(getByText('???')).toBeInTheDocument(); + expect(getByText('Progress Deadline Seconds')).toBeInTheDocument(); + expect(getByText('600')).toBeInTheDocument(); + expect(getByText('Progressing')).toBeInTheDocument(); + expect(getByText('Available')).toBeInTheDocument(); + expect(getAllByText('True')).toHaveLength(2); + }); +}); diff --git a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.tsx b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.tsx new file mode 100644 index 0000000000..6bacad9b16 --- /dev/null +++ b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.tsx @@ -0,0 +1,72 @@ +/* + * 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 { V1Deployment } from '@kubernetes/client-node'; +import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer'; +import { renderCondition } from '../../utils/pod'; +import { Typography, Grid } from '@material-ui/core'; + +export const DeploymentDrawer = ({ + deployment, + expanded, +}: { + deployment: V1Deployment; + expanded?: boolean; +}) => { + return ( + { + const conditions = (deployment.status?.conditions ?? []) + .map(renderCondition) + .reduce((accum, next) => { + accum[next[0]] = next[1]; + return accum; + }, {} as { [key: string]: React.ReactNode }); + + return { + strategy: deployment.spec?.strategy ?? '???', + minReadySeconds: deployment.spec?.minReadySeconds ?? '???', + progressDeadlineSeconds: + deployment.spec?.progressDeadlineSeconds ?? '???', + ...conditions, + }; + }} + > + + + + {deployment.metadata?.name ?? 'unknown object'} + + + + + Deployment + + + + + ); +}; diff --git a/plugins/kubernetes/src/components/DeploymentTables/DeploymentTables.test.tsx b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.test.tsx similarity index 57% rename from plugins/kubernetes/src/components/DeploymentTables/DeploymentTables.test.tsx rename to plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.test.tsx index 3dacac7055..a522af6d4f 100644 --- a/plugins/kubernetes/src/components/DeploymentTables/DeploymentTables.test.tsx +++ b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.test.tsx @@ -16,26 +16,32 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { DeploymentTables } from './DeploymentTables'; +import { DeploymentsAccordions } from './DeploymentsAccordions'; import * as twoDeployFixture from './__fixtures__/2-deployments.json'; import { wrapInTestApp } from '@backstage/test-utils'; -describe('DeploymentTables', () => { +describe('DeploymentsAccordions', () => { it('should render 2 deployments', async () => { const { getByText } = render( wrapInTestApp( - , + , ), ); - // title expect(getByText('dice-roller')).toBeInTheDocument(); - expect(getByText('dice-roller-canary')).toBeInTheDocument(); + expect(getByText('10 pods')).toBeInTheDocument(); + expect(getByText('No pods with errors')).toBeInTheDocument(); + expect(getByText('min replicas 10 / max replicas 15')).toBeInTheDocument(); + expect(getByText('current CPU usage: 30%')).toBeInTheDocument(); + expect(getByText('target CPU usage: 50%')).toBeInTheDocument(); - // pod names - expect(getByText('dice-roller-6c8646bfd-2m5hv')).toBeInTheDocument(); - expect( - getByText('dice-roller-canary-7d64cd756c-55rfq'), - ).toBeInTheDocument(); + expect(getByText('dice-roller-canary')).toBeInTheDocument(); + expect(getByText('2 pods')).toBeInTheDocument(); + expect(getByText('1 pod with errors')).toBeInTheDocument(); }); }); diff --git a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.tsx b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.tsx new file mode 100644 index 0000000000..c2bda7a895 --- /dev/null +++ b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.tsx @@ -0,0 +1,221 @@ +/* + * 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 { DeploymentResources } from '../../types/types'; +import React from 'react'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Divider, + Grid, + Typography, +} from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { V1OwnerReference } from '@kubernetes/client-node/dist/gen/model/v1OwnerReference'; +import { + V1Deployment, + V1Pod, + V1ReplicaSet, + V1HorizontalPodAutoscaler, +} from '@kubernetes/client-node'; +import { StatusError, StatusOK } from '@backstage/core'; +import { PodsTable } from '../Pods'; +import { DeploymentDrawer } from './DeploymentDrawer'; +import { HorizontalPodAutoscalerDrawer } from '../HorizontalPodAutoscalers'; + +type DeploymentsAccordionsProps = { + deploymentResources: DeploymentResources; + clusterPodNamesWithErrors: Set; + children?: React.ReactNode; +}; + +export const DeploymentsAccordions = ({ + deploymentResources, + clusterPodNamesWithErrors, +}: DeploymentsAccordionsProps) => { + const isOwnedBy = ( + ownerReferences: V1OwnerReference[], + obj: V1Pod | V1ReplicaSet | V1Deployment, + ): boolean => { + return ownerReferences?.some(or => or.name === obj.metadata?.name); + }; + + return ( + + {deploymentResources.deployments.map((deployment, i) => ( + + {deploymentResources.replicaSets + // Filter out replica sets with no replicas + .filter(rs => rs.status && rs.status.replicas > 0) + // Find the replica sets this deployment owns + .filter(rs => + isOwnedBy(rs.metadata?.ownerReferences ?? [], deployment), + ) + .map((rs, j) => { + // Find the pods this replica set owns and render them in the table + const ownedPods = deploymentResources.pods.filter(pod => + isOwnedBy(pod.metadata?.ownerReferences ?? [], rs), + ); + + const matchingHpa = deploymentResources.horizontalPodAutoscalers.find( + (hpa: V1HorizontalPodAutoscaler) => { + return ( + (hpa.spec?.scaleTargetRef?.kind ?? '').toLowerCase() === + 'deployment' && + (hpa.spec?.scaleTargetRef?.name ?? '') === + (deployment.metadata?.name ?? 'unknown-deployment') + ); + }, + ); + + return ( + + + + ); + })} + + ))} + + ); +}; + +type DeploymentAccordionProps = { + deployment: V1Deployment; + ownedPods: V1Pod[]; + matchingHpa?: V1HorizontalPodAutoscaler; + clusterPodNamesWithErrors: Set; + children?: React.ReactNode; +}; + +const DeploymentAccordion = ({ + deployment, + ownedPods, + matchingHpa, + clusterPodNamesWithErrors, +}: DeploymentAccordionProps) => { + const podsWithErrors = ownedPods.filter(p => + clusterPodNamesWithErrors.has(p.metadata?.name ?? ''), + ); + + return ( + + }> + + + + + + + ); +}; + +type DeploymentSummaryProps = { + deployment: V1Deployment; + numberOfCurrentPods: number; + numberOfPodsWithErrors: number; + hpa?: V1HorizontalPodAutoscaler; + children?: React.ReactNode; +}; + +const DeploymentSummary = ({ + deployment, + numberOfCurrentPods, + numberOfPodsWithErrors, + hpa, +}: DeploymentSummaryProps) => { + return ( + + + + + + + + {hpa && ( + + + + + + min replicas {hpa.spec?.minReplicas ?? '?'} / max replicas{' '} + {hpa.spec?.maxReplicas ?? '?'} + + + + + current CPU usage:{' '} + {hpa.status?.currentCPUUtilizationPercentage ?? '?'}% + + + + + target CPU usage:{' '} + {hpa.spec?.targetCPUUtilizationPercentage ?? '?'}% + + + + + + )} + + + {numberOfCurrentPods} pods + + + {numberOfPodsWithErrors > 0 ? ( + + {numberOfPodsWithErrors} pod + {numberOfPodsWithErrors > 1 ? 's' : ''} with errors + + ) : ( + No pods with errors + )} + + + + ); +}; diff --git a/plugins/kubernetes/src/components/DeploymentTables/__fixtures__/2-deployments.json b/plugins/kubernetes/src/components/DeploymentsAccordions/__fixtures__/2-deployments.json similarity index 97% rename from plugins/kubernetes/src/components/DeploymentTables/__fixtures__/2-deployments.json rename to plugins/kubernetes/src/components/DeploymentsAccordions/__fixtures__/2-deployments.json index e51d0d0b5f..f5efdbf1cb 100644 --- a/plugins/kubernetes/src/components/DeploymentTables/__fixtures__/2-deployments.json +++ b/plugins/kubernetes/src/components/DeploymentsAccordions/__fixtures__/2-deployments.json @@ -4431,5 +4431,89 @@ "updatedReplicas": 2 } } + ], + "horizontalPodAutoscalers": [ + { + "apiVersion": "autoscaling/v1", + "kind": "HorizontalPodAutoscaler", + "metadata": { + "annotations": { + "autoscaling.alpha.kubernetes.io/conditions": "[{\"type\":\"AbleToScale\",\"status\":\"True\",\"lastTransitionTime\":\"2021-01-05T10:26:04Z\",\"reason\":\"SucceededGetScale\",\"message\":\"the HPA controller was able to get the target's current scale\"},{\"type\":\"ScalingActive\",\"status\":\"False\",\"lastTransitionTime\":\"2021-01-05T10:26:04Z\",\"reason\":\"FailedGetResourceMetric\",\"message\":\"the HPA was unable to compute the replica count: unable to get metrics for resource cpu: no metrics returned from resource metrics API\"}]", + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"autoscaling/v1\",\"kind\":\"HorizontalPodAutoscaler\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"},\"spec\":{\"maxReplicas\":15,\"minReplicas\":10,\"scaleTargetRef\":{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"name\":\"dice-roller\"},\"targetCPUUtilizationPercentage\":50}}\n" + }, + "creationTimestamp": "2021-01-05T10:25:48Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "managedFields": [ + { + "apiVersion": "autoscaling/v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:kubectl.kubernetes.io/last-applied-configuration": {} + }, + "f:labels": { + ".": {}, + "f:backstage.io/kubernetes-id": {} + } + }, + "f:spec": { + "f:maxReplicas": {}, + "f:minReplicas": {}, + "f:scaleTargetRef": { + "f:apiVersion": {}, + "f:kind": {}, + "f:name": {} + }, + "f:targetCPUUtilizationPercentage": {} + } + }, + "manager": "kubectl-client-side-apply", + "operation": "Update", + "time": "2021-01-05T10:25:48Z" + }, + { + "apiVersion": "autoscaling/v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + "f:autoscaling.alpha.kubernetes.io/conditions": {} + } + }, + "f:status": { + "f:currentReplicas": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "time": "2021-01-05T10:26:04Z" + } + ], + "name": "dice-roller", + "namespace": "default", + "resourceVersion": "598", + "selfLink": "/apis/autoscaling/v1/namespaces/default/horizontalpodautoscalers/dice-roller", + "uid": "dd7c5329-567c-43c2-b159-756808d90a8e" + }, + "spec": { + "maxReplicas": 15, + "minReplicas": 10, + "scaleTargetRef": { + "apiVersion": "apps/v1", + "kind": "Deployment", + "name": "dice-roller" + }, + "targetCPUUtilizationPercentage": 50 + }, + "status": { + "currentReplicas": 10, + "desiredReplicas": 0, + "currentCPUUtilizationPercentage": 30 + } + } ] } diff --git a/plugins/kubernetes/src/components/DeploymentTables/index.ts b/plugins/kubernetes/src/components/DeploymentsAccordions/index.ts similarity index 90% rename from plugins/kubernetes/src/components/DeploymentTables/index.ts rename to plugins/kubernetes/src/components/DeploymentsAccordions/index.ts index dac38c5c81..923fec624b 100644 --- a/plugins/kubernetes/src/components/DeploymentTables/index.ts +++ b/plugins/kubernetes/src/components/DeploymentsAccordions/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { DeploymentTables } from './DeploymentTables'; +export { DeploymentsAccordions } from './DeploymentsAccordions'; diff --git a/plugins/kubernetes/src/components/ErrorReporting/ErrorReporting.tsx b/plugins/kubernetes/src/components/ErrorReporting/ErrorReporting.tsx new file mode 100644 index 0000000000..c986c0158d --- /dev/null +++ b/plugins/kubernetes/src/components/ErrorReporting/ErrorReporting.tsx @@ -0,0 +1,135 @@ +/* + * 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 * as React from 'react'; +import { Table, TableColumn, InfoCard } from '@backstage/core'; +import { DetectedError, DetectedErrorsByCluster } from '../../error-detection'; +import { Chip, Typography, Grid } from '@material-ui/core'; +import EmptyStateImage from '../../../assets/emptystate.svg'; + +type ErrorReportingProps = { + detectedErrors: DetectedErrorsByCluster; +}; + +const columns: TableColumn[] = [ + { + title: 'cluster', + width: '15%', + render: (detectedError: DetectedError) => detectedError.cluster, + }, + { + title: 'kind', + width: '15%', + render: (detectedError: DetectedError) => detectedError.kind, + }, + { + title: 'name', + width: '30%', + render: (detectedError: DetectedError) => { + const errorCount = detectedError.names.length; + + if (errorCount === 0) { + // This shouldn't happen + return null; + } + + const displayName = detectedError.names[0]; + + const otherErrorCount = errorCount - 1; + + return ( + <> + {displayName}{' '} + {otherErrorCount > 0 && ( + 1 ? 's' : '' + }`} + size="small" + /> + )} + + ); + }, + }, + { + title: 'messages', + width: '40%', + render: (detectedError: DetectedError) => ( + <> + {detectedError.message.map((m, i) => ( +
{m}
+ ))} + + ), + }, +]; + +const sortBySeverity = (a: DetectedError, b: DetectedError) => { + if (a.severity < b.severity) { + return 1; + } else if (b.severity < a.severity) { + return -1; + } + return 0; +}; + +export const ErrorReporting = ({ detectedErrors }: ErrorReportingProps) => { + const errors = Array.from(detectedErrors.values()) + .flat() + .sort(sortBySeverity); + + return ( + <> + {errors.length === 0 ? ( + + + + ) : ( +
+ )} + + ); +}; + +export const ErrorEmptyState = () => { + return ( + + + + Nice! There are no errors to report! + + + + EmptyState + + + ); +}; diff --git a/plugins/kubernetes/src/components/ConfigMaps/index.ts b/plugins/kubernetes/src/components/ErrorReporting/index.ts similarity index 92% rename from plugins/kubernetes/src/components/ConfigMaps/index.ts rename to plugins/kubernetes/src/components/ErrorReporting/index.ts index b1c7d37a5a..87ce11ffd8 100644 --- a/plugins/kubernetes/src/components/ConfigMaps/index.ts +++ b/plugins/kubernetes/src/components/ErrorReporting/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { ConfigMaps } from './ConfigMaps'; +export { ErrorReporting } from './ErrorReporting'; diff --git a/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalers.test.tsx b/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalerDrawer.test.tsx similarity index 59% rename from plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalers.test.tsx rename to plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalerDrawer.test.tsx index eb5d407c76..fc64de0f0a 100644 --- a/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalers.test.tsx +++ b/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalerDrawer.test.tsx @@ -16,37 +16,35 @@ import React from 'react'; import { render } from '@testing-library/react'; -import * as hpaFixture from './__fixtures__/horizontalpodautoscalers.json'; +import * as hpas from './__fixtures__/horizontalpodautoscalers.json'; import { wrapInTestApp } from '@backstage/test-utils'; -import { HorizontalPodAutoscalers } from './HorizontalPodAutoscalers'; +import { HorizontalPodAutoscalerDrawer } from './HorizontalPodAutoscalerDrawer'; -describe('HorizontalPodAutoscalers', () => { - it('should render horizontalpodautoscaler', async () => { - const { getByText, getAllByText } = render( +describe('HorizontalPodAutoscalersDrawer', () => { + it('should render hpa drawer', async () => { + const { getByText } = render( wrapInTestApp( - , + +

CHILD

+
, ), ); - // title expect(getByText('dice-roller')).toBeInTheDocument(); - expect(getByText('Horizontal Pod Autoscaler')).toBeInTheDocument(); - - expect(getByText('Scaling Target')).toBeInTheDocument(); - expect(getByText('Api Version: apps/v1')).toBeInTheDocument(); - expect(getByText('Kind: Deployment')).toBeInTheDocument(); - expect(getByText('Name: dice-roller')).toBeInTheDocument(); - expect(getByText('Min Replicas')).toBeInTheDocument(); - expect(getByText('Max Replicas')).toBeInTheDocument(); - expect(getByText('15')).toBeInTheDocument(); - expect(getByText('Current Replicas')).toBeInTheDocument(); - expect(getByText('Desired Replicas')).toBeInTheDocument(); - expect(getByText('0')).toBeInTheDocument(); + expect(getByText('CHILD')).toBeInTheDocument(); + expect(getByText('HorizontalPodAutoscaler')).toBeInTheDocument(); + expect(getByText('YAML')).toBeInTheDocument(); expect(getByText('Target CPU Utilization Percentage')).toBeInTheDocument(); expect(getByText('50')).toBeInTheDocument(); expect(getByText('Current CPU Utilization Percentage')).toBeInTheDocument(); - expect(getByText('Last Scale Time')).toBeInTheDocument(); - expect(getAllByText('unknown')).toHaveLength(2); - expect(getAllByText('10')).toHaveLength(2); + expect(getByText('30')).toBeInTheDocument(); + expect(getByText('Min Replicas')).toBeInTheDocument(); + expect(getByText('10')).toBeInTheDocument(); + expect(getByText('Max Replicas')).toBeInTheDocument(); + expect(getByText('15')).toBeInTheDocument(); + expect(getByText('Current Replicas')).toBeInTheDocument(); + expect(getByText('13')).toBeInTheDocument(); + expect(getByText('Desired Replicas')).toBeInTheDocument(); + expect(getByText('14')).toBeInTheDocument(); }); }); diff --git a/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalerDrawer.tsx b/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalerDrawer.tsx new file mode 100644 index 0000000000..00ab2cad84 --- /dev/null +++ b/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalerDrawer.tsx @@ -0,0 +1,51 @@ +/* + * 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 { V1HorizontalPodAutoscaler } from '@kubernetes/client-node'; +import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer'; + +export const HorizontalPodAutoscalerDrawer = ({ + hpa, + expanded, + children, +}: { + hpa: V1HorizontalPodAutoscaler; + expanded?: boolean; + children?: React.ReactNode; +}) => { + return ( + { + return { + targetCPUUtilizationPercentage: + hpa.spec?.targetCPUUtilizationPercentage, + currentCPUUtilizationPercentage: + hpa.status?.currentCPUUtilizationPercentage, + minReplicas: hpa.spec?.minReplicas, + maxReplicas: hpa.spec?.maxReplicas, + currentReplicas: hpa.status?.currentReplicas, + desiredReplicas: hpa.status?.desiredReplicas, + }; + }} + > + {children} + + ); +}; diff --git a/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalers.tsx b/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalers.tsx deleted file mode 100644 index b4540840d2..0000000000 --- a/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalers.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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 { Grid } from '@material-ui/core'; -import { V1HorizontalPodAutoscaler } from '@kubernetes/client-node'; -import { InfoCard, StructuredMetadataTable } from '@backstage/core'; -import { orUnknown } from '../../utils'; - -type HorizontalPodAutoscalersProps = { - hpas: V1HorizontalPodAutoscaler[]; - children?: React.ReactNode; -}; - -export const HorizontalPodAutoscalers = ({ - hpas, -}: HorizontalPodAutoscalersProps) => { - return ( - - {hpas.map((hpa, i) => { - return ( - - -
- -
-
-
- ); - })} -
- ); -}; diff --git a/plugins/kubernetes/src/components/HorizontalPodAutoscalers/__fixtures__/horizontalpodautoscalers.json b/plugins/kubernetes/src/components/HorizontalPodAutoscalers/__fixtures__/horizontalpodautoscalers.json index 6bc6028246..6afdda48ed 100644 --- a/plugins/kubernetes/src/components/HorizontalPodAutoscalers/__fixtures__/horizontalpodautoscalers.json +++ b/plugins/kubernetes/src/components/HorizontalPodAutoscalers/__fixtures__/horizontalpodautoscalers.json @@ -74,8 +74,9 @@ "targetCPUUtilizationPercentage": 50 }, "status": { - "currentReplicas": 10, - "desiredReplicas": 0 + "currentReplicas": 13, + "desiredReplicas": 14, + "currentCPUUtilizationPercentage": 30 } } ] diff --git a/plugins/kubernetes/src/components/HorizontalPodAutoscalers/index.ts b/plugins/kubernetes/src/components/HorizontalPodAutoscalers/index.ts index 3eebbe9813..b5ca533079 100644 --- a/plugins/kubernetes/src/components/HorizontalPodAutoscalers/index.ts +++ b/plugins/kubernetes/src/components/HorizontalPodAutoscalers/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { HorizontalPodAutoscalers } from './HorizontalPodAutoscalers'; +export { HorizontalPodAutoscalerDrawer } from './HorizontalPodAutoscalerDrawer'; diff --git a/plugins/kubernetes/src/components/Ingresses/Ingresses.test.tsx b/plugins/kubernetes/src/components/Ingresses/Ingresses.test.tsx deleted file mode 100644 index f2507a3f39..0000000000 --- a/plugins/kubernetes/src/components/Ingresses/Ingresses.test.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 { render } from '@testing-library/react'; -import * as ingressesFixture from './__fixtures__/ingress.json'; -import { wrapInTestApp } from '@backstage/test-utils'; -import { Ingresses } from './Ingresses'; - -describe('Ingresses', () => { - it('should render ingress', async () => { - const { getByText } = render( - wrapInTestApp( - , - ), - ); - - // title - expect(getByText('dice-roller')).toBeInTheDocument(); - expect(getByText('Ingress')).toBeInTheDocument(); - - // values - expect(getByText('Backend')).toBeInTheDocument(); - expect(getByText('Ip: 192.168.64.2')).toBeInTheDocument(); - expect(getByText('Rules')).toBeInTheDocument(); - expect(getByText('Host: nginx')).toBeInTheDocument(); - expect(getByText('Http:')).toBeInTheDocument(); - expect(getByText('Paths:')).toBeInTheDocument(); - expect(getByText('Service Name: dice-roller')).toBeInTheDocument(); - expect(getByText('Service Port: 80')).toBeInTheDocument(); - expect(getByText('Path: /')).toBeInTheDocument(); - expect(getByText('Path Type: ImplementationSpecific')).toBeInTheDocument(); - }); -}); diff --git a/plugins/kubernetes/src/components/Ingresses/Ingresses.tsx b/plugins/kubernetes/src/components/Ingresses/Ingresses.tsx deleted file mode 100644 index 37932601a1..0000000000 --- a/plugins/kubernetes/src/components/Ingresses/Ingresses.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 { Grid } from '@material-ui/core'; -import { ExtensionsV1beta1Ingress } from '@kubernetes/client-node'; -import { InfoCard, StructuredMetadataTable } from '@backstage/core'; - -type IngressesProps = { - ingresses: ExtensionsV1beta1Ingress[]; - children?: React.ReactNode; -}; - -export const Ingresses = ({ ingresses }: IngressesProps) => { - return ( - - {ingresses.map((ingress, i) => { - return ( - - -
- -
-
-
- ); - })} -
- ); -}; diff --git a/plugins/kubernetes/src/components/Ingresses/__fixtures__/ingress.json b/plugins/kubernetes/src/components/Ingresses/__fixtures__/ingress.json deleted file mode 100644 index fd0bc5ec43..0000000000 --- a/plugins/kubernetes/src/components/Ingresses/__fixtures__/ingress.json +++ /dev/null @@ -1,87 +0,0 @@ -[ - { - "metadata": { - "annotations": { - "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"networking.k8s.io/v1beta1\",\"kind\":\"Ingress\",\"metadata\":{\"annotations\":{\"nginx.ingress.kubernetes.io/rewrite-target\":\"/$1\"},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"},\"spec\":{\"rules\":[{\"host\":\"nginx\",\"http\":{\"paths\":[{\"backend\":{\"serviceName\":\"dice-roller\",\"servicePort\":80},\"path\":\"/\"}]}}]}}\n", - "nginx.ingress.kubernetes.io/rewrite-target": "/$1" - }, - "creationTimestamp": "2020-09-28T13:28:00.000Z", - "generation": 1, - "labels": { - "backstage.io/kubernetes-id": "dice-roller" - }, - "managedFields": [ - { - "apiVersion": "networking.k8s.io/v1beta1", - "fieldsType": "FieldsV1", - "fieldsV1": { - "f:metadata": { - "f:annotations": { - ".": {}, - "f:kubectl.kubernetes.io/last-applied-configuration": {}, - "f:nginx.ingress.kubernetes.io/rewrite-target": {} - }, - "f:labels": { - ".": {}, - "f:backstage.io/kubernetes-id": {} - } - }, - "f:spec": { - "f:rules": {} - } - }, - "manager": "kubectl", - "operation": "Update", - "time": "2020-09-28T13:28:21.000Z" - }, - { - "apiVersion": "networking.k8s.io/v1beta1", - "fieldsType": "FieldsV1", - "fieldsV1": { - "f:status": { - "f:loadBalancer": { - "f:ingress": {} - } - } - }, - "manager": "nginx-ingress-controller", - "operation": "Update", - "time": "2020-09-28T13:28:40.000Z" - } - ], - "name": "dice-roller", - "namespace": "default", - "resourceVersion": "699017", - "selfLink": "/apis/networking.k8s.io/v1beta1/namespaces/default/ingresses/dice-roller", - "uid": "e96994c0-49b9-4c1c-8ce0-72c5336fe960" - }, - "spec": { - "rules": [ - { - "host": "nginx", - "http": { - "paths": [ - { - "backend": { - "serviceName": "dice-roller", - "servicePort": 80 - }, - "path": "/", - "pathType": "ImplementationSpecific" - } - ] - } - } - ] - }, - "status": { - "loadBalancer": { - "ingress": [ - { - "ip": "192.168.64.2" - } - ] - } - } - } -] diff --git a/plugins/kubernetes/src/components/KubernetesContent/ErrorPanel.tsx b/plugins/kubernetes/src/components/KubernetesContent/ErrorPanel.tsx index 1b8336dd72..85060fc11b 100644 --- a/plugins/kubernetes/src/components/KubernetesContent/ErrorPanel.tsx +++ b/plugins/kubernetes/src/components/KubernetesContent/ErrorPanel.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { WarningPanel } from '@backstage/core'; import { Typography } from '@material-ui/core'; -import { ClusterObjects } from '../../../../kubernetes-backend/src'; +import { ClusterObjects } from '@backstage/plugin-kubernetes-backend'; const clustersWithErrorsToErrorMessage = ( clustersWithErrors: ClusterObjects[], diff --git a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx index 65c1102d47..293e7a4833 100644 --- a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx +++ b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx @@ -14,89 +14,39 @@ * limitations under the License. */ -import React, { ReactElement, useEffect, useState } from 'react'; -import { Grid, TabProps } from '@material-ui/core'; +import React, { useEffect, useState } from 'react'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Divider, + Grid, + Typography, +} from '@material-ui/core'; import { Config } from '@backstage/config'; import { - CardTab, configApiRef, Content, Page, Progress, - TabbedCard, + StatusError, + StatusOK, useApi, } from '@backstage/core'; import { Entity } from '@backstage/catalog-model'; import { kubernetesApiRef } from '../../api/types'; import { - KubernetesRequestBody, ClusterObjects, - FetchResponse, + KubernetesRequestBody, ObjectsByEntityResponse, } from '@backstage/plugin-kubernetes-backend'; import { kubernetesAuthProvidersApiRef } from '../../kubernetes-auth-provider/types'; -import { DeploymentTables } from '../DeploymentTables'; -import { DeploymentTriple } from '../../types/types'; -import { - ExtensionsV1beta1Ingress, - V1ConfigMap, - V1HorizontalPodAutoscaler, - V1Service, -} from '@kubernetes/client-node'; -import { Services } from '../Services'; -import { ConfigMaps } from '../ConfigMaps'; -import { Ingresses } from '../Ingresses'; -import { HorizontalPodAutoscalers } from '../HorizontalPodAutoscalers'; import { ErrorPanel } from './ErrorPanel'; - -interface GroupedResponses extends DeploymentTriple { - services: V1Service[]; - configMaps: V1ConfigMap[]; - horizontalPodAutoscalers: V1HorizontalPodAutoscaler[]; - ingresses: ExtensionsV1beta1Ingress[]; -} - -// TODO this could probably be a lodash groupBy -const groupResponses = (fetchResponse: FetchResponse[]) => { - return fetchResponse.reduce( - (prev, next) => { - switch (next.type) { - case 'deployments': - prev.deployments.push(...next.resources); - break; - case 'pods': - prev.pods.push(...next.resources); - break; - case 'replicasets': - prev.replicaSets.push(...next.resources); - break; - case 'services': - prev.services.push(...next.resources); - break; - case 'configmaps': - prev.configMaps.push(...next.resources); - break; - case 'horizontalpodautoscalers': - prev.horizontalPodAutoscalers.push(...next.resources); - break; - case 'ingresses': - prev.ingresses.push(...next.resources); - break; - default: - } - return prev; - }, - { - pods: [], - replicaSets: [], - deployments: [], - services: [], - configMaps: [], - horizontalPodAutoscalers: [], - ingresses: [], - } as GroupedResponses, - ); -}; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { DeploymentsAccordions } from '../DeploymentsAccordions'; +import { ErrorReporting } from '../ErrorReporting'; +import { groupResponses } from '../../utils/response'; +import { DetectedError, detectErrors } from '../../error-detection'; type KubernetesContentProps = { entity: Entity; children?: React.ReactNode }; @@ -148,6 +98,11 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => { const clustersWithErrors = kubernetesObjects?.items.filter(r => r.errors.length > 0) ?? []; + const detectedErrors = + kubernetesObjects !== undefined + ? detectErrors(kubernetesObjects) + : new Map(); + return ( @@ -172,11 +127,29 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => { /> )} - {kubernetesObjects?.items.map((item, i) => ( - - - - ))} + {kubernetesObjects && ( + <> + + + + + + + + Your Clusters + + + {kubernetesObjects?.items.map((item, i) => ( + + + + ))} + + + )} @@ -185,67 +158,97 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => { type ClusterProps = { clusterObjects: ClusterObjects; + detectedErrors?: DetectedError[]; children?: React.ReactNode; }; -const Cluster = ({ clusterObjects }: ClusterProps) => { - const [selectedTab, setSelectedTab] = useState('one'); - - const handleChange = (_ev: any, newSelectedTab: string | number) => - setSelectedTab(newSelectedTab); - +const Cluster = ({ clusterObjects, detectedErrors }: ClusterProps) => { const groupedResponses = groupResponses(clusterObjects.resources); - const configMaps = groupedResponses.configMaps; - const hpas = groupedResponses.horizontalPodAutoscalers; - const ingresses = groupedResponses.ingresses; - - const tabs: ReactElement[] = [ - - - , - - - , - ]; - - if (configMaps.length > 0) { - tabs.push( - - - , - ); - } - if (hpas.length > 0) { - tabs.push( - - - , - ); - } - if (ingresses.length > 0) { - tabs.push( - - - , - ); - } + const podsWithErrors = new Set( + detectedErrors + ?.filter(de => de.kind === 'Pod') + .map(de => de.names) + .flat() ?? [], + ); return ( <> - - {tabs} - + + }> + + + + + + ); }; + +type ClusterSummaryProps = { + clusterName: string; + totalNumberOfPods: number; + numberOfPodsWithErrors: number; + children?: React.ReactNode; +}; + +const ClusterSummary = ({ + clusterName, + totalNumberOfPods, + numberOfPodsWithErrors, +}: ClusterSummaryProps) => { + return ( + + + + {clusterName} + + Cluster + + + + + + + + + {totalNumberOfPods} pods + + + {numberOfPodsWithErrors > 0 ? ( + {numberOfPodsWithErrors} pods with errors + ) : ( + No pods with errors + )} + + + + ); +}; diff --git a/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesDrawer.tsx b/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesDrawer.tsx new file mode 100644 index 0000000000..8deb49bf07 --- /dev/null +++ b/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesDrawer.tsx @@ -0,0 +1,205 @@ +/* + * 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, { ChangeEvent, useState } from 'react'; +import { + Button, + Typography, + makeStyles, + IconButton, + createStyles, + Theme, + Drawer, + Switch, + FormControlLabel, + Grid, +} from '@material-ui/core'; +import Close from '@material-ui/icons/Close'; +import { V1ObjectMeta } from '@kubernetes/client-node'; +import { withStyles } from '@material-ui/core/styles'; +import { CodeSnippet, StructuredMetadataTable } from '@backstage/core'; +import jsYaml from 'js-yaml'; + +const useDrawerStyles = makeStyles((theme: Theme) => + createStyles({ + paper: { + width: '50%', + justifyContent: 'space-between', + padding: theme.spacing(2.5), + }, + }), +); + +const useDrawerContentStyles = makeStyles((_: Theme) => + createStyles({ + header: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + }, + options: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-end', + }, + icon: { + fontSize: 20, + }, + content: { + height: '80%', + }, + }), +); + +const PodDrawerButton = withStyles({ + root: { + padding: '6px 5px', + }, + label: { + textTransform: 'none', + }, +})(Button); + +interface KubernetesDrawerable { + metadata?: V1ObjectMeta; +} + +interface KubernetesDrawerProps { + object: T; + renderObject: (obj: T) => object; + buttonVariant?: 'h5' | 'subtitle2'; + kind: string; + expanded?: boolean; + children?: React.ReactNode; +} + +export const KubernetesDrawer = ({ + object, + renderObject, + kind, + buttonVariant = 'subtitle2', + expanded = false, + children, +}: KubernetesDrawerProps) => { + const [isOpen, setIsOpen] = useState(expanded); + const classes = useDrawerStyles(); + + const toggleDrawer = (e: ChangeEvent<{}>, newValue: boolean) => { + e.stopPropagation(); + setIsOpen(newValue); + }; + + return ( + <> + toggleDrawer(e, true)} + onFocus={event => event.stopPropagation()} + > + {children === undefined ? ( + + {object.metadata?.name ?? 'unknown object'} + + ) : ( + children + )} + + toggleDrawer(e, false)} + onClick={event => event.stopPropagation()} + > + + + + ); +}; + +interface KubernetesDrawerContentProps { + toggleDrawer: (e: ChangeEvent<{}>, isOpen: boolean) => void; + object: T; + renderObject: (obj: T) => object; + kind: string; +} + +const KubernetesDrawerContent = ({ + toggleDrawer, + object, + renderObject, + kind, +}: KubernetesDrawerContentProps) => { + const [isYaml, setIsYaml] = useState(false); + + const classes = useDrawerContentStyles(); + + return ( + <> +
+ + + + {object.metadata?.name ?? 'unknown name'} + + + + + {kind} + + + + toggleDrawer(e, false)} + color="inherit" + > + + +
+
+ { + setIsYaml(event.target.checked); + }} + name="YAML" + /> + } + label="YAML" + /> +
+
+ {isYaml && } + {!isYaml && } +
+ + ); +}; diff --git a/plugins/kubernetes/src/components/Pods/PodDrawer.test.tsx b/plugins/kubernetes/src/components/Pods/PodDrawer.test.tsx new file mode 100644 index 0000000000..f166dc5f47 --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/PodDrawer.test.tsx @@ -0,0 +1,91 @@ +/* + * 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 { render } from '@testing-library/react'; +import * as pod from './__fixtures__/pod.json'; +import * as crashingPod from './__fixtures__/crashing-pod.json'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { PodDrawer } from './PodDrawer'; + +describe('PodDrawer', () => { + it('should render pod', async () => { + const { getByText, getAllByText } = render( + wrapInTestApp(), + ); + + expect(getAllByText('dice-roller-6c8646bfd-2m5hv')).toHaveLength(2); + expect(getByText('Pod')).toBeInTheDocument(); + expect(getByText('YAML')).toBeInTheDocument(); + expect(getByText('Images')).toBeInTheDocument(); + expect(getByText('nginx=nginx:1.14.2')).toBeInTheDocument(); + expect(getByText('Phase')).toBeInTheDocument(); + expect(getByText('Running')).toBeInTheDocument(); + expect(getAllByText('Containers Ready')).toHaveLength(2); + expect(getByText('1/1')).toBeInTheDocument(); + expect(getByText('Total Restarts')).toBeInTheDocument(); + expect(getByText('0')).toBeInTheDocument(); + expect(getByText('Container Statuses')).toBeInTheDocument(); + expect(getByText('OK')).toBeInTheDocument(); + expect(getByText('Initialized')).toBeInTheDocument(); + expect(getByText('Ready')).toBeInTheDocument(); + expect(getByText('Pod Scheduled')).toBeInTheDocument(); + expect(getAllByText('True')).toHaveLength(4); + expect(getByText('Exposed Ports')).toBeInTheDocument(); + expect(getByText('Nginx:')).toBeInTheDocument(); + expect(getByText('Container Port: 80')).toBeInTheDocument(); + expect(getByText('Protocol: TCP')).toBeInTheDocument(); + }); + it('should render crashing pod', async () => { + const { getByText, getAllByText } = render( + wrapInTestApp(), + ); + + expect(getAllByText('dice-roller-canary-7d64cd756c-55rfq')).toHaveLength(2); + expect(getByText('Pod')).toBeInTheDocument(); + expect(getByText('YAML')).toBeInTheDocument(); + expect(getByText('Images')).toBeInTheDocument(); + expect(getByText('nginx=nginx:1.14.2')).toBeInTheDocument(); + expect(getByText('other-side-car=nginx:1.14.2')).toBeInTheDocument(); + expect(getByText('side-car=nginx:1.14.2')).toBeInTheDocument(); + expect(getByText('Phase')).toBeInTheDocument(); + expect(getByText('Running')).toBeInTheDocument(); + expect(getAllByText('Containers Ready')).toHaveLength(2); + expect(getByText('1/3')).toBeInTheDocument(); + expect(getByText('Total Restarts')).toBeInTheDocument(); + expect(getByText('76')).toBeInTheDocument(); + expect(getByText('Container Statuses')).toBeInTheDocument(); + expect(getByText('Container: side-car')).toBeInTheDocument(); + expect(getByText('Container: other-side-car')).toBeInTheDocument(); + expect(getAllByText('CrashLoopBackOff')).toHaveLength(2); + expect(getByText('Initialized')).toBeInTheDocument(); + expect(getByText('Ready')).toBeInTheDocument(); + expect(getByText('Pod Scheduled')).toBeInTheDocument(); + expect(getAllByText('True')).toHaveLength(2); + expect(getAllByText('False')).toHaveLength(2); + expect( + getAllByText('containers with unready status: [side-car other-side-car]'), + ).toHaveLength(2); + expect(getByText('Exposed Ports')).toBeInTheDocument(); + expect(getAllByText('Protocol: TCP')).toHaveLength(3); + expect(getByText('Nginx:')).toBeInTheDocument(); + expect(getByText('Container Port: 80')).toBeInTheDocument(); + expect(getByText('Side Car:')).toBeInTheDocument(); + expect(getByText('Container Port: 81')).toBeInTheDocument(); + expect(getByText('Other Side Car:')).toBeInTheDocument(); + expect(getByText('Container Port: 82')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes/src/components/Pods/PodDrawer.tsx b/plugins/kubernetes/src/components/Pods/PodDrawer.tsx new file mode 100644 index 0000000000..ed9e208fb1 --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/PodDrawer.tsx @@ -0,0 +1,69 @@ +/* + * 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 { V1Pod } from '@kubernetes/client-node'; +import { + containersReady, + containerStatuses, + totalRestarts, + imageChips, + renderCondition, +} from '../../utils/pod'; +import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer'; + +export const PodDrawer = ({ + pod, + expanded, +}: { + pod: V1Pod; + expanded?: boolean; +}) => { + return ( + { + const phase = pod.status?.phase ?? 'unknown'; + + const ports = + pod.spec?.containers?.map(c => { + return { + [c.name]: c.ports, + }; + }) ?? 'N/A'; + + const conditions = (pod.status?.conditions ?? []) + .map(renderCondition) + .reduce((accum, next) => { + accum[next[0]] = next[1]; + return accum; + }, {} as { [key: string]: React.ReactNode }); + + return { + images: imageChips(pod), + phase: phase, + 'Containers Ready': containersReady(pod), + 'Total Restarts': totalRestarts(pod), + 'Container Statuses': containerStatuses(pod), + ...conditions, + 'Exposed ports': ports, + }; + }} + /> + ); +}; diff --git a/plugins/kubernetes/src/components/Pods/PodsTable.test.tsx b/plugins/kubernetes/src/components/Pods/PodsTable.test.tsx new file mode 100644 index 0000000000..96f3035372 --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/PodsTable.test.tsx @@ -0,0 +1,67 @@ +/* + * 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 { render } from '@testing-library/react'; +import * as pod from './__fixtures__/pod.json'; +import * as crashingPod from './__fixtures__/crashing-pod.json'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { PodsTable } from './PodsTable'; + +describe('PodsTable', () => { + it('should render pod', async () => { + const { getByText } = render( + wrapInTestApp(), + ); + + // titles + expect(getByText('name')).toBeInTheDocument(); + expect(getByText('phase')).toBeInTheDocument(); + expect(getByText('containers ready')).toBeInTheDocument(); + expect(getByText('total restarts')).toBeInTheDocument(); + expect(getByText('status')).toBeInTheDocument(); + + // values + expect(getByText('dice-roller-6c8646bfd-2m5hv')).toBeInTheDocument(); + expect(getByText('Running')).toBeInTheDocument(); + expect(getByText('1/1')).toBeInTheDocument(); + expect(getByText('0')).toBeInTheDocument(); + expect(getByText('OK')).toBeInTheDocument(); + }); + it('should render crashing pod', async () => { + const { getByText, getAllByText } = render( + wrapInTestApp(), + ); + + // titles + expect(getByText('name')).toBeInTheDocument(); + expect(getByText('phase')).toBeInTheDocument(); + expect(getByText('containers ready')).toBeInTheDocument(); + expect(getByText('total restarts')).toBeInTheDocument(); + expect(getByText('status')).toBeInTheDocument(); + + // values + expect( + getByText('dice-roller-canary-7d64cd756c-55rfq'), + ).toBeInTheDocument(); + expect(getByText('Running')).toBeInTheDocument(); + expect(getByText('1/3')).toBeInTheDocument(); + expect(getByText('76')).toBeInTheDocument(); + expect(getByText('Container: side-car')).toBeInTheDocument(); + expect(getByText('Container: other-side-car')).toBeInTheDocument(); + expect(getAllByText('CrashLoopBackOff')).toHaveLength(2); + }); +}); diff --git a/plugins/kubernetes/src/components/Pods/PodsTable.tsx b/plugins/kubernetes/src/components/Pods/PodsTable.tsx new file mode 100644 index 0000000000..fb2b26cd39 --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/PodsTable.tsx @@ -0,0 +1,74 @@ +/* + * 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 { Table, TableColumn } from '@backstage/core'; +import { V1Pod } from '@kubernetes/client-node'; +import { PodDrawer } from './PodDrawer'; +import { + containersReady, + containerStatuses, + totalRestarts, +} from '../../utils/pod'; + +const columns: TableColumn[] = [ + { + title: 'name', + highlight: true, + render: (pod: V1Pod) => , + }, + { + title: 'phase', + render: (pod: V1Pod) => pod.status?.phase ?? 'unknown', + }, + { + title: 'containers ready', + align: 'center', + render: containersReady, + }, + { + title: 'total restarts', + align: 'center', + render: totalRestarts, + type: 'numeric', + }, + { + title: 'status', + render: containerStatuses, + }, +]; + +type DeploymentTablesProps = { + pods: V1Pod[]; + children?: React.ReactNode; +}; + +export const PodsTable = ({ pods }: DeploymentTablesProps) => { + const tableStyle: React.CSSProperties = { + minWidth: '0', + width: '100%', + }; + + return ( +
+
+ + ); +}; diff --git a/plugins/kubernetes/src/components/Pods/__fixtures__/crashing-pod.json b/plugins/kubernetes/src/components/Pods/__fixtures__/crashing-pod.json new file mode 100644 index 0000000000..79acd2900d --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/__fixtures__/crashing-pod.json @@ -0,0 +1,233 @@ +{ + "metadata": { + "creationTimestamp": "2020-09-25T10:34:01.000Z", + "generateName": "dice-roller-canary-7d64cd756c-", + "labels": { + "app": "dice-roller-canary", + "backstage.io/kubernetes-id": "dice-roller", + "pod-template-hash": "7d64cd756c" + }, + "name": "dice-roller-canary-7d64cd756c-55rfq", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "dice-roller-canary-7d64cd756c", + "uid": "9208395b-a9a7-4e46-b881-6a189f7fbdb0" + } + ], + "resourceVersion": "620452", + "selfLink": "/api/v1/namespaces/default/pods/dice-roller-canary-7d64cd756c-55rfq", + "uid": "65ad28e3-5d51-4b4b-9bf8-4cb069803034" + }, + "spec": { + "containers": [ + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-5gctn", + "readOnly": true + } + ] + }, + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "side-car", + "ports": [ + { + "containerPort": 81, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-5gctn", + "readOnly": true + } + ] + }, + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "other-side-car", + "ports": [ + { + "containerPort": 82, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-5gctn", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "minikube", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "name": "default-token-5gctn", + "secret": { + "defaultMode": 420, + "secretName": "default-token-5gctn" + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T10:34:01.000Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T14:18:53.000Z", + "message": "containers with unready status: [side-car other-side-car]", + "reason": "ContainersNotReady", + "status": "False", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T14:18:53.000Z", + "message": "containers with unready status: [side-car other-side-car]", + "reason": "ContainersNotReady", + "status": "False", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T10:34:01.000Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://6ce15178d114a85f3d2e832de45c3355ab5b71ed5f4d4d225ee1c83bf07f69d9", + "image": "nginx:1.14.2", + "imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d", + "lastState": {}, + "name": "nginx", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2020-09-25T10:34:01.000Z" + } + } + }, + { + "containerID": "docker://b3ce93d7f90bfe22558c61d2505b8473580574accdebb5fa4e51c0729c3511f4", + "image": "nginx:1.14.2", + "imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d", + "lastState": { + "terminated": { + "containerID": "docker://b3ce93d7f90bfe22558c61d2505b8473580574accdebb5fa4e51c0729c3511f4", + "exitCode": 1, + "finishedAt": "2020-09-25T14:18:52.000Z", + "reason": "Error", + "startedAt": "2020-09-25T14:18:50.000Z" + } + }, + "name": "other-side-car", + "ready": false, + "restartCount": 38, + "started": false, + "state": { + "waiting": { + "message": "back-off 5m0s restarting failed container=other-side-car pod=dice-roller-canary-7d64cd756c-55rfq_default(65ad28e3-5d51-4b4b-9bf8-4cb069803034)", + "reason": "CrashLoopBackOff" + } + } + }, + { + "containerID": "docker://b7f0e65a2b8ab48c5f234616cfe8286aa96b55c3ef09c5cfbc4cdbe67a96f8cb", + "image": "nginx:1.14.2", + "imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d", + "lastState": { + "terminated": { + "containerID": "docker://b7f0e65a2b8ab48c5f234616cfe8286aa96b55c3ef09c5cfbc4cdbe67a96f8cb", + "exitCode": 1, + "finishedAt": "2020-09-25T14:18:52.000Z", + "reason": "Error", + "startedAt": "2020-09-25T14:18:50.000Z" + } + }, + "name": "side-car", + "ready": false, + "restartCount": 38, + "started": false, + "state": { + "waiting": { + "message": "back-off 5m0s restarting failed container=side-car pod=dice-roller-canary-7d64cd756c-55rfq_default(65ad28e3-5d51-4b4b-9bf8-4cb069803034)", + "reason": "CrashLoopBackOff" + } + } + } + ], + "hostIP": "192.168.64.2", + "phase": "Running", + "podIP": "172.17.0.16", + "podIPs": [ + { + "ip": "172.17.0.16" + } + ], + "qosClass": "BestEffort", + "startTime": "2020-09-25T10:34:01.000Z" + } +} diff --git a/plugins/kubernetes/src/components/Pods/__fixtures__/pod.json b/plugins/kubernetes/src/components/Pods/__fixtures__/pod.json new file mode 100644 index 0000000000..5092efc089 --- /dev/null +++ b/plugins/kubernetes/src/components/Pods/__fixtures__/pod.json @@ -0,0 +1,139 @@ +{ + "metadata": { + "creationTimestamp": "2020-09-25T09:58:50.000Z", + "generateName": "dice-roller-6c8646bfd-", + "labels": { + "app": "dice-roller", + "backstage.io/kubernetes-id": "dice-roller", + "pod-template-hash": "6c8646bfd" + }, + "name": "dice-roller-6c8646bfd-2m5hv", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "dice-roller-6c8646bfd", + "uid": "5126c354-4310-4e23-a9e4-c9b87cb69792" + } + ], + "resourceVersion": "593216", + "selfLink": "/api/v1/namespaces/default/pods/dice-roller-6c8646bfd-2m5hv", + "uid": "aadb71c0-36fa-43e3-b38a-162f134d4359" + }, + "spec": { + "containers": [ + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-5gctn", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "minikube", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "name": "default-token-5gctn", + "secret": { + "defaultMode": 420, + "secretName": "default-token-5gctn" + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:50.000Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:55.000Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:55.000Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:50.000Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://aa4489297c34c48bb33c18474a8d2b33854a82ed42155680b259f635f556ce70", + "image": "nginx:1.14.2", + "imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d", + "lastState": {}, + "name": "nginx", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2020-09-25T09:58:53.000Z" + } + } + } + ], + "hostIP": "192.168.64.2", + "phase": "Running", + "podIP": "172.17.0.11", + "podIPs": [ + { + "ip": "172.17.0.11" + } + ], + "qosClass": "BestEffort", + "startTime": "2020-09-25T09:58:50.000Z" + } +} diff --git a/plugins/kubernetes/src/components/Ingresses/index.ts b/plugins/kubernetes/src/components/Pods/index.ts similarity index 87% rename from plugins/kubernetes/src/components/Ingresses/index.ts rename to plugins/kubernetes/src/components/Pods/index.ts index ecc67480db..319950b1da 100644 --- a/plugins/kubernetes/src/components/Ingresses/index.ts +++ b/plugins/kubernetes/src/components/Pods/index.ts @@ -13,4 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { Ingresses } from './Ingresses'; +export { PodDrawer } from './PodDrawer'; +export { PodsTable } from './PodsTable'; diff --git a/plugins/kubernetes/src/components/Services/Services.test.tsx b/plugins/kubernetes/src/components/Services/Services.test.tsx deleted file mode 100644 index 5e74499bc5..0000000000 --- a/plugins/kubernetes/src/components/Services/Services.test.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 { render } from '@testing-library/react'; -import { Services } from './Services'; -import * as servicesFixture from './__fixtures__/services.json'; -import { wrapInTestApp } from '@backstage/test-utils'; - -describe('Services', () => { - it('should render 2 services', async () => { - const { getByText, getAllByText } = render( - wrapInTestApp(), - ); - - // common elements - expect(getAllByText('Service')).toHaveLength(2); - expect(getAllByText('Ports')).toHaveLength(2); - expect(getAllByText('Type')).toHaveLength(2); - expect(getAllByText('Protocol: TCP')).toHaveLength(3); - - // service 1 - expect(getByText('dice-roller')).toBeInTheDocument(); - expect(getByText('ClusterIP')).toBeInTheDocument(); - - expect(getByText('Name: port1')).toBeInTheDocument(); - expect(getByText('Port: 80')).toBeInTheDocument(); - expect(getByText('Target Port: 9376')).toBeInTheDocument(); - expect(getByText('Name: port1')).toBeInTheDocument(); - expect(getByText('Port: 81')).toBeInTheDocument(); - expect(getByText('Target Port: 9377')).toBeInTheDocument(); - expect(getByText('10.102.223.105')).toBeInTheDocument(); - - // service 2 - expect(getByText('dice-roller-lb')).toBeInTheDocument(); - expect(getByText('LoadBalancer')).toBeInTheDocument(); - expect(getByText('Node Port: 32276')).toBeInTheDocument(); - expect(getByText('Port: 8765')).toBeInTheDocument(); - expect(getByText('Target Port: 9378')).toBeInTheDocument(); - }); -}); diff --git a/plugins/kubernetes/src/components/Services/Services.tsx b/plugins/kubernetes/src/components/Services/Services.tsx deleted file mode 100644 index d0a4f71eae..0000000000 --- a/plugins/kubernetes/src/components/Services/Services.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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 { Grid } from '@material-ui/core'; -import { V1Service } from '@kubernetes/client-node'; -import { InfoCard, StructuredMetadataTable } from '@backstage/core'; - -type ServicesProps = { - services: V1Service[]; - children?: React.ReactNode; -}; - -export const Services = ({ services }: ServicesProps) => { - return ( - - {services.map((s, i) => { - const metadata: any = {}; - - if (s.status?.loadBalancer?.ingress?.length ?? -1 > 0) { - metadata.loadbalancer = s.status?.loadBalancer; - } - - if (s.spec?.type === 'ClusterIP') { - metadata.clusterIP = s.spec.clusterIP; - } - - return ( - - -
- -
-
-
- ); - })} -
- ); -}; diff --git a/plugins/kubernetes/src/components/Services/__fixtures__/services.json b/plugins/kubernetes/src/components/Services/__fixtures__/services.json deleted file mode 100644 index 7f9b49a9f6..0000000000 --- a/plugins/kubernetes/src/components/Services/__fixtures__/services.json +++ /dev/null @@ -1,164 +0,0 @@ -[ - { - "metadata": { - "annotations": { - "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"},\"spec\":{\"ports\":[{\"name\":\"port1\",\"port\":80,\"protocol\":\"TCP\",\"targetPort\":9376},{\"name\":\"port2\",\"port\":81,\"protocol\":\"TCP\",\"targetPort\":9377}],\"selector\":{\"app\":\"dice-roller\"}}}\n" - }, - "creationTimestamp": "2020-09-23T12:00:55.000Z", - "labels": { - "backstage.io/kubernetes-id": "dice-roller" - }, - "managedFields": [ - { - "apiVersion": "v1", - "fieldsType": "FieldsV1", - "fieldsV1": { - "f:metadata": { - "f:annotations": { - ".": {}, - "f:kubectl.kubernetes.io/last-applied-configuration": {} - }, - "f:labels": { - ".": {}, - "f:backstage.io/kubernetes-id": {} - } - }, - "f:spec": { - "f:ports": { - ".": {}, - "k:{\"port\":80,\"protocol\":\"TCP\"}": { - ".": {}, - "f:name": {}, - "f:port": {}, - "f:protocol": {}, - "f:targetPort": {} - }, - "k:{\"port\":81,\"protocol\":\"TCP\"}": { - ".": {}, - "f:name": {}, - "f:port": {}, - "f:protocol": {}, - "f:targetPort": {} - } - }, - "f:selector": { - ".": {}, - "f:app": {} - }, - "f:sessionAffinity": {}, - "f:type": {} - } - }, - "manager": "kubectl", - "operation": "Update", - "time": "2020-09-28T08:50:11.000Z" - } - ], - "name": "dice-roller", - "namespace": "default", - "resourceVersion": "665838", - "selfLink": "/api/v1/namespaces/default/services/dice-roller", - "uid": "ae9aff92-a525-4bc9-82dc-a0537bf8034c" - }, - "spec": { - "clusterIP": "10.102.223.105", - "ports": [ - { - "name": "port1", - "port": 80, - "protocol": "TCP", - "targetPort": 9376 - }, - { - "name": "port2", - "port": 81, - "protocol": "TCP", - "targetPort": 9377 - } - ], - "selector": { - "app": "dice-roller" - }, - "sessionAffinity": "None", - "type": "ClusterIP" - }, - "status": { - "loadBalancer": {} - } - }, - { - "metadata": { - "annotations": { - "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller-lb\",\"namespace\":\"default\"},\"spec\":{\"ports\":[{\"port\":8765,\"targetPort\":9376}],\"selector\":{\"app\":\"dice-roller\"},\"type\":\"LoadBalancer\"}}\n" - }, - "creationTimestamp": "2020-09-28T08:51:21.000Z", - "labels": { - "backstage.io/kubernetes-id": "dice-roller" - }, - "managedFields": [ - { - "apiVersion": "v1", - "fieldsType": "FieldsV1", - "fieldsV1": { - "f:metadata": { - "f:annotations": { - ".": {}, - "f:kubectl.kubernetes.io/last-applied-configuration": {} - }, - "f:labels": { - ".": {}, - "f:backstage.io/kubernetes-id": {} - } - }, - "f:spec": { - "f:externalTrafficPolicy": {}, - "f:ports": { - ".": {}, - "k:{\"port\":8765,\"protocol\":\"TCP\"}": { - ".": {}, - "f:port": {}, - "f:protocol": {}, - "f:targetPort": {} - } - }, - "f:selector": { - ".": {}, - "f:app": {} - }, - "f:sessionAffinity": {}, - "f:type": {} - } - }, - "manager": "kubectl", - "operation": "Update", - "time": "2020-09-28T08:51:21.000Z" - } - ], - "name": "dice-roller-lb", - "namespace": "default", - "resourceVersion": "665998", - "selfLink": "/api/v1/namespaces/default/services/dice-roller-lb", - "uid": "5554da3b-2041-4403-8cf4-cd2ccae760f8" - }, - "spec": { - "clusterIP": "10.99.205.233", - "externalTrafficPolicy": "Cluster", - "ports": [ - { - "nodePort": 32276, - "port": 8765, - "protocol": "TCP", - "targetPort": 9378 - } - ], - "selector": { - "app": "dice-roller" - }, - "sessionAffinity": "None", - "type": "LoadBalancer" - }, - "status": { - "loadBalancer": {} - } - } -] diff --git a/plugins/kubernetes/src/error-detection/__fixtures__/deploy-bad.json b/plugins/kubernetes/src/error-detection/__fixtures__/deploy-bad.json new file mode 100644 index 0000000000..f07a6f7d69 --- /dev/null +++ b/plugins/kubernetes/src/error-detection/__fixtures__/deploy-bad.json @@ -0,0 +1,119 @@ +{ + "metadata": { + "annotations": { + "deployment.kubernetes.io/revision": "3", + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller-canary\",\"namespace\":\"default\"},\"spec\":{\"replicas\":2,\"selector\":{\"matchLabels\":{\"app\":\"dice-roller-canary\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"dice-roller-canary\",\"backstage.io/kubernetes-id\":\"dice-roller\"}},\"spec\":{\"containers\":[{\"image\":\"nginx:1.14.2\",\"name\":\"nginx\",\"ports\":[{\"containerPort\":80}]},{\"image\":\"nginx:1.14.2\",\"name\":\"side-car\",\"ports\":[{\"containerPort\":81}]},{\"image\":\"nginx:1.14.2\",\"name\":\"other-side-car\",\"ports\":[{\"containerPort\":82}]}]}}}}\n" + }, + "creationTimestamp": "2020-09-25T09:02:53.000Z", + "generation": 3, + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "name": "dice-roller-canary", + "namespace": "default", + "resourceVersion": "620480", + "selfLink": "/apis/apps/v1/namespaces/default/deployments/dice-roller-canary", + "uid": "0b6ae80f-999b-40e9-b116-ea925f0ed07b" + }, + "spec": { + "progressDeadlineSeconds": 600, + "replicas": 2, + "revisionHistoryLimit": 10, + "selector": { + "matchLabels": { + "app": "dice-roller-canary" + } + }, + "strategy": { + "rollingUpdate": { + "maxSurge": "25%", + "maxUnavailable": "25%" + }, + "type": "RollingUpdate" + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "dice-roller-canary", + "backstage.io/kubernetes-id": "dice-roller" + } + }, + "spec": { + "containers": [ + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + }, + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "side-car", + "ports": [ + { + "containerPort": 81, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + }, + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "other-side-car", + "ports": [ + { + "containerPort": 82, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "terminationGracePeriodSeconds": 30 + } + } + }, + "status": { + "conditions": [ + { + "lastTransitionTime": "2020-09-25T09:02:53.000Z", + "lastUpdateTime": "2020-09-25T10:34:04.000Z", + "message": "ReplicaSet \"dice-roller-canary-7d64cd756c\" has successfully progressed.", + "reason": "NewReplicaSetAvailable", + "status": "True", + "type": "Progressing" + }, + { + "lastTransitionTime": "2020-09-25T13:48:06.000Z", + "lastUpdateTime": "2020-09-25T13:48:06.000Z", + "message": "Deployment does not have minimum availability.", + "reason": "MinimumReplicasUnavailable", + "status": "False", + "type": "Available" + } + ], + "observedGeneration": 3, + "replicas": 2, + "unavailableReplicas": 2, + "updatedReplicas": 2 + } +} diff --git a/plugins/kubernetes/src/error-detection/__fixtures__/deploy-healthy.json b/plugins/kubernetes/src/error-detection/__fixtures__/deploy-healthy.json new file mode 100644 index 0000000000..87a7e07b0d --- /dev/null +++ b/plugins/kubernetes/src/error-detection/__fixtures__/deploy-healthy.json @@ -0,0 +1,92 @@ +{ + "metadata": { + "annotations": { + "deployment.kubernetes.io/revision": "2", + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"},\"spec\":{\"replicas\":10,\"selector\":{\"matchLabels\":{\"app\":\"dice-roller\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"dice-roller\",\"backstage.io/kubernetes-id\":\"dice-roller\"}},\"spec\":{\"containers\":[{\"image\":\"nginx:1.14.2\",\"name\":\"nginx\",\"ports\":[{\"containerPort\":80}]}]}}}}\n" + }, + "creationTimestamp": "2020-09-23T12:00:55.000Z", + "generation": 3, + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "name": "dice-roller", + "namespace": "default", + "resourceVersion": "593230", + "selfLink": "/apis/apps/v1/namespaces/default/deployments/dice-roller", + "uid": "7551e949-42d1-4061-83c5-9da107186e47" + }, + "spec": { + "progressDeadlineSeconds": 600, + "replicas": 10, + "revisionHistoryLimit": 10, + "selector": { + "matchLabels": { + "app": "dice-roller" + } + }, + "strategy": { + "rollingUpdate": { + "maxSurge": "25%", + "maxUnavailable": "25%" + }, + "type": "RollingUpdate" + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "dice-roller", + "backstage.io/kubernetes-id": "dice-roller" + } + }, + "spec": { + "containers": [ + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "terminationGracePeriodSeconds": 30 + } + } + }, + "status": { + "availableReplicas": 10, + "conditions": [ + { + "lastTransitionTime": "2020-09-23T12:00:55.000Z", + "lastUpdateTime": "2020-09-24T11:39:28.000Z", + "message": "ReplicaSet \"dice-roller-6c8646bfd\" has successfully progressed.", + "reason": "NewReplicaSetAvailable", + "status": "True", + "type": "Progressing" + }, + { + "lastTransitionTime": "2020-09-25T09:58:55.000Z", + "lastUpdateTime": "2020-09-25T09:58:55.000Z", + "message": "Deployment has minimum availability.", + "reason": "MinimumReplicasAvailable", + "status": "True", + "type": "Available" + } + ], + "observedGeneration": 3, + "readyReplicas": 10, + "replicas": 10, + "updatedReplicas": 10 + } +} diff --git a/plugins/kubernetes/src/error-detection/__fixtures__/hpa-healthy.json b/plugins/kubernetes/src/error-detection/__fixtures__/hpa-healthy.json new file mode 100644 index 0000000000..23edee5a07 --- /dev/null +++ b/plugins/kubernetes/src/error-detection/__fixtures__/hpa-healthy.json @@ -0,0 +1,32 @@ +{ + "metadata": { + "annotations": { + "autoscaling.alpha.kubernetes.io/conditions": "[{\"type\":\"AbleToScale\",\"status\":\"True\",\"lastTransitionTime\":\"2020-09-28T13:28:15Z\",\"reason\":\"SucceededGetScale\",\"message\":\"the HPA controller was able to get the target's current scale\"},{\"type\":\"ScalingActive\",\"status\":\"False\",\"lastTransitionTime\":\"2020-09-28T13:28:15Z\",\"reason\":\"FailedGetResourceMetric\",\"message\":\"the HPA was unable to compute the replica count: unable to get metrics for resource cpu: unable to fetch metrics from resource metrics API: the server could not find the requested resource (get pods.metrics.k8s.io)\"}]", + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"autoscaling/v1\",\"kind\":\"HorizontalPodAutoscaler\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"},\"spec\":{\"maxReplicas\":15,\"minReplicas\":10,\"scaleTargetRef\":{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"name\":\"dice-roller\"},\"targetCPUUtilizationPercentage\":50}}\n" + }, + "creationTimestamp": "2020-09-28T13:28:00.000Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "name": "dice-roller", + "namespace": "default", + "resourceVersion": "698957", + "selfLink": "/apis/autoscaling/v1/namespaces/default/horizontalpodautoscalers/dice-roller", + "uid": "a70c8a90-5605-4d7d-adea-05cfb8d9d446" + }, + "spec": { + "maxReplicas": 15, + "minReplicas": 10, + "scaleTargetRef": { + "apiVersion": "apps/v1", + "kind": "Deployment", + "name": "dice-roller" + }, + "targetCPUUtilizationPercentage": 50 + }, + "status": { + "currentReplicas": 13, + "desiredReplicas": 14, + "currentCPUUtilizationPercentage": 30 + } +} diff --git a/plugins/kubernetes/src/error-detection/__fixtures__/hpa-maxed-out.json b/plugins/kubernetes/src/error-detection/__fixtures__/hpa-maxed-out.json new file mode 100644 index 0000000000..4466e7b4b1 --- /dev/null +++ b/plugins/kubernetes/src/error-detection/__fixtures__/hpa-maxed-out.json @@ -0,0 +1,32 @@ +{ + "metadata": { + "annotations": { + "autoscaling.alpha.kubernetes.io/conditions": "[{\"type\":\"AbleToScale\",\"status\":\"True\",\"lastTransitionTime\":\"2020-09-28T13:28:15Z\",\"reason\":\"SucceededGetScale\",\"message\":\"the HPA controller was able to get the target's current scale\"},{\"type\":\"ScalingActive\",\"status\":\"False\",\"lastTransitionTime\":\"2020-09-28T13:28:15Z\",\"reason\":\"FailedGetResourceMetric\",\"message\":\"the HPA was unable to compute the replica count: unable to get metrics for resource cpu: unable to fetch metrics from resource metrics API: the server could not find the requested resource (get pods.metrics.k8s.io)\"}]", + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"autoscaling/v1\",\"kind\":\"HorizontalPodAutoscaler\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"},\"spec\":{\"maxReplicas\":15,\"minReplicas\":10,\"scaleTargetRef\":{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"name\":\"dice-roller\"},\"targetCPUUtilizationPercentage\":50}}\n" + }, + "creationTimestamp": "2020-09-28T13:28:00.000Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "name": "dice-roller", + "namespace": "default", + "resourceVersion": "698957", + "selfLink": "/apis/autoscaling/v1/namespaces/default/horizontalpodautoscalers/dice-roller", + "uid": "a70c8a90-5605-4d7d-adea-05cfb8d9d446" + }, + "spec": { + "maxReplicas": 10, + "minReplicas": 5, + "scaleTargetRef": { + "apiVersion": "apps/v1", + "kind": "Deployment", + "name": "dice-roller" + }, + "targetCPUUtilizationPercentage": 70 + }, + "status": { + "currentReplicas": 10, + "desiredReplicas": 10, + "currentCPUUtilizationPercentage": 100 + } +} diff --git a/plugins/kubernetes/src/error-detection/__fixtures__/pod-crashing.json b/plugins/kubernetes/src/error-detection/__fixtures__/pod-crashing.json new file mode 100644 index 0000000000..79acd2900d --- /dev/null +++ b/plugins/kubernetes/src/error-detection/__fixtures__/pod-crashing.json @@ -0,0 +1,233 @@ +{ + "metadata": { + "creationTimestamp": "2020-09-25T10:34:01.000Z", + "generateName": "dice-roller-canary-7d64cd756c-", + "labels": { + "app": "dice-roller-canary", + "backstage.io/kubernetes-id": "dice-roller", + "pod-template-hash": "7d64cd756c" + }, + "name": "dice-roller-canary-7d64cd756c-55rfq", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "dice-roller-canary-7d64cd756c", + "uid": "9208395b-a9a7-4e46-b881-6a189f7fbdb0" + } + ], + "resourceVersion": "620452", + "selfLink": "/api/v1/namespaces/default/pods/dice-roller-canary-7d64cd756c-55rfq", + "uid": "65ad28e3-5d51-4b4b-9bf8-4cb069803034" + }, + "spec": { + "containers": [ + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-5gctn", + "readOnly": true + } + ] + }, + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "side-car", + "ports": [ + { + "containerPort": 81, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-5gctn", + "readOnly": true + } + ] + }, + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "other-side-car", + "ports": [ + { + "containerPort": 82, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-5gctn", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "minikube", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "name": "default-token-5gctn", + "secret": { + "defaultMode": 420, + "secretName": "default-token-5gctn" + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T10:34:01.000Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T14:18:53.000Z", + "message": "containers with unready status: [side-car other-side-car]", + "reason": "ContainersNotReady", + "status": "False", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T14:18:53.000Z", + "message": "containers with unready status: [side-car other-side-car]", + "reason": "ContainersNotReady", + "status": "False", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T10:34:01.000Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://6ce15178d114a85f3d2e832de45c3355ab5b71ed5f4d4d225ee1c83bf07f69d9", + "image": "nginx:1.14.2", + "imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d", + "lastState": {}, + "name": "nginx", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2020-09-25T10:34:01.000Z" + } + } + }, + { + "containerID": "docker://b3ce93d7f90bfe22558c61d2505b8473580574accdebb5fa4e51c0729c3511f4", + "image": "nginx:1.14.2", + "imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d", + "lastState": { + "terminated": { + "containerID": "docker://b3ce93d7f90bfe22558c61d2505b8473580574accdebb5fa4e51c0729c3511f4", + "exitCode": 1, + "finishedAt": "2020-09-25T14:18:52.000Z", + "reason": "Error", + "startedAt": "2020-09-25T14:18:50.000Z" + } + }, + "name": "other-side-car", + "ready": false, + "restartCount": 38, + "started": false, + "state": { + "waiting": { + "message": "back-off 5m0s restarting failed container=other-side-car pod=dice-roller-canary-7d64cd756c-55rfq_default(65ad28e3-5d51-4b4b-9bf8-4cb069803034)", + "reason": "CrashLoopBackOff" + } + } + }, + { + "containerID": "docker://b7f0e65a2b8ab48c5f234616cfe8286aa96b55c3ef09c5cfbc4cdbe67a96f8cb", + "image": "nginx:1.14.2", + "imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d", + "lastState": { + "terminated": { + "containerID": "docker://b7f0e65a2b8ab48c5f234616cfe8286aa96b55c3ef09c5cfbc4cdbe67a96f8cb", + "exitCode": 1, + "finishedAt": "2020-09-25T14:18:52.000Z", + "reason": "Error", + "startedAt": "2020-09-25T14:18:50.000Z" + } + }, + "name": "side-car", + "ready": false, + "restartCount": 38, + "started": false, + "state": { + "waiting": { + "message": "back-off 5m0s restarting failed container=side-car pod=dice-roller-canary-7d64cd756c-55rfq_default(65ad28e3-5d51-4b4b-9bf8-4cb069803034)", + "reason": "CrashLoopBackOff" + } + } + } + ], + "hostIP": "192.168.64.2", + "phase": "Running", + "podIP": "172.17.0.16", + "podIPs": [ + { + "ip": "172.17.0.16" + } + ], + "qosClass": "BestEffort", + "startTime": "2020-09-25T10:34:01.000Z" + } +} diff --git a/plugins/kubernetes/src/error-detection/__fixtures__/pod-missing-cm.json b/plugins/kubernetes/src/error-detection/__fixtures__/pod-missing-cm.json new file mode 100644 index 0000000000..3fdd4deaab --- /dev/null +++ b/plugins/kubernetes/src/error-detection/__fixtures__/pod-missing-cm.json @@ -0,0 +1,168 @@ +{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "creationTimestamp": "2021-01-06T14:33:54Z", + "generateName": "dice-roller-bad-cm-855bf85464-", + "labels": { + "app": "dice-roller-bad-cm", + "backstage.io/kubernetes-id": "dice-roller", + "pod-template-hash": "855bf85464" + }, + "name": "dice-roller-bad-cm-855bf85464-mg6xb", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "dice-roller-bad-cm-855bf85464", + "uid": "9bc2a418-60eb-4dfc-9748-78cf49ea9863" + } + ], + "resourceVersion": "2457755284", + "selfLink": "/api/v1/namespaces/default/pods/dice-roller-bad-cm-855bf85464-mg6xb", + "uid": "5f257d3c-a16d-4ef1-9dc9-d11e321a640a" + }, + "spec": { + "containers": [ + { + "env": [ + { + "name": "SOME_ENV_VAR", + "valueFrom": { + "configMapKeyRef": { + "key": "some-key", + "name": "some-cm" + } + } + } + ], + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": { + "limits": { + "cpu": "500m", + "memory": "128Mi" + }, + "requests": { + "cpu": "50m", + "memory": "64Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-dbkkb", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "node1", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "name": "default-token-dbkkb", + "secret": { + "defaultMode": 420, + "secretName": "default-token-dbkkb" + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2021-01-06T14:33:54Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2021-01-06T14:33:54Z", + "message": "containers with unready status: [nginx]", + "reason": "ContainersNotReady", + "status": "False", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2021-01-06T14:33:54Z", + "message": "containers with unready status: [nginx]", + "reason": "ContainersNotReady", + "status": "False", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2021-01-06T14:33:54Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "image": "nginx:1.14.2", + "imageID": "", + "lastState": {}, + "name": "nginx", + "ready": false, + "restartCount": 0, + "started": false, + "state": { + "waiting": { + "message": "configmap \"some-cm\" not found", + "reason": "CreateContainerConfigError" + } + } + } + ], + "hostIP": "10.1.33.73", + "phase": "Pending", + "podIP": "10.1.115.15", + "podIPs": [ + { + "ip": "10.1.115.15" + } + ], + "qosClass": "Burstable", + "startTime": "2021-01-06T14:33:54Z" + } +} diff --git a/plugins/kubernetes/src/error-detection/__fixtures__/pod.json b/plugins/kubernetes/src/error-detection/__fixtures__/pod.json new file mode 100644 index 0000000000..5092efc089 --- /dev/null +++ b/plugins/kubernetes/src/error-detection/__fixtures__/pod.json @@ -0,0 +1,139 @@ +{ + "metadata": { + "creationTimestamp": "2020-09-25T09:58:50.000Z", + "generateName": "dice-roller-6c8646bfd-", + "labels": { + "app": "dice-roller", + "backstage.io/kubernetes-id": "dice-roller", + "pod-template-hash": "6c8646bfd" + }, + "name": "dice-roller-6c8646bfd-2m5hv", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "dice-roller-6c8646bfd", + "uid": "5126c354-4310-4e23-a9e4-c9b87cb69792" + } + ], + "resourceVersion": "593216", + "selfLink": "/api/v1/namespaces/default/pods/dice-roller-6c8646bfd-2m5hv", + "uid": "aadb71c0-36fa-43e3-b38a-162f134d4359" + }, + "spec": { + "containers": [ + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-5gctn", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "minikube", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "name": "default-token-5gctn", + "secret": { + "defaultMode": 420, + "secretName": "default-token-5gctn" + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:50.000Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:55.000Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:55.000Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:50.000Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://aa4489297c34c48bb33c18474a8d2b33854a82ed42155680b259f635f556ce70", + "image": "nginx:1.14.2", + "imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d", + "lastState": {}, + "name": "nginx", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2020-09-25T09:58:53.000Z" + } + } + } + ], + "hostIP": "192.168.64.2", + "phase": "Running", + "podIP": "172.17.0.11", + "podIPs": [ + { + "ip": "172.17.0.11" + } + ], + "qosClass": "BestEffort", + "startTime": "2020-09-25T09:58:50.000Z" + } +} diff --git a/plugins/kubernetes/src/error-detection/common.ts b/plugins/kubernetes/src/error-detection/common.ts new file mode 100644 index 0000000000..626eaf39d4 --- /dev/null +++ b/plugins/kubernetes/src/error-detection/common.ts @@ -0,0 +1,70 @@ +/* + * 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 { + DetectedError, + ErrorDetectable, + ErrorDetectableKind, + ErrorMapper, +} from './types'; + +// Run through the each error mapper for each object +// returning a deduplicated (mostly) result +export const detectErrorsInObjects = ( + objects: T[], + kind: ErrorDetectableKind, + clusterName: string, + errorMappers: ErrorMapper[], +): DetectedError[] => { + // Build up a map of errors + // key: the joined message produced by an error + // value: the error + const errors = new Map(); + + for (const object of objects) { + for (const errorMapper of errorMappers) { + if (errorMapper.errorExists(object)) { + const message = errorMapper.messageAccessor(object); + + // TODO This is not perfect as errors with uuid/hashes/date/times will not be caught by this + const dedupKey = message.join(''); + + const value = errors.get(dedupKey); + + const name = object.metadata?.name ?? 'unknown'; + + if (value !== undefined) { + // This gets translated into the Chip "+5 others" + // in the ErrorReporting component + // but we need to keep the names so we can easily + // find which objects owns the error later + value.names.push(name); + errors.set(dedupKey, value); + } else { + errors.set(dedupKey, { + cluster: clusterName, + kind: kind, + names: [name], + message: message, + severity: errorMapper.severity, + }); + } + } + } + } + + return Array.from(errors.values()); +}; diff --git a/plugins/kubernetes/src/error-detection/deployments.ts b/plugins/kubernetes/src/error-detection/deployments.ts new file mode 100644 index 0000000000..f70d4196cb --- /dev/null +++ b/plugins/kubernetes/src/error-detection/deployments.ts @@ -0,0 +1,49 @@ +import { DetectedError, ErrorMapper } from './types'; +import { V1Deployment } from '@kubernetes/client-node'; +import { detectErrorsInObjects } from './common'; + +/* + * 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. + */ + +const deploymentErrorMappers: ErrorMapper[] = [ + { + // this is probably important + severity: 6, + errorExplanation: 'condition-message-present', + errorExists: deployment => { + return (deployment.status?.conditions ?? []) + .filter(c => c.status === 'False') + .some(c => c.message !== undefined); + }, + messageAccessor: deployment => { + return (deployment.status?.conditions ?? []) + .filter(c => c.status === 'False') + .filter(c => c.message !== undefined) + .map(c => c.message ?? ''); + }, + }, +]; + +export const detectErrorsInDeployments = ( + deployments: V1Deployment[], + clusterName: string, +): DetectedError[] => + detectErrorsInObjects( + deployments, + 'Deployment', + clusterName, + deploymentErrorMappers, + ); diff --git a/plugins/kubernetes/src/error-detection/error-detection.test.ts b/plugins/kubernetes/src/error-detection/error-detection.test.ts new file mode 100644 index 0000000000..8a4575c8be --- /dev/null +++ b/plugins/kubernetes/src/error-detection/error-detection.test.ts @@ -0,0 +1,282 @@ +/* + * 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 { + V1Pod, + V1Deployment, + V1HorizontalPodAutoscaler, +} from '@kubernetes/client-node'; +import { detectErrors } from './error-detection'; +import * as healthyPod from './__fixtures__/pod.json'; +import * as podMissingCm from './__fixtures__/pod-missing-cm.json'; +import * as crashingPod from './__fixtures__/pod-crashing.json'; +import * as healthyDeploy from './__fixtures__/deploy-healthy.json'; +import * as failingDeploy from './__fixtures__/deploy-bad.json'; +import * as healthyHpa from './__fixtures__/hpa-healthy.json'; +import * as maxedOutHpa from './__fixtures__/hpa-maxed-out.json'; +import { + FetchResponse, + ObjectsByEntityResponse, +} from '@backstage/plugin-kubernetes-backend'; + +const CLUSTER_NAME = 'cluster-a'; + +const oneItem = (value: FetchResponse): ObjectsByEntityResponse => { + return { + items: [ + { + cluster: { name: CLUSTER_NAME }, + errors: [], + resources: [value], + }, + ], + }; +}; + +const onePod = (pod: V1Pod): ObjectsByEntityResponse => { + return oneItem({ + type: 'pods', + resources: [pod], + }); +}; + +const oneDeployment = (deployment: V1Deployment): ObjectsByEntityResponse => { + return oneItem({ + type: 'deployments', + resources: [deployment], + }); +}; + +const oneHpa = (hpa: V1HorizontalPodAutoscaler): ObjectsByEntityResponse => { + return oneItem({ + type: 'horizontalpodautoscalers', + resources: [hpa], + }); +}; + +describe('detectErrors', () => { + it('should return errors from different clusters', () => { + const result = detectErrors({ + items: [ + { + cluster: { name: 'cluster-a' }, + errors: [], + resources: [ + { + type: 'pods', + resources: [crashingPod as any], + }, + ], + }, + { + cluster: { name: 'cluster-b' }, + errors: [], + resources: [ + { + type: 'horizontalpodautoscalers', + resources: [maxedOutHpa as any], + }, + ], + }, + { + cluster: { name: 'cluster-c' }, + errors: [], + resources: [ + { + type: 'deployments', + resources: [healthyDeploy as any], + }, + ], + }, + ], + }); + + expect(result.size).toBe(3); + + const errorsFromClusterA = result.get('cluster-a'); + const errorsFromClusterB = result.get('cluster-b'); + const errorsFromClusterC = result.get('cluster-c'); + + expect(errorsFromClusterA).toBeDefined(); + expect(errorsFromClusterA).toHaveLength(4); + + expect(errorsFromClusterB).toBeDefined(); + expect(errorsFromClusterB).toHaveLength(1); + + expect(errorsFromClusterC).toBeDefined(); + expect(errorsFromClusterC).toHaveLength(0); + }); + + it('should detect no errors in healthy pod', () => { + const result = detectErrors(onePod(healthyPod as any)); + + expect(result.size).toBe(1); + + const errors = result.get(CLUSTER_NAME); + + expect(errors).toBeDefined(); + expect(errors).toHaveLength(0); + }); + it('should detect errors in crashing pod', () => { + const result = detectErrors(onePod(crashingPod as any)); + + expect(result.size).toBe(1); + + const errors = result.get(CLUSTER_NAME); + + expect(errors).toBeDefined(); + expect(errors).toHaveLength(4); + + const [err1, err2, err3, err4] = errors ?? []; + + expect(err1).toStrictEqual({ + cluster: 'cluster-a', + kind: 'Pod', + message: [ + 'container=other-side-car restarted 38 times', + 'container=side-car restarted 38 times', + ], + names: ['dice-roller-canary-7d64cd756c-55rfq'], + severity: 4, + }); + + expect(err2).toStrictEqual({ + cluster: 'cluster-a', + kind: 'Pod', + message: [ + 'containers with unready status: [side-car other-side-car]', + 'containers with unready status: [side-car other-side-car]', + ], + names: ['dice-roller-canary-7d64cd756c-55rfq'], + severity: 5, + }); + + expect(err3).toStrictEqual({ + cluster: 'cluster-a', + kind: 'Pod', + message: [ + 'back-off 5m0s restarting failed container=other-side-car pod=dice-roller-canary-7d64cd756c-55rfq_default(65ad28e3-5d51-4b4b-9bf8-4cb069803034)', + 'back-off 5m0s restarting failed container=side-car pod=dice-roller-canary-7d64cd756c-55rfq_default(65ad28e3-5d51-4b4b-9bf8-4cb069803034)', + ], + names: ['dice-roller-canary-7d64cd756c-55rfq'], + severity: 6, + }); + + expect(err4).toStrictEqual({ + cluster: 'cluster-a', + kind: 'Pod', + message: [ + 'container=other-side-car exited with error code (1)', + 'container=side-car exited with error code (1)', + ], + names: ['dice-roller-canary-7d64cd756c-55rfq'], + severity: 4, + }); + }); + it('should detect errors in pod with missing Config Map', () => { + const result = detectErrors(onePod(podMissingCm as any)); + + expect(result.size).toBe(1); + + const errors = result.get(CLUSTER_NAME); + + expect(errors).toBeDefined(); + expect(errors).toHaveLength(2); + + const [err1, err2] = errors ?? []; + + expect(err1).toStrictEqual({ + cluster: 'cluster-a', + kind: 'Pod', + message: [ + 'containers with unready status: [nginx]', + 'containers with unready status: [nginx]', + ], + names: ['dice-roller-bad-cm-855bf85464-mg6xb'], + severity: 5, + }); + + expect(err2).toStrictEqual({ + cluster: 'cluster-a', + kind: 'Pod', + message: ['configmap "some-cm" not found'], + names: ['dice-roller-bad-cm-855bf85464-mg6xb'], + severity: 6, + }); + }); + it('should detect no errors in healthy deployment', () => { + const result = detectErrors(oneDeployment(healthyDeploy as any)); + + expect(result.size).toBe(1); + + const errors = result.get(CLUSTER_NAME); + + expect(errors).toBeDefined(); + expect(errors).toHaveLength(0); + }); + it('should detect in deployment which cant progress', () => { + const result = detectErrors(oneDeployment(failingDeploy as any)); + + expect(result.size).toBe(1); + + const errors = result.get(CLUSTER_NAME); + + expect(errors).toBeDefined(); + expect(errors).toHaveLength(1); + + const [err1] = errors ?? []; + + expect(err1).toStrictEqual({ + cluster: 'cluster-a', + kind: 'Deployment', + message: ['Deployment does not have minimum availability.'], + names: ['dice-roller-canary'], + severity: 6, + }); + }); + it('should detect no errors in healthy hpa', () => { + const result = detectErrors(oneHpa(healthyHpa as any)); + + expect(result.size).toBe(1); + + const errors = result.get(CLUSTER_NAME); + + expect(errors).toBeDefined(); + expect(errors).toHaveLength(0); + }); + it('should detect in maxed out hpa', () => { + const result = detectErrors(oneHpa(maxedOutHpa as any)); + + expect(result.size).toBe(1); + + const errors = result.get(CLUSTER_NAME); + + expect(errors).toBeDefined(); + expect(errors).toHaveLength(1); + + const [err1] = errors ?? []; + + expect(err1).toStrictEqual({ + cluster: 'cluster-a', + kind: 'HorizontalPodAutoscaler', + message: [ + 'Current number of replicas (10) is equal to the configured max number of replicas (10)', + ], + names: ['dice-roller'], + severity: 8, + }); + }); +}); diff --git a/plugins/kubernetes/src/error-detection/error-detection.ts b/plugins/kubernetes/src/error-detection/error-detection.ts new file mode 100644 index 0000000000..a44747969d --- /dev/null +++ b/plugins/kubernetes/src/error-detection/error-detection.ts @@ -0,0 +1,58 @@ +/* + * 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 { DetectedError, DetectedErrorsByCluster } from './types'; +import { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-backend'; +import { groupResponses } from '../utils/response'; +import { detectErrorsInPods } from './pods'; +import { detectErrorsInDeployments } from './deployments'; +import { detectErrorsInHpa } from './hpas'; + +// For each cluster try to find errors in each of the object types provided +// returning a map of cluster names to errors in that cluster +export const detectErrors = ( + objects: ObjectsByEntityResponse, +): DetectedErrorsByCluster => { + const errors: DetectedErrorsByCluster = new Map(); + + for (const clusterResponse of objects.items) { + let clusterErrors: DetectedError[] = []; + + const groupedResponses = groupResponses(clusterResponse.resources); + + clusterErrors = clusterErrors.concat( + detectErrorsInPods(groupedResponses.pods, clusterResponse.cluster.name), + ); + + clusterErrors = clusterErrors.concat( + detectErrorsInDeployments( + groupedResponses.deployments, + clusterResponse.cluster.name, + ), + ); + + clusterErrors = clusterErrors.concat( + detectErrorsInHpa( + groupedResponses.horizontalPodAutoscalers, + clusterResponse.cluster.name, + ), + ); + + errors.set(clusterResponse.cluster.name, clusterErrors); + } + + return errors; +}; diff --git a/plugins/kubernetes/src/error-detection/hpas.ts b/plugins/kubernetes/src/error-detection/hpas.ts new file mode 100644 index 0000000000..53c429b2f2 --- /dev/null +++ b/plugins/kubernetes/src/error-detection/hpas.ts @@ -0,0 +1,50 @@ +/* + * 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 { V1HorizontalPodAutoscaler } from '@kubernetes/client-node'; +import { DetectedError, ErrorMapper } from './types'; +import { detectErrorsInObjects } from './common'; + +const hpaErrorMappers: ErrorMapper[] = [ + { + // this is probably important + severity: 8, + errorExplanation: 'hpa-max-current-replicas', + errorExists: hpa => { + return (hpa.spec?.maxReplicas ?? -1) === hpa.status?.currentReplicas; + }, + messageAccessor: hpa => { + return [ + `Current number of replicas (${ + hpa.status?.currentReplicas + }) is equal to the configured max number of replicas (${ + hpa.spec?.maxReplicas ?? -1 + })`, + ]; + }, + }, +]; + +export const detectErrorsInHpa = ( + hpas: V1HorizontalPodAutoscaler[], + clusterName: string, +): DetectedError[] => + detectErrorsInObjects( + hpas, + 'HorizontalPodAutoscaler', + clusterName, + hpaErrorMappers, + ); diff --git a/plugins/kubernetes/src/components/Services/index.ts b/plugins/kubernetes/src/error-detection/index.ts similarity index 82% rename from plugins/kubernetes/src/components/Services/index.ts rename to plugins/kubernetes/src/error-detection/index.ts index d52ebf5f14..69a4d11851 100644 --- a/plugins/kubernetes/src/components/Services/index.ts +++ b/plugins/kubernetes/src/error-detection/index.ts @@ -13,4 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { Services } from './Services'; + +export type { DetectedError, DetectedErrorsByCluster } from './types'; +export { detectErrors } from './error-detection'; diff --git a/plugins/kubernetes/src/error-detection/pods.ts b/plugins/kubernetes/src/error-detection/pods.ts new file mode 100644 index 0000000000..8b990d0d52 --- /dev/null +++ b/plugins/kubernetes/src/error-detection/pods.ts @@ -0,0 +1,95 @@ +/* + * 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 { V1Pod } from '@kubernetes/client-node'; +import { totalRestarts } from '../utils/pod'; +import { DetectedError, ErrorMapper } from './types'; +import { detectErrorsInObjects } from './common'; + +const podErrorMappers: ErrorMapper[] = [ + { + severity: 5, + errorExplanation: 'status-message', + errorExists: pod => { + return pod.status?.message !== undefined; + }, + messageAccessor: pod => { + return [pod.status?.message ?? '']; + }, + }, + { + severity: 4, + errorExplanation: 'containers-restarting', + errorExists: pod => { + // TODO magic number + return totalRestarts(pod) > 3; + }, + messageAccessor: pod => { + return (pod.status?.containerStatuses ?? []) + .filter(cs => cs.restartCount > 0) + .map(cs => `container=${cs.name} restarted ${cs.restartCount} times`); + }, + }, + { + severity: 5, + errorExplanation: 'condition-message-present', + errorExists: pod => { + return (pod.status?.conditions ?? []).some(c => c.message !== undefined); + }, + messageAccessor: pod => { + return (pod.status?.conditions ?? []) + .filter(c => c.message !== undefined) + .map(c => c.message ?? ''); + }, + }, + { + severity: 6, + errorExplanation: 'container-waiting', + errorExists: pod => { + return (pod.status?.containerStatuses ?? []).some( + cs => cs.state?.waiting?.message !== undefined, + ); + }, + messageAccessor: pod => { + return (pod.status?.containerStatuses ?? []) + .filter(cs => cs.state?.waiting?.message !== undefined) + .map(cs => cs.state?.waiting?.message ?? ''); + }, + }, + { + severity: 4, + errorExplanation: 'container-last-state-error', + errorExists: pod => { + return (pod.status?.containerStatuses ?? []).some( + cs => (cs.lastState?.terminated?.reason ?? '') === 'Error', + ); + }, + messageAccessor: pod => { + return (pod.status?.containerStatuses ?? []) + .filter(cs => (cs.lastState?.terminated?.reason ?? '') === 'Error') + .map( + cs => + `container=${cs.name} exited with error code (${cs.lastState?.terminated?.exitCode})`, + ); + }, + }, +]; + +export const detectErrorsInPods = ( + pods: V1Pod[], + clusterName: string, +): DetectedError[] => + detectErrorsInObjects(pods, 'Pod', clusterName, podErrorMappers); diff --git a/plugins/kubernetes/src/error-detection/types.ts b/plugins/kubernetes/src/error-detection/types.ts new file mode 100644 index 0000000000..817911ce40 --- /dev/null +++ b/plugins/kubernetes/src/error-detection/types.ts @@ -0,0 +1,48 @@ +/* + * 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. + */ + +// Higher is more sever, but it's relative +import { + V1Deployment, + V1HorizontalPodAutoscaler, + V1Pod, +} from '@kubernetes/client-node'; + +export type ErrorSeverity = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; + +export type ErrorDetectable = V1Pod | V1Deployment | V1HorizontalPodAutoscaler; + +export type ErrorDetectableKind = + | 'Pod' + | 'Deployment' + | 'HorizontalPodAutoscaler'; + +export type DetectedErrorsByCluster = Map; + +export interface DetectedError { + severity: ErrorSeverity; + cluster: string; + kind: ErrorDetectableKind; + names: string[]; + message: string[]; +} + +export interface ErrorMapper { + severity: ErrorSeverity; + errorExplanation: string; + errorExists: (object: T) => boolean; + messageAccessor: (object: T) => string[]; +} diff --git a/plugins/kubernetes/src/types/types.ts b/plugins/kubernetes/src/types/types.ts index 09bff511a7..ab12730f72 100644 --- a/plugins/kubernetes/src/types/types.ts +++ b/plugins/kubernetes/src/types/types.ts @@ -14,10 +14,25 @@ * limitations under the License. */ -import { V1Deployment, V1Pod, V1ReplicaSet } from '@kubernetes/client-node'; +import { + V1Deployment, + V1Pod, + V1ReplicaSet, + V1HorizontalPodAutoscaler, + V1Service, + V1ConfigMap, + ExtensionsV1beta1Ingress, +} from '@kubernetes/client-node'; -export interface DeploymentTriple { +export interface DeploymentResources { pods: V1Pod[]; replicaSets: V1ReplicaSet[]; deployments: V1Deployment[]; + horizontalPodAutoscalers: V1HorizontalPodAutoscaler[]; +} + +export interface GroupedResponses extends DeploymentResources { + services: V1Service[]; + configMaps: V1ConfigMap[]; + ingresses: ExtensionsV1beta1Ingress[]; } diff --git a/plugins/kubernetes/src/utils/pod.tsx b/plugins/kubernetes/src/utils/pod.tsx new file mode 100644 index 0000000000..4e317cd606 --- /dev/null +++ b/plugins/kubernetes/src/utils/pod.tsx @@ -0,0 +1,104 @@ +/* + * 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 { V1Pod, V1PodCondition } from '@kubernetes/client-node'; +import React, { Fragment, ReactNode } from 'react'; +import { + StatusAborted, + StatusError, + StatusOK, + SubvalueCell, +} from '@backstage/core'; +import { Chip } from '@material-ui/core'; +import { V1DeploymentCondition } from '@kubernetes/client-node/dist/gen/model/v1DeploymentCondition'; + +export const imageChips = (pod: V1Pod): ReactNode => { + const containerStatuses = pod.status?.containerStatuses ?? []; + const images = containerStatuses.map((cs, i) => { + return ; + }); + + return
{images}
; +}; + +export const containersReady = (pod: V1Pod): string => { + const containerStatuses = pod.status?.containerStatuses ?? []; + const containersReady = containerStatuses.filter(cs => cs.ready).length; + + return `${containersReady}/${containerStatuses.length}`; +}; + +export const totalRestarts = (pod: V1Pod): number => { + const containerStatuses = pod.status?.containerStatuses ?? []; + return containerStatuses?.reduce((a, b) => a + b.restartCount, 0); +}; + +export const containerStatuses = (pod: V1Pod): ReactNode => { + const containerStatuses = pod.status?.containerStatuses ?? []; + const errors = containerStatuses.reduce((accum, next) => { + if (next.state === undefined) { + return accum; + } + + const waiting = next.state.waiting; + const terminated = next.state.terminated; + + const renderCell = (reason: string | undefined) => ( + + Container: {next.name}} + subvalue={reason} + /> +
+
+ ); + + if (waiting) { + accum.push(renderCell(waiting.reason)); + } + + if (terminated) { + accum.push(renderCell(terminated.reason)); + } + + return accum; + }, [] as React.ReactNode[]); + + if (errors.length === 0) { + return OK; + } + + return errors; +}; + +export const renderCondition = ( + condition: V1PodCondition | V1DeploymentCondition, +): [string, ReactNode] => { + const status = condition.status; + + if (status === 'True') { + return [condition.type, True]; + } else if (status === 'False') { + return [ + condition.type, + False} + subvalue={condition.message ?? ''} + />, + ]; + } + return [condition.type, ]; +}; diff --git a/plugins/kubernetes/src/utils/response.ts b/plugins/kubernetes/src/utils/response.ts new file mode 100644 index 0000000000..54b319a6ef --- /dev/null +++ b/plugins/kubernetes/src/utils/response.ts @@ -0,0 +1,62 @@ +/* + * 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 { FetchResponse } from '@backstage/plugin-kubernetes-backend'; +import { GroupedResponses } from '../types/types'; + +// TODO this could probably be a lodash groupBy +export const groupResponses = ( + fetchResponse: FetchResponse[], +): GroupedResponses => { + return fetchResponse.reduce( + (prev, next) => { + switch (next.type) { + case 'deployments': + prev.deployments.push(...next.resources); + break; + case 'pods': + prev.pods.push(...next.resources); + break; + case 'replicasets': + prev.replicaSets.push(...next.resources); + break; + case 'services': + prev.services.push(...next.resources); + break; + case 'configmaps': + prev.configMaps.push(...next.resources); + break; + case 'horizontalpodautoscalers': + prev.horizontalPodAutoscalers.push(...next.resources); + break; + case 'ingresses': + prev.ingresses.push(...next.resources); + break; + default: + } + return prev; + }, + { + pods: [], + replicaSets: [], + deployments: [], + services: [], + configMaps: [], + horizontalPodAutoscalers: [], + ingresses: [], + } as GroupedResponses, + ); +}; diff --git a/plugins/lighthouse/src/components/CreateAudit/index.tsx b/plugins/lighthouse/src/components/CreateAudit/index.tsx index 18f2c30a6e..8f514eb54a 100644 --- a/plugins/lighthouse/src/components/CreateAudit/index.tsx +++ b/plugins/lighthouse/src/components/CreateAudit/index.tsx @@ -79,7 +79,7 @@ export const CreateAuditContent = () => { // TODO use the id from the response to redirect to the audit page for that id when // FAILED and RUNNING audits are supported await lighthouseApi.triggerAudit({ - url, + url: url.replace(/\/$/, ''), options: { lighthouseConfig: { settings: { diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index 2cf1048791..ee54a5c794 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -4,7 +4,7 @@ ### Minor Changes -- 5eb8c9b9e: Fix gitlab scaffolder publisher +- 5eb8c9b9e: Fix GitLab scaffolder publisher ### Patch Changes @@ -28,7 +28,7 @@ ### Patch Changes -- 19554f6d6: Added Github Actions for Create React App, and allow better imports of files inside a module when they're exposed using `files` in `package.json` +- 19554f6d6: Added GitHub Actions for Create React App, and allow better imports of files inside a module when they're exposed using `files` in `package.json` - 33a82a713: GitLab preparer uses the right token (primarily the same one as the publisher, falling back to the integrations token) - aed8f7f12: Clearer error message when preparer or publisher type can't be determined. diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/index.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/index.ts index f7bc535189..80db93d00f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/index.ts @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './preparers'; -export * from './types'; -export * from './file'; -export * from './github'; -export * from './gitlab'; -export * from './azure'; + +export { AzurePreparer } from './azure'; +export { BitbucketPreparer } from './bitbucket'; +export { FilePreparer } from './file'; +export { GithubPreparer } from './github'; +export { GitlabPreparer } from './gitlab'; +export { Preparers } from './preparers'; +export type { PreparerBase, PreparerBuilder, PreparerOptions } from './types'; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/index.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/index.ts index b37baa3246..e55aa0919b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/index.ts @@ -13,8 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './publishers'; -export * from './github'; -export * from './gitlab'; -export * from './azure'; -export * from './types'; +export { AzurePublisher } from './azure'; +export { BitbucketPublisher } from './bitbucket'; +export { GithubPublisher } from './github'; +export type { RepoVisibilityOptions } from './github'; +export { GitlabPublisher } from './gitlab'; +export { Publishers } from './publishers'; +export type { + PublisherBase, + PublisherBuilder, + PublisherOptions, + PublisherResult, +} from './types'; diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 41965cb687..dbbc1dda2e 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -13,7 +13,7 @@ ### Patch Changes -- 19554f6d6: Added Github Actions for Create React App, and allow better imports of files inside a module when they're exposed using `files` in `package.json` +- 19554f6d6: Added GitHub Actions for Create React App, and allow better imports of files inside a module when they're exposed using `files` in `package.json` - Updated dependencies [1dc445e89] - Updated dependencies [342270e4d] - @backstage/core@0.4.2 diff --git a/plugins/sonarqube/README.md b/plugins/sonarqube/README.md index 28bf196ea5..7afc9cc04a 100644 --- a/plugins/sonarqube/README.md +++ b/plugins/sonarqube/README.md @@ -42,6 +42,8 @@ const OverviewContent = ({ entity }: { entity: Entity }) => ( 4. Add the proxy config: + Provide a method for your Backstage backend to get to your SonarQube API end point. + **SonarCloud** ```yaml diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 12f1646517..6b0aeec177 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -40,6 +40,7 @@ "dockerode": "^3.2.1", "express": "^4.17.1", "express-promise-router": "^3.0.3", + "fs-extra": "^9.0.1", "knex": "^0.21.6", "winston": "^3.2.1" }, diff --git a/plugins/techdocs-backend/src/DocsBuilder/builder.ts b/plugins/techdocs-backend/src/DocsBuilder/builder.ts index 863f587a96..3d40f0a870 100644 --- a/plugins/techdocs-backend/src/DocsBuilder/builder.ts +++ b/plugins/techdocs-backend/src/DocsBuilder/builder.ts @@ -13,6 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import fs from 'fs-extra'; +import os from 'os'; +import path from 'path'; import Docker from 'dockerode'; import { Logger } from 'winston'; import { Entity } from '@backstage/catalog-model'; @@ -78,8 +81,17 @@ export class DocsBuilder { const parsedLocationAnnotation = getLocationForEntity(this.entity); this.logger.info(`Running generator on entity ${getEntityId(this.entity)}`); - const { resultDir } = await this.generator.run({ - directory: preparedDir, + // Create a temporary directory to store the generated files in. + const tmpdirPath = os.tmpdir(); + // Fixes a problem with macOS returning a path that is a symlink + const tmpdirResolvedPath = fs.realpathSync(tmpdirPath); + const outputDir = await fs.mkdtemp( + path.join(tmpdirResolvedPath, 'techdocs-tmp-'), + ); + + await this.generator.run({ + inputDir: preparedDir, + outputDir, dockerClient: this.dockerClient, parsedLocationAnnotation, }); @@ -87,9 +99,11 @@ export class DocsBuilder { this.logger.info(`Running publisher on entity ${getEntityId(this.entity)}`); await this.publisher.publish({ entity: this.entity, - directory: resultDir, + directory: outputDir, }); + // TODO: Remove the generated directory once published. + if (!this.entity.metadata.uid) { throw new Error( 'Trying to build documentation for entity not in service catalog', diff --git a/plugins/techdocs/config.d.ts b/plugins/techdocs/config.d.ts index ad2daeb9a3..f9831f27cb 100644 --- a/plugins/techdocs/config.d.ts +++ b/plugins/techdocs/config.d.ts @@ -78,10 +78,13 @@ export interface Config { */ awsS3?: { /** - * Credentials used to access a storage bucket + * (Optional) Credentials used to access a storage bucket. + * If not set, environment variables or aws config file will be used to authenticate. + * https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-environment.html + * https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-shared.html * @visibility secret */ - credentials: { + credentials?: { /** * User access key id * attr: 'accessKeyId' - accepts a string value @@ -96,13 +99,15 @@ export interface Config { secretAccessKey: string; }; /** - * Cloud Storage Bucket Name + * (Required) Cloud Storage Bucket Name * attr: 'bucketName' - accepts a string value - * @visibility secret + * @visibility backend */ bucketName: string; /** - * AWS Region + * (Optional) AWS Region. + * If not set, AWS_REGION environment variable or aws config file will be used. + * https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-region.html * attr: 'region' - accepts a string value * @visibility secret */ @@ -123,23 +128,19 @@ export interface Config { */ googleGcs?: { /** - * API key used to write to a storage bucket. + * (Required) Cloud Storage Bucket Name + * attr: 'bucketName' - accepts a string value + * @visibility backend + */ + bucketName: string; + /** + * (Optional) API key used to write to a storage bucket. + * If not set, environment variables will be used to authenticate. + * Read more: https://cloud.google.com/docs/authentication/production * attr: 'credentials' - accepts a string value * @visibility secret */ - credentials: string; - /** - * GCP Project ID where the Cloud Storage Bucket is hosted. - * attr: 'projectId' - accepts a string value - * @visibility secret - */ - projectId: string; - /** - * Cloud Storage Bucket Name - * attr: 'bucketName' - accepts a string value - * @visibility secret - */ - bucketName: string; + credentials?: string; }; }; }; diff --git a/yarn.lock b/yarn.lock index 557a8e60e5..a0004872ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -87,6 +87,798 @@ resolved "https://registry.npmjs.org/@asyncapi/specs/-/specs-2.7.5.tgz#3a516d198fc41a1103695bd889fdd4fbbebe7f5d" integrity sha512-T1Ham9sqZKCtSowXRPaBCRy2oz3KHglqqrKiaO7lEudpP6lwH5SwXaq4qliyKzWaqd22srJHE4szdsorbFZKlw== +"@aws-crypto/crc32@^1.0.0": + version "1.0.0" + resolved "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-1.0.0.tgz#6a0164fd92bb365860ba6afb5dfef449701eb8ca" + integrity sha512-wr4EyCv3ZfLH3Sg7FErV6e/cLhpk9rUP/l5322y8PRgpQsItdieaLbtE4aDOR+dxl8U7BG9FIwWXH4TleTDZ9A== + dependencies: + tslib "^1.11.1" + +"@aws-crypto/ie11-detection@^1.0.0": + version "1.0.0" + resolved "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-1.0.0.tgz#d3a6af29ba7f15458f79c41d1cd8cac3925e726a" + integrity sha512-kCKVhCF1oDxFYgQrxXmIrS5oaWulkvRcPz+QBDMsUr2crbF4VGgGT6+uQhSwJFdUAQ2A//Vq+uT83eJrkzFgXA== + dependencies: + tslib "^1.11.1" + +"@aws-crypto/sha256-browser@^1.0.0": + version "1.0.0" + resolved "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-1.0.0.tgz#9c34d3b829d922b2c8e077b30a60db53d6befcb1" + integrity sha512-uSufui4ZktC5lYX6bDxgBgNboxGyw9V9V+rlcNsNTxh4YPhOdCslwJMGntiWOBRGAgXhhvWi7FqnTS2SaT3cpg== + dependencies: + "@aws-crypto/ie11-detection" "^1.0.0" + "@aws-crypto/sha256-js" "^1.0.0" + "@aws-crypto/supports-web-crypto" "^1.0.0" + "@aws-sdk/types" "^1.0.0-rc.1" + "@aws-sdk/util-locate-window" "^1.0.0-rc.1" + "@aws-sdk/util-utf8-browser" "^1.0.0-rc.1" + tslib "^1.11.1" + +"@aws-crypto/sha256-js@^1.0.0": + version "1.0.0" + resolved "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-1.0.0.tgz#ca788a3242a4024c386e6b9985da28f290a79ad7" + integrity sha512-89kqtFs/tdHBFHEBXZ4UXlCISswvEor3BVVOriR68Tbk1Qe1zBOZtfbSOt3CDT69z88x5uM558YW9k8I1xei5A== + dependencies: + "@aws-sdk/types" "^1.0.0-rc.1" + "@aws-sdk/util-utf8-browser" "^1.0.0-rc.1" + tslib "^1.11.1" + +"@aws-crypto/supports-web-crypto@^1.0.0": + version "1.0.0" + resolved "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-1.0.0.tgz#c40901bc17ac1e875e248df16a2b47ad8bfd9a93" + integrity sha512-IHLfv+WmVH89EW4n6a5eE8/hUlz6qkWGMn/v4r5ZgzcXdTC5nolii2z3k46y01hWRiC2PPhOdeSLzMUCUMco7g== + dependencies: + tslib "^1.11.1" + +"@aws-sdk/abort-controller@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.1.0.tgz#6b1e6cfe7410ff38ff5efdfc2e16c309d7c4e233" + integrity sha512-rqE/+BBEvF4ZxT+J1hqaSDUzXTcdnyeHQ7vqEyB1UuAmMcq6nIi1rAQKqBXneaIC4WVER3K5f00NKZGLzzqnRg== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/chunked-blob-reader-native@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/chunked-blob-reader-native/-/chunked-blob-reader-native-3.1.0.tgz#49573fe4087f07894deef7a1bf184517c3fb8f24" + integrity sha512-ghBtZkhUWgy51/651l/GUR/qhdqjFR3GSCsz0B7qisrXc8ZNsd7OlXfnTfYNoySxD3XKpbcxsncytH4Hkxgi4A== + dependencies: + "@aws-sdk/util-base64-browser" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/chunked-blob-reader@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/chunked-blob-reader/-/chunked-blob-reader-3.1.0.tgz#0a17272040760ce54ee6cd5b7e56efaca81e07aa" + integrity sha512-/2fxbKwta8ZiSj59B8F3FyVRszo1/VOhpCeL16gmRRNV73rM3IqJD+xOaDdkc/sFYyBeWn/UhwgD98kxae9XsQ== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/client-organizations@^3.2.0": + version "3.2.0" + resolved "https://registry.npmjs.org/@aws-sdk/client-organizations/-/client-organizations-3.2.0.tgz#f9fc15ecc61fdf31d1cc1eb5590e526b60bc83c0" + integrity sha512-zIFHW5tETCeoSkmLh227eG25B4HnngVDktXhbPmp1NJjT2pcZzL5QIXuNcPz3PTbkaDNRdB04uRdDqryK4kY/g== + dependencies: + "@aws-crypto/sha256-browser" "^1.0.0" + "@aws-crypto/sha256-js" "^1.0.0" + "@aws-sdk/config-resolver" "3.2.0" + "@aws-sdk/credential-provider-node" "3.1.0" + "@aws-sdk/fetch-http-handler" "3.2.0" + "@aws-sdk/hash-node" "3.1.0" + "@aws-sdk/invalid-dependency" "3.2.0" + "@aws-sdk/middleware-content-length" "3.2.0" + "@aws-sdk/middleware-host-header" "3.2.0" + "@aws-sdk/middleware-logger" "3.2.0" + "@aws-sdk/middleware-retry" "3.2.0" + "@aws-sdk/middleware-serde" "3.2.0" + "@aws-sdk/middleware-signing" "3.2.0" + "@aws-sdk/middleware-stack" "3.1.0" + "@aws-sdk/middleware-user-agent" "3.2.0" + "@aws-sdk/node-config-provider" "3.1.0" + "@aws-sdk/node-http-handler" "3.2.0" + "@aws-sdk/protocol-http" "3.2.0" + "@aws-sdk/smithy-client" "3.2.0" + "@aws-sdk/url-parser-browser" "3.1.0" + "@aws-sdk/url-parser-node" "3.1.0" + "@aws-sdk/util-base64-browser" "3.1.0" + "@aws-sdk/util-base64-node" "3.1.0" + "@aws-sdk/util-body-length-browser" "3.1.0" + "@aws-sdk/util-body-length-node" "3.1.0" + "@aws-sdk/util-user-agent-browser" "3.2.0" + "@aws-sdk/util-user-agent-node" "3.2.0" + "@aws-sdk/util-utf8-browser" "3.1.0" + "@aws-sdk/util-utf8-node" "3.1.0" + tslib "^2.0.0" + +"@aws-sdk/client-s3@^3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1.0.tgz#9c39f4588420696e1a4920f8315f778a7896b858" + integrity sha512-RX/eBi3mHoJGkS146tA4ScSdOwSLEkHWlETFkcqVYdH6IP5FT9CiwRqSf6aNRE+LQiZXoBxeOdvGFXg2jWg83w== + dependencies: + "@aws-crypto/sha256-browser" "^1.0.0" + "@aws-crypto/sha256-js" "^1.0.0" + "@aws-sdk/config-resolver" "3.1.0" + "@aws-sdk/credential-provider-node" "3.1.0" + "@aws-sdk/eventstream-serde-browser" "3.1.0" + "@aws-sdk/eventstream-serde-config-resolver" "3.1.0" + "@aws-sdk/eventstream-serde-node" "3.1.0" + "@aws-sdk/fetch-http-handler" "3.1.0" + "@aws-sdk/hash-blob-browser" "3.1.0" + "@aws-sdk/hash-node" "3.1.0" + "@aws-sdk/hash-stream-node" "3.1.0" + "@aws-sdk/invalid-dependency" "3.1.0" + "@aws-sdk/md5-js" "3.1.0" + "@aws-sdk/middleware-apply-body-checksum" "3.1.0" + "@aws-sdk/middleware-bucket-endpoint" "3.1.0" + "@aws-sdk/middleware-content-length" "3.1.0" + "@aws-sdk/middleware-expect-continue" "3.1.0" + "@aws-sdk/middleware-host-header" "3.1.0" + "@aws-sdk/middleware-location-constraint" "3.1.0" + "@aws-sdk/middleware-logger" "3.1.0" + "@aws-sdk/middleware-retry" "3.1.0" + "@aws-sdk/middleware-sdk-s3" "3.1.0" + "@aws-sdk/middleware-serde" "3.1.0" + "@aws-sdk/middleware-signing" "3.1.0" + "@aws-sdk/middleware-ssec" "3.1.0" + "@aws-sdk/middleware-stack" "3.1.0" + "@aws-sdk/middleware-user-agent" "3.1.0" + "@aws-sdk/node-config-provider" "3.1.0" + "@aws-sdk/node-http-handler" "3.1.0" + "@aws-sdk/protocol-http" "3.1.0" + "@aws-sdk/smithy-client" "3.1.0" + "@aws-sdk/url-parser-browser" "3.1.0" + "@aws-sdk/url-parser-node" "3.1.0" + "@aws-sdk/util-base64-browser" "3.1.0" + "@aws-sdk/util-base64-node" "3.1.0" + "@aws-sdk/util-body-length-browser" "3.1.0" + "@aws-sdk/util-body-length-node" "3.1.0" + "@aws-sdk/util-user-agent-browser" "3.1.0" + "@aws-sdk/util-user-agent-node" "3.1.0" + "@aws-sdk/util-utf8-browser" "3.1.0" + "@aws-sdk/util-utf8-node" "3.1.0" + "@aws-sdk/util-waiter" "3.1.0" + "@aws-sdk/xml-builder" "3.1.0" + fast-xml-parser "^3.16.0" + tslib "^2.0.0" + +"@aws-sdk/config-resolver@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.1.0.tgz#36987002c18884847aa1c96e0daf546b5f9caff6" + integrity sha512-/8hNlmeUPd1Ey9WqaZsydjWpDh1rcZpzOiiIhszOj3gGhUhsGsYGqP5oCwR9vMJf0Z5+o9diGT1QLMy9Rno6pw== + dependencies: + "@aws-sdk/signature-v4" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/config-resolver@3.2.0": + version "3.2.0" + resolved "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.2.0.tgz#c83de904f155282c91e0972b5053937ea1f05160" + integrity sha512-+PRjXpWq8JlN/ilO2F5sh/HAOkoCJkGnu/e8rfnND88tghVVauFPm7xTR2LHOGp1ugpUJm8lG/iEzpYP1EUcpA== + dependencies: + "@aws-sdk/signature-v4" "3.2.0" + tslib "^1.8.0" + +"@aws-sdk/credential-provider-env@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.1.0.tgz#eaa3ed04960faafd4536e9fbb051d5007985ba03" + integrity sha512-WUSrtg/on6bP6rjOPdjhFMkPZgDoFZZL2FST4y9K6TkRxcDHejgPZmP13L74RBGIOYlcflZAxE/936OpzpsAAA== + dependencies: + "@aws-sdk/property-provider" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/credential-provider-imds@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.1.0.tgz#33d48753dc00bddce79d2aa8076a7cb5bf8562df" + integrity sha512-I8P0ASEjuYpcDMfU2QKlT8vE3nIo803ct4y5Q54Osh5K7+H8Y+raYK9mxxhjycriDXhcQrOe2Rfj+1ARXjdvGw== + dependencies: + "@aws-sdk/property-provider" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/credential-provider-ini@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.1.0.tgz#1a6cf9ab9fa1450d4472b9e371099b0c0283349b" + integrity sha512-6kPFcsnCR1tEaVQPJGY6z58XvbeVtjsdC2srYG/0y87zyWd9awGuvRN4OV6aOr24QmEVaFNyWuE5QBQJ1/7hkA== + dependencies: + "@aws-sdk/property-provider" "3.1.0" + "@aws-sdk/shared-ini-file-loader" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/credential-provider-node@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.1.0.tgz#89bc8803752a3e580c6f2410306c7edad6be7fa2" + integrity sha512-/HNq75cNiXzxm6F+ZeJ3awuqUofUtcr89ZnSnwB1pkicz81yrqAH8EN2dEj90eDo1MdBQHx6rhaKg4SJUaBy3Q== + dependencies: + "@aws-sdk/credential-provider-env" "3.1.0" + "@aws-sdk/credential-provider-imds" "3.1.0" + "@aws-sdk/credential-provider-ini" "3.1.0" + "@aws-sdk/credential-provider-process" "3.1.0" + "@aws-sdk/property-provider" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/credential-provider-process@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.1.0.tgz#ff817b29a9760c463b77be3ce49375eaeb753ef3" + integrity sha512-pecJk5W9LYLk9KOwT5A3X6ECHoakQMvXL/whqDsKdXQ4c5C1pVoV67Jnp3ilMNfr6CauxU8gQs53iPS5LUHH4A== + dependencies: + "@aws-sdk/credential-provider-ini" "3.1.0" + "@aws-sdk/property-provider" "3.1.0" + "@aws-sdk/shared-ini-file-loader" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/eventstream-marshaller@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/eventstream-marshaller/-/eventstream-marshaller-3.1.0.tgz#65a217e37abcaa162276ccb1d4487d42431d1534" + integrity sha512-ZfWK+QPB+nuKfd90ZWpkJtK4wSKYv5qzA59jP1wwEWZ2XzUyTs2FWq4rns4af2UHnd+r5+92V2r/AZUCzT9U8w== + dependencies: + "@aws-crypto/crc32" "^1.0.0" + "@aws-sdk/util-hex-encoding" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/eventstream-serde-browser@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/eventstream-serde-browser/-/eventstream-serde-browser-3.1.0.tgz#e62f745ecb5336a957fe645f6d596501051ab73a" + integrity sha512-a7P8JzJhHTFAc6sY2uBVMr8RpLap58LXOgWUEQmozFOK6Klu2NxsdzfNGJAURsc1tK6cw126fidA9UMH/NR20Q== + dependencies: + "@aws-sdk/eventstream-marshaller" "3.1.0" + "@aws-sdk/eventstream-serde-universal" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/eventstream-serde-config-resolver@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.1.0.tgz#f4f95c4770ea34b06d79ef61eb8cbd73ac8403bc" + integrity sha512-OxgQNKNKOuXAy4ID77EC7glHALGOGdDt+fDteeajEEZ+XXQ2q4maIAbQ8N44bZcy4E8D20YEesaHDAtsDiUvPA== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/eventstream-serde-node@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/eventstream-serde-node/-/eventstream-serde-node-3.1.0.tgz#23d6630385d7023d3da9c905174ee391b5d9e5d6" + integrity sha512-xn3SwHhi9UDi6gE9fQxD8j+fxRzfnMWiu688sneoGSSzZl4e5lZtrRbMGWKdtT840dp+GrEXE6BVobUelOyVHA== + dependencies: + "@aws-sdk/eventstream-marshaller" "3.1.0" + "@aws-sdk/eventstream-serde-universal" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/eventstream-serde-universal@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/eventstream-serde-universal/-/eventstream-serde-universal-3.1.0.tgz#c48f51c92711cf0ec3a8e93b466d4dde8128d936" + integrity sha512-dnrsnLqBnFddxLtFGzDE9SBNl7+xJmgsL7SI9ALqxgvJkK7MVVj9+kzCEyypNHcRCG6OUTypvY+wjgMXJAtHUg== + dependencies: + "@aws-sdk/eventstream-marshaller" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/fetch-http-handler@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.1.0.tgz#2e495cbd5a633c3a5f9935b59faa9c8c0ed8b5e2" + integrity sha512-QCYGCdQuV3XLgqvKoJD3C/GO+cEZQOazII/rPfaeVXquv8Xrn85UJ3PZ3uFCzVVmjnm1RidwseRQoYrSsjmRhQ== + dependencies: + "@aws-sdk/protocol-http" "3.1.0" + "@aws-sdk/querystring-builder" "3.1.0" + "@aws-sdk/util-base64-browser" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/fetch-http-handler@3.2.0": + version "3.2.0" + resolved "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.2.0.tgz#116bfc06b3de797d4c746962874ef00e66fda024" + integrity sha512-Y8cw7+HNhxSpYZmd78Bq46rYUUCqnRH9AgHXkLifsigt4RsEAfYhODhEUQhEhJ/zwgadvj/fIKJWKdSiCbiC1A== + dependencies: + "@aws-sdk/protocol-http" "3.2.0" + "@aws-sdk/querystring-builder" "3.1.0" + "@aws-sdk/util-base64-browser" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/hash-blob-browser@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/hash-blob-browser/-/hash-blob-browser-3.1.0.tgz#5c6548b05d155d3141b47da958be13043a96a17f" + integrity sha512-A/iLk2P/15G//YPlB9DwNPR3J3QlrmJSrHKKYLj/t1DnxiIu3DZNCZjhLmBuQpx14O1GcolmewqJRaAq1qtVcw== + dependencies: + "@aws-sdk/chunked-blob-reader" "3.1.0" + "@aws-sdk/chunked-blob-reader-native" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/hash-node@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/hash-node/-/hash-node-3.1.0.tgz#2d9eb6c0a5b65c179d2e60db941ba921a83e1861" + integrity sha512-zcXjzPuraq6EEEC0N2DI/ng554XiHtLsWGYK2TkW5yS6+2xXiJFny3V5wZHWXsidHOgwgML92r/pU0PeVcTCbg== + dependencies: + "@aws-sdk/util-buffer-from" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/hash-stream-node@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/hash-stream-node/-/hash-stream-node-3.1.0.tgz#05424ef3873be626091b55617fe1df135ee34dcd" + integrity sha512-GQ7EsdH9yIYIVPhtcuwpYaTS3MNq+3jo6mUF06m8YxlG58GNIVz9gG8oMy9ezP5rpIjJX9YhXKN8GgERNxfpvQ== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/invalid-dependency@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-3.1.0.tgz#834c4e4a34d481bd3d4467437441ab035634710d" + integrity sha512-PNKFfIC9w25MA/fV5FnjFfFpbNiLaojgS2RtT/+L5pyic2NJ3spGNlF0pMrnPPtbrFFHaOy/E7bRa0qVtNWEyw== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/invalid-dependency@3.2.0": + version "3.2.0" + resolved "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-3.2.0.tgz#2c2c19cbec17ff28333c68e28420db6902a4374b" + integrity sha512-8hlqE4pWo2DoM6pAqjL+A3zYwUMaxP/RCGO+0zKbZHVw2stdRDzpO+jUfzZHR+mPJ+L/2wPwhK8D9n3yBriNBQ== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/is-array-buffer@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/is-array-buffer/-/is-array-buffer-3.1.0.tgz#7ac296d6408e34083ac007630541a1cdf67387fd" + integrity sha512-wE6Am+/FKuINc/aypXiBiLAatlSyxYQ9wGGQHf2iYOX5d5bHLOVKPoRwcqSCaiaR32aRcS7R+IhgxeBy+ajsMQ== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/md5-js@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/md5-js/-/md5-js-3.1.0.tgz#ae0799f9d9eeab01f4dd72baaa6bbb63cd45ffed" + integrity sha512-QB8D7EdxjOih9NWL/NoJRkgE0RSSJTXQLuQGH9UMj66rUQfGpwvLJDL+PbvoSUJgM8+McEBGyvjLCrDCK5r+qA== + dependencies: + "@aws-sdk/util-utf8-browser" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/middleware-apply-body-checksum@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-apply-body-checksum/-/middleware-apply-body-checksum-3.1.0.tgz#85504e7e7883830ffa06954a07f57b1fdb003408" + integrity sha512-q1Jx8HhC7fQjfrCUjbdlRw5F0Vr+gwjHf4sKd+S+FYO+15+PVtukBiKfP3lEW4KKREjTv1h/qPEjhv+CK4nN7w== + dependencies: + "@aws-sdk/is-array-buffer" "3.1.0" + "@aws-sdk/protocol-http" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/middleware-bucket-endpoint@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.1.0.tgz#38fb980fe99c42ddc264db40b93f28f84b3cc1e2" + integrity sha512-/NZuSbqSNARgsZqrRZ8yDXGqF0MSdA/vhNwXAk8BVz/oav4fveJ4KiMMO7Pq5918ZWJ3v//4bSezTnKLDzKmAw== + dependencies: + "@aws-sdk/protocol-http" "3.1.0" + "@aws-sdk/util-arn-parser" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/middleware-content-length@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-3.1.0.tgz#af100e6f724a188bb2963372de8d40868905be51" + integrity sha512-+QGIYkAch0q3a8LrbCS21MEKlE0WgtZBOJl76M3LZtvnbvgKkAmL4oDc+HjGD1jYuNRSt+hQZgqofuBDe8fyyw== + dependencies: + "@aws-sdk/protocol-http" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/middleware-content-length@3.2.0": + version "3.2.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-3.2.0.tgz#876943c7498e930595621395e5928fc59cf8ed68" + integrity sha512-xS7zdcRcubv7WKs5L/RG69EuuatuPxYNQT4O9epQyUaOpTHKzdVd1wnBK+otWYxI9qnCjIXwwmMyGbJXD9OHhw== + dependencies: + "@aws-sdk/protocol-http" "3.2.0" + tslib "^1.8.0" + +"@aws-sdk/middleware-expect-continue@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.1.0.tgz#3453aeb345ea852ebeefe392f7706b7573dd3c23" + integrity sha512-NCuF3BMf5QqXv4iYr4eGnmg7BfIpFjPvjxapB5cQbzUOXIyUtlYWUu7QrsdBxCsqgOhVJ4mAiqliVsdxML5yZA== + dependencies: + "@aws-sdk/middleware-header-default" "3.1.0" + "@aws-sdk/protocol-http" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/middleware-header-default@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-header-default/-/middleware-header-default-3.1.0.tgz#0b41d113f878d95132e10a48ca4c9667c28cf325" + integrity sha512-BJFQXxkkuIkN679bqqTo9hBUuTFtO4izzSY3WPnnYYcQwYTIH/4JSifCpJWw+AjbvTZ2rA2DrWaTNGz3zuiBGA== + dependencies: + "@aws-sdk/protocol-http" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/middleware-host-header@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.1.0.tgz#61f22f7d7ce8907d0dbc817ff67ec5545012dc51" + integrity sha512-x86fAcqhtK6ObgPJHFLRaZP+cC4gI185QwxqP72balLgIsJJ6InS7441sOMPr5AZ9xgLJyPPKXouQ5zFBYjwpw== + dependencies: + "@aws-sdk/protocol-http" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/middleware-host-header@3.2.0": + version "3.2.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.2.0.tgz#cf5d0868f939834fb45c4490c1118eaf25317505" + integrity sha512-URRRaqO74DSdhqYyKbyCyadJ9No17KBMmgwz2OKecdwp6/pUVNQmlxhj1cG9leWUkBuOHo6weRvWc5zXwvfy1Q== + dependencies: + "@aws-sdk/protocol-http" "3.2.0" + tslib "^1.8.0" + +"@aws-sdk/middleware-location-constraint@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.1.0.tgz#181007815ca9167b333666eb073b131e17d86fe7" + integrity sha512-NR8us/ZVJlxg/10h+dyu6xnKofSBWTp+Ai4yvkEFBsxr9kruVqGMP/2IFXl6iZAY5uy/H+TUJzyYtd4/DUgNgg== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/middleware-logger@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.1.0.tgz#6356f6d79524493a0485bfd86f621d7a882daa1a" + integrity sha512-RYJbms7ECg1FgYmN/IyK9U9nzWZtUmt2ZPBunUqvab/ldjaXpAtJq3IYdl1E/rgbc0LSTRDjyGC4erMDOT8IJg== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/middleware-logger@3.2.0": + version "3.2.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.2.0.tgz#9d4bf84fbf7a0ddc144e6c660f1b806a23a42a6e" + integrity sha512-vkex6bsYqafdvrLe4MXJDJrEfvRb5Z8z01lVlaEV2bzXFqw7WNuFVDRC80GE/YB+r2ds/n9QU4EYUSX9wewoJQ== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/middleware-retry@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.1.0.tgz#4df049c73121a83e19f5078e90a411b93a8cf7de" + integrity sha512-jNewsmLhuSHCtwpuQlsOuQ/Cig7BE4BIF7cfpWaczqYaG1F/rdTlAmb69ugg5pVby7ccj2jgA4aw+achjuzqSw== + dependencies: + "@aws-sdk/protocol-http" "3.1.0" + "@aws-sdk/service-error-classification" "3.1.0" + react-native-get-random-values "^1.4.0" + tslib "^1.8.0" + uuid "^3.0.0" + +"@aws-sdk/middleware-retry@3.2.0": + version "3.2.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.2.0.tgz#72bf988d90d9b0192aa7b290fc340b9f2e57a193" + integrity sha512-roecjUD9di1kqyCewk8pGXcsiHyi8kte5Is5bo9kCPfqHhtDN9L/efOhC+rhT0N9nKplC4rZGE4aopxNhMbLtQ== + dependencies: + "@aws-sdk/protocol-http" "3.2.0" + "@aws-sdk/service-error-classification" "3.2.0" + react-native-get-random-values "^1.4.0" + tslib "^1.8.0" + uuid "^3.0.0" + +"@aws-sdk/middleware-sdk-s3@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.1.0.tgz#e602bde5b9b5abe33e9087f61cf0f0cae9157bad" + integrity sha512-qH2RBt9EdDIO/2PGl7/QyIt9C4WuY6kVfx/S1Bh26wSY9s11VU73CDanZtgNQwIiX20v8DaPlg5dp2hse7Nr7w== + dependencies: + "@aws-sdk/protocol-http" "3.1.0" + "@aws-sdk/util-arn-parser" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/middleware-serde@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-3.1.0.tgz#a80b96dc0008546d9e9e9bce7e9b1ec134edbc6d" + integrity sha512-vXdYlzeBoJCqe+xhlFE9J62EyREvU1MVC6p3m3QPtfXuYlQwRb8WhlFFDFrDdA7V2usBjx5aM67OZnapplyUfg== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/middleware-serde@3.2.0": + version "3.2.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-3.2.0.tgz#aff46b0457f44738cdc542ea4bea01160d437300" + integrity sha512-dWTL5lIjosKCUDyaNtEolaGpo/KwoM1Zx4NdMUBLYBy9v0vKSnpRLPXxyM2EREVJYHTVrvm6UoW8dM+5RFiN7A== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/middleware-signing@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.1.0.tgz#d48c18f159f867ca5efe34c4993880e6820cc11e" + integrity sha512-3zNsonJ/nkBL8gUWr2v1vQ/NKIQQgSjmrxIa4xUxRcqOOO1qgZgjablxYHQlX07B6defhaOS2/p/EDmF+LfPqQ== + dependencies: + "@aws-sdk/protocol-http" "3.1.0" + "@aws-sdk/signature-v4" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/middleware-signing@3.2.0": + version "3.2.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.2.0.tgz#2ad5288d6543281a7e704102f6d446acdb9efaaa" + integrity sha512-XkDu0lMkdlf95nE2aYyAFmXOzBD+noVebyB1dlJDQCZWNzGkefc+8Nez3lCgyd2+KuEyHXMmQsqZwnmygpRvLQ== + dependencies: + "@aws-sdk/protocol-http" "3.2.0" + "@aws-sdk/signature-v4" "3.2.0" + tslib "^1.8.0" + +"@aws-sdk/middleware-ssec@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.1.0.tgz#af57795facc29673568277d1d9aa090e57d69669" + integrity sha512-RGHWxNEMyYHv3et23XjMVS+9HhJIPR+oO21h82Eur4WhA6u3EuiZ+TKuoXZ9DA5V2wVJvPg33NHi0/uu1mah5w== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/middleware-stack@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-3.1.0.tgz#31a69784ff31d70e1176d948f94294563b23b36e" + integrity sha512-lin0C0xPspT/orPMWWHMYG/7Z128NsSj6Khs4G6TH+2rIixXxQtHLen8H2dSPNIYXnLaxvtUDl5VuqjRt+s2Ow== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/middleware-user-agent@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.1.0.tgz#b35ce7fae8f672aa9922bc28082137fa8c2f0486" + integrity sha512-ghUD5ZhGdvPEctacO+INnQpdpQUV8N9hC9We7ftAE75PGBG/aB8qnVWYV8uKAxx8ruNAi+iU4j+4B4qLFNMx2A== + dependencies: + "@aws-sdk/protocol-http" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/middleware-user-agent@3.2.0": + version "3.2.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.2.0.tgz#19e64961feb6a41721e3164538031e21858ad7ee" + integrity sha512-rME49AFlVZSwgxTLQo/5Ok012HRcg9xhSFK4oOEkPJiYyGedvsF+Qy6sz3VjQRL2SvUvoAp03Wqbxe/Keewdbw== + dependencies: + "@aws-sdk/protocol-http" "3.2.0" + tslib "^1.8.0" + +"@aws-sdk/node-config-provider@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/node-config-provider/-/node-config-provider-3.1.0.tgz#74b37f65faa90fa6b4acb640e0f215f50aaa4904" + integrity sha512-zbV5Va40elco6IBddbDmhMQDWazQxHAk4trWFj3MrH36rpWZzWwsRbgjgGv0CcvSy5zP6PLHjdcBPeHS/KmViQ== + dependencies: + "@aws-sdk/property-provider" "3.1.0" + "@aws-sdk/shared-ini-file-loader" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/node-http-handler@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.1.0.tgz#b7fdbc3a1928127b2eb2064177502e11324f7bb8" + integrity sha512-T1yLundJ4tPeIt9IaOk24kBNBCe54UhCXXrh/0SSZZ26DLINPV5mF22wZKxgMpvKTbtJ6vlag+DhJe/ldqJ2qQ== + dependencies: + "@aws-sdk/abort-controller" "3.1.0" + "@aws-sdk/protocol-http" "3.1.0" + "@aws-sdk/querystring-builder" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/node-http-handler@3.2.0": + version "3.2.0" + resolved "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.2.0.tgz#cf6b12b021d61933202522179e5bd5710e565afa" + integrity sha512-C6bwM9RNINF2r7wTjQ7esjwlTnEIUZkJ+hyMGwOUxc4AqHCdc1PGEFwuMjQWhiyUtvR3hd7hfPj5GWvFP/PUTQ== + dependencies: + "@aws-sdk/abort-controller" "3.1.0" + "@aws-sdk/protocol-http" "3.2.0" + "@aws-sdk/querystring-builder" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/property-provider@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.1.0.tgz#19cf1da802a09be429e26f8eafec9b4c3473f8b0" + integrity sha512-NDCI7T8ytbzlXW8axDdtjMhJYRMVbOoPupM+HwR81hT7QoWWNrpCrjFqIv/gJet+ms5i4k80CWrBJd0L0kNl/Q== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/protocol-http@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.1.0.tgz#7da0ebcf02a40a8300f3bd52f9206f25fdf1ca7f" + integrity sha512-61qInY/AESslV6ZYTAgwoB172K/H+5EiXeWnmWExOGH3vkfkkxQBYCTcATdtasP6QYTfYiePhyjJ8eUyQL3C0w== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/protocol-http@3.2.0": + version "3.2.0" + resolved "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.2.0.tgz#0d352045bae38c66a2c97611983bfb545351c4e0" + integrity sha512-7FA23ABTZ18MsQD0GcICj6dU9HRInf6l6XvtnlPUS6ZvjDbsNRqEROUtvjl8dkDDP1mFpEQuHJ5e9MO5peEm7g== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/querystring-builder@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/querystring-builder/-/querystring-builder-3.1.0.tgz#6cb859cfd906faa21de32641a960f080be78f0f3" + integrity sha512-rfVJWrAT+PJ5Dx0arxK5RBMfETezAjKGHxXWf7/9rcxSqoEz8s3tYhZ7/CqcOQqN89X6gJ4veQwGprraIhOZIg== + dependencies: + "@aws-sdk/util-uri-escape" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/querystring-parser@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/querystring-parser/-/querystring-parser-3.1.0.tgz#eec9fcbfc1c907e19909b4436f93a6c9fb063041" + integrity sha512-tqwHKLKplyv940+cR8uW6w2K3IEcGOskJ/8oP1R3RSyyidONSVEShNlpApTQlccfYriieDuEDLVCo4pT7S55kw== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/service-error-classification@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.1.0.tgz#9471f761f36ebe3efeb0791d7e2af517f87ef4a2" + integrity sha512-zUNV9Fyguto8VOhinKvzIoQxwfYMSSLO6xTKJLyTB+cDv51SX5sh+lqX6IKGhuqz2Wse1ynuoLGOnmVoe4aUpQ== + +"@aws-sdk/service-error-classification@3.2.0": + version "3.2.0" + resolved "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.2.0.tgz#286ba27368276132db89f21b79724bf95cbd8d97" + integrity sha512-ZzeSZ0HnJq5zUkA466zVEZAE1SVqYuvGzxRGTEszbMW2eWPLonJgUwiT5nDScmJQcDju044iP//2KnBvn62Rhg== + +"@aws-sdk/shared-ini-file-loader@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.1.0.tgz#b1bc7ed4e16535f20c788915060a121e457efdfb" + integrity sha512-5MxZ/CnSaWvecwtLWmcskMe41zBnAkckQRl+xKygl8wLD/q0goWcmMkA4Sx9fyFnGQtGN/+nNvu0dlG2Arxmvw== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/signature-v4@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.1.0.tgz#b5d480af74629e86163ffe86a3f13a37f1f1b250" + integrity sha512-TFbe/Xf02a4baU1tg/S/w1A5AKaD7YHuqQg4Ak+HbRvAFe77/C/rbqWGnl66BDcukMvx13Ywd5ZkraAVSaTlzg== + dependencies: + "@aws-sdk/is-array-buffer" "3.1.0" + "@aws-sdk/util-hex-encoding" "3.1.0" + "@aws-sdk/util-uri-escape" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/signature-v4@3.2.0": + version "3.2.0" + resolved "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.2.0.tgz#9eb5c4c3f5c3d448f3929f310baa4ad3682d1ddc" + integrity sha512-Fb81gqaSnuCwO9HFYnztksyFud5X6/Ikr95X3pfrtLcHlHe/S079woWmlDUEgy7lw2X2GY94TcX9jlXuMhTfhA== + dependencies: + "@aws-sdk/is-array-buffer" "3.1.0" + "@aws-sdk/util-hex-encoding" "3.1.0" + "@aws-sdk/util-uri-escape" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/smithy-client@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.1.0.tgz#dc60223b9af1e99d076871e8138ea9b6ecbf737b" + integrity sha512-l/xCyQ0xLvIX/VZDYC3CGAphXtV7/t1gqfxfATbx1TD3E23PS0XvaV8JAmMWF+Q+rShk+otuYpdaRjGrygTK5w== + dependencies: + "@aws-sdk/middleware-stack" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/smithy-client@3.2.0": + version "3.2.0" + resolved "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.2.0.tgz#ba93d125f83f385ce6a4600e47339bf036fc7d0a" + integrity sha512-LpBGQ5m/oQ6QkMPQl7yxvBll/NRww4GUCEwpEXw412SUbX7aVTu+CNoUmF2lg8Gwp2XM3NgOkJ9DQ94YwZ/dug== + dependencies: + "@aws-sdk/middleware-stack" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/types@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/types/-/types-3.1.0.tgz#04d77c37a80b422e8123f296338d129e51f3e1fc" + integrity sha512-4Az7cemXCN4Qp8EheNkZTJJqIG0dvCT2KAreJLoclcVTcEFw2rzlATUnSeia1YTRsVd6aNxD001Ug7f3vYcQkw== + +"@aws-sdk/types@^1.0.0-rc.1": + version "1.0.0-rc.10" + resolved "https://registry.npmjs.org/@aws-sdk/types/-/types-1.0.0-rc.10.tgz#729127fbfac5da1a3368ffe6ec2e90acc9ad69c3" + integrity sha512-9gwhYnkTNuYZ+etCtM4T8gjpZ0SWSXbzQxY34UjSS+dt3C/UnbX0J22tMahp/9Z1yCa9pihtXrkD+nO2xn7nVQ== + +"@aws-sdk/url-parser-browser@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/url-parser-browser/-/url-parser-browser-3.1.0.tgz#8e657d9e6cbde454df3b6e597e4fa4b690fe47af" + integrity sha512-XtiorfIxhbJuU1TLp7rL7qMzNY/+9FYisFtCFcrcy7/fsvXUTjjB2u3O4J01m/T6/HVmenV+SFQvs+fYC9U7dA== + dependencies: + "@aws-sdk/querystring-parser" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/url-parser-node@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/url-parser-node/-/url-parser-node-3.1.0.tgz#f49690405195f80157997fd48f99e1e036de3a3f" + integrity sha512-pDQekV4RK2hoiDB4YkMi00oTgU8MoHg+Sve9HaLYCtTuirHMVBswxzGg6MVDWo0tM6WSFbnQRXNZtioUo7ExVA== + dependencies: + "@aws-sdk/querystring-parser" "3.1.0" + tslib "^1.8.0" + url "^0.11.0" + +"@aws-sdk/util-arn-parser@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.1.0.tgz#56c462b2ef6d7cfa098e1a22e44fb3b52da0718c" + integrity sha512-xXL/nadq5mqEw6Mrv1ghoODuyWWsAxvr+rRNgDJOav6mypgEOiLb0ybkqinrH1ogTkAYbegs+uaWxgSPBe9ZSA== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/util-base64-browser@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-base64-browser/-/util-base64-browser-3.1.0.tgz#756253a3fc1ad58c38ad28ed664b701d850d3aa9" + integrity sha512-xkodj0VnkHl1gdYI9Nl4E2Ed+atM3xBTNaedoGnmqoyosMjPRJCpU8uFBmdiF4e+GGPsXlYe9oA/hLyJFxmeSQ== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/util-base64-node@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-base64-node/-/util-base64-node-3.1.0.tgz#027a9e854d204adda56d5f43f8ef4a20532b7ac9" + integrity sha512-FEtnINw2MeD3LAtyGcofah5D8j6OjpmwNKibr7mIgosRO++iVyXe2xa6iOoptZFn5pIU0C4fkJn5o+kjBhRafA== + dependencies: + "@aws-sdk/util-buffer-from" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/util-body-length-browser@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.1.0.tgz#b68018860cd5d63c9327c7a42323c5c75cf514bf" + integrity sha512-vzKDD/p1gcA05jeLmn6+6HdOY4G6Axyp6dj1R1nVeFpPPx6KkFsNGL9/CoaRT2TGv1fHBoDXsve9JRaCxrER4Q== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/util-body-length-node@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-body-length-node/-/util-body-length-node-3.1.0.tgz#c280223066f0ce8fadb002a668a5f5e6ee4de12b" + integrity sha512-MfJoU2wFWkOmbjWDepq5bDGYZlpvtBi2Vs8ZeTcm/4+q+3L9tJ/Zb/Ofx5oeRg9VhCsAjvceQTdX+CAyP8byXA== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/util-buffer-from@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-buffer-from/-/util-buffer-from-3.1.0.tgz#a68b2e191f3737bcd85c33af5a6b0e3e9f974f0e" + integrity sha512-UeC4VKmWYgTXjNdLVHfurrdhznnoxWLUFx8xspyRd58BhSZ5vc5HiiKTPX/CGxzAP/qZG668PaoOJucwmEam4g== + dependencies: + "@aws-sdk/is-array-buffer" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/util-hex-encoding@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.1.0.tgz#937440d60333c1b3e4fbe06012dfdc65c9e297bc" + integrity sha512-MPOsUY3USCUBaqZ3ifgE9il/liVxEKsz6dYQ08pdtWRzZx2CT7kWslQeNAT565pMvktnvdLjfzBw2FwnSI6nqg== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/util-locate-window@^1.0.0-rc.1": + version "1.0.0-rc.8" + resolved "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-1.0.0-rc.8.tgz#d28175aeb9c8ad3940242e615b1503632d3be33d" + integrity sha512-TvqeA4fgmZ0A0x3K+qVj/OSWEFHGZjzpVuyXlm1EYOf7NQ9VWRlokEn1MYKuL+t7al9ZeQyi16D8Dn7DW1eidw== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/util-uri-escape@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-uri-escape/-/util-uri-escape-3.1.0.tgz#1e4450c8e047b542f87172407e2fe0ade7c55227" + integrity sha512-1ZcXVJpsA6uW3tDTQI+Rpawqh76fyHpFc55ST8VGyMgmCzlJzBpYG0ck1kqVRSUP7YyvkJQvHfcm+U6doL5Xkw== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/util-user-agent-browser@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.1.0.tgz#471dec78305f39ac9a05968970917b5c66e4312b" + integrity sha512-w31WYjRTXbBn9z/sRy/4IyjIyEeTOon1JvRzlKDbEWm4JXarVbiJcZKs1U3q+9fS9la5uLM1NEQCsgdI1iYERA== + dependencies: + "@aws-sdk/types" "3.1.0" + bowser "^2.11.0" + tslib "^1.8.0" + +"@aws-sdk/util-user-agent-browser@3.2.0": + version "3.2.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.2.0.tgz#f6fdee45817d80a637d7bd66985f516182b6f550" + integrity sha512-ugkLsntS2wpLWCjPs49wnAGhuIAFyHYzTznp0v+Qj/uOkm0ddq4kWp+FwtMxeQMRY0eoBdfqXJAbJLz8w4xlUQ== + dependencies: + bowser "^2.11.0" + tslib "^1.8.0" + +"@aws-sdk/util-user-agent-node@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.1.0.tgz#3fa66564a5fdb0e2851554688dc507127356f941" + integrity sha512-VyyZUR4vrHyT4aLH+ufoaTxFy65K9OtoCEdH1X748HinLZY9JObCCO6lZVNr5b26fGJrbqzoAqaXWgHkmEUpoQ== + dependencies: + "@aws-sdk/node-config-provider" "3.1.0" + "@aws-sdk/types" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/util-user-agent-node@3.2.0": + version "3.2.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.2.0.tgz#9190f69afdd81bacde16e253af2545a0e3f443cd" + integrity sha512-Ovyq6yc7SL0NWZWBrPKRc0D38xkMH2ulslcnukdNOitv0s8V2Ge09R4T63U0I8IJ7nrKa75Uus6A8vC8Zko3yg== + dependencies: + "@aws-sdk/node-config-provider" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/util-utf8-browser@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.1.0.tgz#7be17b545af101c320d34aace47139cf9987d796" + integrity sha512-vJP20me+Wc1RJHq+Y+gFD25aWhbQte+Qkyh3SOKQ+YvNaMcaeVwOV7b3Y3ItBuMdutHLJWmbJ2wF6dhhpy1kOA== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/util-utf8-browser@^1.0.0-rc.1": + version "1.0.0-rc.8" + resolved "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-1.0.0-rc.8.tgz#bf1f1cfed8c024f43a7c43b643fdf2b4523b5973" + integrity sha512-clncPMJ23rxCIkZ9LoUC8SowwZGxWyN2TwRb0XvW/Cv9EavkRgRCOrCpneGyC326lqtMKx36onnpaSRHxErUYw== + dependencies: + tslib "^1.8.0" + +"@aws-sdk/util-utf8-node@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-utf8-node/-/util-utf8-node-3.1.0.tgz#fd7e506b5fdf404fbbdd9bc46429afeea8114b25" + integrity sha512-lrBLkROMh9kTjHOguusqLvTX5+5O5CVpAGeISZlW6CCx2pMHtVRyE9cdNuRI8aJpyZsU12j8SoaKDUPGD+ixzw== + dependencies: + "@aws-sdk/util-buffer-from" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/util-waiter@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-waiter/-/util-waiter-3.1.0.tgz#e01dbd6bc903e03101152f65ab9d2e0613f08c2f" + integrity sha512-nMaE3aGCZGVQw10IUXBxIbCXkyaX8T1Fn7GvJxeFJflKcXuKFKBfvkhzimSe/hJDC6Ykn7gNuI+EY+ZoCVXqtQ== + dependencies: + "@aws-sdk/abort-controller" "3.1.0" + tslib "^1.8.0" + +"@aws-sdk/xml-builder@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.1.0.tgz#ad7113075416436d8c822199674c45fc6ef42441" + integrity sha512-F6liCbWPMbnJq8d0qgzuXwG5O7jg1hhgiG71TTn83rnc6vFzyw2o0C+ztiqSZsbAq7r2PlEfBPWVD32gTFIXXw== + dependencies: + tslib "^1.8.0" + "@azure/msal-common@^1.6.2": version "1.6.2" resolved "https://registry.npmjs.org/@azure/msal-common/-/msal-common-1.6.2.tgz#91f3732866d727e20f1e142e6e88a981268fbff2" @@ -1744,9 +2536,9 @@ semver "^5.4.1" "@changesets/cli@^2.11.0": - version "2.11.0" - resolved "https://registry.npmjs.org/@changesets/cli/-/cli-2.11.0.tgz#048449289da95f24d96b187f3148c2955065d609" - integrity sha512-imsXWxl+QpLt6PYHSiuZS7xPdKrGs2c8saSG4ENzeZBgnTBMiJdxDloOT6Bmv15ICggHZj9mnaUBNOMPbvnlbA== + version "2.12.0" + resolved "https://registry.npmjs.org/@changesets/cli/-/cli-2.12.0.tgz#26124b051e6ce6dcc5aa4595588c8cb2ce3e4363" + integrity sha512-dGdFkg75zsaEObsG8gwMLglS6sJVjXWwgVTAzEIjqIoWVnKwqZqccTb4gn0noq47uCwy7SqxiAJqGibIy9UOKw== dependencies: "@babel/runtime" "^7.10.4" "@changesets/apply-release-plan" "^4.0.0" @@ -1770,14 +2562,14 @@ fs-extra "^7.0.1" human-id "^1.0.2" is-ci "^2.0.0" - meow "^5.0.0" + meow "^6.0.0" outdent "^0.5.0" p-limit "^2.2.0" preferred-pm "^3.0.0" semver "^5.4.1" spawndamnit "^2.0.0" term-size "^2.1.0" - tty-table "^2.7.0" + tty-table "^2.8.10" "@changesets/config@^1.2.0", "@changesets/config@^1.4.0": version "1.4.0" @@ -1880,12 +2672,7 @@ 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": - version "3.1.1" - resolved "https://registry.npmjs.org/@changesets/types/-/types-3.1.1.tgz#447481380c42044a8788e46c0dbdf592b338b62f" - integrity sha512-XWGEGWXhM92zvBWiQt2sOwhjTt8eCQbrsRbqkv4WYwW3Zsl4qPpvhHsNt845S42dJXrxgjWvId+jxFQocCayNQ== - -"@changesets/types@^3.2.0": +"@changesets/types@^3.0.0", "@changesets/types@^3.1.0", "@changesets/types@^3.1.1", "@changesets/types@^3.2.0": version "3.2.0" resolved "https://registry.npmjs.org/@changesets/types/-/types-3.2.0.tgz#d8306d7219c3b19b6d860ddeb9d7374e2dd6b035" integrity sha512-rAmPtOyXpisEEE25CchKNUAf2ApyAeuZ/h78YDoqKZaCk5tUD0lgYZGPIRV9WTPoqNjJULIym37ogc6pkax5jg== @@ -7675,21 +8462,6 @@ autoprefixer@^9.7.2: postcss "^7.0.32" postcss-value-parser "^4.1.0" -aws-sdk@^2.817.0: - version "2.817.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.817.0.tgz#3a97b690b0ec494cf8ee927affb3973cf26abcc8" - integrity sha512-DZIdWpkcqbqsCz0MEskHsyFaqc6Tk9XIFqXAg1AKHbOgC8nU45bz+Y2osX77pU01JkS/G7OhGtGmlKDrOPvFwg== - dependencies: - buffer "4.9.2" - events "1.1.1" - ieee754 "1.1.13" - jmespath "0.15.0" - querystring "0.2.0" - sax "1.2.1" - url "0.10.3" - uuid "3.3.2" - xml2js "0.4.19" - aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -8334,6 +9106,11 @@ bowser@^1.7.3: resolved "https://registry.npmjs.org/bowser/-/bowser-1.9.4.tgz#890c58a2813a9d3243704334fa81b96a5c150c9a" integrity sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ== +bowser@^2.11.0: + version "2.11.0" + resolved "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" + integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA== + boxen@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" @@ -8563,7 +9340,7 @@ buffer-xor@^1.0.3: resolved "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= -buffer@4.9.2, buffer@^4.3.0: +buffer@^4.3.0: version "4.9.2" resolved "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== @@ -10630,7 +11407,7 @@ debuglog@^1.0.1: resolved "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= -decamelize-keys@^1.0.0: +decamelize-keys@^1.0.0, decamelize-keys@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk= @@ -11664,10 +12441,10 @@ eslint-formatter-friendly@^7.0.0: strip-ansi "5.2.0" text-table "0.2.0" -eslint-import-resolver-node@^0.3.3: - version "0.3.3" - resolved "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz#dbaa52b6b2816b50bc6711af75422de808e98404" - integrity sha512-b8crLDo0M5RSe5YG8Pu2DYBj71tSB6OvXkfzwbJU2w7y8P4/yo0MyF8jU26IEuEuHF2K5/gcAJE3LhQGqBBbVg== +eslint-import-resolver-node@^0.3.4: + version "0.3.4" + resolved "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" + integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA== dependencies: debug "^2.6.9" resolve "^1.13.1" @@ -11698,16 +12475,16 @@ eslint-plugin-graphql@^4.0.0: lodash.without "^4.4.0" eslint-plugin-import@^2.20.2: - version "2.22.0" - resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.0.tgz#92f7736fe1fde3e2de77623c838dd992ff5ffb7e" - integrity sha512-66Fpf1Ln6aIS5Gr/55ts19eUuoDhAbZgnr6UxK5hbDx6l/QgQgx61AePq+BV4PP2uXQFClgMVzep5zZ94qqsxg== + version "2.22.1" + resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz#0896c7e6a0cf44109a2d97b95903c2bb689d7702" + integrity sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw== dependencies: array-includes "^3.1.1" array.prototype.flat "^1.2.3" contains-path "^0.1.0" debug "^2.6.9" doctrine "1.5.0" - eslint-import-resolver-node "^0.3.3" + eslint-import-resolver-node "^0.3.4" eslint-module-utils "^2.6.0" has "^1.0.3" minimatch "^3.0.4" @@ -11979,11 +12756,6 @@ eventemitter3@^4.0.0: resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb" integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg== -events@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" - integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= - events@3.1.0, events@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/events/-/events-3.1.0.tgz#84279af1b34cb75aa88bf5ff291f6d0bd9b31a59" @@ -12264,6 +13036,11 @@ extsprintf@^1.2.0: resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= +fast-base64-decode@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz#b434a0dd7d92b12b43f26819300d2dafb83ee418" + integrity sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q== + fast-deep-equal@2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" @@ -12333,6 +13110,11 @@ fast-text-encoding@^1.0.0: resolved "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53" integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig== +fast-xml-parser@^3.16.0: + version "3.17.6" + resolved "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.17.6.tgz#4f5df8cf927c3e59a10362abcfb7335c34bc5c5f" + integrity sha512-40WHI/5d2MOzf1sD2bSaTXlPn1lueJLAX6j1xH5dSAr6tNeut8B9ktEL6sjAK9yVON4uNj9//axOdBJUuruCzw== + fastest-stable-stringify@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-1.0.1.tgz#9122d406d4c9d98bea644a6b6853d5874b87b028" @@ -13694,6 +14476,11 @@ har-validator@~5.1.3: ajv "^6.5.5" har-schema "^2.0.0" +hard-rejection@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" + integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== + harmony-reflect@^1.4.6: version "1.6.1" resolved "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.1.tgz#c108d4f2bb451efef7a37861fdbdae72c9bdefa9" @@ -14242,11 +15029,6 @@ identity-obj-proxy@3.0.0: dependencies: harmony-reflect "^1.4.6" -ieee754@1.1.13: - version "1.1.13" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" - integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== - ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -15666,11 +16448,6 @@ jest@^26.0.1: import-local "^3.0.2" jest-cli "^26.6.3" -jmespath@0.15.0: - version "0.15.0" - resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" - integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc= - jose@^1.27.1: version "1.27.1" resolved "https://registry.npmjs.org/jose/-/jose-1.27.1.tgz#a1de2ecb5b3ae1ae28f0d9d0cc536349ada27ec8" @@ -16196,7 +16973,7 @@ kind-of@^5.0.0: resolved "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== -kind-of@^6.0.0, kind-of@^6.0.2: +kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: version "6.0.3" resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== @@ -17256,6 +18033,23 @@ meow@^5.0.0: trim-newlines "^2.0.0" yargs-parser "^10.0.0" +meow@^6.0.0: + version "6.1.1" + resolved "https://registry.npmjs.org/meow/-/meow-6.1.1.tgz#1ad64c4b76b2a24dfb2f635fddcadf320d251467" + integrity sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg== + dependencies: + "@types/minimist" "^1.2.0" + camelcase-keys "^6.2.2" + decamelize-keys "^1.1.0" + hard-rejection "^2.1.0" + minimist-options "^4.0.2" + normalize-package-data "^2.5.0" + read-pkg-up "^7.0.1" + redent "^3.0.0" + trim-newlines "^3.0.0" + type-fest "^0.13.1" + yargs-parser "^18.1.3" + merge-deep@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.2.tgz#f39fa100a4f1bd34ff29f7d2bf4508fbb8d83ad2" @@ -17493,6 +18287,15 @@ minimist-options@^3.0.1: arrify "^1.0.1" is-plain-obj "^1.1.0" +minimist-options@^4.0.2: + version "4.1.0" + resolved "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" + integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A== + dependencies: + arrify "^1.0.1" + is-plain-obj "^1.1.0" + kind-of "^6.0.3" + minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5: version "1.2.5" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" @@ -18673,7 +19476,7 @@ p-finally@^2.0.0: resolved "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561" integrity sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw== -p-limit@3.0.2, p-limit@^3.0.2: +p-limit@3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz#1664e010af3cadc681baafd3e2a437be7b0fb5fe" integrity sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg== @@ -18694,7 +19497,7 @@ p-limit@^2.0.0, p-limit@^2.2.0: dependencies: p-try "^2.0.0" -p-limit@^3.0.1: +p-limit@^3.0.1, p-limit@^3.0.2: version "3.1.0" resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== @@ -20678,6 +21481,13 @@ react-motion@^0.5.2: prop-types "^15.5.8" raf "^3.1.0" +react-native-get-random-values@^1.4.0: + version "1.5.0" + resolved "https://registry.npmjs.org/react-native-get-random-values/-/react-native-get-random-values-1.5.0.tgz#91cda18f0e66e3d9d7660ba80c61c914030c1e05" + integrity sha512-LK+Wb8dEimJkd/dub7qziDmr9Tw4chhpzVeQ6JDo4czgfG4VXbptRyOMdu8503RiMF6y9pTH6ZUTkrrpprqT7w== + dependencies: + fast-base64-decode "^1.0.0" + react-popper-tooltip@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/react-popper-tooltip/-/react-popper-tooltip-3.1.1.tgz#329569eb7b287008f04fcbddb6370452ad3f9eac" @@ -21937,11 +22747,6 @@ sanitize-html@^1.27.0: srcset "^2.0.1" xtend "^4.0.1" -sax@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" - integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= - sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: version "1.2.4" resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -23862,6 +24667,11 @@ trim-newlines@^2.0.0: resolved "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20" integrity sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA= +trim-newlines@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30" + integrity sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA== + trim-off-newlines@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3" @@ -23989,6 +24799,11 @@ tslib@^1.10.0, tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== +tslib@^1.8.0: + version "1.14.1" + resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + tslib@^2.0.1: version "2.0.3" resolved "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" @@ -24006,7 +24821,7 @@ tty-browserify@0.0.0: resolved "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= -tty-table@^2.7.0: +tty-table@^2.8.10: version "2.8.13" resolved "https://registry.npmjs.org/tty-table/-/tty-table-2.8.13.tgz#d484a416381973eaebbdf19c79136b390e5c6d70" integrity sha512-eVV/+kB6fIIdx+iUImhXrO22gl7f6VmmYh0Zbu6C196fe1elcHXd7U6LcLXu0YoVPc2kNesWiukYcdK8ZmJ6aQ== @@ -24059,6 +24874,11 @@ type-fest@^0.11.0: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== +type-fest@^0.13.1: + version "0.13.1" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" + integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== + type-fest@^0.3.0: version "0.3.1" resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" @@ -24448,14 +25268,6 @@ url-parse@^1.4.3, url-parse@^1.4.7: querystringify "^2.1.1" requires-port "^1.0.0" -url@0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" - integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ= - dependencies: - punycode "1.3.2" - querystring "0.2.0" - url@^0.11.0, url@~0.11.0: version "0.11.0" resolved "https://registry.npmjs.org/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" @@ -24554,12 +25366,7 @@ utils-merge@1.0.1, utils-merge@1.x.x: resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -uuid@3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" - integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== - -uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2, uuid@^3.4.0: +uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2, uuid@^3.4.0: version "3.4.0" resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== @@ -25290,14 +26097,6 @@ xml-name-validator@^3.0.0: resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== -xml2js@0.4.19: - version "0.4.19" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" - integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== - dependencies: - sax ">=0.6.0" - xmlbuilder "~9.0.1" - xml2js@0.4.x: version "0.4.23" resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" @@ -25311,11 +26110,6 @@ xmlbuilder@^11.0.0, xmlbuilder@~11.0.0: resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== -xmlbuilder@~9.0.1: - version "9.0.7" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" - integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= - xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" @@ -25422,7 +26216,7 @@ yargs-parser@^15.0.1: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^18.1.2: +yargs-parser@^18.1.2, yargs-parser@^18.1.3: version "18.1.3" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==