Merge remote-tracking branch 'upstream/master' into org-repo
This commit is contained in:
@@ -1,6 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes': patch
|
||||
'@backstage/plugin-kubernetes-backend': patch
|
||||
---
|
||||
|
||||
Add AWS auth provider for Kubernetes
|
||||
@@ -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.
|
||||
@@ -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`
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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}
|
||||
```
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes': patch
|
||||
---
|
||||
|
||||
Improve error reporting for plugin misconfiguration.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Use .text instead of .json for ALB key response
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/config-loader': minor
|
||||
---
|
||||
|
||||
Removed support for the deprecated `$data` placeholder.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/techdocs-common': patch
|
||||
---
|
||||
|
||||
Add rate limiter for concurrent execution of file uploads in AWS and Google publishers
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,6 +3,7 @@ name: E2E Test Linux
|
||||
on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '.changeset/**'
|
||||
- 'contrib/**'
|
||||
- 'docs/**'
|
||||
- 'microsite/**'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}}"
|
||||
});
|
||||
@@ -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
@@ -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.
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -39,13 +39,12 @@ internally.
|
||||

|
||||
|
||||
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`.
|
||||
|
||||

|
||||

|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.)_
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
@@ -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 />} />
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
```
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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/*"
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() };
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user