Merge remote-tracking branch 'upstream/master' into org-repo

This commit is contained in:
Nir Gazit
2021-02-04 17:43:06 +02:00
671 changed files with 14653 additions and 4788 deletions
-6
View File
@@ -1,6 +0,0 @@
---
'@backstage/plugin-kubernetes': patch
'@backstage/plugin-kubernetes-backend': patch
---
Add AWS auth provider for Kubernetes
-18
View File
@@ -1,18 +0,0 @@
---
'@backstage/cli': minor
---
We've bumped the `@eslint-typescript` packages to the latest, which now add some additional rules that might cause lint failures.
The main one which could become an issue is the [no-use-before-define](https://eslint.org/docs/rules/no-use-before-define) rule.
Every plugin and app has the ability to override these rules if you want to ignore them for now.
You can reset back to the default behaviour by using the following in your own `.eslint.js`
```js
rules: {
'no-use-before-define': 'off'
}
```
Because of the nature of this change, we're unable to provide a grace period for the update :(
@@ -1,5 +0,0 @@
---
'@backstage/plugin-cost-insights': patch
---
Allow expand functionality to top panel product chart tooltip.
-31
View File
@@ -1,31 +0,0 @@
---
'@backstage/plugin-kafka': minor
'@backstage/plugin-kafka-backend': minor
---
Added support for multiple Kafka clusters and multiple consumers per component.
Note that this introduces several breaking changes.
1. Configuration in `app-config.yaml` has changed to support the ability to configure multiple clusters. This means you are required to update the configs in the following way:
```diff
kafka:
clientId: backstage
- brokers:
- - localhost:9092
+ clusters:
+ - name: prod
+ brokers:
+ - localhost:9092
```
2. Configuration of services has changed as well to support multiple clusters:
```diff
annotations:
- kafka.apache.org/consumer-groups: consumer
+ kafka.apache.org/consumer-groups: prod/consumer
```
3. Kafka Backend API has changed, so querying offsets of a consumer group is now done with the following query path:
`/consumers/${clusterId}/${consumerGroup}/offsets`
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/cli': patch
---
Add `--lax` option to `config:print` and `config:check`, which causes all environment variables to be assumed to be set.
-8
View File
@@ -1,8 +0,0 @@
---
'@backstage/plugin-catalog': patch
---
Add `EntityRefLinks` that shows one or multiple entity ref links.
Change the about card and catalog table to use `EntityRefLinks` due to the
nature of relations to support multiple relations per type.
-12
View File
@@ -1,12 +0,0 @@
---
'@backstage/config-loader': patch
---
Added support for environment variable substitutions in string configuration values using a `${VAR}` placeholder. All environment variables must be available, or the entire expression will be evaluated to `undefined`. To escape a substitution, use `$${...}`, which will end up as `${...}`.
For example:
```yaml
app:
baseUrl: https://${BASE_HOST}
```
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-kubernetes': patch
---
Improve error reporting for plugin misconfiguration.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-auth-backend': patch
---
Use .text instead of .json for ALB key response
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/config-loader': minor
---
Removed support for the deprecated `$data` placeholder.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/techdocs-common': patch
---
Add rate limiter for concurrent execution of file uploads in AWS and Google publishers
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/config-loader': minor
---
Enable further processing of configuration files included using the `$include` placeholder. Meaning that for example for example `$env` includes will be processed as usual in included files.
+3
View File
@@ -130,6 +130,7 @@ middleware
minikube
Minikube
misconfiguration
misconfigured
misgendering
mkdocs
Mkdocs
@@ -217,6 +218,7 @@ Spotify
sqlite
squidfunk
src
stdout
stefanalund
subkey
subtree
@@ -257,6 +259,7 @@ Voi
Wealthsimple
Weaveworks
Webpack
winston
www
WWW
xyz
+4
View File
@@ -15,6 +15,10 @@ jobs:
env:
CI: true
NODE_OPTIONS: --max-old-space-size=4096
INTEGRATION_TEST_GITHUB_TOKEN: ${{ secrets.INTEGRATION_TEST_GITHUB_TOKEN }}
INTEGRATION_TEST_GITLAB_TOKEN: ${{ secrets.INTEGRATION_TEST_GITLAB_TOKEN }}
INTEGRATION_TEST_BITBUCKET_TOKEN: ${{ secrets.INTEGRATION_TEST_BITBUCKET_TOKEN }}
INTEGRATION_TEST_AZURE_TOKEN: ${{ secrets.INTEGRATION_TEST_AZURE_TOKEN }}
steps:
- uses: actions/checkout@v2
+1
View File
@@ -3,6 +3,7 @@ name: E2E Test Linux
on:
pull_request:
paths-ignore:
- '.changeset/**'
- 'contrib/**'
- 'docs/**'
- 'microsite/**'
+5
View File
@@ -1,6 +1,7 @@
name: Master Build Windows
on:
workflow_dispatch:
push:
branches: [master]
@@ -15,6 +16,10 @@ jobs:
env:
CI: true
NODE_OPTIONS: --max-old-space-size=4096
INTEGRATION_TEST_GITHUB_TOKEN: ${{ secrets.INTEGRATION_TEST_GITHUB_TOKEN }}
INTEGRATION_TEST_GITLAB_TOKEN: ${{ secrets.INTEGRATION_TEST_GITLAB_TOKEN }}
INTEGRATION_TEST_BITBUCKET_TOKEN: ${{ secrets.INTEGRATION_TEST_BITBUCKET_TOKEN }}
INTEGRATION_TEST_AZURE_TOKEN: ${{ secrets.INTEGRATION_TEST_AZURE_TOKEN }}
steps:
- uses: actions/checkout@v2
+5
View File
@@ -1,6 +1,7 @@
name: Main Master Build
on:
workflow_dispatch:
push:
branches: [master]
@@ -18,6 +19,10 @@ jobs:
env:
CI: true
NODE_OPTIONS: --max-old-space-size=4096
INTEGRATION_TEST_GITHUB_TOKEN: ${{ secrets.INTEGRATION_TEST_GITHUB_TOKEN }}
INTEGRATION_TEST_GITLAB_TOKEN: ${{ secrets.INTEGRATION_TEST_GITLAB_TOKEN }}
INTEGRATION_TEST_BITBUCKET_TOKEN: ${{ secrets.INTEGRATION_TEST_BITBUCKET_TOKEN }}
INTEGRATION_TEST_AZURE_TOKEN: ${{ secrets.INTEGRATION_TEST_AZURE_TOKEN }}
steps:
- uses: actions/checkout@v2
+110
View File
@@ -0,0 +1,110 @@
name: Tugboat E2E Tests
on: deployment_status
jobs:
set-pending:
if: github.event.deployment_status.state != 'success' && github.event.deployment_status.state != 'failed'
name: Set pending waiting for Tugboat
runs-on: ubuntu-latest
steps:
# Set an initial commit status message to indicate that the tests are
# running.
- name: set pending status
uses: actions/github-script@v3
with:
github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}}
debug: true
script: |
return github.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.sha,
state: 'pending',
context: 'Backstage Tugboat E2E Tests',
description: 'Waiting for Tugboat to complete deployment',
target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}"
});
run-tests:
# Only run after a successful Tugboat deployment.
if: github.event.deployment_status.state == 'success'
name: Run tests against Tugboat deployment
runs-on: ubuntu-latest
steps:
# Set an initial commit status message to indicate that the tests are
# running.
- name: set pending status
uses: actions/github-script@v3
with:
github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}}
debug: true
script: |
return github.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.sha,
state: 'pending',
context: 'Backstage Tugboat E2E Tests',
description: 'Running against tugboat preview',
target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}"
});
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
with:
node-version: '14'
# This is required because the environment_url param that Tugboat uses
# to tell us where the preview is located isn't supported unless you
# specify the custom Accept header when getting the deployment_status,
# and GitHub actions doesn't do that by default. So instead we have to
# load the status object manually and get the data we need.
# https://developer.github.com/changes/2016-04-06-deployment-and-deployment-status-enhancements/
- name: get deployment status
id: get-status-env
uses: actions/github-script@v3
with:
github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}}
result-encoding: string
script: |
const result = await github.repos.getDeploymentStatus({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: context.payload.deployment.id,
status_id: context.payload.deployment_status.id,
headers: {
'Accept': 'application/vnd.github.ant-man-preview+json'
},
});
console.log(result);
return result.data.environment_url;
- name: echo tugboat preview url
run: |
curl ${{steps.get-status-env.outputs.result}}
- name: set status
if: ${{ failure() }}
uses: actions/github-script@v3
with:
github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}}
script: |
return github.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.sha,
state: "error",
context: 'Backstage Tugboat E2E Tests',
target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}"
});
- name: set status
if: ${{ success() }}
uses: actions/github-script@v3
with:
github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}}
script: |
return github.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.sha,
state: "success",
context: 'Backstage Tugboat E2E Tests',
target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}"
});
+17
View File
@@ -0,0 +1,17 @@
services:
backstage:
image: tugboatqa/node:lts
expose: 7000
default: true
commands:
init:
- mkdir -p /etc/service/node
- echo "#!/bin/sh" > /etc/service/node/run
- echo "yarn --cwd ${TUGBOAT_ROOT} start-backend --config ${TUGBOAT_ROOT}/app-config.yaml --config ${TUGBOAT_ROOT}/.tugboat/tugboat.app-config.production.yaml" >> /etc/service/node/run
- chmod +x /etc/service/node/run
build:
- yarn install --frozen-lockfile
- yarn workspace example-app build
start:
# wget the endpoint. Will retry every 2 seconds. 30 retries = 1m for service to come up. Plenty.
- wget -O /dev/null -o /dev/null --tries=30 --timeout=5 --retry-connrefused http://localhost:7000
@@ -0,0 +1,13 @@
app:
title: Backstage Tugboat Preview
baseUrl:
$env: TUGBOAT_DEFAULT_SERVICE_URL
backend:
baseUrl:
$env: TUGBOAT_DEFAULT_SERVICE_URL
cors:
origin:
$env: TUGBOAT_DEFAULT_SERVICE_URL
methods: [GET, POST, PUT, DELETE]
credentials: true
+4 -4
View File
@@ -74,13 +74,11 @@ organization:
# Reference documentation http://backstage.io/docs/features/techdocs/configuration
techdocs:
requestUrl: http://localhost:7000/api/techdocs
storageUrl: http://localhost:7000/api/techdocs/static/docs
builder: 'local' # Alternatives - 'external'
generators:
techdocs: 'docker' # Alternatives - 'local'
publisher:
type: 'local' # Alternatives - 'googleGcs' or 'awsS3'. Read documentation for using alternatives.
type: 'local' # Alternatives - 'googleGcs' or 'awsS3' or 'azureBlobStorage'. Read documentation for using alternatives.
sentry:
organization: my-company
@@ -208,7 +206,7 @@ catalog:
# Backstage example components
- type: file
target: ../catalog-model/examples/all-components.yaml
# Example component for github-actions
# Example component for github-actions and TechDocs
- type: file
target: ../../plugins/github-actions/examples/sample.yaml
# Example component for TechDocs
@@ -243,6 +241,7 @@ scaffolder:
baseUrl: https://gitlab.com
token:
$env: GITLAB_TOKEN
visibility: public # or 'internal' or 'private'
azure:
baseUrl: https://dev.azure.com/{your-organization}
api:
@@ -255,6 +254,7 @@ scaffolder:
$env: BITBUCKET_USERNAME
token:
$env: BITBUCKET_TOKEN
visibility: public # or or 'private'
auth:
environment: development
@@ -0,0 +1,74 @@
---
id: adrs-adr011
title: ADR011: Plugin Package Structure
description: Architecture Decision Record (ADR) for Plugin Package Structure
---
## Context
A core feature of Backstage is the extensibility via plugins. The Backstage
repository is open for contributions of plugins. Even most of the core features
are implemented as plugins. A plugin consists of one or multiple packages in the
`plugins/` directory. Up till now, we have a simple conventions for naming
plugin packages: Plugins are named `x`, with the option of having a related
backend plugin called `x-backend` (where `x` is the plugin name, like `catalog`
or `techdocs`). There is a need for sharing code between the frontend and
backend of a plugin, between backend plugins, or components and hooks between
different frontend plugins
([some examples](https://github.com/backstage/backstage/issues/3655#issuecomment-758166746)).
This results in emerging plugin packages with shared code, like
`packages/catalog-client` or `packages/techdocs-common`.
> There is a common phrase in software development:
> [Naming things is hard](https://martinfowler.com/bliki/TwoHardThings.html)
To keep the contributed plugins consistent, this Architecture Decision Record
provides rules for naming plugin packages.
## Decision
We will place all plugin related code in the `plugins/` directory. The
`packages/` directory is reserved for core package of Backstage.
We follow this structure for plugin packages (where `x` is the plugin name, for
example `catalog` or `techdocs`):
- `x`: Contains the main frontend code of the plugin.
- `x-backend`: Contains the main backend code of the plugin.
- `x-react`: Contains shared widgets, hooks and similar that both the plugin
itself (`x`) and third-party frontend plugins can depend on.
- `x-node`: Contains utilities for backends that both the plugin backend itself
(`x-backend`) and third-party backend plugins can depend on.
- `x-common`: An isomorphic package with platform agnostic models, clients, and
utilities that all packages above or any third-party plugin package can depend
on.
We prefix the package names with `@backstage/plugin-`.
This structure is based on a
[suggestion in issue #3655](https://github.com/backstage/backstage/issues/3655#issuecomment-758166746).
## Consequences
We will actively migrate existing packages that are part of a plugin to the
`plugins/` folder. This affects packages like:
- `packages/techdocs-common` which should be moved to `plugins/techdocs-node`
and named `@backstage/plugin-techdocs-node`.
- `packages/catalog-client` which will be part of a future
`plugins/catalog-common` and named `@backstage/plugin-catalog-common`.
- While the new location of `packages/catalog-model` should be
`plugins/catalog-common` we might want to do an exception here, as it's a very
central package.
The limited set of rules might not be sufficient in the future. If additional
packages are required, we will revisit this decision and extend the pattern.
If possible, we will add tools, such as lint rules, to help enforce the package
names and dependencies between them or CLI commands to generate these packages.
The distinction between core packages and plugins helps us to setup
[CODEOWNERS](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners)
in the repository. We can set the code owners for the `packages/` folder to the
core team and create additional rules (like `plugins/x*`) for plugin
maintainers.
+1
View File
@@ -0,0 +1 @@
<mxfile host="Chrome" modified="2021-01-03T17:00:54.430Z" agent="5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36" version="13.9.5" etag="DwPjZwigIikbKX1hFX51" type="device"><diagram id="WdRoEH4gdfKcJUWp3sm5" name="Page-1">7VpLc6M4EP41PiaFBLbh6LxmLlubSqZ2Z+amBQVUBYgScmzPrx9hJIMQGJzEhtTOxUaNnp++bnU3mtm3yfYLQ1n0Fw1wPINWsJ3ZdzMIAfQ88VdIdqXEs2EpCBkJZKVK8Ex+YSm0pHRNApxrFTmlMSeZLvRpmmKfazLEGN3o1V5orI+aoRAbgmcfxab0XxLwqJS6cFnJv2ISRmpksJALTpCqLFeSRyigm5rIvp/Zt4xSXj4l21scF+ApXMp2Dx1vDxNjOOVDGvwk7Nv6n4erTfLNgzcr9/HvH5sru+zlFcVrueBVlsn58p0CId+QJEapKN3I+phxvO2cCDgsT/AC0wRzthNVZANXAiIZARayvKnwPdSJathCRwqR3NPw0HW1bPEgV34CCs4gFBhdpwEu+rEEEJuIcPycIb94uxHkF7KIJ2LcOzAUp+4t6QQP6uA5JnZt0J0NOduEzsAtFMBlnXhIxUX/qerWqXxyFv2EsuFFCQVNvbrfcpzmhKb5aMRyjsPo6CgCZ1xqKSbVMHyia46f8MtEEQSuDqEzNoLgsyPYRkLQpsrnw9CA8IHRVKhyIKSP8Tok6buPy7cgBVtsnHVRG2eSqwOOC1ELfqZz05ucZh6HDyygjh8cFz8wPdPWB+C0COga+I3voPRosM5AAMamoNUN4UQRbPjJYGQOgoWBoAGcOGhXRRgvSn6M8pz4OlYByqM9rhVwODBC+iGwiWHpmvl4wJZzxEJ8rMOOgKUG9LwFaCVjOEacvOqLaENfjvBIiVjeYZu9xjZbjf0rlykbwVrCQPWjGtqNhuWyjYb7vT+s8h10WL6fDmKf2e67VLN94UdRuIZzVb7b1t/e7VRpS/h31Yl4rjcTxapVUVCNzs63oXQblW9LnW/eqXRTNs7R+3EvzD7zRJy6R9F/IF42VgKmV1uciCxF8dSh7D8ZLxx2ms7F6baw1aQdt2iFLXuWQ1DGIxpSsXv3lbTTwvYZ2MnYSm9MW3lg1k63ne81lmA+zFoKwqBdrVpWVMiPzLcxzsI6bVq2Vl88lBP4UMsNB+Sga2px+A5j1d3HooBiEqaFNglGYiYEhaUhPopX8kVCgmCvBKKPrOg52YbFZ6/r8ksTLP+LbhXr9x/CgH0t8L1JKfcjKX+JSfb1zFbtqunvt+SMnBaqw7MZtXnr+TDpoLORj3THTumaIdPkIWy4Ke7IcTs0w4xVRibrmzSV2DWV+LLwfb7MEVhMTYvbHeVpYzgxNbZND1mo8QPyOZW9TxDDiemybWbRDeSGRhkqlBgSZczOHAsobvQGA+VZMFqizr2eL4ED3fJ3sdTY8eY8SiMfA5r0OXMiRV3w+rCs7ttj0jdm/ZafI+tnW7MRydtkWTNqGErXnuzhR8WxDTdq2RPHNr0u7wJxrG1eGjIU508cW23GaHEs+N9uVNellJ1uWj5+Y2bFtRt1U7ZUuOq+sX3/Gw==</diagram></mxfile>
+2 -1
View File
@@ -86,7 +86,8 @@ to a `guest` identity for all users, without any ID token. To enable sign-in, a
`SignInPage` needs to be configured, which in turn has to supply a user to the
app. The `@backstage/core` package provides a basic sign-in page that allows
both the user and the app developer to choose between a couple of different
sign-in methods.
sign-in methods, or to designate a single provider that may also be logged in to
automatically.
## Further Reading
+3 -3
View File
@@ -7,8 +7,8 @@ description: Installing Kubernetes plugin into Backstage
The Kubernetes feature is a plugin to Backstage, and it is exposed as a tab when
viewing entities in the software catalog.
If you haven't setup Backstage already, start
[here](../../getting-started/index.md).
If you haven't setup Backstage already, read the
[Getting Started](../../getting-started/index.md).
## Adding the Kubernetes frontend plugin
@@ -111,7 +111,7 @@ Backstage app.
## Running Backstage locally
Start the frontend and the backend app by
[running backstage locally](../../getting-started/running-backstage-locally.md).
[running Backstage locally](../../getting-started/running-backstage-locally.md).
## Configuration
+1 -1
View File
@@ -67,7 +67,7 @@ more to come...
- As a contributor I should be able to integrate plugin data to the indexing
process of Backstage Search by using the standardized API.
- As a software engineer I should be able to search for all content (for
example, entities, metadata, documentation) in backstage search.
example, entities, metadata, documentation) in Backstage search.
more to come...
@@ -54,6 +54,11 @@ software catalog API.
"labels": {
"system": "public-websites"
},
"links": [{
"url": "https://admin.example-org.com",
"title": "Admin Dashboard",
"icon": "dashboard"
}],
"tags": ["java"],
"name": "artist-web",
"uid": "2152f463-549d-4d8d-a94d-ce2b7676c6e2"
@@ -81,6 +86,10 @@ metadata:
circleci.com/project-slug: github/example-org/artist-website
tags:
- java
links:
- url: https://admin.example-org.com
title: Admin Dashboard
icon: dashboard
spec:
type: website
lifecycle: production
@@ -314,6 +323,34 @@ This field is optional, and currently has no special semantics.
Each tag must be sequences of `[a-z0-9]` separated by `-`, at most 63 characters
in total.
### `links` [optional]
A list of external hyperlinks related to the entity. Links can provide
additional contextual information that may be located outside of Backstage
itself. For example, an admin dashboard or external CMS page.
Users may add links to descriptor YAML files to provide additional reference
information to external content & resources. Links are not intended to drive any
additional functionality within Backstage, which is best left to `annotations`
and `labels`. It is recommended to use links only when an equivalent well-known
`annotation` does not cover a similar use case.
Fields of a link are:
| Field | Type | Description |
| ------- | ------ | ------------------------------------------------------------------------------------ |
| `url` | String | [Required] A `url` in a standard `uri` format (e.g. `https://example.com/some/page`) |
| `title` | String | [Optional] A user friendly display name for the link. |
| `icon` | String | [Optional] A key representing a visual icon to be displayed in the UI. |
_NOTE_: The `icon` field value is meant to be a semantic key that will map to a
specific icon that may be provided by an icon library (e.g. `material-ui`
icons). These keys should be a sequence of `[a-z0-9A-Z]`, possibly separated by
one of `[-_.]`. Backstage may support some basic icons out of the box, but the
Backstage integrator will ultimately be left to provide the appropriate icon
component mappings. A generic fallback icon would be provided if a mapping
cannot be resolved.
## Common to All Kinds: Relations
The `relations` root field is a read-only list of relations, between the current
@@ -86,7 +86,7 @@ follows:
_note_ Currently the templaters that we provide are basically Docker action
containers that are run on top of the skeleton folder. This keeps dependencies
to a minimum for running backstage scaffolder, but you don't _have_ to use
to a minimum for running Backstage scaffolder, but you don't _have_ to use
Docker. You can `pip install cookiecutter` to run it locally in your backend.
You could create your own templater that spins up an EC2 instance and downloads
the folder and does everything using an AMI if you want. It's entirely up to
+5 -6
View File
@@ -39,13 +39,12 @@ internally.
![Enter some variables](../../assets/software-templates/template-picked.png)
After filling in these variables, you'll get some more fields to fill out which
are required for backstage usage: the owner (which is a `user` in the backstage
system), the `storePath` (which right now must be a GitHub Organisation or
GitHub user and a non-existing GitHub repository name in the format
`organisation/reponame`), and a GitHub team or user account which should be
granted admin access to the repository.
are required for Backstage usage: the owner (which is a `user` in the backstage
system), and the `storePath` which is a destination URL to create for the
provider, for instance `https://github.com/backstage/my-new-repository`, or
`https://gitlab.com/myorg/myrepo`.
![Enter backstage vars](../../assets/software-templates/template-picked-2.png)
![Enter Backstage vars](../../assets/software-templates/template-picked-2.png)
### Run!
@@ -173,7 +173,12 @@ and access to a running Docker daemon. You can create a GitHub access token
docs on creating private GitHub access tokens is available
[here](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token).
Note that the need for private GitHub access tokens will be replaced with GitHub
Apps integration further down the line.
Apps integration further down the line by using the existing `integrations`
config.
> Note: Some of this configuration may already be set up as part of your
> `app-config.yaml`. We're moving away from the duplicated config for
> authentication in the `scaffolder` section and using `integrations` instead.
#### GitHub
@@ -187,10 +192,14 @@ by specifying `visibility` option. Valid options are `public`, `private` and
public within the enterprise.
```yaml
integrations:
github:
- host: github.com
token:
$env: GITHUB_TOKEN
scaffolder:
github:
token:
$env: GITHUB_TOKEN
visibility: public # or 'internal' or 'private'
```
@@ -201,10 +210,9 @@ allows to configure the private access token and the base URL of a GitLab
instance:
```yaml
scaffolder:
integrations:
gitlab:
api:
baseUrl: https://gitlab.com
- host: gitlab.com
token:
$env: GITLAB_TOKEN
```
@@ -218,10 +226,9 @@ will hopefully support on-prem installations as well but that has not been
verified.
```yaml
scaffolder:
integrations:
azure:
baseUrl: https://dev.azure.com/{your-organization}
api:
- host: dev.azure.com
token:
$env: AZURE_TOKEN
```
+6 -6
View File
@@ -108,12 +108,12 @@ providers are used.
| GitLab | Yes ✅ |
| GitLab Enterprise | Yes ✅ |
| File Storage Provider | Support Status |
| --------------------------------- | ----------------------------------------------------------------- |
| Local Filesystem of Backstage app | Yes ✅ |
| Google Cloud Storage (GCS) | Yes ✅ |
| Amazon Web Services (AWS) S3 | Yes ✅ |
| Azure Storage | No ❌ [#3938](https://github.com/backstage/backstage/issues/3938) |
| File Storage Provider | Support Status |
| --------------------------------- | -------------- |
| Local Filesystem of Backstage app | Yes ✅ |
| Google Cloud Storage (GCS) | Yes ✅ |
| Amazon Web Services (AWS) S3 | Yes ✅ |
| Azure Blob Storage | Yes ✅ |
[Reach out to us](#feedback) if you want to request more platforms.
+31 -52
View File
@@ -12,32 +12,31 @@ out-of-the box experience.
<img data-zoomable src="../../assets/techdocs/architecture-basic.drawio.svg" alt="TechDocs Architecture diagram" />
> Note: See below for our recommended deployment architecture which takes care
> of stability, scalability and speed.
> of stability, scalability and speed. Also look at the
> [HOW TO migrate guide](how-to-guides.md#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach).
When you open a TechDocs site in Backstage, the
[TechDocs Reader](./concepts.md#techdocs-reader) makes a request to
`techdocs-backend` with the entity ID and the path of the current page you are
looking at. In response, it receives the static files (HTML, CSS, JSON, etc.) to
render on the page in TechDocs/Backstage.
`techdocs-backend` plugin with the entity ID and the path of the current page
you are looking at. In response, it receives the static files (HTML, CSS, JSON,
etc.) to render on the page in TechDocs/Backstage.
The static files consist of HTML, CSS and Images generated by MkDocs. We remove
all the JavaScript before adding them to Backstage for security reasons. And
there are some additional techdocs metadata JSON files that TechDocs needs to
render a site.
there is an additional `techdocs_metadata.json` file that TechDocs needs to
render a site. It's important that you use either
[techdocs-cli](https://github.com/backstage/techdocs-cli) or
[techdocs-container](https://github.com/backstage/techdocs-container) to
generate the docs for the expected output.
The TechDocs Reader then applies a list of "Transformers" (see
[Concepts](./concepts.md)) which modify the generated static HTML files for a
number of use cases e.g. Remove certain headers, filter out some HTML tags, etc.
Currently, we use the Backstage server's (or techdocs-backend's) local file
system to store the generated files. Publishing to an external storage system
(AWS S3, GCS, etc.) is also possible, but has not been implemented yet.
A word about `UrlReader` vs Git preparer - Right now, we have two ways to fetch
files from its source repository for docs site generation. 1. By using Git
and 2. By directly using Source control (GitHub, Azure, etc.) APIs. This work is
heavily in progress. Please reach out to us on Discord in the #docs-like-code
channel to talk about it.
system to store the generated files. However, it's ideal to use an external
storage system (e.g. AWS S3, GCS or Azure Blob Storage). Read more in
[using cloud storage](using-cloud-storage.md).
## Recommended deployment
@@ -49,22 +48,16 @@ The key difference in the recommended deployment approach is where the docs are
built.
We assume each entity lives in a repository somewhere (GitHub, GitLab, etc.). We
recommend using a CI/CD pipeline with the repository that has a dedicated
step/job to generate docs for TechDocs. The generated static files are then
stored in a cloud storage solution of your choice.
[Track progress here](https://github.com/backstage/backstage/issues/3096).
recommend [using a CI/CD pipeline](configuring-ci-cd.md) with the repository
that has a dedicated step/job to generate docs for TechDocs. The generated
static files are then [stored in a cloud storage](using-cloud-storage.md)
solution of your choice.
Similar to how it is done in the Basic setup, the TechDocs Reader requests
`techdocs-backend` plugin for the docs site. `techdocs-backend` then requests
your configured storage solution for the necessary files and returns them to
TechDocs Reader.
We will provide instructions, scripts and/or templates (e.g. GitHub Actions) to
generate docs in your CI/CD system.
[Track progress here.](https://github.com/backstage/backstage/issues/3400) You
will be able to use `techdocs-cli` to generate docs and publish the generated
docs site files to your cloud storage system.
Note about caching: We have noticed internally that some storage providers can
be quite slow, which is why we are recommending a cache that sits between the
TechDocs Reader and the Storage.
@@ -97,7 +90,7 @@ permissions to publish the generated docs site files.
**Q: Why do you have separate "basic" and "recommended" deployment approaches?**
A: The basic or out-of-the-box setup is what you get when you create a new app
or do a git clone of the Backstage repository. We want the first experience to
or do a `git clone` of the Backstage repository. We want the first experience to
_just work magically_ so that you can have your first experience with TechDocs
which is smooth. However, if you decide to deploy Backstage/TechDocs for
production use, the basic setup would work but there are going to be downsides
@@ -109,19 +102,21 @@ and we welcome such "Alternative" ideas from the community.
**Q: Why don't you recommend techdocs-backend local filesystem to serve static
files?**
A: It would make scaling a Backstage instance harder. Think about the case where
we have distributed Backstage deployments. Using a separate file storage system
for TechDocs makes it easier to do some operations like delete a docs site and
wipe its contents.
A: It makes scaling a Backstage instance harder. Think about the case where we
have distributed Backstage deployments (e.g. multiple Kubernetes pods of your
Backstage app). Using a separate/central file storage system for TechDocs is
necessary to make sure sites are persistent when the servers/pods are restarted
and avoid duplicating sites per instance. By having an external storage, we are
making it easier to do some operations like delete a docs site or wipe its
contents.
**Q: Why aren't docs built on the fly i.e. when users visits a page, generate
docs site in real-time?**
A: Generating the content from Markdown on the fly is not optimal (although that
is how the basic out-of-the-box setup is implemented). Storage solutions act as
a cache for the generated static content. TechDocs is also currently built on
MkDocs which does not allow us to generate docs per-page, so we would have to
build all docs for a entity on every request.
A: Generating the content from Markdown on the fly is not optimal. Storage
solutions act as a cache for the generated static content. TechDocs is also
currently built on MkDocs which does not allow us to generate docs per-page, so
we would have to build all docs for a entity on every request.
**Q. Can you use the techdocs plugin without the techdocs-backend plugin?**
@@ -146,21 +141,5 @@ We currently depend on MkDocs to parse doc sites written in Markdown. And we
store the generated static assets and re-use it later to render in Backstage. A
better (futuristic) approach will be to directly parse whatever type of source
files you have in your docs repository and directly render in Backstage in
real-time.
# Features status
Status of all the features mentioned above.
**In place ✅**
- Basic setup with techdocs-backend file server as storage.
- Basic setup with cloud storage solution.
- `techdocs-cli` is able to generate docs in CI/CD environment.
- `techdocs-cli` is able to publish docs site to any storage.
**Work in progress 🚧**
**Not implemented yet ❌**
- `techdocs-backend` integration with Backstage access control management.
real-time. You can read more in this
[RFC - Simplifying TechDocs Frontend Architecture](https://github.com/backstage/backstage/issues/3998).
+29 -10
View File
@@ -13,15 +13,6 @@ configuration options for TechDocs.
# File: app-config.yaml
techdocs:
# TechDocs makes API calls to techdocs-backend using this URL. e.g. get docs of an entity, get metadata, etc.
requestUrl: http://localhost:7000/api/techdocs
# Just another route in techdocs-backend where TechDocs requests the static files from. This URL uses an HTTP middleware
# to serve files from either a local directory or an External storage provider.
storageUrl: http://localhost:7000/api/techdocs/static/docs
# generators.techdocs can have two values: 'docker' or 'local'. This is to determine how to run the generator - whether to
# spin up the techdocs-container docker image or to run mkdocs locally (assuming all the dependencies are taken care of).
# You want to change this to 'local' if you are running Backstage using your own custom Docker setup and want to avoid running
@@ -44,7 +35,7 @@ techdocs:
# or you want to use External storage providers like Google Cloud Storage, AWS S3, etc.
publisher:
# techdocs.publisher.type can be - 'local' or 'googleGcs' or 'awsS3' (azureStorage to be available in future).
# techdocs.publisher.type can be - 'local' or 'googleGcs' or 'awsS3' or 'azureBlobStorage'.
# When set to 'local', techdocs-backend will create a 'static' directory at its root to store generated documentation files.
# When set to 'googleGcs', techdocs-backend will use a Google Cloud Storage Bucket to store generated documentation files.
# When set to 'awsS3', techdocs-backend will use an Amazon Web Service (AWS) S3 bucket to store generated documentation files.
@@ -84,4 +75,32 @@ techdocs:
# https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-region.html
region:
$env: AWS_REGION
# Required when techdocs.publisher.type is set to 'azureBlobStorage'. Skip otherwise.
azureBlobStorage:
# (Required) Azure Blob Storage Container Name
containerName: 'techdocs-storage'
# (Required) An account name is required to write to a storage blob container.
# https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key
credentials:
accountName:
$env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_NAME
# (Optional) An account key is required to write to a storage container.
# If missing,AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET environment variable will be used.
# https://docs.microsoft.com/en-us/azure/storage/common/storage-auth?toc=/azure/storage/blobs/toc.json
accountKey:
$env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_KEY
# (Optional and Legacy) TechDocs makes API calls to techdocs-backend using this URL. e.g. get docs of an entity, get metadata, etc.
# You don't have to specify this anymore.
requestUrl: http://localhost:7000/api/techdocs
# (Optional and Legacy) Just another route in techdocs-backend where TechDocs requests the static files from. This URL uses an HTTP middleware
# to serve files from either a local directory or an External storage provider.
# You don't have to specify this anymore.
storageUrl: http://localhost:7000/api/techdocs/static/docs
```
@@ -66,9 +66,16 @@ Update your component's entity description by adding the following lines to its
```yaml
metadata:
annotations:
backstage.io/techdocs-ref: dir:./
backstage.io/techdocs-ref: url:https://github.com/org/repo
# Or
# backstage.io/techdocs-ref: url:https://github.com/org/repo/tree/branchName/subFolder
```
The
[`backstage.io/techdocs-ref` annotation](../software-catalog/well-known-annotations.md#backstageiotechdocs-ref)
is used by TechDocs to download the documentation source files for generating an
Entity's TechDocs site.
Create a `/docs` folder in the root of the project with at least an `index.md`
file. _(If you add more markdown files, make sure to update the nav in the
mkdocs.yml file to get a proper navigation for your documentation.)_
+18 -33
View File
@@ -150,26 +150,10 @@ app. Now let us tweak some configurations to suit your needs.
**See [TechDocs Configuration Options](configuration.md) for complete
configuration reference.**
### Setting TechDocs URLs
```yaml
techdocs:
storageUrl: http://localhost:7000/api/techdocs/static/docs
requestUrl: http://localhost:7000/api/techdocs/
```
`requestUrl` is used by TechDocs frontend plugin to discover techdocs-backend
endpoints, and the `storageUrl` is another endpoint in `techdocs-backend` which
acts as a middleware between TechDocs and the storage (where the static
generated docs site are stored). These default values should mostly work for
you. These options will soon be optional to set.
### Should TechDocs Backend generate docs?
```yaml
techdocs:
storageUrl: http://localhost:7000/api/techdocs/static/docs
requestUrl: http://localhost:7000/api/techdocs/
builder: 'local'
```
@@ -180,8 +164,8 @@ pipeline, and are being stored in a storage somewhere.
When `techdocs.builder` is set to `'external'`, TechDocs becomes more or less a
read-only experience where it serves static files from a storage containing all
the generated documentation. Read more in the "Basic" and "Recommended" setup of
TechDocs [here](architecture.md)
the generated documentation. Read more in the "Basic" and "Recommended" sections
of the [TechDocs Architecture](architecture.md).
### Choosing storage (publisher)
@@ -190,14 +174,12 @@ fetch the sites from. This is managed by a
[Publisher](./concepts.md#techdocs-publisher). Examples: Google Cloud Storage,
Amazon S3, or local filesystem of Backstage server.
It is okay to use the local filesystem in a "Basic" setup when you are trying
out Backstage for the first time. Using Cloud Storage is documented
[here](./using-cloud-storage.md).
It is okay to use the local filesystem in a "basic" setup when you are trying
out Backstage for the first time. At a later time, review
[Using Cloud Storage](./using-cloud-storage.md).
```yaml
techdocs:
storageUrl: http://localhost:7000/api/techdocs/static/docs
requestUrl: http://localhost:7000/api/techdocs/
builder: 'local'
publisher:
type: 'local'
@@ -219,6 +201,9 @@ no config is provided.
```yaml
techdocs:
builder: 'local'
publisher:
type: 'local'
generators:
techdocs: local
```
@@ -229,23 +214,23 @@ environment is compatible with techdocs.
You will have to install the `mkdocs` and `mkdocs-techdocs-core` package from
pip, as well as `graphviz` and `plantuml` from your OS package manager (e.g.
apt). See our
[Dockerfile](https://github.com/backstage/techdocs-container/blob/main/Dockerfile)
for the latest requirements. You should be trying to match your Dockerfile with
this one.
[`Dockerfile`](https://github.com/backstage/techdocs-container/blob/main/Dockerfile)
for the latest requirements. You should be trying to match your `Dockerfile`
with this one.
Note: We recommend Python version 3.7 or higher.
Caveat: Please install the `mkdocs-techdocs-core` package after all other Python
packages. The order is important to make sure we get correct version of some of
the dependencies. For example, we want `Markdown` version to be
[3.2.2](https://github.com/backstage/backstage/blob/f9f70c225548017b6a14daea75b00fbd399c11eb/packages/techdocs-container/techdocs-core/requirements.txt#L11).
You can also explicitly install `Markdown==3.2.2` after installing all other
Python packages.
> Caveat: Please install the `mkdocs-techdocs-core` package after all other
> Python packages. The order is important to make sure we get correct version of
> some of the dependencies. For example, we want `Markdown` version to be
> [3.2.2](https://github.com/backstage/backstage/blob/f9f70c225548017b6a14daea75b00fbd399c11eb/packages/techdocs-container/techdocs-core/requirements.txt#L11).
> You can also explicitly install `Markdown==3.2.2` after installing all other
> Python packages.
## Running Backstage locally
Start the frontend and the backend app by
[running backstage locally](../../getting-started/running-backstage-locally.md).
[running Backstage locally](../../getting-started/running-backstage-locally.md).
Open your browser at [http://localhost:3000/docs/](http://localhost:3000/docs/)
to see all your documentation sites.
+30
View File
@@ -5,6 +5,36 @@ sidebar_label: "HOW TO" guides
description: TechDocs "HOW TO" guides related to TechDocs
---
## How to migrate from TechDocs Basic to Recommended deployment approach?
The main difference between TechDocs Basic and Recommended deployment approach
is where the docs are generated and stored. In Basic or the out-of-the-box
setup, docs are generated and stored at the server running your Backstage
instance. But the recommended setup is to generate docs on CI/CD and store the
generated sites to an external storage (e.g. AWS S3 or GCS). TechDocs in your
Backstage instance should turn into read-only mode. Read more details and the
benefits in the [TechDocs Architecture](architecture.md).
Here are the steps needed to switch from the Basic to Recommended setup -
### 1. Prepare a cloud storage
Choose a cloud storage provider like AWS, Google Cloud or Microsoft Azure.
Follow the detailed instructions for
[using cloud storage](using-cloud-storage.md) in TechDocs.
### 2. Publish to storage from CI/CD
Start publishing your TechDocs sites from the CI/CD workflow of each repository
containing the source markdown files. Read the detailed instructions for
[configuring CI/CD](configuring-ci-cd.md).
### 3. Switch TechDocs to read-only mode
In your Backstage instance's `app-config.yaml`, set `techdocs.builder` from
`'local'` to `'external'`. By doing this, TechDocs will not try to generate
docs. Look at [TechDocs configuration](configuration.md) for reference.
## How to use URL Reader in TechDocs Prepare step?
If TechDocs is configured to generate docs, it will first download the
@@ -195,3 +195,94 @@ 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.
## Configuring Azure Blob Storage Container with TechDocs
Follow the
[official Azure Blob Storage documentation](https://docs.microsoft.com/en-us/azure/storage/common/storage-auth?toc=/azure/storage/blobs/toc.json)
for the latest instructions on the following steps involving Azure Blob Storage.
**1. Set `techdocs.publisher.type` config in your `app-config.yaml`**
Set `techdocs.publisher.type` to `'azureBlobStorage'`.
```yaml
techdocs:
publisher:
type: 'azureBlobStorage'
```
**2. Create an Azure Blob Storage Container**
Create a dedicated container for TechDocs sites.
[Refer to the official documentation](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-portal).
TechDocs will publish documentation to this container and will fetch files from
here to serve documentation in Backstage. Note that the container names are
globally unique.
Set the config `techdocs.publisher.azureBlobStorage.containerName` in your
`app-config.yaml` to the name of the container you just created.
```yaml
techdocs:
publisher:
type: 'azureBlobStorage'
azureBlobStorage:
containerName: 'name-of-techdocs-storage-container'
```
**3a. (Recommended) Authentication using environment variable**
Set the config `techdocs.publisher.azureBlobStorage.credentials.accountName` in
your `app-config.yaml` to the your account name.
The storage blob client will automatically use the environment variable
`AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET` to authenticate with
Azure Blob Storage.
[Steps to create the service where the variables can be retrieved from](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal).
https://docs.microsoft.com/en-us/azure/storage/common/storage-auth-aad for more
details.
```yaml
techdocs:
publisher:
type: 'azureBlobStorage'
azureBlobStorage:
containerName: 'name-of-techdocs-storage-bucket'
credentials:
accountName:
$env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_NAME
```
**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.
To get credentials, access the Azure Portal and go to "Settings > Access Keys",
and get your Storage account name and Primary Key.
https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key
for more details.
```yaml
techdocs:
publisher:
type: 'azureBlobStorage'
azureBlobStorage:
containerName: 'name-of-techdocs-storage-bucket'
credentials:
accountName:
$env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_NAME
accountKey:
$env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_KEY
```
**4. That's it!**
Your Backstage app is now ready to use Azure Blob Storage 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 Azure Blob Storage container` in
the logs.
+86 -8
View File
@@ -4,19 +4,97 @@ title: Other
description: Documentation on different ways of Deployment
---
## Deploying Locally
## Docker
### Try on Docker
Here we have an example Dockerfile that you can use to build everything together
in one container. This Dockerfile uses multi-stage builds, and a
`backend:bundle` command from the CLI.
Run the following commands if you have Docker environment
It also provides caching on the `yarn install`'s so that you don't have to do it
unless absolutely necessary.
```bash
$ yarn install
$ yarn docker-build
$ docker run --rm -it -p 7000:7000 -e NODE_ENV=development example-backend:latest
> Note: This Dockerfile assumes that you're running SQLite, or your
> configuration is setup to connect to an external PostgreSQL Database.
```Dockerfile
# Stage 1 - Create yarn install skeleton layer
FROM node:14-buster AS packages
WORKDIR /app
COPY package.json yarn.lock ./
COPY packages packages
# Uncomment this line if you have a local plugins folder
# COPY plugins plugins
RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
# Stage 2 - Install dependencies and build packages
FROM node:14-buster AS build
WORKDIR /app
COPY --from=packages /app .
RUN yarn install --network-timeout 600000 && rm -rf "$(yarn cache dir)"
COPY . .
RUN yarn tsc
RUN yarn --cwd packages/backend backstage-cli backend:bundle --build-dependencies
# Stage 3 - Build the actual backend image and install production dependencies
FROM node:14-buster
WORKDIR /app
# Copy from build stage
COPY --from=build /app/yarn.lock /app/package.json /app/packages/backend/dist/skeleton.tar.gz ./
RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz
RUN yarn install --production --network-timeout 600000 && rm -rf "$(yarn cache dir)"
COPY --from=build /app/packages/backend/dist/bundle.tar.gz .
RUN tar xzf bundle.tar.gz && rm bundle.tar.gz
COPY app-config.yaml app-config.production.yaml ./
CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"]
```
Then open http://localhost:7000 on your browser.
Before building you should also include a `.dockerignore`. This will greatly
improve the context boot up time of Docker as we are no longer sending all of
the `node_modules` into the context. It also helps us avoid some limitations and
errors that may occur when trying to share the `node_modules` folder to inside
the build.
You can add the following contents to the root of your repository at
`.dockerignore` and it might look something like the following:
```dockerignore
.git
node_modules
packages/*/node_modules
plugins/*/node_modules
plugins/*/dist
```
Once you have added both the `Dockerfile` and `.dockerignore` to the root of
your project, and run the following to build the container under a specified
tag.
```sh
$ docker build -t example-deployment .
```
To run the image locally you can run:
```sh
$ docker run -p -it 7000:7000 example-deployment
```
You should then start to get logs in your terminal, and then you can open your
browser at `http://localhost:7000`
## Heroku
@@ -31,7 +31,7 @@ $ yarn start
This should open a local instance of Backstage in your browser, otherwise open
one of the URLs printed in the terminal.
By default, backstage will start on port 3000, however you can override this by
By default, Backstage will start on port 3000, however you can override this by
setting an environment variable `PORT` on your local machine. e.g.
`export PORT=8080` then running `yarn start`. Or `PORT=8080 yarn start`.
+3 -2
View File
@@ -9,8 +9,9 @@ A Backstage Plugin adds functionality to Backstage.
## Create a Plugin
To create a new plugin, make sure you've run `yarn install` and installed
dependencies, then run the following on your command line (invoking the
`backstage-cli`).
dependencies, then run the following on your command line (a shortcut to
invoking the [`backstage-cli create-plugin`](../cli/commands.md#create-plugin))
from the root of your project.
```bash
yarn create-plugin
@@ -59,7 +59,7 @@ accordingly inside `Router.tsx`)
### Import and use router in the APP
In the `app/src/components/catalog/EntityPage.tsx` (app === your folder,
containing backstage app) import your created Router:
containing Backstage app) import your created Router:
```tsx
import { Router as MyPluginRouter } from '@backstage/plugin-my-plugin;
+54
View File
@@ -0,0 +1,54 @@
---
id: observability
title: Observability
# prettier-ignore
description: Adding Observability to Your Plugin
---
This article briefly describes the observability options that are available to a
Backstage integrator.
## Google Analytics
There is a basic Google Analytics integration built into Backstage. You can
enable it by adding the following to your app configuration:
```yaml
app:
googleAnalyticsTrackingId: UA-000000-0
```
Replace the tracking ID with your own.
For more information, learn about Google Analytics
[here](https://marketingplatform.google.com/about/analytics/).
## Logging
The backend supplies a central [winston](https://github.com/winstonjs/winston)
root logger that plugins are expected to use for their logging needs. In the
default production setup, it emits structured JSON logs on stdout, with a field
`"service": "backstage"` and also tagged on a per-plugin basis. Plugins that
want to more finely specify what part of their processes that emitted the log
message should add a `"component"` field to do so.
An example log line could look as follows:
```json
{
"service": "backstage",
"type": "plugin",
"plugin": "catalog",
"component": "catalog-all-locations-refresh",
"level": "info",
"message": "Locations Refresh: Refreshing location bootstrap:bootstrap"
}
```
## Health Checks
The example backend in the Backstage repository
[supplies](https://github.com/backstage/backstage/blob/bc18571b7a742863a770b2a54e785d6bbef7e184/packages/backend/src/index.ts#L99)
a very basic health check endpoint on the `/healthcheck` route. You may add such
a handler to your backend as well, and supply your own logic to it that fits
your particular health checking needs.
+46 -21
View File
@@ -23,35 +23,60 @@ browser APIs or by depending on external modules to do the work.
### Routing
Each plugin is responsible for registering its components to corresponding
routes in the app.
Each plugin can export routable extensions, which are then imported into the app
and mounted at a path.
The app will call the `createPlugin` method on each plugin, passing in a
`router` object with a set of methods on it.
First you will need a `RouteRef` instance to serve as the mount point of your
extensions. This can be used within your own plugin to create a link to the
extension page using `useRouteRef`, as well as for other plugins to link to your
extension.
```jsx
It is best to place these in a separate top-level `src/routes.ts` file, in order
to avoid import cycles, for example like this:
```tsx
/* src/routes.ts */
import { createRouteRef } from '@backstage/core';
// Note: This route ref is for internal use only, don't export it from the plugin
export const rootRouteRef = createRouteRef({
title: 'Example Page',
});
```
Now that we have a `RouteRef`, we import it into `src/plugin.ts`, create our
plugin instance with `createPlugin`, as well as create and wrap our routable
extension using `createRoutableExtension` from `@backstage/core`:
```tsx
/* src/plugin.ts */
import { createPlugin, createRouteRef } from '@backstage/core';
import ExampleComponent from './components/ExampleComponent';
export const rootRouteRef = createRouteRef({
path: '/new-plugin',
title: 'New plugin',
});
export const plugin = createPlugin({
id: 'new-plugin',
register({ router }) {
router.addRoute(rootRouteRef, ExampleComponent);
// Create a plugin instance and export this from your plugin package
export const examplePlugin = createPlugin({
id: 'example',
routes: {
root: rootRouteRef, // This is where the route ref should be exported for usage in the app
},
});
// This creates a routable extension, which are typically full pages of content.
// Each extension should also be exported from your plugin package.
export const ExamplePage = examplePlugin.provide(
createRoutableExtension({
// The component needs to be lazy-loaded. It's what will actually be rendered in the end.
component: () =>
import('./components/ExampleComponent').then(m => m.ExampleComponent),
// This binds the extension to this route ref, which allows for routing within and across plugin extensions
mountPoint: rootRouteRef,
}),
);
```
#### `router` API
This extension can then be imported and used in the app as follow, typically
placed within the top-level `<FlatRoutes>`:
```typescript
addRoute(
target: RouteRef,
Component: ComponentType<any>,
options?: RouteOptions,
): void;
```tsx
<Route route="/any-path" element={<ExamplePage />} />
```
+24 -16
View File
@@ -28,6 +28,7 @@ new-plugin/
index.ts
plugin.test.ts
plugin.ts
routes.ts
jest.config.js
jest.setup.ts
package.json
@@ -56,26 +57,30 @@ package.json to declare the plugin dependencies, metadata and scripts.
In the `src` folder we get to the interesting bits. Check out the `plugin.ts`:
```jsx
import { createPlugin, createRouteRef } from '@backstage/core';
import ExampleComponent from './components/ExampleComponent';
import { createPlugin, createRoutableExtension } from '@backstage/core';
export const rootRouteRef = createRouteRef({
path: '/new-plugin',
title: 'New plugin',
});
import { rootRouteRef } from './routes';
export const plugin = createPlugin({
id: 'new-plugin',
register({ router }) {
router.addRoute(rootRouteRef, ExampleComponent);
export const examplePlugin = createPlugin({
id: 'example',
routes: {
root: rootRouteRef,
},
});
export const ExamplePage = examplePlugin.provide(
createRoutableExtension({
component: () =>
import('./components/ExampleComponent').then(m => m.ExampleComponent),
mountPoint: rootRouteRef,
}),
);
```
This is where the plugin is created and where it hooks into the app by declaring
what component should be shown on what URL. See reference docs for
[createPlugin](../reference/createPlugin.md) or
[router](../reference/createPlugin-router.md).
This is where the plugin is created and where it creates and exports extensions
that can be imported and used the app. See reference docs for
[createPlugin](../reference/createPlugin.md) or introduction to the new
[Composability System](./composability.md).
## Components
@@ -91,12 +96,15 @@ You may tweak these components, rename them and/or replace them completely.
## Connecting the plugin to the Backstage app
There are two things needed for a Backstage app to start making use of a plugin.
There are three things needed for a Backstage app to start making use of a
plugin.
1. Add plugin as dependency in `app/package.json`
2. `import` plugin in `app/src/plugins.ts`
3. Import and use one or more plugin extensions, for example in
`app/src/App.tsx`.
Luckily these two steps happen automatically when you create a plugin with the
Luckily these three steps happen automatically when you create a plugin with the
Backstage CLI.
## Talking to the outside world
-31
View File
@@ -1,31 +0,0 @@
---
id: createPlugin-router
title: createPlugin - router
description: Documentation on createPlugin - router
---
The router that is passed to the `register` function makes it possible for
plugins to hook into routing of the Backstage app and provide the end users with
new views to navigate to. This is done by utilising the following methods on the
`router`:
```typescript
addRoute(
target: RouteRef,
Component: ComponentType<any>,
options?: RouteOptions,
): void;
```
## RouteRef
`addRoute` method is using mutable RouteRefs, which can be created as following:
```ts
import { createRouteRef } from '@backstage/core';
const myPluginRouteRef = createRouteRef({
path: '/my-plugin',
title: 'My Plugin',
});
```
+3 -11
View File
@@ -17,32 +17,24 @@ type PluginConfig = {
};
type PluginHooks = {
router: RouterHooks;
featureFlags: FeatureFlagsHooks;
};
```
- [Read more about the router here](createPlugin-router.md)
- [Read more about feature flags here](createPlugin-feature-flags.md)
## Example Uses
### Creating a basic plugin
Showcasing adding a route and a feature flag.
Showcasing adding a feature flag.
```jsx
import { createPlugin, createRouteRef } from '@backstage/core';
import ExampleComponent from './components/ExampleComponent';
export const rootRouteRef = createRouteRef({
path: '/new-plugin',
title: 'New Plugin',
});
import { createPlugin } from '@backstage/core';
export default createPlugin({
id: 'new-plugin',
register({ router, featureFlags }) {
router.addRoute(rootRouteRef, ExampleComponent);
featureFlags.register('enable-example-component');
},
});
@@ -79,7 +79,7 @@ Referenced by: [getBackstageIdentity](#getbackstageidentity).
<pre>
export type BackstageIdentity = {
/**
* The backstage user ID.
* The Backstage user ID.
*/
id: string;
+12 -16
View File
@@ -349,22 +349,18 @@ const app = createApp({
apis,
plugins: Object.values(plugins),
components: {
SignInPage: props => {
return (
<SignInPage
{...props}
providers={[
{
id: 'github-auth-provider',
title: 'GitHub',
message: 'Simple Backstage Application Login',
apiRef: githubAuthApiRef,
},
]}
align="center"
/>
);
},
SignInPage: props => (
<SignInPage
{...props}
auto
provider={{
id: 'github-auth-provider',
title: 'GitHub',
message: 'Simple Backstage Application Login',
apiRef: githubAuthApiRef,
}}
/>
),
},
});
```
+4 -3
View File
@@ -13,7 +13,6 @@
"Getting Started": [
"getting-started/index",
"getting-started/running-backstage-locally",
"getting-started/installation",
"getting-started/development-environment",
"getting-started/create-an-app",
{
@@ -130,7 +129,8 @@
"ids": [
"plugins/publishing",
"plugins/publish-private",
"plugins/add-to-marketplace"
"plugins/add-to-marketplace",
"plugins/observability"
]
}
],
@@ -188,7 +188,8 @@
"architecture-decisions/adrs-adr007",
"architecture-decisions/adrs-adr008",
"architecture-decisions/adrs-adr009",
"architecture-decisions/adrs-adr010"
"architecture-decisions/adrs-adr010",
"architecture-decisions/adrs-adr011"
],
"Contribute": ["../CONTRIBUTING"],
"Support": ["support/support", "support/project-structure"],
+1
View File
@@ -83,6 +83,7 @@ const siteConfig = {
'https://unpkg.com/medium-zoom@1.0.6/dist/medium-zoom.min.js',
'/js/medium-zoom.js',
'/js/dismissable-banner.js',
'/js/scroll-nav-to-view-in-docs.js',
],
// On page navigation for the current documentation page.
@@ -0,0 +1,23 @@
// On backstage.io/docs pages, scroll the Nav sidebar to focus on
// the page being viewed. Helpful when the Nav is large enough that
// the selected page is hidden somewhere at bottom.
// Credits: https://github.com/facebook/docusaurus/issues/823#issuecomment-421152269
document.addEventListener('DOMContentLoaded', () => {
// Find the active nav item in the sidebar
const item = document.getElementsByClassName('navListItemActive')[0];
if (!item) {
return;
}
const bounding = item.getBoundingClientRect();
if (
bounding.top >= 0 &&
bounding.bottom <=
(window.innerHeight || document.documentElement.clientHeight)
) {
// Already visible. Do nothing.
} else {
// Not visible. Scroll sidebar.
item.scrollIntoView({ block: 'start', inline: 'nearest' });
document.body.scrollTop = document.documentElement.scrollTop = 0;
}
});
+2
View File
@@ -80,6 +80,7 @@ nav:
- Publishing:
- Open source and npm: 'plugins/publishing.md'
- Private/internal (non-open source): 'plugins/publish-private.md'
- Observability: 'plugins/observability.md'
- Configuration:
- Overview: 'conf/index.md'
- Reading Configuration: 'conf/reading.md'
@@ -119,6 +120,7 @@ nav:
- ADR008 - Default Catalog File Name: 'architecture-decisions/adr008-default-catalog-file-name.md'
- ADR009 - Entity References: 'architecture-decisions/adr009-entity-references.md'
- ADR010 - Luxon Date Library: 'architecture-decisions/adr010-luxon-date-library.md'
- ADR011 - Plugin Package Structure: 'architecture-decisions/adr011-plugin-package-structure.md'
- Contribute: '../CONTRIBUTING.md'
- Support:
- 'support/support.md'
+1 -1
View File
@@ -41,7 +41,7 @@
},
"version": "1.0.0",
"devDependencies": {
"@changesets/cli": "^2.11.0",
"@changesets/cli": "^2.14.0",
"@octokit/openapi-types": "^2.2.0",
"@spotify/eslint-config-oss": "^1.0.1",
"@spotify/prettier-config": "^9.0.0",
+97
View File
@@ -1,5 +1,102 @@
# example-app
## 0.2.14
### Patch Changes
- 9d6ef14bc: Migrated to new composability API, exporting the plugin instance as `rollbarPlugin`, the entity page content as `EntityRollbarContent`, and entity conditional as `isRollbarAvailable`. Updated the `EntityPage` for the `example-app` to include a composite `ErrorsSwitcher` component that works with both `Sentry` & `Rollbar`. Also removed the unused and undocumented `RollbarHome` related components.
- Updated dependencies [ceef4dd89]
- Updated dependencies [720149854]
- Updated dependencies [19172f5a9]
- Updated dependencies [4c6a6dddd]
- Updated dependencies [398e1f83e]
- Updated dependencies [87b189d00]
- Updated dependencies [12ece98cd]
- Updated dependencies [d82246867]
- Updated dependencies [7fc89bae2]
- Updated dependencies [c810082ae]
- Updated dependencies [b712841d6]
- Updated dependencies [a5628df40]
- Updated dependencies [5fa3bdb55]
- Updated dependencies [bc5082a00]
- Updated dependencies [6e612ce25]
- Updated dependencies [e44925723]
- Updated dependencies [b37501a3d]
- Updated dependencies [9d6ef14bc]
- Updated dependencies [025e122c3]
- Updated dependencies [e9aab60c7]
- Updated dependencies [21e624ba9]
- Updated dependencies [0269f4fd9]
- Updated dependencies [19fe61c27]
- Updated dependencies [da9f53c60]
- Updated dependencies [a08c4b0b0]
- Updated dependencies [bc5082a00]
- Updated dependencies [bc5082a00]
- Updated dependencies [b37501a3d]
- Updated dependencies [90c8f20b9]
- Updated dependencies [32c95605f]
- Updated dependencies [7881f2117]
- Updated dependencies [8dfdec613]
- Updated dependencies [54c7d02f7]
- Updated dependencies [de98c32ed]
- Updated dependencies [806929fe2]
- Updated dependencies [019fe39a0]
- Updated dependencies [019fe39a0]
- Updated dependencies [11cb5ef94]
- @backstage/plugin-catalog-import@0.3.7
- @backstage/plugin-scaffolder@0.4.2
- @backstage/plugin-cost-insights@0.8.0
- @backstage/cli@0.6.0
- @backstage/plugin-graphiql@0.2.7
- @backstage/core@0.6.0
- @backstage/plugin-api-docs@0.4.4
- @backstage/plugin-catalog@0.3.0
- @backstage/plugin-catalog-react@0.0.2
- @backstage/plugin-org@0.3.5
- @backstage/theme@0.2.3
- @backstage/plugin-lighthouse@0.2.9
- @backstage/plugin-techdocs@0.5.5
- @backstage/plugin-user-settings@0.2.5
- @backstage/catalog-model@0.7.1
- @backstage/plugin-rollbar@0.2.9
- @backstage/plugin-gcp-projects@0.2.4
- @backstage/plugin-tech-radar@0.3.4
- @backstage/plugin-welcome@0.2.5
- @backstage/plugin-explore@0.2.4
- @backstage/plugin-circleci@0.2.7
- @backstage/plugin-cloudbuild@0.2.8
- @backstage/plugin-github-actions@0.3.1
- @backstage/plugin-jenkins@0.3.8
- @backstage/plugin-kafka@0.2.1
- @backstage/plugin-register-component@0.2.8
- @backstage/plugin-search@0.2.7
- @backstage/plugin-sentry@0.3.4
- @backstage/plugin-gitops-profiles@0.2.4
- @backstage/plugin-kubernetes@0.3.8
- @backstage/plugin-newrelic@0.2.4
- @backstage/plugin-pagerduty@0.2.7
## 0.2.13
### Patch Changes
- Updated dependencies [681111228]
- Updated dependencies [12a56cdfe]
- Updated dependencies [8b7ef9f8b]
- Updated dependencies [fac91bcc5]
- Updated dependencies [9dd057662]
- Updated dependencies [234e7d985]
- Updated dependencies [ef7957be4]
- Updated dependencies [0b1182346]
- Updated dependencies [a6e3b9596]
- @backstage/plugin-kubernetes@0.3.7
- @backstage/cli@0.5.0
- @backstage/plugin-cost-insights@0.6.0
- @backstage/plugin-catalog@0.2.14
- @backstage/plugin-catalog-import@0.3.6
- @backstage/plugin-scaffolder@0.4.1
- @backstage/plugin-kafka@0.2.0
## 0.2.12
### Patch Changes
+33 -32
View File
@@ -1,40 +1,41 @@
{
"name": "example-app",
"version": "0.2.12",
"version": "0.2.14",
"private": true,
"bundled": true,
"dependencies": {
"@backstage/catalog-model": "^0.7.0",
"@backstage/cli": "^0.4.7",
"@backstage/core": "^0.5.0",
"@backstage/plugin-api-docs": "^0.4.3",
"@backstage/plugin-catalog": "^0.2.12",
"@backstage/plugin-catalog-import": "^0.3.5",
"@backstage/plugin-circleci": "^0.2.6",
"@backstage/plugin-cloudbuild": "^0.2.7",
"@backstage/plugin-cost-insights": "^0.5.7",
"@backstage/plugin-explore": "^0.2.3",
"@backstage/plugin-gcp-projects": "^0.2.3",
"@backstage/plugin-github-actions": "^0.3.0",
"@backstage/plugin-gitops-profiles": "^0.2.3",
"@backstage/plugin-graphiql": "^0.2.6",
"@backstage/plugin-org": "^0.3.4",
"@backstage/plugin-jenkins": "^0.3.6",
"@backstage/plugin-kafka": "^0.1.1",
"@backstage/plugin-kubernetes": "^0.3.6",
"@backstage/plugin-lighthouse": "^0.2.8",
"@backstage/plugin-newrelic": "^0.2.3",
"@backstage/plugin-pagerduty": "0.2.6",
"@backstage/plugin-register-component": "^0.2.7",
"@backstage/plugin-rollbar": "^0.2.8",
"@backstage/plugin-scaffolder": "^0.4.0",
"@backstage/plugin-sentry": "^0.3.3",
"@backstage/plugin-search": "^0.2.6",
"@backstage/plugin-tech-radar": "^0.3.3",
"@backstage/plugin-techdocs": "^0.5.4",
"@backstage/plugin-user-settings": "^0.2.4",
"@backstage/plugin-welcome": "^0.2.4",
"@backstage/theme": "^0.2.2",
"@backstage/catalog-model": "^0.7.1",
"@backstage/cli": "^0.6.0",
"@backstage/core": "^0.6.0",
"@backstage/plugin-api-docs": "^0.4.4",
"@backstage/plugin-catalog": "^0.3.0",
"@backstage/plugin-catalog-react": "^0.0.2",
"@backstage/plugin-catalog-import": "^0.3.7",
"@backstage/plugin-circleci": "^0.2.7",
"@backstage/plugin-cloudbuild": "^0.2.8",
"@backstage/plugin-cost-insights": "^0.8.0",
"@backstage/plugin-explore": "^0.2.4",
"@backstage/plugin-gcp-projects": "^0.2.4",
"@backstage/plugin-github-actions": "^0.3.1",
"@backstage/plugin-gitops-profiles": "^0.2.4",
"@backstage/plugin-graphiql": "^0.2.7",
"@backstage/plugin-org": "^0.3.5",
"@backstage/plugin-jenkins": "^0.3.8",
"@backstage/plugin-kafka": "^0.2.1",
"@backstage/plugin-kubernetes": "^0.3.8",
"@backstage/plugin-lighthouse": "^0.2.9",
"@backstage/plugin-newrelic": "^0.2.4",
"@backstage/plugin-pagerduty": "0.2.7",
"@backstage/plugin-register-component": "^0.2.8",
"@backstage/plugin-rollbar": "^0.2.9",
"@backstage/plugin-scaffolder": "^0.4.2",
"@backstage/plugin-sentry": "^0.3.4",
"@backstage/plugin-search": "^0.2.7",
"@backstage/plugin-tech-radar": "^0.3.4",
"@backstage/plugin-techdocs": "^0.5.5",
"@backstage/plugin-user-settings": "^0.2.5",
"@backstage/plugin-welcome": "^0.2.5",
"@backstage/theme": "^0.2.3",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@octokit/rest": "^18.0.12",
+15 -14
View File
@@ -15,30 +15,30 @@
*/
import {
createApp,
AlertDisplay,
OAuthRequestDialog,
SignInPage,
createApp,
createRouteRef,
FlatRoutes,
OAuthRequestDialog,
SignInPage,
} from '@backstage/core';
import React from 'react';
import Root from './components/Root';
import * as plugins from './plugins';
import { apis } from './apis';
import { hot } from 'react-hot-loader/root';
import { providers } from './identityProviders';
import { Router as CatalogRouter } from '@backstage/plugin-catalog';
import { Router as DocsRouter } from '@backstage/plugin-techdocs';
import { Router as ImportComponentRouter } from '@backstage/plugin-catalog-import';
import { ExplorePage } from '@backstage/plugin-explore';
import { Router as GraphiQLRouter } from '@backstage/plugin-graphiql';
import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar';
import { Router as LighthouseRouter } from '@backstage/plugin-lighthouse';
import { Router as RegisterComponentRouter } from '@backstage/plugin-register-component';
import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar';
import { Router as DocsRouter } from '@backstage/plugin-techdocs';
import { Router as SettingsRouter } from '@backstage/plugin-user-settings';
import { Router as ImportComponentRouter } from '@backstage/plugin-catalog-import';
import { Route, Navigate } from 'react-router';
import React from 'react';
import { hot } from 'react-hot-loader/root';
import { Navigate, Route } from 'react-router';
import { apis } from './apis';
import { EntityPage } from './components/catalog/EntityPage';
import Root from './components/Root';
import { providers } from './identityProviders';
import * as plugins from './plugins';
const app = createApp({
apis,
@@ -78,6 +78,7 @@ const routes = (
element={<CatalogRouter EntityPage={EntityPage} />}
/>
<Route path="/docs" element={<DocsRouter />} />
<Route path="/explore" element={<ExplorePage />} />
<Route
path="/tech-radar"
element={<TechRadarRouter width={1500} height={800} />}
@@ -20,6 +20,7 @@ import HomeIcon from '@material-ui/icons/Home';
import ExtensionIcon from '@material-ui/icons/Extension';
import RuleIcon from '@material-ui/icons/AssignmentTurnedIn';
import MapIcon from '@material-ui/icons/MyLocation';
import LayersIcon from '@material-ui/icons/Layers';
import LibraryBooks from '@material-ui/icons/LibraryBooks';
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
import MoneyIcon from '@material-ui/icons/MonetizationOn';
@@ -82,6 +83,7 @@ const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarItem icon={HomeIcon} to="/catalog" text="Home" />
<SidebarItem icon={ExtensionIcon} to="api-docs" text="APIs" />
<SidebarItem icon={LibraryBooks} to="docs" text="Docs" />
<SidebarItem icon={LayersIcon} to="explore" text="Explore" />
<SidebarItem icon={CreateComponentIcon} to="create" text="Create..." />
{/* End global nav */}
<SidebarDivider />
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
ApiEntity,
Entity,
@@ -27,11 +28,8 @@ import {
ProvidedApisCard,
ProvidingComponentsCard,
} from '@backstage/plugin-api-docs';
import {
AboutCard,
EntityPageLayout,
useEntity,
} from '@backstage/plugin-catalog';
import { AboutCard, EntityPageLayout } from '@backstage/plugin-catalog';
import { useEntity } from '@backstage/plugin-catalog-react';
import {
isPluginApplicableToEntity as isCircleCIAvailable,
Router as CircleCIRouter,
@@ -50,6 +48,7 @@ import {
LatestRunCard as JenkinsLatestRunCard,
Router as JenkinsRouter,
} from '@backstage/plugin-jenkins';
import { Router as KafkaRouter } from '@backstage/plugin-kafka';
import { Router as KubernetesRouter } from '@backstage/plugin-kubernetes';
import {
EmbeddedRouter as LighthouseRouter,
@@ -57,13 +56,20 @@ import {
LastLighthouseAuditCard,
} from '@backstage/plugin-lighthouse';
import {
OwnershipCard,
MembersListCard,
GroupProfileCard,
MembersListCard,
OwnershipCard,
UserProfileCard,
} from '@backstage/plugin-org';
import {
isPluginApplicableToEntity as isPagerDutyAvailable,
PagerDutyCard,
} from '@backstage/plugin-pagerduty';
import {
isRollbarAvailable,
Router as RollbarRouter,
} from '@backstage/plugin-rollbar';
import { Router as SentryRouter } from '@backstage/plugin-sentry';
import { Router as KafkaRouter } from '@backstage/plugin-kafka';
import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs';
import { Button, Grid } from '@material-ui/core';
import {
@@ -82,10 +88,6 @@ import {
PullRequestsStatsCard,
Router as PullRequestsRouter,
} from '@roadiehq/backstage-plugin-github-pull-requests';
import {
isPluginApplicableToEntity as isPagerDutyAvailable,
PagerDutyCard,
} from '@backstage/plugin-pagerduty';
import {
isPluginApplicableToEntity as isTravisCIAvailable,
RecentTravisCIBuildsWidget,
@@ -156,6 +158,15 @@ const RecentCICDRunsSwitcher = ({ entity }: { entity: Entity }) => {
);
};
export const ErrorsSwitcher = ({ entity }: { entity: Entity }) => {
switch (true) {
case isRollbarAvailable(entity):
return <RollbarRouter entity={entity} />;
default:
return <SentryRouter entity={entity} />;
}
};
const ComponentOverviewContent = ({ entity }: { entity: Entity }) => (
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
@@ -215,9 +226,9 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
element={<CICDSwitcher entity={entity} />}
/>
<EntityPageLayout.Content
path="/sentry"
title="Sentry"
element={<SentryRouter entity={entity} />}
path="/errors/*"
title="Errors"
element={<ErrorsSwitcher entity={entity} />}
/>
<EntityPageLayout.Content
path="/api/*"
@@ -270,9 +281,9 @@ const WebsiteEntityPage = ({ entity }: { entity: Entity }) => (
element={<LighthouseRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/sentry"
title="Sentry"
element={<SentryRouter entity={entity} />}
path="/errors/*"
title="Errors"
element={<ErrorsSwitcher entity={entity} />}
/>
<EntityPageLayout.Content
path="/docs/*"
+1 -1
View File
@@ -18,7 +18,7 @@ export { plugin as LighthousePlugin } from '@backstage/plugin-lighthouse';
export { plugin as CatalogPlugin } from '@backstage/plugin-catalog';
export { plugin as ScaffolderPlugin } from '@backstage/plugin-scaffolder';
export { plugin as TechRadar } from '@backstage/plugin-tech-radar';
export { plugin as Explore } from '@backstage/plugin-explore';
export { explorePlugin } from '@backstage/plugin-explore';
export { plugin as Circleci } from '@backstage/plugin-circleci';
export { plugin as RegisterComponent } from '@backstage/plugin-register-component';
export { plugin as Sentry } from '@backstage/plugin-sentry';
+64
View File
@@ -1,5 +1,69 @@
# @backstage/backend-common
## 0.5.2
### Patch Changes
- 2430ee7c2: Updated the `rootLogger` in `@backstage/backend-common` to support custom logging options. This is useful when you want to make some changes without re-implementing the entire logger and calling `setRootLogger` or `logger.configure`. For example you can add additional `defaultMeta` tags to each log entry. The following changes are included:
- Added `createRootLogger` which accepts winston `LoggerOptions`. These options allow overriding the default keys.
Example Usage:
```ts
// Create the logger
const logger = createRootLogger({
defaultMeta: { appName: 'backstage', appEnv: 'prod' },
});
// Add a custom logger transport
logger.add(new MyCustomTransport());
const config = await loadBackendConfig({
argv: process.argv,
logger: getRootLogger(), // already set to new logger instance
});
```
- Updated dependencies [c4abcdb60]
- Updated dependencies [062df71db]
- Updated dependencies [064c513e1]
- Updated dependencies [e9aab60c7]
- Updated dependencies [3149bfe63]
- Updated dependencies [2e62aea6f]
- @backstage/integration@0.3.2
- @backstage/config-loader@0.5.1
## 0.5.1
### Patch Changes
- 26a3a6cf0: Honor the branch ref in the url when cloning.
This fixes a bug in the scaffolder prepare stage where a non-default branch
was specified in the scaffolder URL but the default branch was cloned.
For example, even though the `other` branch is specified in this example, the
`master` branch was actually cloned:
```yaml
catalog:
locations:
- type: url
target: https://github.com/backstage/backstage/blob/other/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml
```
This also fixes a 404 in the prepare stage for GitLab URLs.
- 664dd08c9: URL Reader's readTree: Fix bug with github.com URLs.
- 9dd057662: Upgrade [git-url-parse](https://www.npmjs.com/package/git-url-parse) to [v11.4.4](https://github.com/IonicaBizau/git-url-parse/pull/125) which fixes parsing an Azure DevOps branch ref.
- Updated dependencies [6800da78d]
- Updated dependencies [9dd057662]
- Updated dependencies [ef7957be4]
- Updated dependencies [ef7957be4]
- Updated dependencies [ef7957be4]
- @backstage/integration@0.3.1
- @backstage/config-loader@0.5.0
## 0.5.0
### Minor Changes
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
"version": "0.5.0",
"version": "0.5.2",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -31,8 +31,8 @@
"dependencies": {
"@backstage/cli-common": "^0.1.1",
"@backstage/config": "^0.1.2",
"@backstage/config-loader": "^0.4.1",
"@backstage/integration": "^0.3.0",
"@backstage/config-loader": "^0.5.1",
"@backstage/integration": "^0.3.2",
"@types/cors": "^2.8.6",
"@types/express": "^4.17.6",
"archiver": "^5.0.2",
@@ -43,7 +43,7 @@
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"fs-extra": "^9.0.1",
"git-url-parse": "^11.4.3",
"git-url-parse": "^11.4.4",
"helmet": "^4.0.0",
"isomorphic-git": "^1.8.0",
"knex": "^0.21.6",
@@ -66,7 +66,7 @@
}
},
"devDependencies": {
"@backstage/cli": "^0.4.7",
"@backstage/cli": "^0.6.0",
"@backstage/test-utils": "^0.1.5",
"@types/archiver": "^5.1.0",
"@types/compression": "^1.7.0",
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as winston from 'winston';
import { TransformableInfo } from 'logform';
@@ -14,5 +14,6 @@
* limitations under the License.
*/
export * from './formats';
export * from './rootLogger';
export * from './voidLogger';
@@ -15,7 +15,7 @@
*/
import * as winston from 'winston';
import { getRootLogger, setRootLogger } from './rootLogger';
import { createRootLogger, getRootLogger, setRootLogger } from './rootLogger';
describe('rootLogger', () => {
it('can replace the default logger', () => {
@@ -29,4 +29,70 @@ describe('rootLogger', () => {
expect.stringContaining('testing'),
);
});
describe('createRootLoger', () => {
it('creates a new logger', () => {
const oldLogger = getRootLogger();
const newLogger = createRootLogger();
expect(oldLogger).not.toBe(newLogger);
});
it('replaces the existing root logger', () => {
const oldLogger = getRootLogger();
createRootLogger();
const newLogger = getRootLogger();
expect(oldLogger).not.toBe(newLogger);
});
it('can append additional default metadata', () => {
const format = winston.format.json();
const logger = createRootLogger({
format,
defaultMeta: {
appName: 'backstage',
appEnv: 'prod',
containerId: 'abc',
},
});
jest.spyOn(format, 'transform');
logger.info('testing');
expect(format.transform).toHaveBeenCalledWith(
expect.objectContaining({
message: 'testing',
service: 'backstage',
appName: 'backstage',
appEnv: 'prod',
containerId: 'abc',
}),
{},
);
});
it('can add override existing transports', () => {
const transport = new winston.transports.Console({ level: 'debug' });
const logger = createRootLogger({ transports: [transport] });
expect(logger.transports.length).toBe(1);
expect(logger.transports[0]).toBe(transport);
});
it('can append an additional transport', () => {
const logger = createRootLogger();
const transport = new winston.transports.Console({ level: 'debug' });
logger.add(transport);
expect(logger.transports.length).toBe(2);
expect(logger.transports[1]).toBe(transport);
expect(logger.transports[1].level).toBe('debug');
});
it('can override default format', () => {
const format = winston.format(() => false)();
const logger = createRootLogger({ format });
expect(
logger.format.transform({ message: 'hello', level: 'info' }),
).toBeFalsy();
});
});
});
@@ -13,23 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { merge } from 'lodash';
import * as winston from 'winston';
import { LoggerOptions } from 'winston';
import { coloredFormat } from './formats';
let rootLogger: winston.Logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format:
process.env.NODE_ENV === 'production'
? winston.format.json()
: coloredFormat,
defaultMeta: { service: 'backstage' },
transports: [
new winston.transports.Console({
silent:
process.env.JEST_WORKER_ID !== undefined && !process.env.LOG_LEVEL,
}),
],
});
let rootLogger: winston.Logger;
export function getRootLogger(): winston.Logger {
return rootLogger;
@@ -38,3 +28,34 @@ export function getRootLogger(): winston.Logger {
export function setRootLogger(newLogger: winston.Logger) {
rootLogger = newLogger;
}
export function createRootLogger(
options: winston.LoggerOptions = {},
env = process.env,
): winston.Logger {
const logger = winston.createLogger(
merge<LoggerOptions, LoggerOptions>(
{
level: env.LOG_LEVEL || 'info',
format: winston.format.combine(
env.NODE_ENV === 'production' ? winston.format.json() : coloredFormat,
),
defaultMeta: {
service: 'backstage',
},
transports: [
new winston.transports.Console({
silent: env.JEST_WORKER_ID !== undefined && !env.LOG_LEVEL,
}),
],
},
options,
),
);
setRootLogger(logger);
return logger;
}
rootLogger = createRootLogger();
@@ -14,7 +14,9 @@
* limitations under the License.
*/
import fs from 'fs';
import * as os from 'os';
import fs from 'fs-extra';
import mockFs from 'mock-fs';
import path from 'path';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
@@ -31,6 +33,8 @@ const treeResponseFactory = ReadTreeResponseFactory.create({
config: new ConfigReader({}),
});
const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp';
describe('AzureUrlReader', () => {
const worker = setupServer();
msw.setupDefaultHandlers(worker);
@@ -139,6 +143,16 @@ describe('AzureUrlReader', () => {
});
describe('readTree', () => {
beforeEach(() => {
mockFs({
[tmpDir]: mockFs.directory(),
});
});
afterEach(() => {
mockFs.restore();
});
const repoBuffer = fs.readFileSync(
path.resolve('src', 'reading', '__fixtures__', 'mock-main.zip'),
);
@@ -200,6 +214,21 @@ describe('AzureUrlReader', () => {
expect(indexMarkdownFile.toString()).toBe('# Test\n');
});
it('creates a directory with the wanted files', async () => {
const response = await processor.readTree(
'https://dev.azure.com/organization/project/_git/repository',
);
const dir = await response.dir({ targetDir: tmpDir });
await expect(
fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'),
).resolves.toBe('site_name: Test\n');
await expect(
fs.readFile(path.join(dir, 'docs', 'index.md'), 'utf8'),
).resolves.toBe('# Test\n');
});
it('throws a NotModifiedError when given a etag in options', async () => {
const fnAzure = async () => {
await processor.readTree(
@@ -16,7 +16,8 @@
import { ConfigReader } from '@backstage/config';
import { msw } from '@backstage/test-utils';
import fs from 'fs';
import fs from 'fs-extra';
import mockFs from 'mock-fs';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import path from 'path';
@@ -53,6 +54,16 @@ describe('BitbucketUrlReader', () => {
});
describe('readTree', () => {
beforeEach(() => {
mockFs({
'/tmp': mockFs.directory(),
});
});
afterEach(() => {
mockFs.restore();
});
const worker = setupServer();
msw.setupDefaultHandlers(worker);
@@ -126,12 +137,12 @@ describe('BitbucketUrlReader', () => {
),
),
rest.get(
'https://api.bitbucket.mycompany.net/rest/api/1.0/repositories/backstage/mock/commits/some-branch',
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/commits',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
values: [{ hash: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }],
values: [{ id: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }],
}),
),
),
@@ -155,6 +166,21 @@ describe('BitbucketUrlReader', () => {
expect(mkDocsFile.toString()).toBe('site_name: Test\n');
});
it('creates a directory with the wanted files', async () => {
const response = await bitbucketProcessor.readTree(
'https://bitbucket.org/backstage/mock',
);
const dir = await response.dir({ targetDir: '/tmp' });
await expect(
fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'),
).resolves.toBe('site_name: Test\n');
await expect(
fs.readFile(path.join(dir, 'docs', 'index.md'), 'utf8'),
).resolves.toBe('# Test\n');
});
it('uses private bitbucket host', async () => {
const response = await hostedBitbucketProcessor.readTree(
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs?at=some-branch',
@@ -185,6 +211,18 @@ describe('BitbucketUrlReader', () => {
expect(indexMarkdownFile.toString()).toBe('# Test\n');
});
it('creates a directory with the wanted files with a subpath', async () => {
const response = await bitbucketProcessor.readTree(
'https://bitbucket.org/backstage/mock/src/master/docs',
);
const dir = await response.dir({ targetDir: '/tmp' });
await expect(
fs.readFile(path.join(dir, 'index.md'), 'utf8'),
).resolves.toBe('# Test\n');
});
it('throws a NotModifiedError when given a etag in options', async () => {
const fnBitbucket = async () => {
await bitbucketProcessor.readTree(
@@ -204,5 +242,17 @@ describe('BitbucketUrlReader', () => {
expect(response.etag).toBe('12ab34cd56ef');
});
it('should throw error when apiBaseUrl is missing', () => {
expect(() => {
/* eslint-disable no-new */
new BitbucketUrlReader(
{
host: 'bitbucket.mycompany.net',
},
{ treeResponseFactory },
);
}).toThrowError('must configure an explicit apiBaseUrl');
});
});
});
@@ -121,31 +121,9 @@ export class BitbucketUrlReader implements UrlReader {
throw new Error(message);
}
// Get the filename of archive from the header of the response
const contentDispositionHeader = archiveBitbucketResponse.headers.get(
'content-disposition',
) as string;
if (!contentDispositionHeader) {
throw new Error(
`Failed to read tree from ${url}. ` +
'Bitbucket API response for downloading archive does not contain content-disposition header ',
);
}
const fileNameRegEx = new RegExp(
/^attachment; filename=(?<fileName>.*).zip$/,
);
const archiveFileName = contentDispositionHeader.match(fileNameRegEx)
?.groups?.fileName;
if (!archiveFileName) {
throw new Error(
`Failed to read tree from ${url}. Bitbucket API response for downloading archive has an unexpected ` +
`format of content-disposition header ${contentDispositionHeader} `,
);
}
return await this.treeResponseFactory.fromZipArchive({
stream: (archiveBitbucketResponse.body as unknown) as Readable,
path: `${archiveFileName}/${filepath}`,
subpath: filepath,
etag: lastCommitShortHash,
filter: options?.filter,
});
@@ -161,13 +139,18 @@ export class BitbucketUrlReader implements UrlReader {
}
private async getLastCommitShortHash(url: string): Promise<string> {
const { name: repoName, owner: project, ref } = parseGitUrl(url);
const { resource, name: repoName, owner: project, ref } = parseGitUrl(url);
let branch = ref;
if (!branch) {
branch = await getBitbucketDefaultBranch(url, this.config);
}
const commitsApiUrl = `${this.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}`;
const isHosted = resource === 'bitbucket.org';
// Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp222
const commitsApiUrl = isHosted
? `${this.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}`
: `${this.config.apiBaseUrl}/projects/${project}/repos/${repoName}/commits`;
const commitsResponse = await fetch(
commitsApiUrl,
@@ -182,14 +165,26 @@ export class BitbucketUrlReader implements UrlReader {
}
const commits = await commitsResponse.json();
if (
commits &&
commits.values &&
commits.values.length > 0 &&
commits.values[0].hash
) {
return commits.values[0].hash.substring(0, 12);
if (isHosted) {
if (
commits &&
commits.values &&
commits.values.length > 0 &&
commits.values[0].hash
) {
return commits.values[0].hash.substring(0, 12);
}
} else {
if (
commits &&
commits.values &&
commits.values.length > 0 &&
commits.values[0].id
) {
return commits.values[0].id.substring(0, 12);
}
}
throw new Error(`Failed to read response from ${commitsApiUrl}`);
}
}
@@ -17,7 +17,8 @@
import { ConfigReader } from '@backstage/config';
import { GithubCredentialsProvider } from '@backstage/integration';
import { msw } from '@backstage/test-utils';
import fs from 'fs';
import fs from 'fs-extra';
import mockFs from 'mock-fs';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import path from 'path';
@@ -107,6 +108,16 @@ describe('GithubUrlReader', () => {
});
describe('readTree', () => {
beforeEach(() => {
mockFs({
'/tmp': mockFs.directory(),
});
});
afterEach(() => {
mockFs.restore();
});
const repoBuffer = fs.readFileSync(
path.resolve(
'src',
@@ -227,6 +238,21 @@ describe('GithubUrlReader', () => {
expect(indexMarkdownFile.toString()).toBe('# Test\n');
});
it('creates a directory with the wanted files', async () => {
const response = await githubProcessor.readTree(
'https://github.com/backstage/mock',
);
const dir = await response.dir({ targetDir: '/tmp' });
await expect(
fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'),
).resolves.toBe('site_name: Test\n');
await expect(
fs.readFile(path.join(dir, 'docs', 'index.md'), 'utf8'),
).resolves.toBe('# Test\n');
});
it('should use the headers from the credentials provider to the fetch request', async () => {
expect.assertions(2);
@@ -293,6 +319,18 @@ describe('GithubUrlReader', () => {
expect(indexMarkdownFile.toString()).toBe('# Test\n');
});
it('creates a directory with the wanted files with subpath', async () => {
const response = await githubProcessor.readTree(
'https://github.com/backstage/mock/tree/main/docs',
);
const dir = await response.dir({ targetDir: '/tmp' });
await expect(
fs.readFile(path.join(dir, 'index.md'), 'utf8'),
).resolves.toBe('# Test\n');
});
it('throws a NotModifiedError when given a etag in options', async () => {
const fnGithub = async () => {
await githubProcessor.readTree('https://github.com/backstage/mock', {
@@ -338,5 +376,20 @@ describe('GithubUrlReader', () => {
};
await expect(fnGithub).rejects.toThrow(NotFoundError);
});
it('should throw error when apiBaseUrl is missing', () => {
expect(() => {
/* eslint-disable no-new */
new GithubUrlReader(
{
host: 'ghe.mycompany.net',
},
{
treeResponseFactory,
credentialsProvider: mockCredentialsProvider,
},
);
}).toThrowError('must configure an explicit apiBaseUrl');
});
});
});
@@ -166,37 +166,11 @@ export class GithubUrlReader implements UrlReader {
throw new Error(message);
}
// Get the filename of archive from the header of the response
const contentDispositionHeader = archive.headers.get(
'content-disposition',
) as string;
if (!contentDispositionHeader) {
throw new Error(
`Failed to read tree from ${url}. ` +
'GitHub API response for downloading archive does not contain content-disposition header ',
);
}
const fileNameRegEx = new RegExp(
/^attachment; filename=(?<fileName>.*).tar.gz$/,
);
const archiveFileName = contentDispositionHeader.match(fileNameRegEx)
?.groups?.fileName;
if (!archiveFileName) {
throw new Error(
`Failed to read tree from ${url}. GitHub API response for downloading archive has an unexpected ` +
`format of content-disposition header ${contentDispositionHeader} `,
);
}
// The path includes the name of the directory inside the tarball and a sub path
// if requested in readTree.
const path = `${archiveFileName}/${filepath}`;
return await this.deps.treeResponseFactory.fromTarArchive({
// TODO(Rugvip): Underlying implementation of fetch will be node-fetch, we probably want
// to stick to using that in exclusively backend code.
stream: (archive.body as unknown) as Readable,
path,
subpath: filepath,
etag: commitSha,
filter: options?.filter,
});
@@ -16,7 +16,8 @@
import { ConfigReader } from '@backstage/config';
import { msw } from '@backstage/test-utils';
import fs from 'fs';
import fs from 'fs-extra';
import mockFs from 'mock-fs';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import path from 'path';
@@ -153,6 +154,16 @@ describe('GitlabUrlReader', () => {
});
describe('readTree', () => {
beforeEach(() => {
mockFs({
'/tmp': mockFs.directory(),
});
});
afterEach(() => {
mockFs.restore();
});
const archiveBuffer = fs.readFileSync(
path.resolve('src', 'reading', '__fixtures__', 'gitlab-archive.zip'),
);
@@ -254,6 +265,21 @@ describe('GitlabUrlReader', () => {
expect(indexMarkdownFile.toString()).toBe('# Test\n');
});
it('creates a directory with the wanted files', async () => {
const response = await gitlabProcessor.readTree(
'https://gitlab.com/backstage/mock',
);
const dir = await response.dir({ targetDir: '/tmp' });
await expect(
fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'),
).resolves.toBe('site_name: Test\n');
await expect(
fs.readFile(path.join(dir, 'docs', 'index.md'), 'utf8'),
).resolves.toBe('# Test\n');
});
it('returns the wanted files from hosted gitlab', async () => {
worker.use(
rest.get(
@@ -296,6 +322,18 @@ describe('GitlabUrlReader', () => {
expect(indexMarkdownFile.toString()).toBe('# Test\n');
});
it('creates a directory with the wanted files with subpath', async () => {
const response = await gitlabProcessor.readTree(
'https://gitlab.com/backstage/mock/tree/main/docs',
);
const dir = await response.dir({ targetDir: '/tmp' });
await expect(
fs.readFile(path.join(dir, 'index.md'), 'utf8'),
).resolves.toBe('# Test\n');
});
it('throws a NotModifiedError when given a etag in options', async () => {
const fnGitlab = async () => {
await gitlabProcessor.readTree('https://gitlab.com/backstage/mock', {
@@ -341,5 +379,17 @@ describe('GitlabUrlReader', () => {
};
await expect(fnGithub).rejects.toThrow(NotFoundError);
});
it('should throw error when apiBaseUrl is missing', () => {
expect(() => {
/* eslint-disable no-new */
new GitlabUrlReader(
{
host: 'gitlab.mycompany.com',
},
{ treeResponseFactory },
);
}).toThrowError('must configure an explicit apiBaseUrl');
});
});
});
@@ -51,6 +51,12 @@ export class GitlabUrlReader implements UrlReader {
deps: { treeResponseFactory: ReadTreeResponseFactory },
) {
this.treeResponseFactory = deps.treeResponseFactory;
if (!config.apiBaseUrl) {
throw new Error(
`GitLab integration for '${config.host}' must configure an explicit apiBaseUrl`,
);
}
}
async read(url: string): Promise<Buffer> {
@@ -140,33 +146,9 @@ export class GitlabUrlReader implements UrlReader {
throw new Error(message);
}
// Get the filename of archive from the header of the response
const contentDispositionHeader = archiveGitLabResponse.headers.get(
'content-disposition',
) as string;
if (!contentDispositionHeader) {
throw new Error(
`Failed to read tree from ${url}. ` +
'GitLab API response for downloading archive does not contain content-disposition header ',
);
}
const fileNameRegEx = new RegExp(
/^attachment; filename="(?<fileName>.*).zip"$/,
);
const archiveFileName = contentDispositionHeader.match(fileNameRegEx)
?.groups?.fileName;
if (!archiveFileName) {
throw new Error(
`Failed to read tree from ${url}. GitLab API response for downloading archive has an unexpected ` +
`format of content-disposition header ${contentDispositionHeader} `,
);
}
const path = filepath ? `${archiveFileName}/${filepath}/` : '';
return await this.treeResponseFactory.fromZipArchive({
stream: (archiveGitLabResponse.body as unknown) as Readable,
path,
subpath: filepath,
etag: commitSha,
filter: options?.filter,
});
@@ -0,0 +1,164 @@
/*
* 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 { ConfigReader } from '@backstage/config';
import { getVoidLogger } from '../logging';
import { UrlReaders } from './UrlReaders';
const reader = UrlReaders.default({
logger: getVoidLogger(),
config: new ConfigReader({
// The tokens in this config provide read only access to the backstage-verification repos
integrations: {
github: [
{
host: 'github.com',
token:
process.env.INTEGRATION_TEST_GITHUB_TOKEN ||
`${86}af${617}d9c3c8bf958b37a${630691452765}bb0b0a`,
},
],
gitlab: [
{
host: 'gitlab.com',
token:
process.env.INTEGRATION_TEST_GITLAB_TOKEN || 'tveGtSHDBJM9ZRHZNRfm',
},
],
bitbucket: [
{
host: 'bitbucket.org',
username: 'backstage-verification',
appPassword:
process.env.INTEGRATION_TEST_BITBUCKET_TOKEN ||
'H79MAAhtbZwCafkVTrrQ',
},
],
azure: [
{
host: 'dev.azure.com',
// lasts until 2022-01-28
token:
process.env.INTEGRATION_TEST_AZURE_TOKEN ||
`myvyavvfojh6wvw4ose4bfywqttqx${5}z${5}zs${5}bdxauqaek3yinkazq`,
},
],
},
}),
});
function withRetries(count: number, fn: () => Promise<void>) {
return async () => {
let error;
for (let i = 0; i < count; i++) {
try {
await fn();
return;
} catch (err) {
error = err;
}
}
throw error;
};
}
describe('UrlReaders', () => {
jest.setTimeout(30_000);
it(
'should read data from azure',
withRetries(3, async () => {
const data = await reader.read(
'https://dev.azure.com/backstage-verification/test-templates/_git/test-templates?path=%2Ftemplate.yaml',
);
expect(data.toString()).toContain('test-template-azure');
const res = await reader.readTree(
'https://dev.azure.com/backstage-verification/test-templates/_git/test-templates?path=%2F{{cookiecutter.name}}',
);
const files = await res.files();
expect(files).toEqual([
{
path: 'catalog-info.yaml',
content: expect.any(Function),
},
]);
}),
);
it(
'should read data from gitlab',
withRetries(3, async () => {
const data = await reader.read(
'https://gitlab.com/backstage-verification/test-templates/-/blob/master/template.yaml',
);
expect(data.toString()).toContain('test-template-gitlab');
const res = await reader.readTree(
'https://gitlab.com/backstage-verification/test-templates/-/tree/master/{{cookiecutter.name}}',
);
const files = await res.files();
expect(files).toEqual([
{
path: 'catalog-info.yaml',
content: expect.any(Function),
},
]);
}),
);
it(
'should read data from bitbucket',
withRetries(3, async () => {
const data = await reader.read(
'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml',
);
expect(data.toString()).toContain('test-template-bitbucket');
const res = await reader.readTree(
'https://bitbucket.org/backstage-verification/test-template/src/master/{{cookiecutter.name}}',
);
const files = await res.files();
expect(files).toEqual([
{
path: 'catalog-info.yaml',
content: expect.any(Function),
},
]);
}),
);
it(
'should read data from github',
withRetries(3, async () => {
const data = await reader.read(
'https://github.com/backstage-verification/test-templates/blob/master/template.yaml',
);
expect(data.toString()).toContain('test-template-github');
const res = await reader.readTree(
'https://github.com/backstage-verification/test-templates/tree/master/{{cookiecutter.name}}',
);
const files = await res.files();
expect(files).toEqual([
{
path: 'catalog-info.yaml',
content: expect.any(Function),
},
]);
}),
);
});
@@ -24,8 +24,9 @@ import { ZipArchiveResponse } from './ZipArchiveResponse';
type FromArchiveOptions = {
// A binary stream of a tar archive.
stream: Readable;
// If set, the root of the tree will be set to the given directory path.
path?: string;
// If unset, the files at the root of the tree will be read.
// subpath must not contain the name of the top level directory.
subpath?: string;
// etag of the blob
etag: string;
// Filter passed on from the ReadTreeOptions
@@ -45,7 +46,7 @@ export class ReadTreeResponseFactory {
async fromTarArchive(options: FromArchiveOptions): Promise<ReadTreeResponse> {
return new TarArchiveResponse(
options.stream,
options.path ?? '',
options.subpath ?? '',
this.workDir,
options.etag,
options.filter,
@@ -55,7 +56,7 @@ export class ReadTreeResponseFactory {
async fromZipArchive(options: FromArchiveOptions): Promise<ReadTreeResponse> {
return new ZipArchiveResponse(
options.stream,
options.path ?? '',
options.subpath ?? '',
this.workDir,
options.etag,
options.filter,
@@ -38,7 +38,7 @@ describe('TarArchiveResponse', () => {
it('should read files', async () => {
const stream = fs.createReadStream('/test-archive.tar.gz');
const res = new TarArchiveResponse(stream, 'mock-main/', '/tmp', 'etag');
const res = new TarArchiveResponse(stream, '', '/tmp', 'etag');
const files = await res.files();
expect(files).toEqual([
@@ -61,12 +61,8 @@ describe('TarArchiveResponse', () => {
it('should read files with filter', async () => {
const stream = fs.createReadStream('/test-archive.tar.gz');
const res = new TarArchiveResponse(
stream,
'mock-main/',
'/tmp',
'etag',
path => path.endsWith('.yml'),
const res = new TarArchiveResponse(stream, '', '/tmp', 'etag', path =>
path.endsWith('.yml'),
);
const files = await res.files();
@@ -83,7 +79,7 @@ describe('TarArchiveResponse', () => {
it('should read as archive and files', async () => {
const stream = fs.createReadStream('/test-archive.tar.gz');
const res = new TarArchiveResponse(stream, 'mock-main/', '/tmp', 'etag');
const res = new TarArchiveResponse(stream, '', '/tmp', 'etag');
const buffer = await res.archive();
await expect(res.archive()).rejects.toThrow(
@@ -115,24 +111,18 @@ describe('TarArchiveResponse', () => {
const res = new TarArchiveResponse(stream, '', '/tmp', 'etag');
const dir = await res.dir();
await expect(
fs.readFile(resolvePath(dir, 'mock-main/mkdocs.yml'), 'utf8'),
fs.readFile(resolvePath(dir, 'mkdocs.yml'), 'utf8'),
).resolves.toBe('site_name: Test\n');
await expect(
fs.readFile(resolvePath(dir, 'mock-main/docs/index.md'), 'utf8'),
fs.readFile(resolvePath(dir, 'docs/index.md'), 'utf8'),
).resolves.toBe('# Test\n');
});
it('should extract archive into directory with a subpath', async () => {
const stream = fs.createReadStream('/test-archive.tar.gz');
const res = new TarArchiveResponse(
stream,
'mock-main/docs/',
'/tmp',
'etag',
);
const res = new TarArchiveResponse(stream, 'docs', '/tmp', 'etag');
const dir = await res.dir();
expect(dir).toMatch(/^[\/\\]tmp[\/\\].*$/);
@@ -144,12 +134,8 @@ describe('TarArchiveResponse', () => {
it('should extract archive into directory with a subpath and filter', async () => {
const stream = fs.createReadStream('/test-archive.tar.gz');
const res = new TarArchiveResponse(
stream,
'mock-main/',
'/tmp',
'etag',
path => path.endsWith('.yml'),
const res = new TarArchiveResponse(stream, '', '/tmp', 'etag', path =>
path.endsWith('.yml'),
);
const dir = await res.dir({ targetDir: '/tmp' });
@@ -15,7 +15,7 @@
*/
import tar, { Parse, ParseStream, ReadEntry } from 'tar';
import path from 'path';
import platformPath from 'path';
import fs from 'fs-extra';
import { Readable, pipeline as pipelineCb } from 'stream';
import { promisify } from 'util';
@@ -30,6 +30,10 @@ import {
const TarParseStream = (Parse as unknown) as { new (): ParseStream };
const pipeline = promisify(pipelineCb);
// Matches a directory name + one `/` at the start of any string,
// containing any character except `/` one or more times, and ending with a `/`
// e.g. Will match `dirA/` in `dirA/dirB/file.ext`
const directoryNameRegex = /^[^\/]+\//;
/**
* Wraps a tar archive stream into a tree response reader.
@@ -78,14 +82,18 @@ export class TarArchiveResponse implements ReadTreeResponse {
return;
}
// File path relative to the root extracted directory. Will remove the
// top level dir name from the path since its name is hard to predetermine.
const relativePath = entry.path.replace(directoryNameRegex, '');
if (this.subPath) {
if (!entry.path.startsWith(this.subPath)) {
if (!relativePath.startsWith(this.subPath)) {
entry.resume();
return;
}
}
const path = entry.path.slice(this.subPath.length);
const path = relativePath.slice(this.subPath.length);
if (this.filter) {
if (!this.filter(path)) {
entry.resume();
@@ -97,7 +105,10 @@ export class TarArchiveResponse implements ReadTreeResponse {
await pipeline(entry, concatStream(resolve));
});
files.push({ path, content: () => content });
files.push({
path,
content: () => content,
});
entry.resume();
});
@@ -136,9 +147,11 @@ export class TarArchiveResponse implements ReadTreeResponse {
const dir =
options?.targetDir ??
(await fs.mkdtemp(path.join(this.workDir, 'backstage-')));
(await fs.mkdtemp(platformPath.join(this.workDir, 'backstage-')));
const strip = this.subPath ? this.subPath.split('/').length - 1 : 0;
// Equivalent of tar --strip-components=N
// When no subPath is given, remove just 1 top level directory
const strip = this.subPath ? this.subPath.split('/').length : 1;
await pipeline(
this.stream,
@@ -146,7 +159,10 @@ export class TarArchiveResponse implements ReadTreeResponse {
strip,
cwd: dir,
filter: path => {
if (this.subPath && !path.startsWith(this.subPath)) {
// File path relative to the root extracted directory. Will remove the
// top level dir name from the path since its name is hard to predetermine.
const relativePath = path.replace(directoryNameRegex, '');
if (this.subPath && !relativePath.startsWith(this.subPath)) {
return false;
}
if (this.filter) {
@@ -38,7 +38,7 @@ describe('ZipArchiveResponse', () => {
it('should read files', async () => {
const stream = fs.createReadStream('/test-archive.zip');
const res = new ZipArchiveResponse(stream, 'mock-main/', '/tmp', 'etag');
const res = new ZipArchiveResponse(stream, '', '/tmp', 'etag');
const files = await res.files();
expect(files).toEqual([
@@ -61,12 +61,8 @@ describe('ZipArchiveResponse', () => {
it('should read files with filter', async () => {
const stream = fs.createReadStream('/test-archive.zip');
const res = new ZipArchiveResponse(
stream,
'mock-main/',
'/tmp',
'etag',
path => path.endsWith('.yml'),
const res = new ZipArchiveResponse(stream, '', '/tmp', 'etag', path =>
path.endsWith('.yml'),
);
const files = await res.files();
@@ -83,7 +79,7 @@ describe('ZipArchiveResponse', () => {
it('should read as archive and files', async () => {
const stream = fs.createReadStream('/test-archive.zip');
const res = new ZipArchiveResponse(stream, 'mock-main/', '/tmp', 'etag');
const res = new ZipArchiveResponse(stream, '', '/tmp', 'etag');
const buffer = await res.archive();
await expect(res.archive()).rejects.toThrow(
@@ -117,22 +113,17 @@ describe('ZipArchiveResponse', () => {
const dir = await res.dir();
await expect(
fs.readFile(resolvePath(dir, 'mock-main/mkdocs.yml'), 'utf8'),
fs.readFile(resolvePath(dir, 'mkdocs.yml'), 'utf8'),
).resolves.toBe('site_name: Test\n');
await expect(
fs.readFile(resolvePath(dir, 'mock-main/docs/index.md'), 'utf8'),
fs.readFile(resolvePath(dir, 'docs/index.md'), 'utf8'),
).resolves.toBe('# Test\n');
});
it('should extract archive into directory with a subpath', async () => {
const stream = fs.createReadStream('/test-archive.zip');
const res = new ZipArchiveResponse(
stream,
'mock-main/docs/',
'/tmp',
'etag',
);
const res = new ZipArchiveResponse(stream, 'docs/', '/tmp', 'etag');
const dir = await res.dir();
expect(dir).toMatch(/^[\/\\]tmp[\/\\].*$/);
@@ -144,12 +135,8 @@ describe('ZipArchiveResponse', () => {
it('should extract archive into directory with a subpath and filter', async () => {
const stream = fs.createReadStream('/test-archive.zip');
const res = new ZipArchiveResponse(
stream,
'mock-main/',
'/tmp',
'etag',
path => path.endsWith('.yml'),
const res = new ZipArchiveResponse(stream, '', '/tmp', 'etag', path =>
path.endsWith('.yml'),
);
const dir = await res.dir({ targetDir: '/tmp' });
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import path from 'path';
import platformPath from 'path';
import fs from 'fs-extra';
import unzipper, { Entry } from 'unzipper';
import archiver from 'archiver';
@@ -25,6 +25,11 @@ import {
ReadTreeResponseDirOptions,
} from '../types';
// Matches a directory name + one `/` at the start of any string,
// containing any character except / one or more times, and ending with a `/`
// e.g. Will match `dirA/` in `dirA/dirB/file.ext`
const directoryNameRegex = /^[^\/]+\//;
/**
* Wraps a zip archive stream into a tree response reader.
*/
@@ -60,18 +65,26 @@ export class ZipArchiveResponse implements ReadTreeResponse {
this.read = true;
}
private getPath(entry: Entry): string {
return entry.path.slice(this.subPath.length);
// Will remove the top level dir name from the path since its name is hard to predetermine.
private stripTopDirectory(path: string): string {
return path.replace(directoryNameRegex, '');
}
// File path relative to the root extracted directory or a sub directory if subpath is set.
private getInnerPath(path: string): string {
return path.slice(this.subPath.length);
}
private shouldBeIncluded(entry: Entry): boolean {
const strippedPath = this.stripTopDirectory(entry.path);
if (this.subPath) {
if (!entry.path.startsWith(this.subPath)) {
if (!strippedPath.startsWith(this.subPath)) {
return false;
}
}
if (this.filter) {
return this.filter(this.getPath(entry));
return this.filter(this.getInnerPath(entry.path));
}
return true;
}
@@ -91,7 +104,7 @@ export class ZipArchiveResponse implements ReadTreeResponse {
if (this.shouldBeIncluded(entry)) {
files.push({
path: this.getPath(entry),
path: this.getInnerPath(this.stripTopDirectory(entry.path)),
content: () => entry.buffer(),
});
} else {
@@ -115,7 +128,7 @@ export class ZipArchiveResponse implements ReadTreeResponse {
.pipe(unzipper.Parse())
.on('entry', (entry: Entry) => {
if (entry.type === 'File' && this.shouldBeIncluded(entry)) {
archive.append(entry, { name: this.getPath(entry) });
archive.append(entry, { name: this.getInnerPath(entry.path) });
} else {
entry.autodrain();
}
@@ -131,7 +144,7 @@ export class ZipArchiveResponse implements ReadTreeResponse {
const dir =
options?.targetDir ??
(await fs.mkdtemp(path.join(this.workDir, 'backstage-')));
(await fs.mkdtemp(platformPath.join(this.workDir, 'backstage-')));
await this.stream
.pipe(unzipper.Parse())
@@ -139,12 +152,14 @@ export class ZipArchiveResponse implements ReadTreeResponse {
// Ignore directory entries since we handle that with the file entries
// as a zip can have files with directories without directory entries
if (entry.type === 'File' && this.shouldBeIncluded(entry)) {
const entryPath = this.getPath(entry);
const dirname = path.dirname(entryPath);
const entryPath = this.getInnerPath(
this.stripTopDirectory(entry.path),
);
const dirname = platformPath.dirname(entryPath);
if (dirname) {
await fs.mkdirp(path.join(dir, dirname));
await fs.mkdirp(platformPath.join(dir, dirname));
}
entry.pipe(fs.createWriteStream(path.join(dir, entryPath)));
entry.pipe(fs.createWriteStream(platformPath.join(dir, entryPath)));
} else {
entry.autodrain();
}
@@ -81,6 +81,9 @@ export type ReadTreeResponseDirOptions = {
};
export type ReadTreeResponse = {
/**
* files() returns an array of all the files inside the tree and corresponding functions to read their content.
*/
files(): Promise<ReadTreeResponseFile[]>;
archive(): Promise<NodeJS.ReadableStream>;
+10 -1
View File
@@ -86,13 +86,22 @@ export class Git {
return git.commit({ fs, dir, message, author, committer });
}
async clone({ url, dir }: { url: string; dir: string }): Promise<void> {
async clone({
url,
dir,
ref,
}: {
url: string;
dir: string;
ref?: string;
}): Promise<void> {
this.config.logger?.info(`Cloning repo {dir=${dir},url=${url}}`);
return git.clone({
fs,
http,
url,
dir,
ref,
singleBranch: true,
depth: 1,
onProgress: this.onProgressHandler(),
+48
View File
@@ -1,5 +1,53 @@
# example-backend
## 0.2.14
### Patch Changes
- Updated dependencies [c777df180]
- Updated dependencies [2430ee7c2]
- Updated dependencies [3149bfe63]
- Updated dependencies [6e612ce25]
- Updated dependencies [e44925723]
- Updated dependencies [9d6ef14bc]
- Updated dependencies [a26668913]
- Updated dependencies [025e122c3]
- Updated dependencies [e9aab60c7]
- Updated dependencies [24e47ef1e]
- Updated dependencies [7881f2117]
- Updated dependencies [529d16d27]
- Updated dependencies [cdea0baf1]
- Updated dependencies [11cb5ef94]
- @backstage/plugin-techdocs-backend@0.5.5
- @backstage/backend-common@0.5.2
- @backstage/plugin-catalog-backend@0.6.0
- @backstage/catalog-model@0.7.1
- example-app@0.2.14
- @backstage/plugin-scaffolder-backend@0.6.0
- @backstage/plugin-app-backend@0.3.6
## 0.2.13
### Patch Changes
- Updated dependencies [26a3a6cf0]
- Updated dependencies [681111228]
- Updated dependencies [664dd08c9]
- Updated dependencies [9dd057662]
- Updated dependencies [234e7d985]
- Updated dependencies [d7b1d317f]
- Updated dependencies [a91aa6bf2]
- Updated dependencies [39b05b9ae]
- Updated dependencies [4eaa06057]
- @backstage/backend-common@0.5.1
- @backstage/plugin-scaffolder-backend@0.5.2
- @backstage/plugin-kubernetes-backend@0.2.6
- @backstage/plugin-catalog-backend@0.5.5
- @backstage/plugin-kafka-backend@0.2.0
- @backstage/plugin-auth-backend@0.2.12
- example-app@0.2.13
- @backstage/plugin-app-backend@0.3.5
## 0.2.12
### Patch Changes
+12 -12
View File
@@ -1,6 +1,6 @@
{
"name": "example-backend",
"version": "0.2.12",
"version": "0.2.14",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -27,24 +27,24 @@
"migrate:create": "knex migrate:make -x ts"
},
"dependencies": {
"@backstage/backend-common": "^0.5.0",
"@backstage/catalog-model": "^0.7.0",
"@backstage/backend-common": "^0.5.2",
"@backstage/catalog-model": "^0.7.1",
"@backstage/config": "^0.1.2",
"@backstage/plugin-app-backend": "^0.3.4",
"@backstage/plugin-auth-backend": "^0.2.11",
"@backstage/plugin-catalog-backend": "^0.5.4",
"@backstage/plugin-app-backend": "^0.3.6",
"@backstage/plugin-auth-backend": "^0.2.12",
"@backstage/plugin-catalog-backend": "^0.6.0",
"@backstage/plugin-graphql-backend": "^0.1.5",
"@backstage/plugin-kubernetes-backend": "^0.2.5",
"@backstage/plugin-kafka-backend": "^0.1.1",
"@backstage/plugin-kubernetes-backend": "^0.2.6",
"@backstage/plugin-kafka-backend": "^0.2.0",
"@backstage/plugin-proxy-backend": "^0.2.4",
"@backstage/plugin-rollbar-backend": "^0.1.7",
"@backstage/plugin-scaffolder-backend": "^0.5.0",
"@backstage/plugin-techdocs-backend": "^0.5.4",
"@backstage/plugin-scaffolder-backend": "^0.6.0",
"@backstage/plugin-techdocs-backend": "^0.5.5",
"@gitbeaker/node": "^28.0.2",
"@octokit/rest": "^18.0.12",
"azure-devops-node-api": "^10.1.1",
"dockerode": "^3.2.1",
"example-app": "^0.2.12",
"example-app": "^0.2.14",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"knex": "^0.21.6",
@@ -54,7 +54,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.4.7",
"@backstage/cli": "^0.6.0",
"@types/dockerode": "^3.2.1",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5"
+2 -2
View File
@@ -29,12 +29,12 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.7.0",
"@backstage/catalog-model": "^0.7.1",
"@backstage/config": "^0.1.2",
"cross-fetch": "^3.0.6"
},
"devDependencies": {
"@backstage/cli": "^0.4.7",
"@backstage/cli": "^0.6.0",
"@types/jest": "^26.0.7",
"msw": "^0.21.2"
},
+9
View File
@@ -1,5 +1,14 @@
# @backstage/catalog-model
## 0.7.1
### Patch Changes
- 6e612ce25: Adds a new optional `links` metadata field to the Entity class within the `catalog-model` package (as discussed in [[RFC] Entity Links](https://github.com/backstage/backstage/issues/3787)). This PR adds support for the entity links only. Follow up PR's will introduce the UI component to display them.
- 025e122c3: Replace `yup` with `ajv`, for validation of catalog entities.
- 7881f2117: Introduce json schema variants of the `yup` validation schemas
- 11cb5ef94: Implement matchEntityWithRef for client side filtering of entities by ref matching
## 0.7.0
### Minor Changes
@@ -3,6 +3,11 @@ kind: Group
metadata:
name: acme-corp
description: The acme-corp organization
links:
- url: http://www.acme.com/
title: Website
- url: https://meta.wikimedia.org/wiki/
title: Intranet
spec:
type: organization
profile:
@@ -6,6 +6,13 @@ metadata:
tags:
- store
- rest
links:
- url: https://github.com/swagger-api/swagger-petstore
title: GitHub Repo
icon: github
- url: https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml
title: API Spec
icon: code
spec:
type: openapi
lifecycle: experimental
@@ -5,6 +5,10 @@ metadata:
description: The Smartylighting Streetlights API allows you to remotely manage the city lights.
tags:
- mqtt
links:
- url: https://github.com/asyncapi/asyncapi/blob/master/examples/1.2.0/streetlights.yml
title: Source Code
icon: code
spec:
type: asyncapi
lifecycle: production
@@ -3,6 +3,10 @@ kind: API
metadata:
name: starwars-graphql
description: SWAPI GraphQL Schema
links:
- url: https://github.com/graphql/swapi-graphql
title: GitHub Repo
icon: github
spec:
type: graphql
lifecycle: production
@@ -6,6 +6,12 @@ metadata:
tags:
- java
- data
links:
- url: https://example.com/apm/artists-lookup
title: APM
icon: dashboard
- url: https://example.com/logs/artists-lookup
title: Logs
spec:
type: service
lifecycle: experimental
@@ -3,6 +3,10 @@ kind: Component
metadata:
name: petstore
description: Petstore
links:
- url: https://github.com/swagger-api/swagger-petstore
title: GitHub Repo
icon: github
spec:
type: service
lifecycle: experimental
@@ -3,5 +3,11 @@ kind: Domain
metadata:
name: artists
description: Everything related to artists
links:
- url: http://example.com/domain/artists/
title: Domain Readme
- url: http://example.com/domains/artists/dashboard
title: Domain Metrics Dashboard
icon: dashboard
spec:
owner: team-a
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/catalog-model",
"version": "0.7.0",
"version": "0.7.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -32,13 +32,14 @@
"@backstage/config": "^0.1.2",
"@types/json-schema": "^7.0.5",
"@types/yup": "^0.29.8",
"ajv": "^7.0.3",
"json-schema": "^0.2.5",
"lodash": "^4.17.15",
"uuid": "^8.0.0",
"yup": "^0.29.3"
},
"devDependencies": {
"@backstage/cli": "^0.4.7",
"@backstage/cli": "^0.6.0",
"@types/express": "^4.17.6",
"@types/jest": "^26.0.7",
"@types/lodash": "^4.14.151",
@@ -1,4 +1,3 @@
import { Entity } from './entity';
/*
* Copyright 2020 Spotify AB
*
@@ -15,8 +14,8 @@ import { Entity } from './entity';
* limitations under the License.
*/
import { Entity, EntityPolicy } from './entity';
import { EntityPolicies } from './EntityPolicies';
import { EntityPolicy } from './types';
describe('EntityPolicies', () => {
const p1: jest.Mocked<EntityPolicy> = { enforce: jest.fn() };
+1 -2
View File
@@ -14,8 +14,7 @@
* limitations under the License.
*/
import { Entity } from './entity';
import { EntityPolicy } from './types';
import { Entity, EntityPolicy } from './entity';
// Helper that requires that all of a set of policies can be successfully
// applied
@@ -125,6 +125,11 @@ export type EntityMeta = JsonObject & {
* various ways.
*/
tags?: string[];
/**
* A list of external hyperlinks related to the entity.
*/
links?: EntityLink[];
};
/**
@@ -161,3 +166,23 @@ export type EntityRelationSpec = {
*/
target: EntityName;
};
/**
* A link to external information that is related to the entity.
*/
export type EntityLink = {
/**
* The url to the external site, document, etc.
*/
url: string;
/**
* An optional descriptive title for the link.
*/
title?: string;
/**
* An optional semantic key that represents a visual icon.
*/
icon?: string;
};
@@ -20,12 +20,14 @@ export {
} from './constants';
export type {
Entity,
EntityLink,
EntityMeta,
EntityRelation,
EntityRelationSpec,
} from './Entity';
export * from './policies';
export {
compareEntityToRef,
getEntityName,
parseEntityName,
parseEntityRef,
@@ -15,7 +15,7 @@
*/
import lodash from 'lodash';
import { EntityPolicy } from '../../types';
import { EntityPolicy } from './types';
import { ENTITY_DEFAULT_NAMESPACE } from '../constants';
import { Entity } from '../Entity';
@@ -38,6 +38,10 @@ describe('FieldFormatEntityPolicy', () => {
tags:
- java
- data-service
links:
- url: https://example.org
title: Website
icon: website
spec:
custom: stuff
`);
@@ -110,4 +114,110 @@ describe('FieldFormatEntityPolicy', () => {
data.metadata.tags.push('Hello World');
await expect(policy.enforce(data)).rejects.toThrow(/tags.*"Hello World"/i);
});
it('accepts missing links', async () => {
delete data.metadata.links;
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('accepts empty links array', async () => {
data.metadata.links = [];
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('accepts multiple links', async () => {
data.metadata.links = [{ url: 'http://foo' }, { url: 'https://bar' }];
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects missing link url value', async () => {
data.metadata.links = [{}];
await expect(policy.enforce(data)).rejects.toThrow(/links.0.url/i);
});
it('rejects a single bad missing link url value', async () => {
data.metadata.links = [{ url: 'http://good' }, { url: '' }];
await expect(policy.enforce(data)).rejects.toThrow(
/links.1.url.*valid url/i,
);
});
it('rejects empty link url value', async () => {
data.metadata.links = [{ url: '' }];
await expect(policy.enforce(data)).rejects.toThrow(/links.0.url.*/i);
});
it('rejects bad link url value', async () => {
data.metadata.links = [{ url: 'invalid' }];
await expect(policy.enforce(data)).rejects.toThrow(
/links.0.url.*"invalid"/i,
);
});
it('accepts missing link title', async () => {
data.metadata.links = [{ url: 'http://foo', icon: 'dashboard' }];
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects empty link title', async () => {
data.metadata.links = [{ url: 'http://foo', title: '' }];
await expect(policy.enforce(data)).rejects.toThrow(/links.0.title.*""/i);
});
it('rejects bad link title', async () => {
data.metadata.links = [{ url: 'http://foo', title: 123 }];
await expect(policy.enforce(data)).rejects.toThrow(/links.0.title.*"123"/i);
});
it.each([[123], [{}], [[]]])(
'rejects bad link title %s',
async (title: unknown) => {
data.metadata.links = [{ url: 'http://foo', title }];
await expect(policy.enforce(data)).rejects.toThrow(/links.0.title.*/i);
},
);
it('rejects a single bad link title', async () => {
data.metadata.links = [
{ url: 'http://foo', title: 'good' },
{ url: 'http://foo', title: '' },
];
await expect(policy.enforce(data)).rejects.toThrow(/links.1.title.*""/i);
});
it('accepts missing link icon', async () => {
data.metadata.links = [{ url: 'http://foo', title: 'foo' }];
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects empty link icon', async () => {
data.metadata.links = [{ url: 'http://foo', icon: '' }];
await expect(policy.enforce(data)).rejects.toThrow(/links.0.icon.*""/i);
});
it.each([['dashboard'], ['admin-dashboard'], ['foo_dashboard']])(
'accepts valid link icon',
async icon => {
data.metadata.links = [{ url: 'http://foo', icon }];
await expect(policy.enforce(data)).resolves.toBe(data);
},
);
it.each([[123], [{}], [[]], ['abc xyz']])(
'rejects bad link icon value %s',
async (icon: unknown) => {
data.metadata.links = [{ url: 'http://foo', icon }];
await expect(policy.enforce(data)).rejects.toThrow(/links.0.icon.*/i);
},
);
it('rejects a single bad link icon value', async () => {
data.metadata.links = [
{ url: 'http://foo', icon: 'good' },
{ url: 'http://foo', icon: 'not good' },
];
await expect(policy.enforce(data)).rejects.toThrow(
/links.1.icon.*"not good"/i,
);
});
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { EntityPolicy } from '../../types';
import { EntityPolicy } from './types';
import {
CommonValidatorFunctions,
KubernetesValidatorFunctions,
@@ -83,6 +83,12 @@ export class FieldFormatEntityPolicy implements EntityPolicy {
expectation =
'a string that is a sequence of [a-zA-Z][a-z0-9A-Z], at most 63 characters in total';
break;
case 'isValidUrl':
expectation = 'a string that is a valid url';
break;
case 'isValidString':
expectation = 'a non empty string';
break;
default:
expectation = undefined;
break;
@@ -134,6 +140,23 @@ export class FieldFormatEntityPolicy implements EntityPolicy {
require(`tags.${i}`, tags[i], this.validators.isValidTag);
}
const links = entity.metadata.links ?? [];
for (let i = 0; i < links.length; ++i) {
require(`links.${i}.url`, links[i]
?.url, CommonValidatorFunctions.isValidUrl);
optional(
`links.${i}.title`,
links[i]?.title,
CommonValidatorFunctions.isValidString,
);
optional(
`links.${i}.icon`,
links[i]?.icon,
KubernetesValidatorFunctions.isValidObjectName,
);
}
return entity;
}
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { EntityPolicy } from '../../types';
import { EntityPolicy } from './types';
import { Entity } from '../Entity';
const defaultKnownFields = ['apiVersion', 'kind', 'metadata', 'spec'];

Some files were not shown because too many files have changed in this diff Show More