Merge branch 'master' of https://github.com/backstage/backstage into marley/7678-pull-request-dashboard

Signed-off-by: Marley Powell <Marley.Powell@exclaimer.com>
This commit is contained in:
Marley Powell
2021-11-18 11:25:35 +00:00
435 changed files with 12940 additions and 1307 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/dev-utils': patch
---
Add theme switcher to sidebar of dev app.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-catalog': patch
---
Remove the "View Api" icon in the AboutCard, as the information is misleading for some users and is
duplicated in the tabs above.
+14
View File
@@ -0,0 +1,14 @@
---
'@backstage/config-loader': minor
---
Update `loadConfig` to return `LoadConfigResult` instead of an array of `AppConfig`.
This function is primarily used internally by other config loaders like `loadBackendConfig` which means no changes are required for most users.
If you use `loadConfig` directly you will need to update your usage from:
```diff
- const appConfigs = await loadConfig(options)
+ const { appConfigs } = await loadConfig(options)
```
@@ -0,0 +1,5 @@
---
'@backstage/plugin-cost-insights': patch
---
Fixed generation of sample data in the example Cost Insights client
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/cli': patch
'@backstage/cli-common': patch
---
Keep backstage.json in sync
The `versions:bump` script now takes care about updating the `version` property inside `backstage.json` file. The file is created if is not present.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/catalog-client': patch
---
Update to the right version of @backstage/errors
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/catalog-model': patch
'@backstage/cli': patch
'@backstage/config-loader': patch
'@backstage/plugin-scaffolder': patch
---
Update the json-schema dependency version.
+42
View File
@@ -0,0 +1,42 @@
---
'@backstage/plugin-allure': patch
'@backstage/plugin-api-docs': patch
'@backstage/plugin-azure-devops': patch
'@backstage/plugin-badges': patch
'@backstage/plugin-bazaar': patch
'@backstage/plugin-catalog': patch
'@backstage/plugin-catalog-graph': patch
'@backstage/plugin-catalog-import': patch
'@backstage/plugin-catalog-react': patch
'@backstage/plugin-circleci': patch
'@backstage/plugin-cloudbuild': patch
'@backstage/plugin-code-coverage': patch
'@backstage/plugin-config-schema': patch
'@backstage/plugin-cost-insights': patch
'@backstage/plugin-explore': patch
'@backstage/plugin-firehydrant': patch
'@backstage/plugin-gcp-projects': patch
'@backstage/plugin-git-release-manager': patch
'@backstage/plugin-github-actions': patch
'@backstage/plugin-gitops-profiles': patch
'@backstage/plugin-home': patch
'@backstage/plugin-ilert': patch
'@backstage/plugin-jenkins': patch
'@backstage/plugin-kafka': patch
'@backstage/plugin-kubernetes': patch
'@backstage/plugin-lighthouse': patch
'@backstage/plugin-newrelic': patch
'@backstage/plugin-pagerduty': patch
'@backstage/plugin-rollbar': patch
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-search': patch
'@backstage/plugin-sentry': patch
'@backstage/plugin-splunk-on-call': patch
'@backstage/plugin-tech-radar': patch
'@backstage/plugin-techdocs': patch
'@backstage/plugin-todo': patch
'@backstage/plugin-user-settings': patch
'@backstage/plugin-xcmetrics': patch
---
Refactor out the deprecated path and icon from RouteRefs
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
remove double config dep
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kafka-backend': patch
---
Update Kafka configuration types
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
Added the `isDatabaseConflictError` function.
+11
View File
@@ -0,0 +1,11 @@
---
'@backstage/create-app': patch
---
Create backstage.json file
`@backstage/create-app` will create a new `backstage.json` file. At this point, the file will contain a `version` property, representing the version of `@backstage/create-app` used for creating the application. If the backstage's application has been bootstrapped using an older version of `@backstage/create-app`, the `backstage.json` file can be created and kept in sync, together with all the changes of the latest version of backstage, by running the following script:
```bash
yarn backstage-cli versions:bump
```
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/backend-common': patch
'@backstage/cli': patch
---
Update internal usage of `configLoader.loadConfig` that now returns an object instead of an array of configs.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/core-components': patch
'@backstage/plugin-user-settings': patch
---
Add Props Icon for Sidebar Item SidebarSearchField and Settings
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-bazaar': patch
'@backstage/plugin-bazaar-backend': patch
---
A Bazaar project has been extended with the following fields: size, start date (optional), end date (optional) and a responsible person.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': patch
---
Changing the `Header` styles to use more theme variables. With this the title `font-size` will not change on resizing the window.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-plugin-api': patch
---
Add deprecation warnings around `title` `icon` and `path` as they are no longer controlled when creating `routeRefs`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/create-app': patch
---
Refactor and add regression tests for create-app tasks
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/plugin-catalog-backend': patch
---
This fixes a bug where locations couldn't be added unless the processing engine is started.
It's now possible to run the catalog backend without starting the processing engine and still allowing locations registrations.
This is done by refactor the `EntityProvider.connect` to happen outside the engine.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
Paths can be specified in backend.reading.allow to further restrict allowed targets
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-azure-devops': patch
---
Added entity view for Azure Repo Pull Requests
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/core-app-api': patch
'@backstage/test-utils': patch
---
Migrated to using new `ErrorApiError` and `ErrorApiErrorContext` names.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/core-components': patch
'@backstage/plugin-auth-backend': patch
---
Update OAuthAdapter to create identity.token from identity.idToken if it does not exist, and prevent overwrites to identity.toke. Update login page commonProvider to prefer .token over .idToken
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Skip empty file names when scaffolding with nunjucks
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Adjust entity query construction to ensure sub-queries are always isolated from one another.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
Adding config prop `pluginDivisionMode` to allow plugins using the `pg` client to create their own management schemas in the db. This allows `pg` client plugins to work in separate schemas in the same db.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
Add `AbortSignal` support to `UrlReader`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-app-api': patch
---
Deprecated the `BackstagePluginWithAnyOutput` type.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-app-api': patch
---
Start exporting and marking several types as public to address errors in the API report.
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search': patch
---
Smaller UX improvements to search components such as optional autoFocus prop to SearchBar components, decreased debounce value and closing modal on link click of SearchModal, terminology updates of SearchResultPager.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Added a scaffolder backend module template for the `create` command.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-plugin-api': minor
---
Remove exports of unused types(`RouteOptions` and `RoutePath`).
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Fixed a bug where calling `backstage-cli backend:bundle --build-dependencies` with no dependencies to be built would cause all monorepo packages to be built instead.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
Do not redact empty or one-character strings. These imply that it's just a test or local dev, and unnecessarily ruin the log output.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-shortcuts': patch
---
Add tooltip to display shortcut title on hover. This should improve the readability of shortcuts with long names, which are cutoff by ellipsis.
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-azure-devops': patch
'@backstage/plugin-azure-devops-backend': patch
'@backstage/plugin-azure-devops-common': minor
---
Improved Date handling for the Azure DevOps set of plugins by using strings and letting the frontend handle the conversion to DateTime
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs': patch
---
Fixed entity triplet case handling for certain locales.
@@ -0,0 +1,5 @@
---
'@backstage/techdocs-common': patch
---
Added OpenStack Swift case migration support.
@@ -0,0 +1,5 @@
---
'@techdocs/cli': patch
---
Add support for specifying bucketRootPath for AWS and GCS publishers
@@ -0,0 +1,6 @@
---
'@backstage/techdocs-common': patch
'@backstage/plugin-techdocs-backend': patch
---
Allow amazon web services s3 buckets to pass an server side encryption configuration so they can publish to encrypted buckets
+5
View File
@@ -0,0 +1,5 @@
---
'@techdocs/cli': patch
---
Adds ability to use encrypted S3 buckets by utilizing the SSE option in the AWS SDK
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs': patch
---
Use a better checkbox rendering in a task list.
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs': patch
---
Updates reader component used to display techdocs documentation. A previous change made this component not usable out of a page which don't have entityRef in url parameters. Reader component EntityRef parameter is now used instead of url parameters. Techdocs documentation component can now be used in our custom pages.
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/plugin-tech-insights-backend': patch
---
Add catalog fact retrievers
Add fact retrievers which generate facts related to the completeness
of entity data in the catalog.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
bump `@rollup/plugin-commonjs` from 17.1.0 to 21.0.1
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Introduces new `backstage-cli create` command to replace `create-plugin` and make space for creating a wider array of things. The create command also adds a new template for creating isomorphic common plugin packages.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
adds getDefaultProcessor method to CatalogBuilder
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-plugin-api': patch
---
Deprecated the `Error` and `ErrorContext` types, replacing them with identical `ErrorApiError` and `ErrorApiErrorContext` types.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Expose template metadata to custom action handler in Scaffolder.
+2
View File
@@ -18,7 +18,9 @@
/plugins/techdocs-backend @backstage/techdocs-core
/plugins/ilert @backstage/reviewers @yacut
/plugins/home @backstage/techdocs-core
/packages/embedded-techdocs-app @backstage/techdocs-core
/packages/search-common @backstage/techdocs-core
/packages/techdocs-cli @backstage/techdocs-core
/packages/techdocs-common @backstage/techdocs-core
/.changeset/cost-insights-* @backstage/reviewers @backstage/silver-lining
/.changeset/search-* @backstage/techdocs-core
+4
View File
@@ -52,6 +52,7 @@ configmaps
configs
const
cookiecutter
cron
css
Datadog
dataflow
@@ -62,6 +63,7 @@ Debounce
declaratively
deduplicated
deps
dependabot
destructured
dev
devops
@@ -162,6 +164,8 @@ Monorepo
monorepos
msgraph
msw
mutex
mutexes
mysql
namespace
namespaced
+42
View File
@@ -0,0 +1,42 @@
name: Techdocs E2E Test
on:
pull_request:
paths-ignore:
- '.changeset/**'
- 'contrib/**'
- 'docs/**'
- 'microsite/**'
jobs:
verify:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x]
env:
CI: true
NODE_OPTIONS: --max-old-space-size=4096
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- name: install dependencies
run: yarn install --frozen-lockfile
- name: generate types
run: yarn tsc
- name: build techdocs-cli
working-directory: packages/techdocs-cli
run: yarn build
- name: Install mkdocs & techdocs-core
run: python -m pip install mkdocs-techdocs-core
- name: techdocs-cli e2e test
working-directory: packages/techdocs-cli
run: yarn test:e2e:ci
+5 -1
View File
@@ -64,4 +64,8 @@
| [SoundCloud](https://www.soundcloud.com) | [Julio Zynger](https://github.com/julioz) | Developer portal as a [humane registry](https://martinfowler.com/bliki/HumaneRegistry.html) for the organization: catalog of people, services, documentation, feature toggles, escalation policies, etc. |
| [Volvofinans Bank](https://www.volvofinans.se) | [Johan Hammar](https://github.com/johanhammar) | Developer portal enabling engineers to manage and explore software and documentation. |
| [Palo Alto Networks](https://www.paloaltonetworks.com) | [Jeremy Guarini](https://github.com/jeremyguarini), [Brian Lomeland](https://github.com/bbbmmmlll), [Palo Alto Networks](https://github.com/PaloAltoNetworks) | Developer portal, service catalog, documentation and tooling |
| [Signal Iduna Group](https://www.signal-iduna.de/) | [Jonas Thomsen](https://github.com/JoThomsen) | Developer Portal, documentation, monitoring, service catalog for our insurance ecosystem
| [Signal Iduna Group](https://www.signal-iduna.de/) | [Jonas Thomsen](https://github.com/JoThomsen) | Developer Portal, documentation, monitoring, service catalog for our insurance ecosystem |
| [Tradeshift](https://www.tradeshift.com/) | [Soren Mathiasen](https://github.com/sorenmat) | Developer Portal: documentation, monitoring, service templates, service catalog for our micro services |
| [Unity](https://unity.com) | [Ted Cordery](https://github.com/TeddyBallGame) | A centralized service catalog with documentation for our service engineers. |
| [PicPay](https://www.picpay.com) | [Luis Baroni](https://github.com/lcsbaroni), [Renata Poluceno](https://github.com/renatapoluceno), [PicPay](https://github.com/picpay) | Developer portal for building services throught templates, service catalog with ownership of services, documentation and metrics providing autonomy and visibility for all. |
| [Epic Games](https://www.epicgames.com) | [Brian Jung](https://github.com/brian-at-epic), [Jeff Goldian](https://github.com/jeffgoldian-Epic) | Developer Portal: Service Catalog, Documentation, Software Templates and more making our internal teams' lives easier! |
+1 -1
View File
@@ -117,7 +117,7 @@ through the proxy.
Learn more about [the different components](overview/what-is-backstage.md) that
make up Backstage.
### Why can't I dynamically install plugins without modifications the app?
### Why can't I dynamically install plugins without modifications to the app?
This decision is part of the core architecture and development flow of
Backstage. Plugins have a lot of freedom in what they provide and how they are
+1 -1
View File
@@ -199,7 +199,7 @@ export a class that `implements` the target API, for example:
```ts
export class IgnoringErrorApi implements ErrorApi {
post(error: Error, context?: ErrorContext) {
post(error: ErrorApiError, context?: ErrorApiErrorContext) {
// ignore error
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

@@ -144,7 +144,8 @@ spec:
Note that to be able to read from targets that are outside of the normal
integration points such as `github.com`, you'll need to explicitly allow it by
adding an entry in the `backend.reading.allow` list. For example:
adding an entry in the `backend.reading.allow` list. Paths can be specified to
further restrict targets For example:
```yml
backend:
@@ -153,6 +154,8 @@ backend:
allow:
- host: example.com
- host: '*.examples.org'
- host: example.net
paths: ['/api/']
```
## Common to All Kinds: The Envelope
@@ -8,6 +8,10 @@ If you're wanting to extend the functionality of the Scaffolder, you can do so
by writing custom actions which can be used along side our
[built-in actions](./builtin-actions.md).
> Note: When adding custom actions, the actions array will **replace the
> built-in actions too**. To ensure you can continue to include he builtin
> actions, see below to include them during registration of your action.
### Writing your Custom Action
Your custom action can live where you choose, but simplest is to include it
@@ -91,6 +95,8 @@ argument. It looks like the following:
- `createTemporaryDirectory` a function to call to give you a temporary
directory somewhere on the runner so you can store some files there rather
than polluting the `workspacePath`
- `ctx.metadata` - an object containing a `name` field, indicating the template
name. More metadata fields may be added later.
### Registering Custom Actions
@@ -113,22 +119,6 @@ return await createRouter({
There's another property you can pass here, which is an array of `actions` which
will set the available actions that the scaffolder has access to.
```ts
const actions = [createNewFileAction()];
return await createRouter({
containerRunner,
logger,
config,
database,
catalogClient,
reader,
actions,
});
```
**NOTE** - the actions array will replace the built-in actions too, so if you
want to have those as well as your new one, you'll need to do the following:
```ts
import { createBuiltinActions } from '@backstage/plugin-scaffolder-backend';
import { ScmIntegrations } from '@backstage/integration';
@@ -144,7 +134,6 @@ const builtInActions = createBuiltinActions({
});
const actions = [...builtInActions, createNewFileAction()];
return await createRouter({
containerRunner,
logger,
@@ -284,8 +284,8 @@ to publish to. And it can be any host that is listed in your `integrations`
config in `app-config.yaml`.
The `RepoUrlPicker` is a custom field that we provide part of the
`plugin-scaffolder`. It's currently not possible to create your own fields yet,
but contributions are welcome! :)
`plugin-scaffolder`. You can provide your own custom fields by
[writing your own Custom Field Extensions](./writing-custom-field-extensions.md)
#### The Owner Picker
+241
View File
@@ -0,0 +1,241 @@
# TechDocs CLI
Utility command line interface for managing TechDocs sites in
[Backstage](https://github.com/backstage/backstage).
https://backstage.io/docs/features/techdocs/techdocs-overview
## Features
- Supports local development/preview of a TechDocs site in a Backstage app.
- Supports generation and publishing of a documentation site in a CI/CD
workflow.
```bash
techdocs-cli --help
Usage: techdocs-cli [options] [command]
Options:
-V, --version output the version number
-h, --help display help for command
Commands:
generate|build [options] Generate TechDocs documentation site using mkdocs.
publish [options] Publish generated TechDocs site to an external storage AWS S3,
Google GCS, etc.
serve:mkdocs [options] Serve a documentation project locally using mkdocs serve.
serve [options] Serve a documentation project locally in a Backstage app-like
environment
help [command] display help for command
```
## Installation
You can always use [`npx`](https://github.com/npm/npx) to run the latest version
of `techdocs-cli` -
```bash
npx @techdocs/cli [command]
```
Or you can install it using [npm](https://www.npmjs.com/package/@techdocs/cli) -
```bash
npm install -g @techdocs/cli
techdocs-cli [command]
```
## Usage
### Preview TechDocs site locally in a Backstage like environment
```bash
techdocs-cli serve
```
![A preview of techdocs-cli serve command](../../assets/features/techdocs/techdocs-cli-serve-preview.png)
By default, Docker and
[techdocs-container](https://github.com/backstage/techdocs-container) is used to
make sure all the dependencies are installed. However, Docker can be disabled
with `--no-docker` flag.
The command starts two local servers - an MkDocs preview server on port 8000 and
a Backstage app server on port 3000. The Backstage app has a custom TechDocs API
implementation, which uses the MkDocs preview server as a proxy to fetch the
generated documentation files and assets.
NOTE: When using a custom `techdocs` docker image, make sure the entry point is
also `ENTRYPOINT ["mkdocs"]`.
Command reference:
```bash
Usage: techdocs-cli serve [options]
Serve a documentation project locally in a Backstage app-like environment
Options:
-i, --docker-image <DOCKER_IMAGE> The mkdocs docker container to use (default: "spotify/techdocs")
--no-docker Do not use Docker, use MkDocs executable in current user environment.
--mkdocs-port <PORT> Port for MkDocs server to use (default: "8000")
-v --verbose Enable verbose output. (default: false)
-h, --help display help for command
```
### Generate TechDocs site from a documentation project
```bash
techdocs-cli generate
```
Alias: `techdocs-cli build`
The generate command uses the
[`@backstage/techdocs-common`](https://github.com/backstage/backstage/tree/master/packages/techdocs-common)
package from Backstage for consistency. A Backstage app can also generate and
publish TechDocs sites if `techdocs.builder` is set to `'local'` in
`app-config.yaml`. See
[configuration reference](https://backstage.io/docs/features/techdocs/configuration).
By default, this command uses Docker and
[techdocs-container](https://github.com/backstage/techdocs-container) to make
sure all the dependencies are installed. But it can be disabled using
`--no-docker` flag.
Command reference:
```bash
techdocs-cli generate --help
Usage: techdocs-cli generate|build [options]
Generate TechDocs documentation site using MkDocs.
Options:
--source-dir <PATH> Source directory containing mkdocs.yml and docs/ directory. (default: ".")
--output-dir <PATH> Output directory containing generated TechDocs site. (default: "./site/")
--docker-image <DOCKER_IMAGE> The mkdocs docker container to use (default: "spotify/techdocs:v0.3.4")
--no-pull Do not pull the latest docker image
--no-docker Do not use Docker, use MkDocs executable and plugins in current user environment.
--techdocs-ref <HOST_TYPE:URL> The repository hosting documentation source files e.g.
github:https://ghe.mycompany.net.com/org/repo.
This value is same as the backstage.io/techdocs-ref annotation of the corresponding
Backstage entity.
It is completely fine to skip this as it is only being used to set repo_url in mkdocs.yml
if not found.
--etag <ETAG> A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored
in techdocs_metadata.json.
-v --verbose Enable verbose output. (default: false)
-h, --help display help for command
```
### Publish generated TechDocs sites
```bash
techdocs-cli publish --publisher-type <awsS3|googleGcs|azureBlobStorage> --storage-name <bucket/container name> --entity <namespace/kind/name>
```
After generating a TechDocs site using `techdocs-cli generate`, use the publish
command to upload the static generated files on a cloud storage (AWS/GCS) bucket
or (Azure) container which your Backstage app can read from.
The value for `--entity` must be the Backstage entity which the generated
TechDocs site belongs to. You can find the values in your Entity's
`catalog-info.yaml` file. If namespace is missing in the `catalog-info.yaml`,
use `default`. The directory structure used in the storage bucket is
`namespace/kind/name/<files>`.
Note that the values are case-sensitive. An example for `--entity` is
`default/Component/<entityName>`.
Command reference:
```bash
Usage: techdocs-cli publish [options]
Publish generated TechDocs site to an external storage AWS S3, Google GCS, etc.
Options:
--publisher-type <TYPE> (Required always) awsS3 | googleGcs | azureBlobStorage
- same as techdocs.publisher.type in Backstage
app-config.yaml
--storage-name <BUCKET/CONTAINER NAME> (Required always) In case of AWS/GCS, use the bucket
name. In case of Azure, use container name. Same as
techdocs.publisher.[TYPE].bucketName
--entity <NAMESPACE/KIND/NAME> (Required always) Entity uid separated by / in
namespace/kind/name order (case-sensitive). Example:
default/Component/myEntity
--legacyUseCaseSensitiveTripletPaths Publishes objects with cased entity triplet prefix when set (e.g. namespace/Kind/name).
Only use if your TechDocs backend is configured the same way
--azureAccountName <AZURE ACCOUNT NAME> (Required for Azure) specify when --publisher-type
azureBlobStorage
--azureAccountKey <AZURE ACCOUNT KEY> Azure Storage Account key to use for authentication.
If not specified, you must set AZURE_TENANT_ID,
AZURE_CLIENT_ID & AZURE_CLIENT_SECRET as environment
variables.
--awsRoleArn <AWS ROLE ARN> Optional AWS ARN of role to be assumed.
--awsEndpoint <AWS ENDPOINT> Optional AWS endpoint to send requests to.
--awsS3ForcePathStyle Optional AWS S3 option to force path style.
--directory <PATH> Path of the directory containing generated files to
publish (default: "./site/")
-h, --help display help for command
```
### Migrate content for case-insensitive access
Prior to the beta version of TechDocs (`v[0.11.0]`), TechDocs were stored in
object storage using a case-sensitive entity triplet (e.g.
`default/API/name/index.html`). This resulted in a limitation where that exact
case was required in the Backstage URL in order to read/render TechDocs content.
As of `v[0.11.0]` of the TechDocs plugin, any case is allowed in the URL (e.g.
`default/api/name`), matching the behavior of the Catalog plugin.
Backstage instances created with TechDocs `v[0.11.0]` or later do not need this
command. However, when upgrading to this version from an older version of
TechDocs, the `migrate` command can be used prior to deployment to ensure docs
remain accessible without having to rebuild all docs.
Prior to upgrading to `v[0.11.0]` or greater, run this command to copy all
assets to their lower-case triplet equivalents like this:
```bash
techdocs-cli migrate --publisher-type <awsS3|googleGcs|azureBlobStorage> --storage-name <bucket/container name> --verbose
```
Once migrated and the upgraded version of the Backstage plugin has been
deployed, you can clean up the legacy, case-sensitive triplet files by
re-running the command with the `--removeOriginal` flag passed, which _moves_
(rather than copies) the files. Note: this deletes files and is therefore a
destructive operation that should performed with caution.
```bash
techdocs-cli migrate --publisher-type <awsS3|googleGcs|azureBlobStorage> --storage-name <bucket/container name> --removeOriginal --verbose
```
Afterward, update your TechDocs CLI to `v[0.7.0]` to ensure further publishing
happens using a lower-case entity triplet.
Note: arguments for this command largely match those of the `publish` command,
depending on your chosen storage provider. Run `techdocs-cli migrate --help` for
details.
#### Authentication
You need to make sure that your environment is able to authenticate with the
target cloud provider. `techdocs-cli` uses the official Node.js clients provided
by AWS (v2), Google Cloud and Azure. You can authenticate using environment
variables and/or by other means (`~/.aws/credentials`, `~/.config/gcloud` etc.)
Refer to the Authentication section of the following documentation depending
upon your cloud storage provider -
- [Google Cloud Storage](https://backstage.io/docs/features/techdocs/using-cloud-storage#configuring-google-gcs-bucket-with-techdocs)
- [AWS S3](https://backstage.io/docs/features/techdocs/using-cloud-storage#configuring-aws-s3-bucket-with-techdocs)
- [Azure Blob Storage](https://backstage.io/docs/features/techdocs/using-cloud-storage#configuring-azure-blob-storage-container-with-techdocs)
## Development
You are welcome to contribute to TechDocs CLI to improve it and support new
features! See the project
[README](https://github.com/backstage/backstage/blob/main/src/packages/techdocs-cli/README.md)
for more information.
+6
View File
@@ -106,6 +106,12 @@ techdocs:
# This allows providers like LocalStack, Minio and Wasabi (and possibly others) to be used to host tech docs.
s3ForcePathStyle: false
# (Optional) AWS Server Side Encryption
# Defaults to undefined.
# If not set, encrypted buckets will fail to publish.
# https://docs.aws.amazon.com/AmazonS3/latest/userguide/specifying-s3-encryption.html
sse: 'aws:kms' # or AES256
# Required when techdocs.publisher.type is set to 'azureBlobStorage'. Skip otherwise.
azureBlobStorage:
+39
View File
@@ -41,6 +41,7 @@ lint Lint a package
test Run tests, forwarding args to Jest, defaulting to watch mode
clean Delete cache directories
create Open up an interactive guide to creating new things in your app
create-plugin Creates a new plugin in the current repository
remove-plugin Removes plugin in the current repository
@@ -277,6 +278,44 @@ Options:
-h, --help display help for command
```
## create
Scope: `root`
The `create` command opens up an interactive guide for you to create new things
in your app. If you do not pass in any options it is completely interactive, but
it is possible to pre-select what you want to create using the `--select` flag,
and provide options using `--options`, for example:
```bash
backstage-cli create --select plugin --option id=foo
```
This command is typically added as script in the root `package.json` to be
executed with `yarn backstage-create`, using options that are appropriate for
the organization that owns the app repo. For example you may have it set up like
this:
```json
{
"scripts": {
"backstage-create": "backstage-cli create --scope internal --no-private --npm-registry https://acme.org/npm"
}
}
```
```text
Usage: backstage-cli create [options]
Options:
--select <name> Select the thing you want to be creating upfront
--option <name>=<value> Pre-fill options for the creation process (default: [])
--scope <scope> The scope to use for new packages
--npm-registry <URL> The package registry to use for new packages
--no-private Do not mark new packages as private
-h, --help display help for command
```
## create-plugin
Scope: `root`
+2 -2
View File
@@ -1,7 +1,7 @@
---
title: Splunk On-Call
author: Spotify
authorUrl: https://github.com/spotify
author: ayshiff
authorUrl: https://github.com/ayshiff
category: Monitoring
description: Splunk On-Call offers a simple way to identify incidents and escalation policies.
documentation: https://github.com/backstage/backstage/tree/master/plugins/splunk-on-call
+1
View File
@@ -105,6 +105,7 @@
"features/techdocs/configuration",
"features/techdocs/using-cloud-storage",
"features/techdocs/configuring-ci-cd",
"features/techdocs/cli",
"features/techdocs/how-to-guides",
"features/techdocs/troubleshooting",
"features/techdocs/faqs"
+27 -63
View File
@@ -1624,15 +1624,6 @@ cli-width@^2.0.0:
resolved "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48"
integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==
clipboard@^2.0.0:
version "2.0.6"
resolved "https://registry.npmjs.org/clipboard/-/clipboard-2.0.6.tgz#52921296eec0fdf77ead1749421b21c968647376"
integrity sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg==
dependencies:
good-listener "^1.2.2"
select "^1.1.2"
tiny-emitter "^2.0.0"
clone-response@1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
@@ -1687,9 +1678,9 @@ color-name@^1.0.0, color-name@~1.1.4:
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
color-string@^1.5.4:
version "1.5.4"
resolved "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz#dd51cd25cfee953d138fe4002372cc3d0e504cb6"
integrity sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==
version "1.6.0"
resolved "https://registry.npmjs.org/color-string/-/color-string-1.6.0.tgz#c3915f61fe267672cb7e1e064c9d692219f6c312"
integrity sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==
dependencies:
color-name "^1.0.0"
simple-swizzle "^0.2.2"
@@ -2158,11 +2149,6 @@ delayed-stream@~1.0.0:
resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
delegate@^3.1.2:
version "3.2.0"
resolved "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166"
integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==
depd@~1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
@@ -3097,13 +3083,6 @@ globule@^1.0.0:
lodash "~4.17.10"
minimatch "~3.0.2"
good-listener@^1.2.2:
version "1.2.2"
resolved "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
integrity sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=
dependencies:
delegate "^3.1.2"
got@^7.0.0:
version "7.1.0"
resolved "https://registry.npmjs.org/got/-/got-7.1.0.tgz#05450fd84094e6bbea56f451a43a9c289166385a"
@@ -3276,9 +3255,9 @@ highlight.js@^9.16.2:
integrity sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA==
hosted-git-info@^2.1.4:
version "2.8.8"
resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
version "2.8.9"
resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
hsl-regex@^1.0.0:
version "1.0.0"
@@ -4815,9 +4794,9 @@ path-key@^2.0.0, path-key@^2.0.1:
integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
path-parse@^1.0.6:
version "1.0.6"
resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
version "1.0.7"
resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-to-regexp@0.1.7:
version "0.1.7"
@@ -4850,6 +4829,11 @@ performance-now@^2.1.0:
resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
picocolors@^0.2.1:
version "0.2.1"
resolved "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f"
integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==
pify@^2.0.0, pify@^2.2.0, pify@^2.3.0:
version "2.3.0"
resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@@ -5189,13 +5173,12 @@ postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0:
integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.23, postcss@^7.0.27, postcss@^7.0.32:
version "7.0.35"
resolved "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24"
integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==
version "7.0.39"
resolved "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309"
integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==
dependencies:
chalk "^2.4.2"
picocolors "^0.2.1"
source-map "^0.6.1"
supports-color "^6.1.0"
prepend-http@^1.0.1:
version "1.0.4"
@@ -5213,11 +5196,9 @@ prettier@^2.4.1:
integrity sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==
prismjs@^1.22.0:
version "1.23.0"
resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33"
integrity sha512-c29LVsqOaLbBHuIbsTxaKENh1N2EQBOHaWv7gkHN4dgRbxSREqDnDbtFJYdpPauS4YCplMSNCABQ6Eeor69bAA==
optionalDependencies:
clipboard "^2.0.0"
version "1.25.0"
resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.25.0.tgz#6f822df1bdad965734b310b315a23315cf999756"
integrity sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg==
process-nextick-args@~2.0.0:
version "2.0.1"
@@ -5709,11 +5690,6 @@ seek-bzip@^1.0.5:
dependencies:
commander "^2.8.1"
select@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=
semver-regex@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338"
@@ -5766,9 +5742,9 @@ serve-static@1.14.1:
send "0.17.1"
set-getter@^0.1.0:
version "0.1.0"
resolved "https://registry.npmjs.org/set-getter/-/set-getter-0.1.0.tgz#d769c182c9d5a51f409145f2fba82e5e86e80376"
integrity sha1-12nBgsnVpR9AkUXy+6guXoboA3Y=
version "0.1.1"
resolved "https://registry.npmjs.org/set-getter/-/set-getter-0.1.1.tgz#a3110e1b461d31a9cfc8c5c9ee2e9737ad447102"
integrity sha512-9sVWOy+gthr+0G9DzqqLaYNA7+5OKkSmcqjL9cBpDEaZrr3ShQlyX2cZ/O/ozE41oxn/Tt0LGEM/w4Rub3A3gw==
dependencies:
to-object-path "^0.3.0"
@@ -6150,13 +6126,6 @@ supports-color@^5.3.0:
dependencies:
has-flag "^3.0.0"
supports-color@^6.1.0:
version "6.1.0"
resolved "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
dependencies:
has-flag "^3.0.0"
supports-color@^7.1.0:
version "7.2.0"
resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
@@ -6250,11 +6219,6 @@ timsort@^0.3.0:
resolved "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tiny-emitter@^2.0.0:
version "2.1.0"
resolved "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
tiny-lr@^1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.1.1.tgz#9fa547412f238fedb068ee295af8b682c98b2aab"
@@ -6502,9 +6466,9 @@ url-parse-lax@^3.0.0:
prepend-http "^2.0.0"
url-parse@^1.4.3:
version "1.4.7"
resolved "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278"
integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==
version "1.5.3"
resolved "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862"
integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==
dependencies:
querystringify "^2.1.1"
requires-port "^1.0.0"
+1 -1
View File
@@ -61,7 +61,6 @@ nav:
- Writing Custom Actions: 'features/software-templates/writing-custom-actions.md'
- Writing Templates (Legacy): 'features/software-templates/legacy.md'
- Migrating from v1alpha1 to v1beta2 templates: 'features/software-templates/migrating-from-v1alpha1-to-v1beta2.md'
- Backstage Search:
- Overview: 'features/search/README.md'
- Getting Started: 'features/search/getting-started.md'
@@ -78,6 +77,7 @@ nav:
- TechDocs Configuration Options: 'features/techdocs/configuration.md'
- Using Cloud Storage: 'features/techdocs/using-cloud-storage.md'
- Configuring CI/CD to generate and publish TechDocs sites: 'features/techdocs/configuring-ci-cd.md'
- CLI: 'features/techdocs/cli.md'
- HOW TO guides: 'features/techdocs/how-to-guides.md'
- Troubleshooting: 'features/techdocs/troubleshooting.md'
- FAQ: 'features/techdocs/FAQ.md'
+6 -2
View File
@@ -23,13 +23,16 @@
"lint:all": "lerna run lint --",
"lint:type-deps": "node scripts/check-type-dependencies.js",
"docker-build": "yarn tsc && yarn workspace example-backend build --build-dependencies && yarn workspace example-backend build-image",
"create-plugin": "backstage-cli create-plugin --scope backstage --no-private",
"backstage-create": "backstage-cli create --scope backstage --no-private",
"create-plugin": "yarn backstage-create --select plugin",
"remove-plugin": "backstage-cli remove-plugin",
"release": "changeset version && yarn diff --yes && yarn prettier --write '{packages,plugins}/*/{package.json,CHANGELOG.md}' && yarn install",
"prettier:check": "prettier --check .",
"lerna": "lerna",
"storybook": "yarn workspace storybook start",
"build-storybook": "yarn workspace storybook build-storybook",
"techdocs-cli": "node scripts/techdocs-cli.js",
"techdocs-cli:dev": "cross-env TECHDOCS_CLI_DEV_MODE=true node scripts/techdocs-cli.js",
"prepare": "husky install",
"lock:check": "yarn-lock-check"
},
@@ -50,7 +53,7 @@
},
"version": "1.0.0",
"dependencies": {
"@microsoft/api-documenter": "^7.13.47",
"@microsoft/api-documenter": "^7.13.68",
"@microsoft/api-extractor": "^7.18.7",
"@microsoft/api-extractor-model": "^7.13.5",
"@microsoft/tsdoc": "^0.13.2"
@@ -61,6 +64,7 @@
"@spotify/prettier-config": "^11.0.0",
"@types/webpack": "^5.28.0",
"command-exists": "^1.2.9",
"cross-env": "^7.0.0",
"concurrently": "^6.0.0",
"eslint-plugin-notice": "^0.9.10",
"fs-extra": "9.1.0",
@@ -35,6 +35,7 @@ import {
} from '@backstage/plugin-api-docs';
import {
EntityAzurePipelinesContent,
EntityAzurePullRequestsContent,
isAzureDevOpsAvailable,
} from '@backstage/plugin-azure-devops';
import { EntityBadgesDialog } from '@backstage/plugin-badges';
@@ -264,6 +265,18 @@ const errorsContent = (
</EntitySwitch>
);
const pullRequestsContent = (
<EntitySwitch>
<EntitySwitch.Case if={isAzureDevOpsAvailable}>
<EntityAzurePullRequestsContent defaultLimit={25} />
</EntitySwitch.Case>
<EntitySwitch.Case>
<EntityGithubPullRequestsContent />
</EntitySwitch.Case>
</EntitySwitch>
);
const overviewContent = (
<Grid container spacing={3} alignItems="stretch">
{entityWarningContent}
@@ -368,7 +381,7 @@ const serviceEntityPage = (
</EntityLayout.Route>
<EntityLayout.Route path="/pull-requests" title="Pull Requests">
<EntityGithubPullRequestsContent />
{pullRequestsContent}
</EntityLayout.Route>
<EntityLayout.Route path="/code-insights" title="Code Insights">
@@ -427,7 +440,7 @@ const websiteEntityPage = (
</EntityLayout.Route>
<EntityLayout.Route path="/pull-requests" title="Pull Requests">
<EntityGithubPullRequestsContent />
{pullRequestsContent}
</EntityLayout.Route>
<EntityLayout.Route path="/code-insights" title="Code Insights">
+10 -3
View File
@@ -6,6 +6,7 @@
/// <reference types="node" />
/// <reference types="webpack-env" />
import { AbortSignal as AbortSignal_2 } from 'node-abort-controller';
import { AwsS3Integration } from '@backstage/integration';
import { AzureIntegration } from '@backstage/integration';
import { BitbucketIntegration } from '@backstage/integration';
@@ -48,7 +49,7 @@ export class AwsS3UrlReader implements UrlReader {
// (undocumented)
read(url: string): Promise<Buffer>;
// (undocumented)
readTree(url: string): Promise<ReadTreeResponse>;
readTree(url: string, options?: ReadTreeOptions): Promise<ReadTreeResponse>;
// (undocumented)
readUrl(url: string, options?: ReadUrlOptions): Promise<ReadUrlResponse>;
// (undocumented)
@@ -72,7 +73,7 @@ export class AzureUrlReader implements UrlReader {
// (undocumented)
readTree(url: string, options?: ReadTreeOptions): Promise<ReadTreeResponse>;
// (undocumented)
readUrl(url: string, _options?: ReadUrlOptions): Promise<ReadUrlResponse>;
readUrl(url: string, options?: ReadUrlOptions): Promise<ReadUrlResponse>;
// (undocumented)
search(url: string, options?: SearchOptions): Promise<SearchResponse>;
// (undocumented)
@@ -94,7 +95,7 @@ export class BitbucketUrlReader implements UrlReader {
// (undocumented)
readTree(url: string, options?: ReadTreeOptions): Promise<ReadTreeResponse>;
// (undocumented)
readUrl(url: string, _options?: ReadUrlOptions): Promise<ReadUrlResponse>;
readUrl(url: string, options?: ReadUrlOptions): Promise<ReadUrlResponse>;
// (undocumented)
search(url: string, options?: SearchOptions): Promise<SearchResponse>;
// (undocumented)
@@ -374,6 +375,9 @@ export class GitlabUrlReader implements UrlReader {
export { isChildPath };
// @public
export function isDatabaseConflictError(e: unknown): boolean;
// @public
export function loadBackendConfig(options: {
logger: Logger_2;
@@ -415,6 +419,7 @@ export type ReadTreeOptions = {
},
): boolean;
etag?: string;
signal?: AbortSignal_2;
};
// @public
@@ -468,6 +473,7 @@ export type ReadTreeResponseFile = {
// @public
export type ReadUrlOptions = {
etag?: string;
signal?: AbortSignal_2;
};
// @public
@@ -505,6 +511,7 @@ export type RunContainerOptions = {
// @public
export type SearchOptions = {
etag?: string;
signal?: AbortSignal_2;
};
// @public
+21
View File
@@ -69,6 +69,19 @@ export interface Config {
* Defaults to true if unspecified.
*/
ensureExists?: boolean;
/**
* How plugins databases are managed/divided in the provided database instance.
*
* `database` -> Plugins are each given their own database to manage their schemas/tables.
*
* `schema` -> Plugins will be given their own schema (in the specified/default database)
* to manage their tables.
*
* NOTE: Currently only supported by the `pg` client.
*
* @default database
*/
pluginDivisionMode?: 'database' | 'schema';
/** Plugin specific database configuration and client override */
plugin?: {
[pluginId: string]: {
@@ -130,6 +143,14 @@ export interface Config {
* The host may also contain a port, for example `example.com:8080`.
*/
host: string;
/**
* An optional list of paths. In case they are present only targets matching
* any of them will are allowed. You can use trailing slashes to make sure only
* subdirectories are allowed, for example `/mydir/` will allow targets with
* paths like `/mydir/a` but will block paths like `/mydir2`.
*/
paths?: string[];
}>;
};
+1
View File
@@ -62,6 +62,7 @@
"minimatch": "^3.0.4",
"minimist": "^1.2.5",
"morgan": "^1.10.0",
"node-abort-controller": "^3.0.1",
"raw-body": "^2.4.1",
"selfsigned": "^1.10.7",
"stoppable": "^1.1.0",
+7 -5
View File
@@ -200,7 +200,7 @@ export async function loadBackendConfig(options: {
});
const config = new ObservableConfigProxy(options.logger);
const configs = await loadConfig({
const { appConfigs } = await loadConfig({
configRoot: paths.targetRoot,
configPaths: [],
configTargets: configTargets,
@@ -227,14 +227,16 @@ export async function loadBackendConfig(options: {
});
options.logger.info(
`Loaded config from ${configs.map(c => c.context).join(', ')}`,
`Loaded config from ${appConfigs.map(c => c.context).join(', ')}`,
);
config.setConfig(ConfigReader.fromConfigs(configs));
config.setConfig(ConfigReader.fromConfigs(appConfigs));
// Subscribe to config changes and update the redaction list for logging
updateRedactionList(schema, configs, options.logger);
config.subscribe(() => updateRedactionList(schema, configs, options.logger));
updateRedactionList(schema, appConfigs, options.logger);
config.subscribe(() =>
updateRedactionList(schema, appConfigs, options.logger),
);
return config;
}
@@ -15,13 +15,18 @@
*/
import { ConfigReader } from '@backstage/config';
import { omit } from 'lodash';
import { createDatabaseClient, ensureDatabaseExists } from './connection';
import {
createDatabaseClient,
ensureDatabaseExists,
ensureSchemaExists,
} from './connection';
import { DatabaseManager } from './DatabaseManager';
jest.mock('./connection', () => ({
...jest.requireActual('./connection'),
createDatabaseClient: jest.fn(),
ensureDatabaseExists: jest.fn(),
ensureSchemaExists: jest.fn(),
}));
describe('DatabaseManager', () => {
@@ -314,5 +319,191 @@ describe('DatabaseManager', () => {
expect.stringContaining('userdbname'),
);
});
it('plugin sets schema override for pg client', async () => {
const overrideConfig = {
backend: {
database: {
client: 'pg',
pluginDivisionMode: 'schema',
connection: {
host: 'localhost',
user: 'foo',
password: 'bar',
database: 'foodb',
},
},
},
};
const testManager = DatabaseManager.fromConfig(
new ConfigReader(overrideConfig),
);
const pluginId = 'schemaoverride';
await testManager.forPlugin(pluginId).getClient();
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
const [baseConfig, overrides] = mockCalls[0];
expect(baseConfig.get()).toMatchObject({
client: 'pg',
connection: config.backend.database.connection,
});
expect(overrides).toMatchObject({
searchPath: [pluginId],
});
});
it('plugin does not provide schema override for non pg client', async () => {
const testManager = DatabaseManager.fromConfig(
new ConfigReader({
backend: {
database: {
client: 'sqlite3',
pluginDivisionMode: 'schema',
connection: {
host: 'localhost',
user: 'foo',
password: 'bar',
database: 'foodb',
},
},
},
}),
);
const pluginId = 'any-plugin';
await testManager.forPlugin(pluginId).getClient();
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
const [baseConfig, overrides] = mockCalls[0];
expect(baseConfig.get()).toMatchObject({
client: 'sqlite3',
connection: config.backend.database.connection,
});
expect(overrides).not.toHaveProperty('searchPath');
});
it('plugin does not provide schema override if pluginDivisionMode is set to database', async () => {
const testManager = DatabaseManager.fromConfig(
new ConfigReader({
backend: {
database: {
client: 'pg',
pluginDivisionMode: 'database',
connection: 'some-file-path',
},
},
}),
);
const pluginId = 'any-plugin';
await testManager.forPlugin(pluginId).getClient();
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
const [_baseConfig, overrides] = mockCalls[0];
expect(overrides).not.toHaveProperty('searchPath');
});
it('plugin does not provide schema override if pluginDivisionMode is not set', async () => {
const testManager = DatabaseManager.fromConfig(
new ConfigReader({
backend: {
database: {
client: 'pg',
connection: {
host: 'localhost',
user: 'foo',
password: 'bar',
database: 'foodb',
},
},
},
}),
);
const pluginId = 'schemaoverride';
await testManager.forPlugin(pluginId).getClient();
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
const [_baseConfig, overrides] = mockCalls[0];
expect(overrides).not.toHaveProperty('searchPath');
});
it('pluginDivisionMode ensures that each plugin schema exists', async () => {
const testManager = DatabaseManager.fromConfig(
new ConfigReader({
backend: {
database: {
client: 'pg',
pluginDivisionMode: 'schema',
connection: {
host: 'localhost',
user: 'foo',
password: 'bar',
database: 'foodb',
},
},
},
}),
);
const pluginId = 'testdbname';
await testManager.forPlugin(pluginId).getClient();
const mockCalls = mocked(ensureSchemaExists).mock.calls;
const [_, schemaName] = mockCalls[0];
expect(schemaName).toEqual('testdbname');
});
it('pluginDivisionMode allows connection overrides for plugins', async () => {
const testManager = DatabaseManager.fromConfig(
new ConfigReader({
backend: {
database: {
client: 'pg',
pluginDivisionMode: 'schema',
connection: {
host: 'localhost',
user: 'foo',
password: 'bar',
database: 'foodb',
},
plugin: {
testdbname: {
connection: {
database: 'database_name_overriden',
host: 'newhost',
},
},
},
},
},
}),
);
const pluginId = 'testdbname';
await testManager.forPlugin(pluginId).getClient();
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
const [baseConfig, overrides] = mockCalls[0];
expect(baseConfig.get()).toMatchObject({
client: 'pg',
connection: {
database: 'database_name_overriden',
host: 'newhost',
user: 'foo',
password: 'bar',
},
});
expect(overrides).toHaveProperty('searchPath', ['testdbname']);
expect(overrides).toHaveProperty(
'connection.database',
'database_name_overriden',
);
});
});
});
@@ -13,17 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Knex } from 'knex';
import { omit } from 'lodash';
import { Config, ConfigReader } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import {
createDatabaseClient,
ensureDatabaseExists,
createNameOverride,
ensureDatabaseExists,
normalizeConnection,
createSchemaOverride,
ensureSchemaExists,
createDatabaseClient,
} from './connection';
import { PluginDatabaseManager } from './types';
import { mergeDatabaseConfig } from './config';
/**
* Provides a config lookup path for a plugin's config block.
@@ -79,14 +83,15 @@ export class DatabaseManager {
* Provides the canonical database name for a given plugin.
*
* This method provides the effective database name which is determined using global
* and plugin specific database config. If no explicit database name is configured,
* this method will provide a generated name which is the pluginId prefixed with
* 'backstage_plugin_'.
* and plugin specific database config. If no explicit database name is configured
* and `pluginDivisionMode` is not `schema`, this method will provide a generated name
* which is the pluginId prefixed with 'backstage_plugin_'. If `pluginDivisionMode` is
* `schema`, it will fallback to using the default database for the knex instance.
*
* @param pluginId Lookup the database name for given plugin
* @returns String representing the plugin's database name
*/
private getDatabaseName(pluginId: string): string {
private getDatabaseName(pluginId: string): string | undefined {
const connection = this.getConnectionConfig(pluginId);
if (this.getClientType(pluginId).client === 'sqlite3') {
@@ -95,11 +100,16 @@ export class DatabaseManager {
(connection as Knex.Sqlite3ConnectionConfig)?.filename ?? ':memory:'
);
}
const databaseName = (connection as Knex.ConnectionConfig)?.database;
// `pluginDivisionMode` as `schema` should use overridden databaseName if supplied or fallback to default knex database
if (this.getPluginDivisionModeConfig() === 'schema') {
return databaseName;
}
// all other supported databases should fallback to an auto-prefixed name
return (
(connection as Knex.ConnectionConfig)?.database ??
`${this.prefix}${pluginId}`
);
return databaseName ?? `${this.prefix}${pluginId}`;
}
/**
@@ -136,13 +146,18 @@ export class DatabaseManager {
);
}
private getPluginDivisionModeConfig(): string {
return this.config.getOptionalString('pluginDivisionMode') ?? 'database';
}
/**
* Provides a Knex connection plugin config by combining base and plugin config.
*
* This method provides a baseConfig for a plugin database connector. If the client type
* has not been overridden, the global connection config will be included with plugin
* specific config as the base. Values from the plugin connection take precedence over the
* base. Base database name is omitted for all supported databases excluding SQLite.
* base. Base database name is omitted for all supported databases excluding SQLite unless
* `pluginDivisionMode` is set to `schema`.
*/
private getConnectionConfig(
pluginId: string,
@@ -153,10 +168,13 @@ export class DatabaseManager {
this.config.get('connection'),
this.config.getString('client'),
);
// As databases cannot be shared, the `database` property from the base connection
// is omitted. SQLite3's `filename` property is an exception as this is used as a
// Databases cannot be shared unless the `pluginDivisionMode` is set to `schema`. The
// `database` property from the base connection is omitted unless `pluginDivisionMode`
// is set to `schema`. SQLite3's `filename` property is an exception as this is used as a
// directory elsewhere so we preserve `filename`.
baseConnection = omit(baseConnection, 'database');
if (this.getPluginDivisionModeConfig() !== 'schema') {
baseConnection = omit(baseConnection, 'database');
}
// get and normalize optional plugin specific database connection
const connection = normalizeConnection(
@@ -165,7 +183,7 @@ export class DatabaseManager {
);
return {
// include base connection if client type has not been overriden
// include base connection if client type has not been overridden
...(overridden ? {} : baseConnection),
...connection,
};
@@ -187,6 +205,16 @@ export class DatabaseManager {
};
}
/**
* Provides a partial Knex.Config database schema override for a given plugin.
*
* @param pluginId Target plugin to get database schema override
* @returns Partial Knex.Config with database schema override
*/
private getSchemaOverrides(pluginId: string): Knex.Config | undefined {
return createSchemaOverride(this.getClientType(pluginId).client, pluginId);
}
/**
* Provides a partial Knex.Config database name override for a given plugin.
*
@@ -194,10 +222,10 @@ export class DatabaseManager {
* @returns Partial Knex.Config with database name override
*/
private getDatabaseOverrides(pluginId: string): Knex.Config {
return createNameOverride(
this.getClientType(pluginId).client,
this.getDatabaseName(pluginId),
);
const databaseName = this.getDatabaseName(pluginId);
return databaseName
? createNameOverride(this.getClientType(pluginId).client, databaseName)
: {};
}
/**
@@ -211,8 +239,8 @@ export class DatabaseManager {
this.getConfigForPlugin(pluginId) as JsonObject,
);
if (this.getEnsureExistsConfig(pluginId)) {
const databaseName = this.getDatabaseName(pluginId);
const databaseName = this.getDatabaseName(pluginId);
if (databaseName && this.getEnsureExistsConfig(pluginId)) {
try {
await ensureDatabaseExists(pluginConfig, databaseName);
} catch (error) {
@@ -222,9 +250,24 @@ export class DatabaseManager {
}
}
return createDatabaseClient(
pluginConfig,
let schemaOverrides;
if (this.getPluginDivisionModeConfig() === 'schema') {
try {
schemaOverrides = this.getSchemaOverrides(pluginId);
await ensureSchemaExists(pluginConfig, pluginId);
} catch (error) {
throw new Error(
`Failed to connect to the database to make sure that schema for plugin '${pluginId}' exists, ${error}`,
);
}
}
const databaseClientOverrides = mergeDatabaseConfig(
{},
this.getDatabaseOverrides(pluginId),
schemaOverrides,
);
return createDatabaseClient(pluginConfig, databaseClientOverrides);
}
}
@@ -18,8 +18,24 @@ import { ConfigReader } from '@backstage/config';
import {
createDatabaseClient,
createNameOverride,
createSchemaOverride,
ensureSchemaExists,
parseConnectionString,
} from './connection';
import { pgConnector } from './connectors';
const mocked = (f: Function) => f as jest.Mock;
jest.mock('./connectors', () => {
const connectors = jest.requireActual('./connectors');
return {
...connectors,
pgConnector: {
...connectors.pgConnector,
ensureSchemaExists: jest.fn(),
},
};
});
describe('database connection', () => {
describe('createDatabaseClient', () => {
@@ -152,4 +168,63 @@ describe('database connection', () => {
expect(() => parseConnectionString('sqlite://')).toThrow();
});
});
describe('createSchemaOverride', () => {
it('returns Knex config for postgres', () => {
expect(createSchemaOverride('pg', 'testpg')).toHaveProperty(
'searchPath',
['testpg'],
);
});
it('throws error for sqlite', () => {
expect(createSchemaOverride('sqlite3', 'testsqlite')).toBeUndefined();
});
it('returns Knex config for mysql', () => {
expect(createSchemaOverride('mysql', 'testmysql')).toBeUndefined();
});
it('throws an error for unknown connection', () => {
expect(createSchemaOverride('unknown', 'testname')).toBeUndefined();
});
});
describe('ensureSchemaExists', () => {
it('returns sucessfully with pg client', async () => {
await ensureSchemaExists(
new ConfigReader({
client: 'pg',
schema: 'catalog',
connection: 'postgresql://testuser:testpass@acme:5432/userdbname',
}),
'catalog',
);
const mockCalls = mocked(
pgConnector.ensureSchemaExists as Function,
).mock.calls.splice(-1);
const [baseConfig, schemaName] = mockCalls[0];
expect(baseConfig.get()).toMatchObject({
client: 'pg',
connection: 'postgresql://testuser:testpass@acme:5432/userdbname',
});
expect(schemaName).toEqual('catalog');
});
it('throws error for non pg client', () => {
return expect(
ensureSchemaExists(
new ConfigReader({
client: 'sqlite3',
schema: 'catalog',
connection: ':memory:',
}),
'catalog',
),
).resolves.toBeUndefined();
});
});
});
@@ -82,6 +82,23 @@ export async function ensureDatabaseExists(
);
}
/**
* Ensures that the given schemas all exist, creating them if they do not.
*
* @public
*/
export async function ensureSchemaExists(
dbConfig: Config,
...schemas: Array<string>
): Promise<void> {
const client: DatabaseClient = dbConfig.getString('client');
return await ConnectorMapping[client]?.ensureSchemaExists?.(
dbConfig,
...schemas,
);
}
/**
* Provides a Knex.Config object with the provided database name for a given client.
*/
@@ -99,6 +116,23 @@ export function createNameOverride(
}
}
/**
* Provides a Knex.Config object with the provided database schema for a given client. Currently only supported by `pg`.
*/
export function createSchemaOverride(
client: string,
name: string,
): Partial<Knex.Config | undefined> {
try {
return ConnectorMapping[client]?.createSchemaOverride?.(name);
} catch (e) {
throw new InputError(
`Unable to create database schema override for '${client}' connector`,
e,
);
}
}
/**
* Parses a connection string for a given client and provides a connection config.
*/
@@ -0,0 +1,25 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 defaultSchemaOverride from './defaultSchemaOverride';
describe('defaultNameOverride()', () => {
it('returns a partial knex static connection config with searchPath set to [schemaName]', () => {
const schemaName = 'schemaName';
expect(defaultSchemaOverride(schemaName)).toHaveProperty('searchPath', [
schemaName,
]);
});
});
@@ -0,0 +1,29 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { Knex } from 'knex';
/**
* Provides a partial knex config with schema name override.
*
* @param name schema name to get config override for
*/
export default function defaultSchemaOverride(
name: string,
): Partial<Knex.Config> {
return {
searchPath: [name],
};
}
@@ -76,6 +76,24 @@ describe('postgres', () => {
});
});
it('overrides the schema name', () => {
const mockConnection = {
...createMockConnection(),
schema: 'schemaName',
};
expect(
buildPgDatabaseConfig(createConfig(mockConnection), {
searchPath: ['schemaName'],
}),
).toEqual({
client: 'pg',
connection: mockConnection,
searchPath: ['schemaName'],
useNullAsDefault: true,
});
});
it('adds additional config settings', () => {
const mockConnection = createMockConnection();
@@ -21,6 +21,7 @@ import { ForwardedError } from '@backstage/errors';
import { mergeDatabaseConfig } from '../config';
import { DatabaseConnector } from '../types';
import defaultNameOverride from './defaultNameOverride';
import defaultSchemaOverride from './defaultSchemaOverride';
/**
* Creates a knex postgres database connection
@@ -135,6 +136,29 @@ export async function ensurePgDatabaseExists(
}
}
/**
* Creates the missing Postgres schema if it does not exist
*
* @param dbConfig The database config
* @param schemas The name of the schemas to create
*/
export async function ensurePgSchemaExists(
dbConfig: Config,
...schemas: Array<string>
): Promise<void> {
const admin = createPgDatabaseClient(dbConfig);
try {
const ensureSchema = async (database: string) => {
await admin.raw(`CREATE SCHEMA IF NOT EXISTS ??`, [database]);
};
await Promise.all(schemas.map(ensureSchema));
} finally {
await admin.destroy();
}
}
/**
* PostgreSQL database connector.
*
@@ -143,6 +167,8 @@ export async function ensurePgDatabaseExists(
export const pgConnector: DatabaseConnector = Object.freeze({
createClient: createPgDatabaseClient,
ensureDatabaseExists: ensurePgDatabaseExists,
ensureSchemaExists: ensurePgSchemaExists,
createNameOverride: defaultNameOverride,
createSchemaOverride: defaultSchemaOverride,
parseConnectionString: parsePgConnectionString,
});
@@ -28,3 +28,4 @@ export {
} from './connection';
export type { PluginDatabaseManager } from './types';
export { isDatabaseConflictError } from './util';
@@ -45,6 +45,11 @@ export interface DatabaseConnector {
* database name.
*/
createNameOverride(name: string): Partial<Knex.Config>;
/**
* createSchemaOverride provides a partial knex config sufficient to override a
* PostgreSQL schema name within utilizing the `searchPath` knex configuration.
*/
createSchemaOverride?(name: string): Partial<Knex.Config>;
/**
* parseConnectionString produces a knex connection config object representing
* a database connection string.
@@ -64,4 +69,16 @@ export interface DatabaseConnector {
dbConfig: Config,
...databases: Array<string>
): Promise<void>;
/**
* ensureSchemaExists performs a side-effect to ensure schema names passed in are
* present.
*
* Calling this function on schemas which already exist should do nothing.
* Missing schemas should be created if needed.
*/
ensureSchemaExists?(
dbConfig: Config,
...schemas: Array<string>
): Promise<void>;
}
@@ -0,0 +1,33 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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.
*/
/**
* Tries to deduce whether a thrown error is a database conflict.
*
* @public
* @param e - A thrown error
* @returns True if the error looks like it was a conflict error thrown by a
* known database engine
*/
export function isDatabaseConflictError(e: unknown) {
const message = (e as any)?.message;
return (
typeof message === 'string' &&
(/SQLITE_CONSTRAINT: UNIQUE/.test(message) ||
/unique constraint/.test(message))
);
}
@@ -48,6 +48,19 @@ describe('rootLogger', () => {
);
});
it('redacts but ignores empty and one-character secrets', () => {
const logger = createRootLogger();
jest.spyOn(logger, 'write');
setRootLoggerRedactionList(['SECRET-1', 'SECRET_2', 'Q', '']);
logger.info('Logging SECRET-1 and SECRET_2 and Q');
expect(logger.write).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Logging [REDACTED] and [REDACTED] and Q',
}),
);
});
describe('createRootLogger', () => {
it('creates a new logger', () => {
const oldLogger = getRootLogger();
@@ -21,7 +21,7 @@ import { coloredFormat } from './formats';
import { escapeRegExp } from '../util/escapeRegExp';
let rootLogger: winston.Logger;
let redactionRegExp: RegExp;
let redactionRegExp: RegExp | undefined;
/** @public */
export function getRootLogger(): winston.Logger {
@@ -34,11 +34,18 @@ export function setRootLogger(newLogger: winston.Logger) {
}
export function setRootLoggerRedactionList(redactionList: string[]) {
if (redactionList.length) {
// Exclude secrets that are empty or just one character in length. These
// typically mean that you are running local dev or tests, or using the
// --lax flag which sets things to just 'x'. So exclude those.
const filtered = redactionList.filter(r => r.length > 1);
if (filtered.length) {
redactionRegExp = new RegExp(
`(${redactionList.map(escapeRegExp).join('|')})`,
`(${filtered.map(escapeRegExp).join('|')})`,
'g',
);
} else {
redactionRegExp = undefined;
}
}
@@ -18,6 +18,7 @@ import aws, { Credentials, S3 } from 'aws-sdk';
import { CredentialsOptions } from 'aws-sdk/lib/credentials';
import {
ReaderFactory,
ReadTreeOptions,
ReadTreeResponse,
ReadTreeResponseFactory,
ReadUrlOptions,
@@ -92,7 +93,7 @@ export class AwsS3UrlReader implements UrlReader {
) {}
/**
* If accesKeyId and secretAccessKey are missing, the standard credentials provider chain will be used:
* If accessKeyId and secretAccessKey are missing, the standard credentials provider chain will be used:
* https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/DefaultAWSCredentialsProviderChain.html
*/
private static buildCredentials(
@@ -154,9 +155,10 @@ export class AwsS3UrlReader implements UrlReader {
};
}
const response = this.deps.s3.getObject(params);
const buffer = await getRawBody(response.createReadStream());
const etag = (await response.promise()).ETag;
const request = this.deps.s3.getObject(params);
options?.signal?.addEventListener('abort', () => request.abort());
const buffer = await getRawBody(request.createReadStream());
const etag = (await request.promise()).ETag;
return {
buffer: async () => buffer,
@@ -171,7 +173,10 @@ export class AwsS3UrlReader implements UrlReader {
}
}
async readTree(url: string): Promise<ReadTreeResponse> {
async readTree(
url: string,
options?: ReadTreeOptions,
): Promise<ReadTreeResponse> {
try {
const { path, bucket, region } = parseURL(url);
const allObjects: ObjectList = [];
@@ -180,13 +185,13 @@ export class AwsS3UrlReader implements UrlReader {
let output: ListObjectsV2Output;
do {
aws.config.update({ region: region });
output = await this.deps.s3
.listObjectsV2({
Bucket: bucket,
ContinuationToken: continuationToken,
Prefix: path,
})
.promise();
const request = this.deps.s3.listObjectsV2({
Bucket: bucket,
ContinuationToken: continuationToken,
Prefix: path,
});
options?.signal?.addEventListener('abort', () => request.abort());
output = await request.promise();
if (output.Contents) {
output.Contents.forEach(contents => {
allObjects.push(contents);
@@ -55,21 +55,34 @@ export class AzureUrlReader implements UrlReader {
) {}
async read(url: string): Promise<Buffer> {
const response = await this.readUrl(url);
return response.buffer();
}
async readUrl(
url: string,
options?: ReadUrlOptions,
): Promise<ReadUrlResponse> {
// TODO: etag is not implemented yet.
const { signal } = options ?? {};
const builtUrl = getAzureFileFetchUrl(url);
let response: Response;
try {
response = await fetch(
builtUrl,
getAzureRequestOptions(this.integration.config),
);
response = await fetch(builtUrl, {
...getAzureRequestOptions(this.integration.config),
...(signal && { signal }),
});
} catch (e) {
throw new Error(`Unable to read ${url}, ${e}`);
}
// for private repos when PAT is not valid, Azure API returns a http status code 203 with sign in page html
if (response.ok && response.status !== 203) {
return Buffer.from(await response.arrayBuffer());
return {
buffer: async () => Buffer.from(await response.arrayBuffer()),
};
}
const message = `${url} could not be read as ${builtUrl}, ${response.status} ${response.statusText}`;
@@ -79,19 +92,12 @@ export class AzureUrlReader implements UrlReader {
throw new Error(message);
}
async readUrl(
url: string,
_options?: ReadUrlOptions,
): Promise<ReadUrlResponse> {
// TODO etag is not implemented yet.
const buffer = await this.read(url);
return { buffer: async () => buffer };
}
async readTree(
url: string,
options?: ReadTreeOptions,
): Promise<ReadTreeResponse> {
const { etag, filter, signal } = options ?? {};
// TODO: Support filepath based reading tree feature like other providers
// Get latest commit SHA
@@ -109,16 +115,16 @@ export class AzureUrlReader implements UrlReader {
}
const commitSha = (await commitsAzureResponse.json()).value[0].commitId;
if (options?.etag && options.etag === commitSha) {
if (etag && etag === commitSha) {
throw new NotModifiedError();
}
const archiveAzureResponse = await fetch(
getAzureDownloadUrl(url),
getAzureRequestOptions(this.integration.config, {
const archiveAzureResponse = await fetch(getAzureDownloadUrl(url), {
...getAzureRequestOptions(this.integration.config, {
Accept: 'application/zip',
}),
);
...(signal && { signal }),
});
if (!archiveAzureResponse.ok) {
const message = `Failed to read tree from ${url}, ${archiveAzureResponse.status} ${archiveAzureResponse.statusText}`;
if (archiveAzureResponse.status === 404) {
@@ -139,7 +145,7 @@ export class AzureUrlReader implements UrlReader {
return await this.deps.treeResponseFactory.fromZipArchive({
stream: archiveAzureResponse.body as unknown as Readable,
etag: commitSha,
filter: options?.filter,
filter,
subpath,
});
}
@@ -158,6 +164,7 @@ export class AzureUrlReader implements UrlReader {
const tree = await this.readTree(treeUrl.toString(), {
etag: options?.etag,
signal: options?.signal,
filter: p => (matcher ? matcher.match(p) : true),
});
const files = await tree.files();
@@ -77,18 +77,33 @@ export class BitbucketUrlReader implements UrlReader {
}
async read(url: string): Promise<Buffer> {
const response = await this.readUrl(url);
return response.buffer();
}
async readUrl(
url: string,
options?: ReadUrlOptions,
): Promise<ReadUrlResponse> {
// TODO: etag is not supported yet
const { signal } = options ?? {};
const bitbucketUrl = getBitbucketFileFetchUrl(url, this.integration.config);
const options = getBitbucketRequestOptions(this.integration.config);
const requestOptions = getBitbucketRequestOptions(this.integration.config);
let response: Response;
try {
response = await fetch(bitbucketUrl.toString(), options);
response = await fetch(bitbucketUrl.toString(), {
...requestOptions,
...(signal && { signal }),
});
} catch (e) {
throw new Error(`Unable to read ${url}, ${e}`);
}
if (response.ok) {
return Buffer.from(await response.arrayBuffer());
return {
buffer: async () => Buffer.from(await response.arrayBuffer()),
};
}
const message = `${url} could not be read as ${bitbucketUrl}, ${response.status} ${response.statusText}`;
@@ -98,15 +113,6 @@ export class BitbucketUrlReader implements UrlReader {
throw new Error(message);
}
async readUrl(
url: string,
_options?: ReadUrlOptions,
): Promise<ReadUrlResponse> {
// TODO etag is not implemented yet.
const buffer = await this.read(url);
return { buffer: async () => buffer };
}
async readTree(
url: string,
options?: ReadTreeOptions,
@@ -77,6 +77,10 @@ describe('FetchUrlReader', () => {
{ host: 'example.com:700' },
{ host: '*.examples.org' },
{ host: '*.examples.org:700' },
{
host: 'foobar.org',
paths: ['/dir1/'],
},
],
},
},
@@ -106,6 +110,9 @@ describe('FetchUrlReader', () => {
expect(predicate(new URL('https://examples.org:700/test'))).toBe(false);
expect(predicate(new URL('https://a.examples.org:700/test'))).toBe(true);
expect(predicate(new URL('https://a.b.examples.org:700/test'))).toBe(true);
expect(predicate(new URL('https://foobar.org/dir1/subpath'))).toBe(true);
expect(predicate(new URL('https://foobar.org/dir12'))).toBe(false);
expect(predicate(new URL('https://foobar.org/'))).toBe(false);
});
describe('read', () => {
@@ -24,6 +24,7 @@ import {
SearchResponse,
UrlReader,
} from './types';
import path from 'path';
/**
* A UrlReader that does a plain fetch of the URL.
@@ -39,18 +40,30 @@ export class FetchUrlReader implements UrlReader {
* `host`:
* Either full hostnames to match, or subdomain wildcard matchers with a leading `*`.
* For example `example.com` and `*.example.com` are valid values, `prod.*.example.com` is not.
*
* `paths`:
* An optional list of paths which are allowed. If the list is omitted all paths are allowed.
*/
static factory: ReaderFactory = ({ config }) => {
const predicates =
config
.getOptionalConfigArray('backend.reading.allow')
?.map(allowConfig => {
const paths = allowConfig.getOptionalStringArray('paths');
const checkPath = paths
? (url: URL) => {
const targetPath = path.posix.normalize(url.pathname);
return paths.some(allowedPath =>
targetPath.startsWith(allowedPath),
);
}
: (_url: URL) => true;
const host = allowConfig.getString('host');
if (host.startsWith('*.')) {
const suffix = host.slice(1);
return (url: URL) => url.host.endsWith(suffix);
return (url: URL) => url.host.endsWith(suffix) && checkPath(url);
}
return (url: URL) => url.host === host;
return (url: URL) => url.host === host && checkPath(url);
}) ?? [];
const reader = new FetchUrlReader();
@@ -73,6 +86,7 @@ export class FetchUrlReader implements UrlReader {
headers: {
...(options?.etag && { 'If-None-Match': options.etag }),
},
signal: options?.signal,
});
} catch (e) {
throw new Error(`Unable to read ${url}, ${e}`);
@@ -110,6 +110,7 @@ export class GithubUrlReader implements UrlReader {
...(options?.etag && { 'If-None-Match': options.etag }),
Accept: 'application/vnd.github.v3.raw',
},
signal: options?.signal,
});
} catch (e) {
throw new Error(`Unable to read ${url}, ${e}`);
@@ -164,7 +165,7 @@ export class GithubUrlReader implements UrlReader {
repoDetails.repo.archive_url,
commitSha,
filepath,
{ headers },
{ headers, signal: options?.signal },
options,
);
}
@@ -188,7 +189,7 @@ export class GithubUrlReader implements UrlReader {
repoDetails.repo.archive_url,
commitSha,
filepath,
{ headers },
{ headers, signal: options?.signal },
);
return { files, etag: commitSha };
@@ -66,6 +66,7 @@ export class GitlabUrlReader implements UrlReader {
url: string,
options?: ReadUrlOptions,
): Promise<ReadUrlResponse> {
const { etag, signal } = options ?? {};
const builtUrl = await getGitLabFileFetchUrl(url, this.integration.config);
let response: Response;
@@ -73,8 +74,9 @@ export class GitlabUrlReader implements UrlReader {
response = await fetch(builtUrl, {
headers: {
...getGitLabRequestOptions(this.integration.config).headers,
...(options?.etag && { 'If-None-Match': options.etag }),
...(etag && { 'If-None-Match': etag }),
},
...(signal && { signal }),
});
} catch (e) {
throw new Error(`Unable to read ${url}, ${e}`);
@@ -102,6 +104,7 @@ export class GitlabUrlReader implements UrlReader {
url: string,
options?: ReadTreeOptions,
): Promise<ReadTreeResponse> {
const { etag, signal } = options ?? {};
const { ref, full_name, filepath } = parseGitUrl(url);
// Use GitLab API to get the default branch
@@ -140,7 +143,10 @@ export class GitlabUrlReader implements UrlReader {
full_name,
)}/repository/commits?${commitsReqParams.toString()}`,
).toString(),
getGitLabRequestOptions(this.integration.config),
{
...getGitLabRequestOptions(this.integration.config),
...(signal && { signal }),
},
);
if (!commitsGitlabResponse.ok) {
const message = `Failed to read tree (branch) from ${url}, ${commitsGitlabResponse.status} ${commitsGitlabResponse.statusText}`;
@@ -152,7 +158,7 @@ export class GitlabUrlReader implements UrlReader {
const commitSha = (await commitsGitlabResponse.json())[0].id;
if (options?.etag && options.etag === commitSha) {
if (etag && etag === commitSha) {
throw new NotModifiedError();
}
@@ -161,7 +167,10 @@ export class GitlabUrlReader implements UrlReader {
`${this.integration.config.apiBaseUrl}/projects/${encodeURIComponent(
full_name,
)}/repository/archive?sha=${branch}`,
getGitLabRequestOptions(this.integration.config),
{
...getGitLabRequestOptions(this.integration.config),
...(signal && { signal }),
},
);
if (!archiveGitLabResponse.ok) {
const message = `Failed to read tree (archive) from ${url}, ${archiveGitLabResponse.status} ${archiveGitLabResponse.statusText}`;
@@ -191,6 +200,7 @@ export class GitlabUrlReader implements UrlReader {
const tree = await this.readTree(treeUrl, {
etag: options?.etag,
signal: options?.signal,
filter: path => matcher.match(stripFirstDirectoryFromPath(path)),
});
const files = await tree.files();
@@ -17,6 +17,7 @@
import { Readable } from 'stream';
import { Logger } from 'winston';
import { Config } from '@backstage/config';
import { AbortSignal } from 'node-abort-controller';
/**
* A generic interface for fetching plain data from URLs.
@@ -101,6 +102,15 @@ export type ReadUrlOptions = {
* of the response along with a new ETag.
*/
etag?: string;
/**
* An abort signal to pass down to the underlying request.
*
* @remarks
*
* Not all reader implementations may take this field into account.
*/
signal?: AbortSignal;
};
/**
@@ -165,6 +175,15 @@ export type ReadTreeOptions = {
* rest of the response along with a new ETag.
*/
etag?: string;
/**
* An abort signal to pass down to the underlying request.
*
* @remarks
*
* Not all reader implementations may take this field into account.
*/
signal?: AbortSignal;
};
/**
@@ -291,6 +310,15 @@ export type SearchOptions = {
* search will return the rest of SearchResponse along with a new etag.
*/
etag?: string;
/**
* An abort signal to pass down to the underlying request.
*
* @remarks
*
* Not all reader implementations may take this field into account.
*/
signal?: AbortSignal;
};
/**
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
};
+35
View File
@@ -0,0 +1,35 @@
# @backstage/backend-tasks
Common distributed task management for Backstage backends.
## Usage
Add the library to your backend package:
```sh
# From your Backstage root directory
cd packages/backend
yarn add @backstage/backend-tasks
```
then make use of its facilities as necessary:
```typescript
import { TaskScheduler } from '@backstage/backend-tasks';
import { Duration } from 'luxon';
const scheduler = TaskScheduler.fromConfig(rootConfig).forPlugin('my-plugin');
await scheduler.scheduleTask({
id: 'refresh-things',
frequency: Duration.fromObject({ minutes: 10 }),
fn: async () => {
await entityProvider.run();
},
});
```
## Documentation
- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md)
- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md)
+45
View File
@@ -0,0 +1,45 @@
## API Report File for "@backstage/backend-tasks"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { AbortSignal as AbortSignal_2 } from 'node-abort-controller';
import { Config } from '@backstage/config';
import { DatabaseManager } from '@backstage/backend-common';
import { Duration } from 'luxon';
import { Logger as Logger_2 } from 'winston';
// @public
export interface PluginTaskScheduler {
scheduleTask(task: TaskDefinition): Promise<void>;
}
// @public
export interface TaskDefinition {
fn: TaskFunction;
frequency: Duration;
id: string;
initialDelay?: Duration;
signal?: AbortSignal_2;
timeout: Duration;
}
// @public
export type TaskFunction =
| ((abortSignal: AbortSignal_2) => void | Promise<void>)
| (() => void | Promise<void>);
// @public
export class TaskScheduler {
constructor(databaseManager: DatabaseManager, logger: Logger_2);
forPlugin(pluginId: string): PluginTaskScheduler;
// (undocumented)
static fromConfig(
config: Config,
options?: {
databaseManager?: DatabaseManager;
logger?: Logger_2;
},
): TaskScheduler;
}
```
@@ -0,0 +1,64 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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.
*/
// @ts-check
/**
* @param {import('knex').Knex} knex
*/
exports.up = async function up(knex) {
//
// tasks
//
await knex.schema.createTable('backstage_backend_tasks__tasks', table => {
table.comment('Tasks used for scheduling work on multiple workers');
table
.text('id')
.primary()
.notNullable()
.comment('The unique ID of this particular task');
table
.text('settings_json')
.notNullable()
.comment('JSON serialized object with properties for this task');
table
.dateTime('next_run_start_at')
.notNullable()
.comment('The next time that the task should be started');
table
.text('current_run_ticket')
.nullable()
.comment('A unique ticket for the current task run');
table
.dateTime('current_run_started_at')
.nullable()
.comment('The time that the current task run started');
table
.dateTime('current_run_expires_at')
.nullable()
.comment('The time that the current task run will time out');
});
};
/**
* @param {import('knex').Knex} knex
*/
exports.down = async function down(knex) {
//
// tasks
//
await knex.schema.dropTable('backstage_backend_tasks__tasks');
};
+55
View File
@@ -0,0 +1,55 @@
{
"name": "@backstage/backend-tasks",
"description": "Common distributed task management library for Backstage backends",
"version": "0.1.0",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "packages/backend-tasks"
},
"keywords": [
"backstage"
],
"license": "Apache-2.0",
"scripts": {
"build": "backstage-cli build --outputs cjs,types",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.9.8",
"@backstage/config": "^0.1.11",
"@backstage/errors": "^0.1.4",
"@backstage/types": "^0.1.1",
"@types/luxon": "^2.0.4",
"knex": "^0.95.1",
"lodash": "^4.17.21",
"luxon": "^2.0.2",
"node-abort-controller": "^3.0.1",
"uuid": "^8.0.0",
"winston": "^3.2.1",
"zod": "^3.9.5"
},
"devDependencies": {
"@backstage/backend-test-utils": "^0.1.8",
"@backstage/cli": "^0.8.1",
"jest": "^26.0.1",
"wait-for-expect": "^3.0.2"
},
"files": [
"dist",
"migrations/**/*.{js,d.ts}"
]
}
@@ -0,0 +1,31 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { resolvePackagePath } from '@backstage/backend-common';
import { Knex } from 'knex';
import { DB_MIGRATIONS_TABLE } from './tables';
const migrationsDir = resolvePackagePath(
'@backstage/backend-tasks',
'migrations',
);
export async function migrateBackendTasks(knex: Knex): Promise<void> {
await knex.migrate.latest({
directory: migrationsDir,
tableName: DB_MIGRATIONS_TABLE,
});
}
@@ -0,0 +1,27 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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.
*/
export const DB_MIGRATIONS_TABLE = 'backstage_backend_tasks__knex_migrations';
export const DB_TASKS_TABLE = 'backstage_backend_tasks__tasks';
export type DbTasksRow = {
id: string;
settings_json: string;
next_run_start_at: Date;
current_run_ticket?: string;
current_run_started_at?: Date | string;
current_run_expires_at?: Date | string;
};

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