Merge branch 'master' of github.com:backstage/backstage into blam/isomorphic-git

* 'master' of github.com:backstage/backstage: (430 commits)
  Fix techdocs for url locations (#3589)
  Slightly more padding in dense tables (#3571)
  build(deps-dev): bump docusaurus in /microsite (#3543)
  fix(lighthouse): Typo & example code tweaks (#3573)
  build(deps-dev): bump @types/testing-library__jest-dom (#3596)
  app,backend,create-app: add files declaration to package.jsons
  cli: update experimental backend:bundle command to output archives to dist
  catalog-backend: read all relations at once
  Deduplicate @changesets/config in yarn.lock
  update dep version
  Change sample repo URL
  Convert card to full Material-UI components
  fix import
  Increase pageSize for search result view (#3565)
  core-api: update ApiFactory type to correctly infer API type and disallow mismatched implementations
  update dependencies version
  Update CHANGELOG.md
  bump app template deps accordingly
  Delete PagerdutyCard.tsx
  remove plugin-catalog dep
  ...
This commit is contained in:
blam
2020-12-07 12:48:02 +01:00
620 changed files with 22947 additions and 4347 deletions
@@ -1,13 +0,0 @@
---
'@backstage/backend-common': patch
'@backstage/cli': patch
'@backstage/core': patch
'@backstage/plugin-cost-insights': patch
'@backstage/plugin-lighthouse': patch
'@backstage/plugin-rollbar': patch
'@backstage/plugin-sentry': patch
'@backstage/plugin-techdocs': patch
'@backstage/plugin-user-settings': patch
---
Added configuration schema
-27
View File
@@ -1,27 +0,0 @@
---
'@backstage/backend-common': minor
'@backstage/cli': minor
'@backstage/config-loader': minor
---
Added support for loading and validating configuration schemas, as well as declaring config visibility through schemas.
The new `loadConfigSchema` function exported by `@backstage/config-loader` allows for the collection and merging of configuration schemas from all nearby dependencies of the project.
A configuration schema is declared using the `https://backstage.io/schema/config-v1` JSON Schema meta schema, which is based on draft07. The only difference to the draft07 schema is the custom `visibility` keyword, which is used to indicate whether the given config value should be visible in the frontend or not. The possible values are `frontend`, `backend`, and `secret`, where `backend` is the default. A visibility of `secret` has the same scope at runtime, but it will be treated with more care in certain contexts, and defining both `frontend` and `secret` for the same value in two different schemas will result in an error during schema merging.
Packages that wish to contribute configuration schema should declare it in a root `"configSchema"` field in `package.json`. The field can either contain an inlined JSON schema, or a relative path to a schema file. Schema files can be in either `.json` or `.d.ts` format.
TypeScript configuration schema files should export a single `Config` type, for example:
```ts
export interface Config {
app: {
/**
* Frontend root URL
* @visibility frontend
*/
baseUrl: string;
};
}
```
-7
View File
@@ -1,7 +0,0 @@
---
'@backstage/plugin-app-backend': minor
---
Use new config schema support to automatically inject config with frontend visibility, in addition to the existing env schema injection.
This removes the confusing behavior where configuration was only injected into the app at build time. Any runtime configuration (except for environment config) in the backend used to only apply to the backend itself, and not be injected into the frontend.
+23
View File
@@ -0,0 +1,23 @@
---
'@backstage/core-api': patch
'@backstage/dev-utils': patch
---
Update ApiFactory type to correctly infer API type and disallow mismatched implementations.
This fixes for example the following code:
```ts
interface MyApi {
myMethod(): void
}
const myApiRef = createApiRef<MyApi>({...});
createApiFactory({
api: myApiRef,
deps: {},
// This should've caused an error, since the empty object does not fully implement MyApi
factory: () => ({}),
})
```
@@ -1,5 +0,0 @@
---
'@backstage/plugin-cost-insights': patch
---
remove excessive margin from cost overview banner
@@ -1,5 +0,0 @@
---
'@backstage/plugin-cost-insights': minor
---
remove cost insights currency feature flag
@@ -1,5 +0,0 @@
---
'@backstage/plugin-cost-insights': patch
---
Fix savings/excess display calculation
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Update experimental backend bundle command to only output archives to `dist/` instead of a full workspace mirror in `dist-workspace/`.
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/plugin-circleci': patch
'@backstage/plugin-jenkins': patch
---
Refactor to support ADR004 module exporting.
For more information, see https://backstage.io/docs/architecture-decisions/adrs-adr004.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Batch the fetching of relations
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-catalog': patch
'@backstage/plugin-pagerduty': patch
---
Added pagerduty plugin to example app
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-import': patch
---
Align plugin ID and fix variable typo
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-import': patch
---
Add register existing component instructions
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-catalog-backend': patch
---
An entity A, that exists in the catalog, can no longer be overwritten by registering a different location that also tries to supply an entity with the same kind+namespace+name. Writes of that new entity will instead be rejected with a log message similar to `Rejecting write of entity Component:default/artist-lookup from file:/Users/freben/dev/github/backstage/packages/catalog-model/examples/components/artist-lookup-component.yaml because entity existed from github:https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/artist-lookup-component.yaml`
+13
View File
@@ -0,0 +1,13 @@
---
'@backstage/create-app': patch
---
Add `"files": ["dist"]` to both app and backend packages. This ensures that packaged versions of these packages do not contain unnecessary files.
To apply this change to an existing app, add the following to `packages/app/package.json` and `packages/backend/package.json`:
```json
"files": [
"dist"
]
```
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/cli': patch
---
Support specifying listen host/port for frontend
-6
View File
@@ -1,6 +0,0 @@
---
'@backstage/backend-common': patch
'@backstage/integration': patch
---
Added the integration package
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/backend-common': minor
---
Refactored UrlReader.readTree to be required and accept (url, options)
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Fix config schema for `.app.listen`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search': patch
---
change default size for pageSize in search result view
+8
View File
@@ -0,0 +1,8 @@
---
'example-backend': patch
'@backstage/plugin-scaffolder-backend': patch
'@backstage/plugin-techdocs-backend': patch
'@backstage/create-app': patch
---
Unify `dockerode` library and type dependency versions
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/catalog-client': minor
---
Changed the getEntities interface to (1) nest parameters in an object, (2) support field selection, and (3) return an object with an items field for future extension
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/catalog-model': minor
'@backstage/plugin-catalog-backend': minor
---
Remove the deprecated fields `ancestors` and `descendants` from the `Group` entity.
See https://github.com/backstage/backstage/issues/3049 and the PRs linked from it for details.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Extracted pushToRemote function for reuse between publishers
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/theme': patch
---
Add a little more padding in dense tables
+14
View File
@@ -0,0 +1,14 @@
---
'@backstage/core': minor
---
Introducing a new optional property within `app-config.yaml` called `auth.environment` to have configurable environment value for `auth.providers`
**Default Value:** 'development'
**Optional Values:** 'production' | 'development'
**Migration-steps:**
- To override the default value, one could simply introduce the new property `environment` within the `auth` section of the `config.yaml`
- re-run the build to reflect the changed configs
+25 -9
View File
@@ -1,4 +1,5 @@
abc
andrewthauer
Apdex
api
Api
@@ -9,21 +10,24 @@ async
Avro
backrub
Balachandran
benjdlambert
Bigtable
Billett
Blackbox
bool
boolean
builtins
Chai
changeset
changesets
Changesets
changset
chanwit
Chanwit
cisphobia
cissexist
classname
cli
cloudbuild
cncf
codeblocks
Codecov
@@ -50,6 +54,7 @@ Dockerfile
Dockerize
dockerode
Docusaurus
dzolotusky
eg
Ek
env
@@ -59,13 +64,16 @@ facto
failover
Figma
Firekube
freben
Fredrik
github
Github
gitlab
Gitlab
Grafana
graphql
graphviz
Gustavsson
Hackathons
haproxy
heroku
@@ -74,6 +82,7 @@ horizontalpodautoscalers
Hostname
http
https
Iain
img
incentivised
inlined
@@ -93,8 +102,8 @@ learnings
lerna
Lerna
magiclink
Maintainership
mailto
maintainership
Malus
md
microsite
@@ -111,6 +120,7 @@ msw
namespace
namespaces
Namespaces
namespacing
neuro
newrelic
nginx
@@ -121,6 +131,7 @@ npm
nvm
oauth
Oauth
oidc
Okta
Oldsberg
onboarding
@@ -159,22 +170,27 @@ Rollup
Rosaceae
rst
rsync
rugvip
ruleset
sam
scaffolded
scaffolder
Scaffolder
semlas
semver
Serverless
Sinon
smartsymobls
Snyk
sparklines
Spotifiers
spotify
Spotify
squidfunk
src
stefanalund
subkey
subtree
superfences
Superfences
superset
@@ -192,10 +208,13 @@ theres
toc
tolerations
Tolerations
toolchain
toolsets
tooltip
tooltips
touchpoints
ui
untracked
upvote
url
utils
@@ -204,14 +223,11 @@ Voi
Wealthsimple
Weaveworks
Webpack
www
WWW
xyz
yaml
Zalando
Zhou
Billett
cloudbuild
Grafana
Iain
Snyk
www
WWW
Zolotusky
zoomable
+1 -1
View File
@@ -19,4 +19,4 @@ jobs:
# Calls out to `changeset version`, but also runs prettier
version: yarn release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
+1
View File
@@ -96,6 +96,7 @@ typings/
.nuxt
dist
dist-types
dist-workspace
# Gatsby files
.cache/
+1 -1
View File
@@ -6,4 +6,4 @@ registry "https://registry.npmjs.org/"
disable-self-update-check true
lastUpdateCheck 1580389148099
yarn-path ".yarn/releases/yarn-1.22.1.js"
network-timeout 600000
network-timeout 300000
+1 -5
View File
@@ -1,13 +1,9 @@
# Backstage Changelog
This is a best-effort changelog where we manually collect breaking changes. It is not an exhaustive list of all changes or even features added.
This changelog is no longer being updated and will be removed in the future, as each package now has its own changelog instead. It was a best-effort changelog where we manually collected breaking changes during the `v0.1.1-alpha.<n>` releases.
If you encounter issues while upgrading to a newer version, don't hesitate to reach out on [Discord](https://discord.gg/EBHEGzX) or [open an issue](https://github.com/backstage/backstage/issues/new/choose)!
## Next Release
> Collect changes for the next release below
## v0.1.1-alpha.26
### @backstage/cli
+31 -38
View File
@@ -1,52 +1,45 @@
# Backstage Governance
# Process for becoming a maintainer
This document defines project governance for the project.
## a) Your organization is not yet a maintainer
## Maintainers
- Express interest to the sponsors that your organization is interested in becoming a maintainer. Becoming a maintainer generally means that you are going to be spending substantial time on Backstage for the foreseeable future. You should have domain expertise and be extremely proficient in TypeScript.
- We will expect you to start contributing increasingly complicated PRs, under the guidance of the existing maintainers.
- We may ask you to do some PRs from our backlog.
- As you gain experience with the code base and our standards, we will ask you to do code reviews for incoming PRs.
- After a period of approximately 2-3 months of working together and making sure we see eye to eye, the existing sponsors and maintainers will confer and decide whether to grant maintainer status or not. We make no guarantees on the length of time this will take, but 2-3 months is the approximate goal.
Backstage Maintainers have write access to the Backstage GitHub repository https://github.com/backstage/backstage. The current maintainers can be found in [MAINTAINERS](MAINTAINERS.md).
This privilege is granted with some expectation of responsibility: maintainers are people who care about the Backstage project and want to help it grow and improve. A maintainer is not just someone who can make changes, but someone who has demonstrated his or her ability to collaborate with the team, get the most knowledgeable people to review code, contribute high-quality code, and follow through to fix issues (in code or tests).
A maintainer is a contributor to the Backstage project's success and a citizen helping the project succeed.
## Becoming a Maintainer
## b) Your organization is currently a maintainer
To become a maintainer you need to demonstrate the following:
- commitment to the project
- participate in discussions, contributions, code reviews for 3 months or more,
- perform code reviews for 10 non-trivial pull requests,
- contribute 10 non-trivial pull requests and have them merged into master,
- ability to write good code,
- ability to collaborate with the team,
- understanding of how the team works (policies, processes for testing and code review, etc),
- understanding of the project's code base and coding style.
- First decide whether your organization really needs more people with maintainer access. Valid reasons are "blast radius", a large organization that is working on multiple unrelated projects, etc.
- Contact a sponsor for your organization and express interest.
- Start doing PRs and code reviews under the guidance of your maintainer.
- After a period of 1-2 months the existing sponsors will discuss granting maintainer access.
- Maintainer access can be upgraded to sponsor access after another conference of the existing sponsors.
## Changes in Maintainership
# Maintainer responsibilities
A new maintainer must be proposed by an existing maintainer by opening an issue (with title `Maintainer Nomination`) to the Backstage GitHub repository (https://github.com/backstage/backstage) containing the following information:
- Monitor email aliases.
- Monitor Discord (delayed response is perfectly acceptable).
- Triage GitHub issues and perform pull request reviews for other maintainers and the community.
- Triage build issues - file issues for known flaky builds or bugs, and either fix or find someone to fix any master build breakages.
- During GitHub issue triage, apply all applicable ([labels](https://github.com/backstage/backstage/labels)) to each new issue. Labels are extremely useful for future issue follow up. Which labels to apply is somewhat subjective so just use your best judgment. A few of the most important labels that are not self explanatory are:
- good first issue: Mark any issue that can reasonably be accomplished by a new contributor with this label.
- help wanted: Unless it is immediately obvious that someone is going to work on an issue (and if so assign it), mark it help wanted.
- Make sure that ongoing PRs are moving forward at the right pace or closing them.
- Participate when called upon in the security release process. Note that although this should be a rare occurrence, if a serious vulnerability is found, the process may take up to several full days of work to implement. This reality should be taken into account when discussing time commitment obligations with employers.
- In general, continue to be willing to spend at least 25% of one's time working on Backstage (~1.25 business days per week).
- We currently maintain an "on-call" rotation within the maintainers. Each on-call is 1 week. Although all maintainers are welcome to perform all of the above tasks, it is the on-call maintainer's responsibility to triage incoming issues/questions and marshal ongoing work forward. To reiterate, it is not the responsibility of the on-call maintainer to answer all questions and do all reviews, but it is their responsibility to make sure that everything is being actively covered by someone.
- nominee's first and last name,
- nominee's email address and GitHub user name,
- an explanation of why the nominee should be a maintainer,
- a list of links to non-trivial pull requests (top 10) authored by the nominee.
# When does a maintainer lose maintainer status
Maintainers can be removed by a 2/3 majority vote.
If a maintainer is no longer interested or cannot perform the maintainer duties listed above, they should volunteer to be moved to emeritus status. In extreme cases this can also occur by a vote of the sponsors and maintainers per the voting process below.
## Approving PRs
# Conflict resolution and voting
PRs may be merged after receiving at least one approval from a maintainer.
In general, we prefer that technical issues and maintainer membership are amicably worked out between the persons involved. If a dispute cannot be decided independently, the sponsors and maintainers can be called in to decide an issue. If the sponsors and maintainers themselves cannot decide an issue, the issue will be resolved by voting. The voting process is a simple majority in which each sponsor receives two votes and each maintainer receives one vote.
## GitHub Project Administration
# Adding new projects to the Backstage GitHub organization
Maintainers will be added to the collaborators list of the Backstage repository with "Write" access.
## Changes in Governance
All changes in Governance require a 2/3 majority vote.
## Other Changes
Unless specified above, all other changes to the project require a 2/3 majority vote.
Additionally, any maintainer may request that any change require a 2/3 majority vote.
New projects will be added to the Backstage organization via GitHub issue discussion in one of the existing projects in the organization. Once sufficient discussion has taken place (~3-5 business days but depending on the volume of conversation), the maintainers of the project where the issue was opened (since different projects in the organization may have different maintainers) will decide whether the new project should be added. See the section above on voting if the maintainers cannot easily decide.
-21
View File
@@ -1,21 +0,0 @@
# Maintainers
- See [CONTRIBUTING.md](CONTRIBUTING.md) for general contribution guidelines.
## Current Maintainers 🏓
- Stefan Ålund - Spotify (GitHub: @stefanalund, Discord: @stalund)
- Patrik Oldsberg - Spotify (GitHub: @Rugvip, Discord: @Rugvip)
- Fredrik Adelöw - Spotify (GitHub: @freben, Discord: @freben)
- Ben Lambert - Spotify (GitHub: @benjdlambert, Discord: @blam)
## Plugin maintainers 🧩
Teams and individuals that maintain a plugin (or another non-core module of the code) can get write access to that part of the repo using CODEOWNERS.
## Hall of Fame 👏
People that have made significant contributions to the project and earned write access:
- Andrew Thauer - Wealthsimple (GitHub: @andrewthauer)
- Oliver Sand - SDA SE (GitHub: @Fox32)
+24
View File
@@ -0,0 +1,24 @@
- See [CONTRIBUTING.md](CONTRIBUTING.md) for general contribution guidelines.
- See [GOVERNANCE.md](GOVERNANCE.md) for governance guidelines and responsibilities.
This page lists all active sponsors and maintainers.
# Sponsors
- Niklas Gustavsson ([protocol7](https://github.com/protocol7)) (ngn@spotify.com)
- Dave Zolotusky ([dzolotusky](https://github.com/dzolotusky)) (dzolo@spotify.com)
- Lee Mills ([leemills83](https://github.com/leemills83)) (leem@spotify.com)
# Maintainers
- Patrik Oldsberg ([rugvip](https://github.com/rugvip)) (Discord: @Rugvip)
- Fredrik Adelöw ([freben](https://github.com/freben)) (Discord: @freben)
- Ben Lambert ([benjdlambert](https://github.com/benjdlambert)) (Discord: @blam)
- Stefan Ålund ([stefanalund](https://github.com/stefanalund)) (Discord: @stalund)
# Friends of Backstage
People that have made significant contributions to the project and earned write access.
- Andrew Thauer - Wealthsimple (GitHub: [andrewthauer](https://github.com/andrewthauer))
- Oliver Sand - SDA SE (GitHub: [Fox32](https://github.com/Fox32))
+45 -4
View File
@@ -38,7 +38,7 @@ proxy:
headers:
Authorization:
$env: TRAVISCI_AUTH_TOKEN
travis-api-version: 3
travis-api-version: '3'
'/newrelic/apm/api':
target: https://api.newrelic.com/v2
@@ -46,6 +46,12 @@ proxy:
X-Api-Key:
$env: NEW_RELIC_REST_API_KEY
'/pagerduty':
target: https://api.pagerduty.com
headers:
Authorization:
$env: PAGERDUTY_TOKEN
'/buildkite/api':
target: https://api.buildkite.com/v2/
headers:
@@ -66,8 +72,8 @@ sentry:
rollbar:
organization: my-company
accountToken:
$env: ROLLBAR_ACCOUNT_TOKEN
# NOTE: The rollbar-backend & accountToken key may be deprecated in the future (replaced by a proxy config)
accountToken: my-rollbar-account-token
lighthouse:
baseUrl: http://localhost:3003
@@ -140,6 +146,19 @@ catalog:
# dn: ou=access,ou=groups,ou=example,dc=example,dc=net
# options:
# filter: (&(objectClass=some-group-class)(!(groupType=email)))
microsoftGraphOrg:
### Example for how to add your Microsoft Graph tenant
#providers:
# - target: https://graph.microsoft.com/v1.0
# authority: https://login.microsoftonline.com
# tenantId:
# $env: MICROSOFT_GRAPH_TENANT_ID
# clientId:
# $env: MICROSOFT_GRAPH_CLIENT_ID
# clientSecret:
# $env: MICROSOFT_GRAPH_CLIENT_SECRET_TOKEN
# userFilter: accountEnabled eq true and userType eq 'member'
# groupFilter: securityEnabled eq false and mailEnabled eq true and groupTypes/any(c:c+eq+'Unified')
locations:
# Backstage example components
@@ -176,8 +195,11 @@ scaffolder:
api:
token:
$env: AZURE_TOKEN
auth:
environment: development
### Providing an auth.session.secret will enable session support in the auth-backend
# session:
# secret: custom session secret
providers:
google:
development:
@@ -222,6 +244,20 @@ auth:
$env: AUTH_OAUTH2_AUTH_URL
tokenUrl:
$env: AUTH_OAUTH2_TOKEN_URL
oidc:
development:
metadataUrl:
$env: AUTH_OIDC_METADATA_URL
clientId:
$env: AUTH_OIDC_CLIENT_ID
clientSecret:
$env: AUTH_OIDC_CLIENT_SECRET
authorizationUrl:
$env: AUTH_OIDC_AUTH_URL
tokenUrl:
$env: AUTH_OIDC_TOKEN_URL
tokenSignedResponseAlg:
$env: AUTH_OIDC_TOKEN_SIGNED_RESPONSE_ALG
auth0:
development:
clientId:
@@ -261,6 +297,9 @@ costInsights:
bigQuery:
name: BigQuery
icon: search
events:
name: Events
icon: data
metrics:
DAU:
name: Daily Active Users
@@ -277,3 +316,5 @@ homepage:
timezone: 'Europe/Stockholm'
- label: TYO
timezone: 'Asia/Tokyo'
pagerduty:
eventsBaseUrl: 'https://events.pagerduty.com/v2'
+18 -16
View File
@@ -48,10 +48,9 @@ source candidates. (And we'll probably end up writing some brand new ones, too.)
### What's the roadmap for Backstage?
We envision three phases, which you can learn about in
[our project roadmap](https://github.com/backstage/backstage#project-roadmap).
Even though the open source version of Backstage is relatively new compared to
our internal version, we have already begun work on various aspects of all three
phases. Looking at the
[our project roadmap](overview/roadmap.md). Even though the open source version
of Backstage is relatively new compared to our internal version, we have already
begun work on various aspects of all three phases. Looking at the
[milestones for active issues](https://github.com/backstage/backstage/milestones)
will also give you a sense of our progress.
@@ -115,8 +114,7 @@ type of content. Plugins all use a common set of platform APIs and reusable UI
components. Plugins can fetch data either from the backend or an API exposed
through the proxy.
Learn more about
[the different components](https://github.com/backstage/backstage#overview) that
Learn more about [the different components](overview/what-is-backstage.md) that
make up Backstage.
### Do I have to write plugins in TypeScript?
@@ -126,17 +124,17 @@ APIs in TypeScript, but aren't forcing it on individual plugins.
### How do I find out if a plugin already exists?
Before you write a plugin,
You can browse and search for all available plugins in the
[Plugin Marketplace](https://backstage.io/plugins).
If you can't find it in the marketplace, before you write a plugin
[search the plugin issues](https://github.com/backstage/backstage/issues?q=is%3Aissue+label%3Aplugin+)
to see if it already exists or is in the works. If no one's thought of it yet,
great! Open a new issue as
to see if is in the works. If no one's thought of it yet, great! Open a new
issue as
[a plugin suggestion](https://github.com/backstage/backstage/issues/new/choose)
and describe what your plugin will do. This will help coordinate our
contributors' efforts and avoid duplicating existing functionality.
You can browse and search for all available plugins in the
[Plugin Marketplace](https://backstage.io/plugins).
### Which plugin is used the most at Spotify?
By far, our most-used plugin is our TechDocs plugin, which we use for creating
@@ -182,6 +180,10 @@ comes to [deployment](https://backstage.io/docs/getting-started/deployment-k8s),
the system integrator (typically, the infrastructure team in your organization)
maintains Backstage in your own environment.
For more information, see our
[Owners](https://github.com/backstage/backstage/blob/master/OWNERS.md) and
[Governance](https://github.com/backstage/backstage/blob/master/GOVERNANCE.md).
### Does Spotify provide a managed version of Backstage?
No, this is not a service offering. We build the piece of software, and someone
@@ -215,14 +217,14 @@ data is shared with.
Yes. The core frontend framework could be used for building any large-scale web
application where (1) multiple teams are building separate parts of the app, and
(2) you want the overall experience to be consistent. That being said, in
[Phase 2](https://github.com/backstage/backstage#project-roadmap) of the project
we will add features that are needed for developer portals and systems for
managing software ecosystems. Our ambition will be to keep Backstage modular.
[Phase 2](overview/roadmap.md) of the project we will add features that are
needed for developer portals and systems for managing software ecosystems. Our
ambition will be to keep Backstage modular.
### How can I get involved?
Jump right in! Come help us fix some of the
[early bugs and first issues](https://github.com/backstage/backstage/labels/good%20first%20issue)
[early bugs and good first issues](https://github.com/backstage/backstage/contribute)
or reach [a new milestone](https://github.com/backstage/backstage/milestones).
Or write an open source plugin for Backstage, like this
[Lighthouse plugin](https://github.com/backstage/backstage/tree/master/plugins/lighthouse).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 128 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 64 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

+103 -44
View File
@@ -6,57 +6,71 @@ description: Documentation on Auth backend classes
## How Does Authentication Work?
The Backstage application can use various authentication providers for
authentication. A provider has to implement an `AuthProviderRouteHandlers`
interface for handling authentication. This interface consists of four methods.
Each of these methods is hosted at an endpoint `/auth/[provider]/method`, where
`method` performs a certain operation as follows:
The Backstage application can use various external authentication providers for
authentication. An external provider is wrapped using an
`AuthProviderRouteHandlers` interface for handling authentication. This
interface consists of four methods. Each of these methods is hosted at an
endpoint (by default) `/api/auth/[provider]/method`, where `method` performs a
certain operation as follows:
```
/auth/[provider]/start -> start
/auth/[provider]/handler/frame -> frameHandler
/auth/[provider]/refresh -> refresh
/auth/[provider]/logout -> logout
/auth/[provider]/start -> Initiate a login from the web page
/auth/[provider]/handler/frame -> Handle a finished authentication operation
/auth/[provider]/refresh -> Refresh the validity of a login
/auth/[provider]/logout -> Log out a logged-in user
```
For more information on how these methods are used and for which purpose, refer
to the [OAuth documentation](oauth.md).
The flow is as follows:
For details on the parameters, input and output conditions for each method,
refer to the type documentation under
`plugins/auth-backend/src/providers/types.ts`.
1. A user attempts to sign in.
2. A popup window is opened, pointing to the `auth` endpoint. That endpoint does
initial preparations and then re-directs the user to an external
authenticator, still inside the popup.
3. The authenticator validates the user and returns the result of the validation
(success OR failure), to the wrapper's endpoint (`handler/frame`).
4. The `handler/frame` rendered b´webpage will issue the appropriate response to
the webpage that opened the popup window, and the popup is closed.
5. The user signs out by clicking on a UI interface and the webpage makes a
request to logout the user.
There are currently two different classes for two authentication mechanisms that
implement this interface: an `OAuthAdapter` for [OAuth](https://oauth.net/2/)
based mechanisms and a `SAMLAuthProvider` for
[SAML](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html)
based mechanisms.
[SAML](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html).
### OAuth mechanisms
If you do not have an `OAuth2` or `SAML` based authentication provider, look in
the section [below](#implementing-your-own-auth-wrapper).
### OAuth Mechanisms
For more information on how these methods are used and for which purpose, refer
to the [OAuth documentation](oauth.md).
Currently OAuth is assumed to be the de facto authentication mechanism for
Backstage based applications.
Backstage comes with a "batteries-included" set of supported commonly used OAuth
providers: Okta, GitHub, Google, GitLab, and a generic OAuth2 provider.
providers: Okta, GitHub, Google, GitLab, and a generic OAuth2 provider. For a
list of available providers, look at the available wrappers in
`backstage/plugins/auth-backend/src/providers/`.
All of these use the authorization flow of OAuth2 to implement authentication.
All of these use the **authorization flow** of OAuth2 to implement
authentication.
If your authentication provider is any of the above mentioned (except generic
OAuth2) providers, you can configure them by setting the right variables in
`app-config.yaml` under the `auth` section.
If your authentication provider is any of the above mentioned providers, you can
configure them by setting the right variables in `app-config.yaml` under the
`auth` section.
### Configuration
Each authentication provider (except SAML) needs five parameters: an OAuth
client ID, a client secret, an authorization endpoint and a token endpoint, and
an app origin. The app origin is the URL at which the frontend of the
application is hosted, and it is read from the `app.baseUrl` config. This is
required because the application opens a popup window to perform the
authentication, and once the flow is completed, the popup window sends a
`postMessage` to the frontend application to indicate the result of the
operation. Also this URL is used to verify that authentication requests are
coming from only this endpoint.
client ID, a client secret, an authorization endpoint, a token endpoint, and an
app origin. The app origin is the URL at which the frontend of the application
is hosted, and it is read from the `app.baseUrl` config. This is required
because the application opens a popup window to perform the authentication, and
once the flow is completed, the popup window sends a `postMessage` to the
frontend application to indicate the result of the operation. Also this URL is
used to verify that authentication requests are coming from only this endpoint.
These values are configured via the `app-config.yaml` present in the root of
your app folder.
@@ -85,20 +99,60 @@ auth:
...
```
## Technical Notes
## Implementing Your Own Auth Wrapper
### OAuthEnvironmentHandler
The core interface of any auth wrapper is the `AuthProviderRouteHandlers`
interface. This interface has four methods corresponding to the API described in
the initial section. Any auth wrapper will have to implement this interface.
The concept of an "env" is core to the way the auth backend works. It uses an
When initiating a login, a pop-up window is created by the frontend, to allow
the user to initiate a login. This login request is done to the `/start`
endpoint which is handled by the `start` method.
The `start` method re-directs to the external auth provider who authenticates
the request and re-directs the request to the `/frame/handler` endpoint, which
is handled by the `frameHandler` method.
The `frameHandler` returns an HTML response, containing a script that does a
`postMessage` to the frontend's window, containing the result of the request.
The `WebMessageResponse` type is the message sent by the `postMessage` to the
frontend.
A `postMessageResponse` utility function wraps the logic of generating a
`postMessage` response that ensures that CORS is successfully handled. This
function takes an `express.Response`, a `WebMessageResponse` and the URL of the
frontend (`appOrigin`) as parameters and return an HTML page with the script and
the message.
### OAuth Wrapping Interfaces.
Each OAuth external provider is supported by a corresponding
[Passport](https://github.com/jaredhanson/passport) strategy. For a generic
OAuth2 provider, passport has a `passport-oauth2` strategy. The strategy class
handles the implementation details of working with each provider.
Each strategy is wrapped by an `OAuthHandlers` interface.
This interface cannot be directly used as an Express HTTP request handler. To do
so, `OAuthHandlers` are wrapped in an `OAuthAdapter`, which implements the
`AuthProviderRouterHandlers` interface.
#### Env
The concept of an `env` is core to the way the auth backend works. It uses an
`env` query parameter to identify the environment in which the application is
running (`development`, `staging`, `production`, etc). Each runtime can support
multiple environments at the same time and the right handler for each request is
identified and dispatched to based on the `env` parameter. All
`AuthProviderRouteHandlers` are wrapped within an `OAuthEnvironmentHandler`.
running (`development`, `staging`, `production`, etc). Each runtime can
simultaneously support multiple environments at the same time and the right
handler for each request is identified and dispatched to, based on the `env`
parameter.
To instantiate multiple OAuth providers for different environments, use
`OAuthEnvironmentHandler` is a utility wrapper for an `OAuthHandlers` that
implements the `AuthProviderRouteHandlers` interface while supporting multiple
`env`s.
To instantiate OAuth providers (the same but for different environments), use
`OAuthEnvironmentHandler.mapConfig`. It's a helper to iterate over a
configuration object that is a map of environment to configurations. See one of
configuration object that is a map of environments to configurations. See one of
the existing OAuth providers for an example of how it is used.
Given the following configuration:
@@ -113,13 +167,18 @@ production:
```
The `OAuthEnvironmentHandler.mapConfig(config, envConfig => ...)` call will
split the `config` by the top level `development` and `production` keys, and
pass on each block as `envConfig`.
split the config by the top level `development` and `production` keys, and pass
on each block as `envConfig`.
For a list of currently available providers, look in the `factories` module
located in `plugins/auth-backend/src/providers/factories.ts`
For convenience, the `AuthProviderFactory` is a factory function that has to be
implemented which can then generate a `AuthProviderRouteHandlers` for a given
provider.
### OAuth2 provider
All of the supported providers provide an `AuthProviderFactory` that returns an
`OAuthEnvironmentHandler`, capable of handling authentication for multiple
environments.
### OAuth2 Provider
The `oauth2` provider abstracts a generic **OAuth2 + OIDC** based authentication
provider. What this means is that after the application has been given
+1 -1
View File
@@ -93,6 +93,6 @@ sign-in methods.
More details are provided in dedicated sections of the documentation.
- [OAuth](./oauth.md): Description of the generic OAuth flow implemented by the
[auth-backend](../../plugins/auth-backend).
[auth-backend](https://github.com/backstage/backstage/tree/master/plugins/auth-backend).
- [Glossary](./glossary.md): Glossary of some common terms related to the auth
flows.
@@ -31,6 +31,7 @@ we recommend that you name them `catalog-info.yaml`.
- [Kind: Resource](#kind-resource)
- [Kind: System](#kind-system)
- [Kind: Domain](#kind-domain)
- [Kind: Location](#kind-location)
## Overall Shape Of An Entity
@@ -43,7 +44,7 @@ software catalog API.
"kind": "Component",
"metadata": {
"annotations": {
"backstage.io/managed-by-location": "file:/tmp/component-info.yaml",
"backstage.io/managed-by-location": "file:/tmp/catalog-info.yaml",
"example.com/service-discovery": "artistweb",
"circleci.com/project-slug": "github/example-org/artist-website"
},
@@ -93,6 +94,43 @@ significance and have reserved purposes and distinct shapes.
See below for details about these fields.
## Substitutions In The Descriptor Format
The descriptor format supports substitutions using `$text`, `$json`, and
`$yaml`.
Placeholders like `$json: https://example.com/entity.json` are substituted by
the content of the referenced file. Files can be referenced from any configured
integration similar to locations by passing an absolute URL. It's also possible
to reference relative files like `./referenced.yaml` from the same location.
Relative references are handled relative to the folder of the
`catalog-info.yaml` that contains the placeholder. There are three different
types of placeholders:
- `$text`: Interprets the contents of the referenced file as plain text and
embeds it as a string.
- `$json`: Interprets the contents of the referenced file as JSON and embeds the
parsed structure.
- `$yaml`: Interprets the contents of the referenced file as YAML and embeds the
parsed structure.
For example, this can be used to load the definition of an API entity from a web
server and embed it as a string in the field `spec.definition`:
```yaml
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: petstore
description: The Petstore API
spec:
type: openapi
lifecycle: production
owner: petstore@example.com
definition:
$text: https://petstore.swagger.io/v2/swagger.json
```
## Common to All Kinds: The Envelope
The root envelope object has the following structure.
@@ -344,7 +382,7 @@ spec:
type: website
lifecycle: production
owner: artist-relations@example.com
implementsApis:
providesApis:
- artist-api
```
@@ -408,12 +446,35 @@ group of people in an organizational structure.
### `spec.implementsApis` [optional]
**NOTE**: This field was marked for deprecation on Nov 25nd, 2020. It will be
removed entirely from the model on Dec 14th, 2020 in the repository and will not
be present in released packages following the next release after that. Please
update your code to not consume this field before the removal date.
Links APIs that are implemented by the component, e.g. `artist-api`. This field
is optional.
The software catalog expects a list of one or more strings that references the
names of other entities of the `kind` `API`.
This field has the same behavior as `spec.providesApis`.
### `spec.providesApis` [optional]
Links APIs that are provided by the component, e.g. `artist-api`. This field is
optional.
The software catalog expects a list of one or more strings that references the
names of other entities of the `kind` `API`.
### `spec.consumesApis` [optional]
Links APIs that are consumed by the component, e.g. `artist-api`. This field is
optional.
The software catalog expects a list of one or more strings that references the
names of other entities of the `kind` `API`.
## Kind: Template
Describes the following entity kind:
@@ -592,6 +653,9 @@ The current set of well-known and common values for this field is:
[OpenAPI](https://swagger.io/specification/) version 2 or version 3 spec.
- `asyncapi` - An API definition based on the
[AsyncAPI](https://www.asyncapi.com/docs/specifications/latest/) spec.
- `graphql` - An API definition based on
[GraphQL schemas](https://spec.graphql.org/) for consuming
[GraphQL](https://graphql.org/) based APIs.
- `grpc` - An API definition based on
[Protocol Buffers](https://developers.google.com/protocol-buffers) to use with
[gRPC](https://grpc.io/).
@@ -660,9 +724,7 @@ metadata:
spec:
type: business-unit
parent: ops
ancestors: [ops, global-synergies, acme-corp]
children: [backstage, other]
descendants: [backstage, other, team-a, team-b, team-c, team-d]
```
In addition to the [common envelope metadata](#common-to-all-kinds-the-metadata)
@@ -698,20 +760,6 @@ namespace as the user. Only `Group` entities may be referenced. Most commonly,
this field points to a group in the same namespace, so in those cases it is
sufficient to enter only the `metadata.name` field of that group.
### `spec.ancestors` [required]
The recursive list of parents up the hierarchy, by stepping through parents one
by one. The list must be present, but may be empty if `parent` is not present.
The first entry in the list is equal to `parent`, and then the following ones
are progressively farther up the hierarchy.
The entries of this array are
[entity references](https://backstage.io/docs/features/software-catalog/references),
with the default kind `Group` and the default namespace equal to the same
namespace as the user. Only `Group` entities may be referenced. Most commonly,
these entries point to groups in the same namespace, so in those cases it is
sufficient to enter only the `metadata.name` field of those groups.
### `spec.children` [required]
The immediate child groups of this group in the hierarchy (whose `parent` field
@@ -726,20 +774,6 @@ namespace as the user. Only `Group` entities may be referenced. Most commonly,
these entries point to groups in the same namespace, so in those cases it is
sufficient to enter only the `metadata.name` field of those groups.
### `spec.descendants` [required]
The immediate and recursive child groups of this group in the hierarchy
(children, and children's children, etc.). The list must be present, but may be
empty if there are no child groups. The items are not guaranteed to be ordered
in any particular way.
The entries of this array are
[entity references](https://backstage.io/docs/features/software-catalog/references),
with the default kind `Group` and the default namespace equal to the same
namespace as the user. Only `Group` entities may be referenced. Most commonly,
these entries point to groups in the same namespace, so in those cases it is
sufficient to enter only the `metadata.name` field of those groups.
## Kind: User
Describes the following entity kind:
@@ -811,3 +845,58 @@ This kind is not yet defined, but is reserved [for future use](system-model.md).
## Kind: Domain
This kind is not yet defined, but is reserved [for future use](system-model.md).
## Kind: Location
Describes the following entity kind:
| Field | Value |
| ------------ | ----------------------- |
| `apiVersion` | `backstage.io/v1alpha1` |
| `kind` | `Location` |
A location is a marker that references other places to look for catalog data.
Descriptor files for this kind may look as follows.
```yaml
apiVersion: backstage.io/v1alpha1
kind: Location
metadata:
name: org-data
spec:
type: url
targets:
- http://github.com/myorg/myproject/org-data-dump/catalog-info-staff.yaml
- http://github.com/myorg/myproject/org-data-dump/catalog-info-consultants.yaml
```
In addition to the [common envelope metadata](#common-to-all-kinds-the-metadata)
shape, this kind has the following structure.
### `apiVersion` and `kind` [required]
Exactly equal to `backstage.io/v1alpha1` and `Location`, respectively.
### `spec.type` [optional]
The single location type, that's common to the targets specified in the spec. If
it is left out, it is inherited from the location type that originally read the
entity data. For example, if you have a `url` type location, that when read
results in a `Location` kind entity with no `spec.type`, then the referenced
targets in the entity will implicitly also be of `url` type. This is useful
because you can define a hierarchy of things in a directory structure using
relative target paths (see below), and it will work out no matter if it's
consumed locally on disk from a `file` location, or as uploaded on a VCS.
### `spec.target` [optional]
A single target as a string. Can be either an absolute path/URL (depending on
the type), or a relative path such as `./details/catalog-info.yaml` which is
resolved relative to the location of this Location entity itself.
### `spec.targets` [optional]
A list of targets as strings. They can all be either absolute paths/URLs
(depending on the type), or relative paths such as `./details/catalog-info.yaml`
which are resolved relative to the location of this Location entity itself.
@@ -1,7 +1,7 @@
---
id: extending-the-model
title: Extending the model
description: Documentation on Extending the model
description: Documentation on extending the catalog model
---
The Backstage catalog [entity data model](descriptor-format.md) is based on the
@@ -28,63 +28,324 @@ Backstage comes with a number of catalog concepts out of the box:
We'll list different possibilities for extending this below.
## Adding a New apiVersion of an Existing Kind
Example intents:
> "I want to evolve this core kind, tweaking the semantics a bit so I will bump
> the apiVersion a step"
> "This core kind is a decent fit but we want to evolve it at will so we'll move
> it to our own company's apiVersion space and use that instead of
> `backstage.io`."
The `backstage.io` apiVersion space is reserved for use by the Backstage
maintainers. Please do not change or add versions within that space.
If you add an [apiVersion](descriptor-format.md#apiversion-and-kind-required)
space of your own, you are effectively branching out from the underlying kind
and making your own. An entity kind is identified by the apiVersion + kind pair,
so even though the resulting entity may be similar to the core one, there will
be no guarantees that plugins will be able to parse or understand its data. See
below about adding a new kind.
## Adding a New Kind
> TODO: Fill in
Example intents:
> "The kinds that come with the package are lacking. I want to model this other
> thing that is a poor fit for either of the builtins."
> "This core kind is a decent fit but we want to evolve it at will so we'll move
> it to our own company's apiVersion space and use that instead of
> `backstage.io`."
A [kind](descriptor-format.md#apiversion-and-kind-required) is an overarching
family, or an idea if you will, of entities that also share a schema. Backstage
comes with a number of builtin ones that we believe are useful for a large
variety of needs that one may want to model in Backstage. The primary ambition
is to map things to these kinds, but sometimes you may want or need to extend
beyond them.
Introducing a new apiVersion is basically the same as adding a new kind. Bear in
mind that most plugins will be compiled against the builtin
`@backstage/catalog-model` package and have expectations that kinds align with
that.
The catalog backend itself, from a storage and API standpoint, does not care
about the kind of entities it stores. Extending with new kinds is mainly a
matter of permitting them to pass validation when building the backend catalog
using the `CatalogBuilder`, and then to make plugins be able to understand the
new kind.
For the consuming side, it's a different story. Adding a kind has a very large
impact. The very foundation of Backstage is to attach behavior and views and
functionality to entities that we ascribe some meaning to. There will be many
places where code checks `if (kind === 'X')` for some hard coded `X`, and casts
it to a concrete type that it imported from a package such as
`@backstage/catalog-model`.
If you want to model something that doesn't feel like a fit for either of the
builtin kinds, feel free to reach out to the Backstage maintainers to discuss
how to best proceed.
If you end up adding that new kind, you must namespace its `apiVersion`
accordingly with a prefix that makes sense, typically based on your organization
name - e.g. `my-company.net/v1`. Also do pick a new `kind` identifier that does
not collide with the builtin kinds.
## Adding a New Type of an Existing Kind
Backstage natively supports tracking of the following component
[`type`](descriptor-format.md)'s:
Example intents:
- Services
- Websites
- Libraries
- Documentation
- Other
> "This is clearly a component, but it's of a type that doesn't quite fit with
> the ones I've seen before."
![](../../assets/software-catalog/bsc-extend.png)
> "We don't call our teams "team", can't we put "flock" as the group type?"
Since these types are likely not the only kind of software you will want to
track in Backstage, it is possible to add your own software types that fit your
organization's data model. Inside Spotify our model has grown significantly over
the years, and now includes ML models, Apps, data pipelines and many more.
Some entity kinds have a `type` field in its spec. This is where an organization
are free to express the variety of entities within a kind. This field is
expected to follow some taxonomy that makes sense for yourself. The chosen value
may affect what operations and views are enabled in Backstage for that entity.
Inside Spotify our model has grown significantly over the years, and our
component types now include ML models, apps, data pipelines and many more.
It might be tempting to put software that doesn't fit into any of the existing
types into Other. There are a few reasons why we advise against this; firstly,
we have found that it is preferred to match the conceptual model that your
engineers have when describing your software. Secondly, Backstage helps your
engineers manage their software by integrating the infrastructure tooling
through plugins. Different plugins are used for managing different types of
components.
types into an Other catch-all type. There are a few reasons why we advise
against this; firstly, we have found that it is preferred to match the
conceptual model that your engineers have when describing your software.
Secondly, Backstage helps your engineers manage their software by integrating
the infrastructure tooling through plugins. Different plugins are used for
managing different types of components.
For example, the
[Lighthouse plugin](https://github.com/backstage/backstage/tree/master/plugins/lighthouse)
only makes sense for Websites. The more specific you can be in how you model
your software, the easier it is to provide plugins that are contextual.
> TODO: Fill in
Adding a new type takes relatively little effort and carries little risk. Any
type value is accepted by the catalog backend, but plugins may have to be
updated if you want particular behaviors attached to that new type.
## Changing the Validation Rules for The Entity Envelope or Metadata Fields
Example intents:
> "We want to import our old catalog but the default set of allowed characters
> for a metadata.name are too strict."
> "I want to change the rules for annotations so that I'm allowed to store any
> data in annotation values, not just strings."
After pieces of raw entity data have been read from a location, they are passed
through a fixed number of so called `Validators`, as part of the entity policy
check step. They ensure that the types and syntax of the base envelope and
metadata make sense - in short, things that aren't entity-kind-specific. Some or
all of these validators can be replaced when building the backend catalog using
the `CatalogBuilder`.
The risk and impact of this type of extension varies, based on what it is that
you want to do. For example, extending the valid character set for kinds,
namespaces and names can be fairly harmless, with a few notable exceptions -
there is code that expects these to never ever contain a colon or slash, for
example, and introducing URL-unsafe characters risks breaking plugins that
aren't careful about encoding arguments. Supporting non-strings in annotations
may be possible but has not yet been tried out in the real world - there is
likely to be some level of plugin breakage that can be hard to predict.
Before making this kind of extension, we recommend that you contact the
Backstage maintainers or a support partner to discuss your use case.
## Changing the Validation Rules for Core Entity Fields
> TODO: Fill in
Example intent:
> "I don't like that the owner is mandatory. I'd like it to be optional."
After reading and policy-checked entity data from a location, it is sent through
the processor chain looking for processors that implement the
`validateEntityKind` step, to see that the data is of a known kind and abides by
its schema. There is a builtin processor that implements this for all known core
kinds and matches the data against their fixed validation schema. This processor
can be replaced when building the backend catalog using the `CatalogBuilder`,
with a processor of your own that validates the data differently.
This type of extension is high risk, and may have high impact across the
ecosystem depending on the type of change that is made. It is therefore not
recommended in normal cases. There will be a large number of plugins and
processors - and even the core itself - that make assumptions about the shape of
the data and import the typescript data type from the `@backstage/catalog-model`
package.
## Adding New Fields to the Metadata Object
> TODO: Fill in
Example intent:
> "Our entities have this auxiliary property that I would like to express for
> several entity kinds and it doesn't really fit as a spec field."
The metadata object is currently left open for extension. Any unknown fields
found in the metadata will just be stored verbatim in the catalog. However we
want to caution against extending the metadata excessively. Firstly, you run the
risk of colliding with future extensions to the model. Secondly, it is common
that this type of extension lives more comfortably elsewhere - primarily in the
metadata labels or annotations, but sometimes you even may want to make a new
component type or similar instead.
There are some situations where metadata can be the right place. If you feel
that you have run into such a case and that it would apply to others, do feel
free to contact the Backstage maintainers or a support partner to discuss your
use case. Maybe we can extend the core model to benefit both you and others.
## Adding New Fields to the Spec Object of an Existing Kind
> TODO: Fill in
Example intent:
> "The builtin Component kind is fine but we want to add an additional field to
> the spec for describing whether it's in prod or staging."
A kind's schema validation typically doesn't forbid "unknown" fields in an
entity `spec`, and the catalog will happily store whatever is in it. So doing
this will usually work from the catalog's point of view.
Adding fields like this is subject to the same risks as mentioned about metadata
extensions above. Firstly, you run the risk of colliding with future extensions
to the model. Secondly, it is common that this type of extension lives more
comfortably elsewhere - primarily in the metadata labels or annotations, but
sometimes you even may want to make a new component type or similar instead.
There are some situations where the spec can be the right place. If you feel
that you have run into such a case and that it would apply to others, do feel
free to contact the Backstage maintainers or a support partner to discuss your
use case. Maybe we can extend the core model to benefit both you and others.
## Adding a New Annotation
> TODO: Fill in
Example intents:
> "Our custom made build system has the concept of a named pipeline-set, and we
> want to associate individual components with their corresponding pipeline-sets
> so we can show their build status."
> "We have an alerting system that automatically monitors service health, and
> there's this integration key that binds the service to an alerts pool. We want
> to be able to show the ongoing alerts for our services in Backstage so it'd be
> nice to attach that integration key to the entity somehow."
Annotations are mainly intended to be consumed by plugins, for feature detection
or linking into external systems. Sometimes they are added by humans, but often
they are automatically generated at ingestion time by processors. There is a set
of [well-known annotations](well-known-annotations.md), but you are free to add
additional ones. This carries no risk or impact to other systems as long as you
abide by the following naming rules.
- The `backstage.io` annotation prefix is reserved for use by the Backstage
maintainers. Reach out to us if you feel that you would like to make an
addition to that prefix.
- Annotations that pertain to a well known third party system should ideally be
prefixed with a domain, in a way that makes sense to a reader and connects it
clearly to the system (or the maker of the system). For example, you might use
a `pagerduty.com` prefix for pagerduty related annotations, but maybe not
`ldap.com` for LDAP annotations since it's not directly affiliated with or
owned by an LDAP foundation/company/similar.
- Annotations that have no prefix at all, are considered local to your Backstage
instance and can be used freely as such, but you should not make use of them
outside of your organization. For example, if you were to open source a plugin
that generates or consumes annotations, then those annotations must be
properly prefixed with your company domain or a domain that pertains to the
annotation at hand.
## Adding a New Label
> TODO: Fill in
Example intents:
> "Our process reaping system wants to periodically scrape for components that
> have a certain property."
> "It'd be nice if our service owners could just tag their components somehow to
> let the CD system know to automatically generate SRV records or not for that
> service."
Labels are mainly intended to be used for filtering of entities, by external
systems that want to find entities that have some certain property. This is
sometimes used for feature detection / selection. An example could be to add a
label `deployments.my-company.net/register-srv: "true"`.
At the time of writing this, the use of labels is very limited and we are still
settling together with the community on how to best use them. If you feel that
your use case fits the labels best, we would appreciate if you let the Backstage
maintainers know.
You are free to add labels. This carries no risk or impact to other systems as
long as you abide by the following naming rules.
- The `backstage.io` label prefix is reserved for use by the Backstage
maintainers. Reach out to us if you feel that you would like to make an
addition to that prefix.
- Labels that pertain to a well known third party system should ideally be
prefixed with a domain, in a way that makes sense to a reader and connects it
clearly to the system (or the maker of the system). For example, you might use
a `pagerduty.com` prefix for pagerduty related labels, but maybe not
`ldap.com` for LDAP labels since it's not directly affiliated with or owned by
an LDAP foundation/company/similar.
- Labels that have no prefix at all, are considered local to your Backstage
instance and can be used freely as such, but you should not make use of them
outside of your organization. For example, if you were to open source a plugin
that generates or consumes labels, then those labels must be properly prefixed
with your company domain or a domain that pertains to the label at hand.
## Adding a New Relation Type
> TODO: Fill in
Example intents:
> "We have this concept of service maintainership, separate from ownership, that
> we would like to make relations to individual users for."
> We feel that we want to explicitly model the team-to-global-department mapping
> as a relation, because it is core to our org setup and we frequently query for
> it.
Any processor can emit relations for entities as they are being processed, and
new processors can be added when building the backend catalog using the
`CatalogBuilder`. They can emit relations based on the entity data itself, or
based on information gathered from elsewhere. Relations are directed and go from
a source entity to a target entity. They are also tied to the entity that
originated them - the one that was subject to processing when the relation was
emitted. Relations may be dangling (referencing something that does not actually
exist by that name in the catalog), and callers need to be aware of that.
There is a set of [well-known relations](well-known-relations.md), but you are
free to emit your own as well. You cannot change the fact that they are directed
and have a source and target that have to be an
[entity reference](references.md), but you can invent your own types. You do not
have to make any changes to the catalog backend in order to accept new relation
types.
At the time of writing this, we do not have any namespacing/prefixing scheme for
relation types. The type is also not validated to contain only some particular
set of characters. Until rules for this are settled, you should stick to using
only letters, dashes and digits, and to avoid collisions with future core
relation types, you may want to prefix the type somehow. For example:
`myCompany-maintainerOf` + `myCompany-maintainedBy`.
If you have a suggestion for a relation type to be elevated to the core
offering, reach out to the Backstage maintainers or a support partner.
## Using a Well-Known Relation Type for a New Purpose
Example intents:
> "The ownerOf/ownedBy relation types sound like a good fit for expressing how
> users are technical owners of our company specific ServiceAccount kind, and we
> want to reuse those relation types for that."
At the time of writing, this is uncharted territory. If the documented use of a
relation states that one end of the relation commonly is a User or a Group, for
example, then consumers are likely to have conditional statements on the form
`if (x.kind === 'User') {} else {}`, which get confused when an unexpected kind
appears.
If you want to extend the use of an established relation type in a way that has
an effect outside of your organization, reach out to the Backstage maintainers
or a support partner to discuss risk/impact. It may even be that one end of the
relation could be considered for addition to the core.
+2 -2
View File
@@ -51,7 +51,7 @@ spec:
type: service
lifecycle: experimental
owner: group:pet-managers
implementsApis:
providesApis:
- petstore
- internal/streetlights
- hello-world
@@ -66,7 +66,7 @@ catalog that is of kind `Group`, namespace `default` (which, actually, also can
be left out in its own yaml file because that's the default value there too),
and name `pet-managers`.
The entries in `implementsApis` are also references. In this case, none of them
The entries in `providesApis` are also references. In this case, none of them
needs to specify a kind since we know from the context that that's the only kind
that's supported here. The second entry specifies a namespace but the other ones
don't, and in this context, the default is to refer to the same namespace as the
@@ -190,9 +190,25 @@ metadata:
```
The value of these annotations are the corresponding attributes that were found
when ingestion the entity from LDAP. Not all of them may be present, depending
when ingesting the entity from LDAP. Not all of them may be present, depending
on what attributes that the server presented at ingestion time.
### graph.microsoft.com/tenant-id, graph.microsoft.com/group-id, graph.microsoft.com/user-id
```yaml
# Example:
metadata:
annotations:
graph.microsoft.com/tenant-id: 6902611b-ffc1-463f-8af3-4d5285dc057b
graph.microsoft.com/group-id: c57e8ba2-6cc4-1039-9ebc-d5f241a7ca21
graph.microsoft.com/user-id: 2de244b5-104b-4e8f-a3b8-dce3c31e54b6
```
The value of these annotations are the corresponding attributes that were found
when ingesting the entity from the Microsoft Graph API. Not all of them may be
present, depending on what attributes that the server presented at ingestion
time.
### sonarqube.org/project-key
```yaml
@@ -222,22 +238,9 @@ annotation, with the same value format.
### backstage.io/definition-at-location
This annotation allowed to load the API definition from another location. Now
placeholders can be used instead:
```
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: petstore
description: The Petstore API
spec:
type: openapi
lifecycle: production
owner: petstore@example.com
definition:
$text: https://petstore.swagger.io/v2/swagger.json
```
This annotation allowed to load the API definition from another location. Use
[substitution](./descriptor-format.md#substitutions-in-the-descriptor-format)
instead.
## Links
@@ -45,17 +45,28 @@ entity, but there will always be one ultimate owner.
This relation is commonly generated based on `spec.owner` of the owned entity,
where present.
### `consumesApi` and `providesApi`
### `providesApi` and `apiProvidedBy`
A relation with an [API](descriptor-format.md#kind-api) entity, typically from a
[Component](descriptor-format.md#kind-component) or
[System](descriptor-format.md#kind-system).
These relations express that a component or system either exposes an API -
meaning that it hosts callable endpoints from which you can consume that API -
or that they are dependent on being able to consume that API.
These relations express that a component or system exposes an API - meaning that
it hosts callable endpoints from which you can consume that API.
This relation is commonly generated based on `spec.implementsApis` of the
This relation is commonly generated based on `spec.providesApis` of the
component or system in question.
### `consumesApi` and `apiConsumedBy`
A relation with an [API](descriptor-format.md#kind-api) entity, typically from a
[Component](descriptor-format.md#kind-component) or
[System](descriptor-format.md#kind-system).
These relations express that a component or system consumes an API - meaning
that it depends on endpoints of the API.
This relation is commonly generated based on `spec.consumesApis` of the
component or system in question.
### `dependsOn` and `dependencyOf`
@@ -55,6 +55,10 @@ contains more information about the required fields.
Once we have a `template.yaml` ready, we can then add it to the service catalog
for use by the scaffolder.
_NOTE_: When the `publish` step is completed, it is currently assumed by the
scaffolder that the final repository should contain a `catalog-info.yaml` in
order to register this with the Catalog in Backstage.
Currently the catalog supports loading definitions from GitHub + Local Files. To
load from other places, not only will there need to be another preparer, but the
support to load the location will also need to be added to the Catalog.
+149 -3
View File
@@ -1,7 +1,153 @@
---
id: architecture
title: Architecture
description: Documentation on Architecture
title: TechDocs Architecture
description: Documentation on TechDocs Architecture
---
![TechDocs Big Picture](../../assets/techdocs/techdocs_big_picture.png)
## Basic (out-of-the-box)
When you deploy Backstage (with TechDocs enabled by default), you get a basic
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.
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.
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.
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.
## Recommended deployment
This is how we recommend deploying TechDocs in production environment.
<img data-zoomable src="../../assets/techdocs/architecture-recommended.drawio.svg" alt="TechDocs Architecture diagram" />
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 build 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).
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
build 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 build 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.
_Feel free to suggest better ideas to us in #docs-like-code channel in Discord
or via a GitHub issue._
### Security consideration
Our biggest security concern is managing the access to the docs in the cloud
storage. We also want to have only one security solution for all different types
of storage (GCS, AWS, custom SFTP server, etc.) Restricting access to the
storage and only allowing `techdocs-backend` to fetch files is a good way to
achieve this.
This would also allow us to use the access control management Backstage when
that is ready.
[Track progress here.](https://github.com/backstage/backstage/issues/3218)
In theory, you can directly enable TechDocs Reader to read from your storage.
But, you will have to think about how to do it without the docs being public and
how access to user groups is managed.
For cloud storage access tokens, `techdocs-backend` only needs a token with Read
permissions. But in your CI/CD system, there needs to be a token with Write
permissions to publish the generated docs site files.
## FAQs
**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
_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
as you scale with the number of documentation sites and sizes of them. So you
would want to make sure the deployment is as stable as possible. Hence there is
a recommended approach. There can be even more deployment approaches to TechDocs
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.
**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 build docs per-page, so we would have to build
all docs for a entity on every request.
# Future work
_Ideas here are far fetched and not in the project's milestone for near future
(~6 months)._
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.
**Work in progress 🚧**
- Basic setup with cloud storage solution.
**Not implemented yet ❌**
- `techdocs-cli` is able to generate docs in CI/CD environment.
- `techdocs-cli` is able to publish docs site to any storage.
- `techdocs-backend` integration with Backstage access control management.
+2 -2
View File
@@ -95,7 +95,7 @@ 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/spotify/backstage/blob/master/packages/techdocs-container/Dockerfile)
[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.
@@ -104,7 +104,7 @@ 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/spotify/backstage/blob/f9f70c225548017b6a14daea75b00fbd399c11eb/packages/techdocs-container/techdocs-core/requirements.txt#L11).
[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.
@@ -6,7 +6,53 @@ description: Documentation on How Configuring App with plugins
## Adding existing plugins to your app
Coming soon!
The following steps assume that you have created a new Backstage app and want to
add an existing plugin to it. We are using the
[CircleCI](https://github.com/backstage/backstage/blob/master/plugins/circleci/README.md)
plugin in this example.
1. Add the plugin's NPM package to the repo:
```bash
yarn add @backstage/plugin-circleci
```
2. Add the plugin itself:
```js
// packages/app/src/plugins.ts
export { plugin as Circleci } from '@backstage/plugin-circleci';
```
3. Register the plugin router:
```jsx
// packages/app/src/components/catalog/EntityPage.tsx
import { Router as CircleCIRouter } from '@backstage/plugin-circleci';
// Then somewhere inside <EntityPageLayout>
<EntityPageLayout.Content
path="/ci-cd/*"
title="CI/CD"
element={<CircleCIRouter />}
/>;
```
Note that stand-alone plugins that are not "attached" to the Software Catalog
would be added outside the `EntityPage`.
4. [Optional] Add proxy config:
```yaml
// app-config.yaml
proxy:
'/circleci/api':
target: https://circleci.com/api/v1.1
headers:
Circle-Token:
$env: CIRCLECI_AUTH_TOKEN
```
### Adding a plugin page to the Sidebar
+37 -1
View File
@@ -14,7 +14,7 @@ need to run Backstage in your own environment.
To create a Backstage app, you will need to have
[Node.js](https://nodejs.org/en/download/) Active LTS Release installed
(currently v12).
(currently v14).
Backstage provides a utility for creating new apps. It guides you through the
initial setup of selecting the name of the app and a database for the backend.
@@ -38,6 +38,42 @@ app-folder is the name that was provided when prompted.
Inside that directory, it will generate all the files and folder structure
needed for you to run your app.
### Linking in local Backstage packages
It can often be useful to try out changes to the packages in the main Backstage
repo within your own app. For example if you want to make modifications to
`@backstage/core` and try them out in your app.
To link in external packages, add them to your `package.json` and `lerna.json`
workspace paths. These can be either relative or absolute paths with or without
globs. For example:
```json
"packages": [
"packages/*",
"plugins/*",
"../backstage/packages/core", // New path added to work on @backstage/core
],
```
Then reinstall packages to make yarn set up symlinks:
```bash
yarn install
```
With this in place you can now modify the `@backstage/core` package within the
main repo, and have those changes be reflected and tested in your app. Simply
run your app using `yarn start` as normal.
Note that for backend packages you need to make sure that linked packages are
not dependencies of any non-linked package. If you for example want to work on
`@backstage/backend-common`, you need to also link in other backend plugins and
packages that depend on `@backstage/backend-common`, or temporarily disable
those plugins in your backend. This is because the transformation of backend
module tree stops whenever a non-local package is encountered, and from that
point node will `require` packages directly for that entire module subtree.
### Troubleshooting
The create app command doesn't always work as expected, this is a collection of
@@ -8,19 +8,19 @@ description: Documentation on How to run Backstage Locally
- Node.js
First make sure you are using Node.js with an Active LTS Release, currently v12.
First make sure you are using Node.js with an Active LTS Release, currently v14.
This is made easy with a version manager such as
[nvm](https://github.com/nvm-sh/nvm) which allows for version switching.
```bash
# Installing a new version
nvm install 12
> Downloading and installing node v12.18.3...
> Now using node v12.18.3 (npm v6.14.6)
nvm install 14
> Downloading and installing node v14.15.1...
> Now using node v14.15.1 (npm v6.14.8)
# Checking your version
node --version
> v12.18.3
> v14.15.1
```
- Yarn
+384
View File
@@ -0,0 +1,384 @@
---
id: stability-index
title: Stability Index
description:
An overview of the commitment to stability for different parts of the
Backstage codebase.
---
## Overview
The purpose of the Backstage Stability Index is to communicate the stability of
various parts of the project. It is tracked using a scoring system where a
higher score indicates a higher level of stability and is a commitment to
smoother transitions between breaking changes. Importantly, the Stability Index
does not supersede [semver](https://semver.org/), meaning we will still adhere
to semver and only do breaking changes in minor releases as long as we are on
`0.x`.
Each package or section is assigned a stability score between 0 and 3, with each
point building on top of the previous one:
- **0** - Breaking changes are noted in the changelog, and documentation is
updated.
- **1** - The changelog entry includes a clearly documented upgrade path,
providing guidance for how to migrate previous usage patterns to the new
version.
- **2** - Breaking changes always include a deprecation phase where both the old
and the new APIs can be used in parallel. This deprecation must have been
released for at least two weeks before the deprecated API is removed in a
minor version bump.
- **3** - The time limit for the deprecation is 3 months instead of two weeks.
TL;DR:
- **0** - There's a changelog entry.
- **1** - There's a migration guide.
- **2** - 2 weeks of deprecation.
- **3** - 3 months of deprecation.
## Packages
### [`example-app`](https://github.com/backstage/backstage/tree/master/packages/app/)
This is the `packages/app` package, and it serves as an example as well as
utility for local development in the main Backstage repo.
Stability: `N/A`
### [`example-backend`](https://github.com/backstage/backstage/tree/master/packages/backend/)
This is the `packages/backend` package, and it serves as an example as well as
utility for local development in the main Backstage repo.
Stability: `N/A`
### [`backend-common`](https://github.com/backstage/backstage/tree/master/packages/backend-common/)
A collection of common helpers to be used by both backend plugins, and for
constructing backend packages.
Stability: `1`
### [`catalog-client`](https://github.com/backstage/backstage/tree/master/packages/catalog-client/)
An HTTP client for interacting with the catalog backend. Usable both in frontend
and Backend.
Stability: `0`. This is a very new addition and we have some immediate changes
planned.
### [`catalog-model`](https://github.com/backstage/backstage/tree/master/packages/catalog-model/)
Contains the core catalog model, and utilities for working with entities. Usable
both in frontend and Backend.
Stability: `2`. The catalog model is evolving, but because of the broad usage we
want to ensure some stability.
### [`cli`](https://github.com/backstage/backstage/tree/master/packages/cli/)
The main toolchain used for Backstage development. The various CLI commands and
options passed to those commands, as well as the environment variables read by
the CLI, are considered to be the interface that the stability index refers to.
The build output may change over time and is not considered a breaking change
unless it is likely to affect external tooling.
Stability: `2`
### [`cli-common`](https://github.com/backstage/backstage/tree/master/packages/cli-common/)
Lightweight utilities used by the various Backstage CLIs, not intended for
external use.
Stability: `N/A`
### [`config`](https://github.com/backstage/backstage/tree/master/packages/config/)
Provides the logic and interfaces for reading static configuration.
Stability: `2`
### [`config-loader`](https://github.com/backstage/backstage/tree/master/packages/config-loader/)
Used to load in static configuration, mainly for use by the CLI and
@backstage/backend-common.
Stability: `1`. Mainly intended for internal use.
### [`core`](https://github.com/backstage/backstage/tree/master/packages/core/)
#### Section: React Components
All of the React components exported from `src/components/` and `src/layout/`
Stability: `1`. These components have not received a proper review of the API,
but we also want to ensure stability.
#### Section: Plugin API
The parts of the core API that are used by plugins, and the way plugins expose
functionality to apps and other plugins. Includes for example `createPlugin`,
`createRouteRef`, `createApiRef`.
Stability: `2`. There are planned breaking changes around the way that plugins
expose features and do routing. We still commit to keeping a short deprecation
period so that plugins outside of the main repo have time to migrate.
#### Section: App API
The APIs used exclusively in the app, such as `createApp` and the system icons.
Stability: `2`
#### Section: Utility API Definitions
The type declarations of the core Utility APIs.
Stability: `2`. Changes to the Utility API type declarations need time to
propagate.
#### Section: Utility API Implementations
The interfaces and default implementations for various Utility APIs, such as
ErrorApi, IdentityApi, the auth APIs, etc.
Stability: `1`. Most changes to the core utility APIs will not lead to
widespread breaking changes since most apps rely on the default implementations.
### [`core-api`](https://github.com/backstage/backstage/tree/master/packages/core-api/)
The non-visual parts of @backstage/core. Everything in this packages is
re-exported from @backstage/core, and this package should not be used directly.
Stability: See @backstage/core
### [`create-app`](https://github.com/backstage/backstage/tree/master/packages/create-app/)
The CLI used to scaffold new Backstage projects.
Stability: `2`
### [`dev-utils`](https://github.com/backstage/backstage/tree/master/packages/dev-utils/)
Provides utilities for developing plugins in isolation.
Stability: `0`. This package is largely broken and needs updates.
### [`docgen`](https://github.com/backstage/backstage/tree/master/packages/docgen/)
Internal CLI utility for generating API Documentation.
Stability: `N/A`
### [`e2e-test`](https://github.com/backstage/backstage/tree/master/packages/e2e-test/)
Internal CLI utility for running e2e tests.
Stability: `N/A`
### [`storybook`](https://github.com/backstage/backstage/tree/master/packages/storybook/)
Internal storybook build for publishing stories to
https://backstage.io/storybook
Stability: `N/A`
### [`test-utils`](https://github.com/backstage/backstage/tree/master/packages/test-utils/)
Utilities for writing tests for Backstage plugins and apps.
Stability: `2`
### [`test-utils-core`](https://github.com/backstage/backstage/tree/master/packages/test-utils-core/)
Internal testing utilities that are separated out for usage in
@backstage/core-api. All exports are re-exported by @backstage/test-utils. This
package should not be depended on directly.
Stability: See @backstage/test-utils
### [`theme`](https://github.com/backstage/backstage/tree/master/packages/theme/)
The core Backstage MUI theme along with customization utilities.
#### Section: TypeScript
This is the TypeScript API exported by the theme package.
Stability: `2`
#### Section: Visual Theme
The visual theme exported by the theme packages, where for example changing a
color could be considered a breaking change.
Stability: `1`
## Plugins
Plugins are rarely marked as stable as the `@backstage/core` plugin API is under
heavy development.
Many backend plugins are split into "REST API" and "TypeScript Interface"
sections. The "TypeScript Interface" refers to the API used to integrate the
plugin into the backend.
Any plugin that is not listed below is untracked and can generally be considered
unstable with a score of `0`. Open a Pull Request if you want your plugin to be
added!
### [`api-docs`](https://github.com/backstage/backstage/tree/master/plugins/api-docs/)
Components to discover and display API entities as an extension to the catalog
plugin.
Stability: `0`
### [`app-backend`](https://github.com/backstage/backstage/tree/master/plugins/app-backend/)
A backend plugin that can be used to serve the frontend app and inject
configuration.
Stability: `2`
### [`auth-backend`](https://github.com/backstage/backstage/tree/master/plugins/auth-backend/)
A backend plugin that implements the backend portion of the various
authentication flows used in Backstage.
#### Section: REST API
Stability: `2`
#### Section: TypeScript Interface
Stability: `1`
### [`catalog`](https://github.com/backstage/backstage/tree/master/plugins/catalog/)
The frontend plugin for the catalog, with the table and building blocks for the
entity pages.
Stability: `1`. We're planning some work to overhaul how entity pages are
constructed.
### [`catalog-backend`](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend/)
The backend API for the catalog, also exposes the processing subsystem for
customization of the catalog. Powers the @backstage/plugin-catalog frontend
plugin.
#### Section: REST API
Stability: `1`. There are plans to remove and rework some endpoints.
#### Section: TypeScript Interface
Stability: `1`. There are plans to rework parts of the Processor interface.
### [`catalog-graphql`](https://github.com/backstage/backstage/tree/master/plugins/catalog-graphql/)
Provides the catalog schema and resolvers for the graphql backend.
Stability: `0`. Under heavy development and subject to change.
### [`explore`](https://github.com/backstage/backstage/tree/master/plugins/explore/)
A frontend plugin that introduces the concept of exploring internal and external
tooling in an organization.
Stability: `0`. Only an example at the moment and not customizable.
### [`graphiql`](https://github.com/backstage/backstage/tree/master/plugins/graphiql/)
Integrates GraphiQL as a tool to browse GraphQL API endpoints inside Backstage.
Stability: `1`
### [`graphql`](https://github.com/backstage/backstage/tree/master/plugins/graphql/)
A backend plugin that provides
Stability: `0`. Under heavy development and subject to change.
### [`kubernetes`](https://github.com/backstage/backstage/tree/master/plugins/kubernetes/)
The frontend component of the Kubernetes plugin, used to browse and visualize
Kubernetes resources.
Stability: `1`.
### [`kubernetes-backend`](https://github.com/backstage/backstage/tree/master/plugins/kubernetes-backend/)
The backend component of the Kubernetes plugin, used to fetch Kubernetes
resources from clusters and associate them with entities in the Catalog.
Stability: `1`.
### [`proxy-backend`](https://github.com/backstage/backstage/tree/master/plugins/proxy-backend/)
A backend plugin used to set up proxying to other endpoints based on static
configuration.
Stability: `1`
### [`register-component`](https://github.com/backstage/backstage/tree/master/plugins/register-component/)
A frontend plugin that allows the user to register entity locations in the
catalog.
Stability: `0`. This plugin is likely to be replaced by a generic entity import
plugin instead.
### [`scaffolder`](https://github.com/backstage/backstage/tree/master/plugins/scaffolder/)
The frontend scaffolder plugin where one can browse templates and initiate
scaffolding jobs.
Stability: `1`
### [`scaffolder-backend`](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-backend/)
The backend scaffolder plugin that provides an implementation for templates in
the catalog.
Stability: `1`. There is planned work to rework the scaffolder in
https://github.com/backstage/backstage/issues/2771.
### [`tech-radar`](https://github.com/backstage/backstage/tree/master/plugins/tech-radar/)
Visualize the your company's official guidelines of different areas of software
development.
Stability: `0`
### [`techdocs`](https://github.com/backstage/backstage/tree/master/plugins/techdocs/)
The frontend component of the TechDocs plugin, used to browse technical
documentation of entities.
Stability: `1`
### [`techdocs-backend`](https://github.com/backstage/backstage/tree/master/plugins/techdocs-backend/)
The backend component of the TechDocs plugin, used to transform and serve
TechDocs.
Stability: `0`
### [`user-settings`](https://github.com/backstage/backstage/tree/master/plugins/user-settings/)
A frontend plugin that provides a page where the user can tweak various
settings.
Stability: `1`
### [`welcome`](https://github.com/backstage/backstage/tree/master/plugins/welcome/)
A plugin that can be used to welcome the user to Backstage.
Stability: `0`. This used to be the start page for the example app, but has been
replaced by the catalog plugin. It is still viewable at `/welcome` but may be
removed.
+20 -13
View File
@@ -52,19 +52,7 @@ configuration will lead to the proxy acting on backend requests to
The value inside each route is either a simple URL string, or an object on the
format accepted by
[http-proxy-middleware](https://www.npmjs.com/package/http-proxy-middleware). It
is also possible to limit the forwarded HTTP methods with the configuration
`allowedMethods`, for example `allowedMethods: ['GET']` to enforce read-only
access.
By default, the proxy will only forward safe HTTP request headers to the target.
Those are based on the headers that are considered safe for CORS and includes
headers like `content-type` or `last-modified`, as well as all headers that are
set by the proxy. If the proxy should forward other headers like
`authorization`, this must be enabled by the `allowedHeaders` config, for
example `allowedHeaders: ['Authorization']`. This should help to not
accidentally forward confidential headers (`cookie`, `X-Auth-Request-User`) to
third-parties.
[http-proxy-middleware](https://www.npmjs.com/package/http-proxy-middleware).
If the value is a string, it is assumed to correspond to:
@@ -85,3 +73,22 @@ except with the following caveats for convenience:
`'^/api/proxy/larger-example/v1/': '/'` is added. That means that a request to
`/api/proxy/larger-example/v1/some/path` will be translated to a request to
`http://larger.example.com:8080/svc.v1/some/path`.
There are also additional settings:
- `allowedMethods`: Limit the forwarded HTTP methods. For example
`allowedMethods: ['GET']` enforces read-only access.
- `allowedHeaders`: A list of headers that should be forwarded to and received
from the target.
By default, the proxy will only forward safe HTTP request headers to the target.
Those are based on the headers that are considered safe for CORS and includes
headers like `content-type` or `last-modified`, as well as all headers that are
set by the proxy. If the proxy should forward other headers like
`authorization`, this must be enabled by the `allowedHeaders` config, for
example `allowedHeaders: ['Authorization']`. This should help to not
accidentally forward confidential headers (`cookie`, `X-Auth-Request-User`) to
third-parties.
The same logic applies to headers that are sent from the target back to the
frontend.
+9 -5
View File
@@ -4,7 +4,7 @@ title: createPlugin
description: Documentation on createPlugin
---
Taking a plugin config as argument and returns a new plugin.
Takes a plugin config as an argument and returns a new plugin.
## Plugin Config
@@ -28,18 +28,22 @@ type PluginHooks = {
### Creating a basic plugin
Showcasing adding multiple routes, a feature flag and a redirect.
Showcasing adding a route and a feature flag.
```jsx
import { createPlugin } from '@backstage/core';
import { createPlugin, createRouteRef } from '@backstage/core';
import ExampleComponent from './components/ExampleComponent';
export const rootRouteRef = createRouteRef({
path: '/new-plugin',
title: 'New Plugin',
});
export default createPlugin({
id: 'new-plugin',
register({ router, featureFlags }) {
router.addRoute(rootRouteRef, ExampleComponent);
featureFlags.register('enable-example-component');
router.registerRoute('/new-plugin', ExampleComponent);
},
});
```
+6
View File
@@ -89,6 +89,12 @@ are separated out into their own folder, see further down.
There are no "core" packages in the backend. Instead we have `backend-common`
which contains helper middleware and other utils.
- [`catalog-client`](https://github.com/backstage/backstage/tree/master/packages/catalog-client) -
An isomorphic client to interact with the Software Catalog. Backend plugins
can use the package directly. Frontend plugins can use the client by using
`@backstage/plugin-catalog` in combination with `useApi` and the
`catalogApiRef`.
- [`catalog-model/`](https://github.com/backstage/backstage/tree/master/packages/catalog-model) -
You can consider this to be a library for working with the catalog of sorts.
It contains the definition of an
+5
View File
@@ -204,3 +204,8 @@ For more information about custom pages, click [here](https://docusaurus.io/docs
# Full Documentation
Full documentation can be found on the [website](https://docusaurus.io/).
## Additional notes
- If you want to make images zoomable on click, add the `data-zoomable` attribute to your `img` element.
- In a docs or blog `.md` file, convert `![This is image](/microsite/static/img/code.png)` syntax to `<img data-zoomable src="/microsite/static/img/code.png" alt="This is image" />`
+1 -1
View File
@@ -5,5 +5,5 @@ authorUrl: https://sda.se/
category: Discovery
description: Components to discover and display API entities as an extension to the catalog plugin.
documentation: https://github.com/backstage/backstage/blob/master/plugins/api-docs/README.md
iconUrl: https://thecoders.io/wp-content/uploads/2019/11/tech-swagger.svg
iconUrl: https://raw.githubusercontent.com/vscode-icons/vscode-icons/master/icons/file_type_swagger.svg
npmPackageName: '@backstage/plugin-api-docs'
+1 -1
View File
@@ -5,5 +5,5 @@ authorUrl: https://roadie.io
category: Monitoring
description: View AWS Lambda functions for your components in Backstage.
documentation: https://roadie.io/backstage/plugins/aws-lambda
iconUrl: https://roadie.io/static/77f62f79e27ae8565496e4df7eef8be5/45f2b/logo.png
iconUrl: https://roadie.io/images/logos/lambda.png
npmPackageName: '@roadiehq/backstage-plugin-aws-lambda'
+12
View File
@@ -0,0 +1,12 @@
---
title: Buildkite
author: roadie.io
authorUrl: https://roadie.io
category: CI
description: View Buildkite CI builds for your service in Backstage.
documentation: https://roadie.io/backstage/plugins/buildkite
iconUrl: https://roadie.io/images/logos/buildkite.png
npmPackageName: '@roadiehq/backstage-plugin-buildkite'
tags:
- ci
- cd
@@ -5,5 +5,5 @@ authorUrl: https://roadie.io/
category: Monitoring
description: View Firebase Functions details for your service in Backstage.
documentation: https://roadie.io/backstage/plugins/firebase-functions
iconUrl: https://roadie.io/static/49fb23200ad0eaa6703b4ddf75c78cf1/45f2b/logo-vertical.png
iconUrl: https://roadie.io/images/logos/firebase.png
npmPackageName: '@roadiehq/backstage-plugin-firebase-functions'
+1 -1
View File
@@ -5,5 +5,5 @@ authorUrl: https://roadie.io
category: Monitoring
description: View GitHub Insights for your components in Backstage.
documentation: https://roadie.io/backstage/plugins/github-insights
iconUrl: https://roadie.io/static/2ad5123c425908efde0c922d707e737b/06c84/code-icon.png
iconUrl: https://roadie.io/images/logos/insights.png
npmPackageName: '@roadiehq/backstage-plugin-github-insights'
@@ -5,5 +5,5 @@ authorUrl: https://roadie.io/
category: CI
description: View GitHub pull requests for your service in Backstage.
documentation: https://roadie.io/backstage/plugins/github-pull-requests
iconUrl: https://roadie.io/static/7f13bb8d861d8dedc5112fb939d215f9/351f2/GitHub-Mark-Light-120px-plus.png
iconUrl: https://roadie.io/images/logos/github.png
npmPackageName: '@roadiehq/backstage-plugin-github-pull-requests'
+9
View File
@@ -0,0 +1,9 @@
---
title: Jira
author: roadie.io
authorUrl: https://roadie.io
category: Project Management
description: View Jira summary for your projects in Backstage.
documentation: https://roadie.io/backstage/plugins/jira
iconUrl: https://roadie.io/images/logos/jira.png
npmPackageName: '@roadiehq/backstage-plugin-jira'
+12
View File
@@ -0,0 +1,12 @@
---
title: Kubernetes
author: Spotify
authorUrl: https://github.com/spotify
category: Kubernetes
description: Surfaces components in a Kubernetes container orchestration environment into the Backstage catalog.
documentation: https://github.com/backstage/backstage/tree/master/plugins/kubernetes
iconUrl: https://raw.githubusercontent.com/cncf/artwork/master/projects/kubernetes/icon/color/kubernetes-icon-color.png
npmPackageName: '@backstage/plugin-kubernetes'
tags:
- kubernetes
- k8s
@@ -5,5 +5,5 @@ authorUrl: https://roadie.io/
category: Security
description: View Security Insights for your components in Backstage.
documentation: https://roadie.io/backstage/plugins/security-insights
iconUrl: https://roadie.io/static/7f13bb8d861d8dedc5112fb939d215f9/351f2/GitHub-Mark-Light-120px-plus.png
iconUrl: https://roadie.io/images/logos/github.png
npmPackageName: '@roadiehq/backstage-plugin-security-insights'
+1 -1
View File
@@ -5,5 +5,5 @@ authorUrl: https://roadie.io/
category: CI
description: View Travis CI builds for your service in Backstage.
documentation: https://roadie.io/backstage/plugins/travis-ci
iconUrl: https://roadie.io/static/af2941eaf0af675facb281d566f42e14/45f2b/travis-ci-mascot-200x200.png
iconUrl: https://roadie.io/images/logos/travis.png
npmPackageName: '@roadiehq/backstage-plugin-travis-ci'
+2 -2
View File
@@ -15,9 +15,9 @@
},
"devDependencies": {
"@spotify/prettier-config": "^9.0.0",
"docusaurus": "^2.0.0-alpha.66",
"docusaurus": "^2.0.0-alpha.378053ac5",
"js-yaml": "^3.14.0",
"prettier": "^2.0.5"
"prettier": "^2.2.1"
},
"prettier": "@spotify/prettier-config"
}
+1
View File
@@ -7,6 +7,7 @@
"overview/vision",
"overview/background",
"overview/adopting",
"overview/stability-index",
"overview/logos"
],
"Getting Started": [
+5 -1
View File
@@ -86,7 +86,11 @@ const siteConfig = {
},
// Add custom scripts here that would be placed in <script> tags.
scripts: ['https://buttons.github.io/buttons.js'],
scripts: [
'https://buttons.github.io/buttons.js',
'https://unpkg.com/medium-zoom@1.0.6/dist/medium-zoom.min.js',
'/js/medium-zoom.js',
],
// On page navigation for the current documentation page.
onPageNav: 'separate',
+5
View File
@@ -1094,3 +1094,8 @@ code {
margin: 0 1.5em;
}
}
/* Zoomed images using the medium-zoom library should be on top of screen. */
.medium-zoom-image {
z-index: 10000;
}
+11
View File
@@ -0,0 +1,11 @@
// Ref: https://github.com/francoischalifour/medium-zoom#options
window.addEventListener(
'load',
() => {
mediumZoom('[data-zoomable]', {
margin: 20,
background: '#000',
});
},
false,
);
+572 -504
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,6 +1,9 @@
site_name: 'Backstage'
site_description: 'Main documentation for Backstage features and platform APIs'
plugins:
- techdocs-core
nav:
- Overview:
- What is Backstage?: 'overview/what-is-backstage.md'
@@ -111,6 +114,3 @@ nav:
- 'support/support.md'
- 'support/project-structure.md'
- FAQ: FAQ.md
plugins:
- techdocs-core
+2 -1
View File
@@ -37,7 +37,8 @@
]
},
"resolutions": {
"**/@roadiehq/backstage-plugin-*/@backstage/core": "0.3.0"
"**/@roadiehq/**/@backstage/core": "*",
"**/@roadiehq/**/@backstage/catalog-model": "*"
},
"version": "1.0.0",
"devDependencies": {
+123
View File
@@ -1,5 +1,128 @@
# example-app
## 0.2.5
### Patch Changes
- Updated dependencies [7eb8bfe4a]
- Updated dependencies [fe7257ff0]
- Updated dependencies [a2cfa311a]
- Updated dependencies [69f38457f]
- Updated dependencies [bec334b33]
- Updated dependencies [303c5ea17]
- Updated dependencies [b4488ddb0]
- Updated dependencies [4a655c89d]
- Updated dependencies [08835a61d]
- Updated dependencies [a9fd599f7]
- Updated dependencies [8a16e8af8]
- Updated dependencies [bcc211a08]
- Updated dependencies [00670a96e]
- Updated dependencies [da2ad65cb]
- Updated dependencies [ebf37bbae]
- @backstage/plugin-api-docs@0.3.1
- @backstage/plugin-cost-insights@0.4.2
- @backstage/plugin-sentry@0.2.4
- @backstage/plugin-welcome@0.2.2
- @backstage/cli@0.4.0
- @backstage/catalog-model@0.4.0
- @backstage/plugin-catalog-import@0.3.0
- @backstage/plugin-scaffolder@0.3.2
- @backstage/plugin-kubernetes@0.3.1
- @backstage/plugin-techdocs@0.3.1
- @backstage/plugin-catalog@0.2.5
- @backstage/test-utils@0.1.4
- @backstage/plugin-circleci@0.2.3
- @backstage/plugin-cloudbuild@0.2.3
- @backstage/plugin-github-actions@0.2.3
- @backstage/plugin-jenkins@0.3.2
- @backstage/plugin-lighthouse@0.2.4
- @backstage/plugin-register-component@0.2.3
- @backstage/plugin-rollbar@0.2.5
- @backstage/plugin-search@0.2.2
## 0.2.4
### Patch Changes
- Updated dependencies [294295453]
- Updated dependencies [f3bb55ee3]
- Updated dependencies [4b53294a6]
- Updated dependencies [6f70ed7a9]
- Updated dependencies [ab94c9542]
- Updated dependencies [3a201c5d5]
- Updated dependencies [f538e2c56]
- Updated dependencies [2daf18e80]
- Updated dependencies [069cda35f]
- Updated dependencies [8697dea5b]
- Updated dependencies [b623cc275]
- @backstage/cli@0.3.2
- @backstage/plugin-api-docs@0.3.0
- @backstage/plugin-techdocs@0.3.0
- @backstage/plugin-catalog@0.2.4
- @backstage/catalog-model@0.3.1
- @backstage/plugin-rollbar@0.2.4
## 0.2.3
### Patch Changes
- 475fc0aaa: Using the search field in the sidebar now navigates to the search result page.
- Updated dependencies [475fc0aaa]
- Updated dependencies [1166fcc36]
- Updated dependencies [29a0ccab2]
- Updated dependencies [8e6728e25]
- Updated dependencies [c93a14b49]
- Updated dependencies [ef2831dde]
- Updated dependencies [2a71f4bab]
- Updated dependencies [1185919f3]
- Updated dependencies [a8de7f554]
- Updated dependencies [faf311c26]
- Updated dependencies [31d8b6979]
- Updated dependencies [991345969]
- Updated dependencies [475fc0aaa]
- @backstage/core@0.3.2
- @backstage/catalog-model@0.3.0
- @backstage/plugin-kubernetes@0.3.0
- @backstage/cli@0.3.1
- @backstage/plugin-cost-insights@0.4.1
- @backstage/plugin-scaffolder@0.3.1
- @backstage/plugin-register-component@0.2.2
- @backstage/plugin-circleci@0.2.2
- @backstage/plugin-search@0.2.1
- @backstage/plugin-api-docs@0.2.2
- @backstage/plugin-catalog@0.2.3
- @backstage/plugin-cloudbuild@0.2.2
- @backstage/plugin-github-actions@0.2.2
- @backstage/plugin-jenkins@0.3.1
- @backstage/plugin-lighthouse@0.2.3
- @backstage/plugin-rollbar@0.2.3
- @backstage/plugin-sentry@0.2.3
- @backstage/plugin-techdocs@0.2.3
## 0.2.2
### Patch Changes
- 3efd03c0e: Removed obsolete CircleCI proxy config from example-app
- Updated dependencies [1722cb53c]
- Updated dependencies [1722cb53c]
- Updated dependencies [17a9f48f6]
- Updated dependencies [4040d4fcb]
- Updated dependencies [f360395d0]
- Updated dependencies [259d848ee]
- Updated dependencies [8b7737d0b]
- Updated dependencies [902340451]
- @backstage/cli@0.3.0
- @backstage/core@0.3.1
- @backstage/plugin-cost-insights@0.4.0
- @backstage/plugin-lighthouse@0.2.2
- @backstage/plugin-rollbar@0.2.2
- @backstage/plugin-sentry@0.2.2
- @backstage/plugin-techdocs@0.2.2
- @backstage/plugin-user-settings@0.2.2
- @backstage/plugin-catalog@0.2.2
- @backstage/test-utils@0.1.3
## 0.2.1
### Patch Changes
+32 -36
View File
@@ -1,44 +1,46 @@
{
"name": "example-app",
"version": "0.2.1",
"version": "0.2.5",
"private": true,
"bundled": true,
"dependencies": {
"@backstage/catalog-model": "^0.2.0",
"@backstage/cli": "^0.2.0",
"@backstage/core": "^0.3.0",
"@backstage/plugin-api-docs": "^0.2.1",
"@backstage/plugin-catalog": "^0.2.1",
"@backstage/plugin-circleci": "^0.2.1",
"@backstage/plugin-cloudbuild": "^0.2.1",
"@backstage/plugin-cost-insights": "^0.3.0",
"@backstage/catalog-model": "^0.4.0",
"@backstage/cli": "^0.4.0",
"@backstage/core": "^0.3.2",
"@backstage/plugin-api-docs": "^0.3.1",
"@backstage/plugin-catalog": "^0.2.5",
"@backstage/plugin-catalog-import": "^0.3.0",
"@backstage/plugin-circleci": "^0.2.3",
"@backstage/plugin-cloudbuild": "^0.2.3",
"@backstage/plugin-cost-insights": "^0.4.2",
"@backstage/plugin-explore": "^0.2.1",
"@backstage/plugin-gcp-projects": "^0.2.1",
"@backstage/plugin-github-actions": "^0.2.1",
"@backstage/plugin-github-actions": "^0.2.3",
"@backstage/plugin-gitops-profiles": "^0.2.1",
"@backstage/plugin-graphiql": "^0.2.1",
"@backstage/plugin-jenkins": "^0.3.0",
"@backstage/plugin-kubernetes": "^0.2.1",
"@backstage/plugin-lighthouse": "^0.2.1",
"@backstage/plugin-jenkins": "^0.3.2",
"@backstage/plugin-kubernetes": "^0.3.1",
"@backstage/plugin-lighthouse": "^0.2.4",
"@backstage/plugin-newrelic": "^0.2.1",
"@backstage/plugin-register-component": "^0.2.1",
"@backstage/plugin-rollbar": "^0.2.1",
"@backstage/plugin-scaffolder": "^0.3.0",
"@backstage/plugin-sentry": "^0.2.1",
"@backstage/plugin-search": "^0.2.0",
"@backstage/plugin-pagerduty": "0.2.1",
"@backstage/plugin-register-component": "^0.2.3",
"@backstage/plugin-rollbar": "^0.2.5",
"@backstage/plugin-scaffolder": "^0.3.2",
"@backstage/plugin-sentry": "^0.2.4",
"@backstage/plugin-search": "^0.2.2",
"@backstage/plugin-tech-radar": "^0.3.0",
"@backstage/plugin-techdocs": "^0.2.1",
"@backstage/plugin-user-settings": "^0.2.1",
"@backstage/plugin-welcome": "^0.2.1",
"@backstage/test-utils": "^0.1.2",
"@backstage/plugin-techdocs": "^0.3.1",
"@backstage/plugin-user-settings": "^0.2.2",
"@backstage/plugin-welcome": "^0.2.2",
"@backstage/test-utils": "^0.1.4",
"@backstage/theme": "^0.2.1",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@octokit/rest": "^18.0.0",
"@roadiehq/backstage-plugin-github-insights": "^0.2.12",
"@roadiehq/backstage-plugin-github-pull-requests": "^0.6.2",
"@roadiehq/backstage-plugin-travis-ci": "^0.2.7",
"@roadiehq/backstage-plugin-buildkite": "^0.1.2",
"@roadiehq/backstage-plugin-buildkite": "^0.1.3",
"@roadiehq/backstage-plugin-github-insights": "^0.2.16",
"@roadiehq/backstage-plugin-github-pull-requests": "^0.6.3",
"@roadiehq/backstage-plugin-travis-ci": "^0.2.8",
"history": "^5.0.0",
"prop-types": "^15.7.2",
"react": "^16.12.0",
@@ -87,14 +89,8 @@
"last 1 safari version"
]
},
"license": "Apache-2.0",
"proxy": {
"/circleci/api": {
"target": "https://circleci.com/api/v1.1",
"changeOrigin": true,
"pathRewrite": {
"^/circleci/api/": "/"
}
}
}
"files": [
"dist"
],
"license": "Apache-2.0"
}
+5
View File
@@ -34,6 +34,7 @@ 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 SettingsRouter } from '@backstage/plugin-user-settings';
import { Router as ImportComponentRouter } from '@backstage/plugin-catalog-import';
import { Route, Routes, Navigate } from 'react-router';
import { EntityPage } from './components/catalog/EntityPage';
@@ -67,6 +68,10 @@ const catalogRouteRef = createRouteRef({
const AppRoutes = () => (
<Routes>
<Navigate key="/" to="/catalog" />
<Route
path="/catalog-import/*"
element={<ImportComponentRouter catalogRouteRef={catalogRouteRef} />}
/>
<Route
path={`${catalogRouteRef.path}/*`}
element={<CatalogRouter EntityPage={EntityPage} />}
+2 -8
View File
@@ -33,12 +33,12 @@ import {
SidebarContext,
SidebarItem,
SidebarDivider,
SidebarSearchField,
SidebarSpace,
} from '@backstage/core';
import { NavLink } from 'react-router-dom';
import { graphiQLRouteRef } from '@backstage/plugin-graphiql';
import { Settings as SidebarSettings } from '@backstage/plugin-user-settings';
import { SidebarSearch } from '@backstage/plugin-search';
const useSidebarLogoStyles = makeStyles({
root: {
@@ -73,17 +73,11 @@ const SidebarLogo: FC<{}> = () => {
);
};
const handleSearch = (query: string): void => {
// XXX (@koroeskohr): for testing purposes
// eslint-disable-next-line no-console
console.log(query);
};
const Root: FC<{}> = ({ children }) => (
<SidebarPage>
<Sidebar>
<SidebarLogo />
<SidebarSearchField onSearch={handleSearch} />
<SidebarSearch />
<SidebarDivider />
{/* Global nav, not org-specific */}
<SidebarItem icon={HomeIcon} to="/catalog" text="Home" />
@@ -18,7 +18,7 @@ import { CICDSwitcher } from './EntityPage';
import { UrlPatternDiscovery, ApiProvider, ApiRegistry } from '@backstage/core';
import {
buildKiteApiRef,
BuildKiteApi,
BuildkiteApi,
} from '@roadiehq/backstage-plugin-buildkite';
import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils';
@@ -42,11 +42,11 @@ describe('EntityPage Test', () => {
const discoveryApi = UrlPatternDiscovery.compile('http://exampleapi.com');
const apis = ApiRegistry.from([
[buildKiteApiRef, new BuildKiteApi({ discoveryApi })],
[buildKiteApiRef, new BuildkiteApi({ discoveryApi })],
]);
describe('CICDSwitcher Test', () => {
it('Should render BuildKite View', async () => {
it('Should render Buildkite View', async () => {
const renderedComponent = await renderWithEffects(
wrapInTestApp(
<ApiProvider apis={apis}>
@@ -13,63 +13,70 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ApiEntity, Entity } from '@backstage/catalog-model';
import { EmptyState } from '@backstage/core';
import {
isPluginApplicableToEntity as isTravisCIAvailable,
RecentTravisCIBuildsWidget,
Router as TravisCIRouter,
} from '@roadiehq/backstage-plugin-travis-ci';
ApiDefinitionCard,
Router as ApiDocsRouter,
} from '@backstage/plugin-api-docs';
import {
AboutCard,
EntityPageLayout,
useEntity,
} from '@backstage/plugin-catalog';
import {
isPluginApplicableToEntity as isCircleCIAvailable,
Router as CircleCIRouter,
} from '@backstage/plugin-circleci';
import {
isPluginApplicableToEntity as isCloudbuildAvailable,
Router as CloudbuildRouter,
} from '@backstage/plugin-cloudbuild';
import {
isPluginApplicableToEntity as isGitHubActionsAvailable,
RecentWorkflowRunsCard,
Router as GitHubActionsRouter,
} from '@backstage/plugin-github-actions';
import {
Router as CloudbuildRouter,
isPluginApplicableToEntity as isCloudbuildAvailable,
} from '@backstage/plugin-cloudbuild';
import {
Router as JenkinsRouter,
isPluginApplicableToEntity as isJenkinsAvailable,
LatestRunCard as JenkinsLatestRunCard,
Router as JenkinsRouter,
} from '@backstage/plugin-jenkins';
import {
isPluginApplicableToEntity as isCircleCIAvailable,
Router as CircleCIRouter,
} from '@backstage/plugin-circleci';
import { Router as ApiDocsRouter } from '@backstage/plugin-api-docs';
import { Router as SentryRouter } from '@backstage/plugin-sentry';
import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs';
import { Router as KubernetesRouter } from '@backstage/plugin-kubernetes';
import {
Router as GitHubInsightsRouter,
isPluginApplicableToEntity as isGitHubAvailable,
ReadMeCard,
LanguagesCard,
ReleasesCard,
} from '@roadiehq/backstage-plugin-github-insights';
import React, { ReactNode } from 'react';
import {
AboutCard,
EntityPageLayout,
useEntity,
} from '@backstage/plugin-catalog';
import { Entity } from '@backstage/catalog-model';
import { Button, Grid } from '@material-ui/core';
import { EmptyState } from '@backstage/core';
import {
EmbeddedRouter as LighthouseRouter,
LastLighthouseAuditCard,
isPluginApplicableToEntity as isLighthouseAvailable,
} from '@backstage/plugin-lighthouse/';
LastLighthouseAuditCard,
} from '@backstage/plugin-lighthouse';
import { Router as SentryRouter } from '@backstage/plugin-sentry';
import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs';
import { Button, Grid } from '@material-ui/core';
import {
isPluginApplicableToEntity as isBuildkiteAvailable,
Router as BuildkiteRouter,
} from '@roadiehq/backstage-plugin-buildkite';
import {
isPluginApplicableToEntity as isGitHubAvailable,
LanguagesCard,
ReadMeCard,
ReleasesCard,
Router as GitHubInsightsRouter,
} from '@roadiehq/backstage-plugin-github-insights';
import {
Router as PullRequestsRouter,
isPluginApplicableToEntity as isPullRequestsAvailable,
PullRequestsStatsCard,
Router as PullRequestsRouter,
} from '@roadiehq/backstage-plugin-github-pull-requests';
import {
Router as BuildKiteRouter,
isPluginApplicableToEntity as isBuildKiteAvailable,
} from '@roadiehq/backstage-plugin-buildkite';
isPluginApplicableToEntity as isPagerDutyAvailable,
PagerDutyCard,
} from '@backstage/plugin-pagerduty';
import {
isPluginApplicableToEntity as isTravisCIAvailable,
RecentTravisCIBuildsWidget,
Router as TravisCIRouter,
} from '@roadiehq/backstage-plugin-travis-ci';
import React, { ReactNode } from 'react';
export const CICDSwitcher = ({ entity }: { entity: Entity }) => {
// This component is just an example of how you can implement your company's logic in entity page.
@@ -77,12 +84,12 @@ export const CICDSwitcher = ({ entity }: { entity: Entity }) => {
switch (true) {
case isJenkinsAvailable(entity):
return <JenkinsRouter entity={entity} />;
case isBuildKiteAvailable(entity):
return <BuildKiteRouter entity={entity} />;
case isGitHubActionsAvailable(entity):
return <GitHubActionsRouter entity={entity} />;
case isBuildkiteAvailable(entity):
return <BuildkiteRouter entity={entity} />;
case isCircleCIAvailable(entity):
return <CircleCIRouter entity={entity} />;
case isGitHubActionsAvailable(entity):
return <GitHubActionsRouter entity={entity} />;
case isCloudbuildAvailable(entity):
return <CloudbuildRouter entity={entity} />;
case isTravisCIAvailable(entity):
@@ -134,11 +141,16 @@ const RecentCICDRunsSwitcher = ({ entity }: { entity: Entity }) => {
);
};
const OverviewContent = ({ entity }: { entity: Entity }) => (
const ComponentOverviewContent = ({ entity }: { entity: Entity }) => (
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<AboutCard entity={entity} variant="gridItem" />
</Grid>
{isPagerDutyAvailable(entity) && (
<Grid item md={6}>
<PagerDutyCard entity={entity} />
</Grid>
)}
<RecentCICDRunsSwitcher entity={entity} />
{isGitHubAvailable(entity) && (
<>
@@ -169,7 +181,7 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout.Content
path="/"
title="Overview"
element={<OverviewContent entity={entity} />}
element={<ComponentOverviewContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/ci-cd/*"
@@ -214,7 +226,7 @@ const WebsiteEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout.Content
path="/"
title="Overview"
element={<OverviewContent entity={entity} />}
element={<ComponentOverviewContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/ci-cd/*"
@@ -253,12 +265,13 @@ const WebsiteEntityPage = ({ entity }: { entity: Entity }) => (
/>
</EntityPageLayout>
);
const DefaultEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<OverviewContent entity={entity} />}
element={<ComponentOverviewContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/docs/*"
@@ -268,8 +281,7 @@ const DefaultEntityPage = ({ entity }: { entity: Entity }) => (
</EntityPageLayout>
);
export const EntityPage = () => {
const { entity } = useEntity();
export const ComponentEntityPage = ({ entity }: { entity: Entity }) => {
switch (entity?.spec?.type) {
case 'service':
return <ServiceEntityPage entity={entity} />;
@@ -279,3 +291,47 @@ export const EntityPage = () => {
return <DefaultEntityPage entity={entity} />;
}
};
const ApiOverviewContent = ({ entity }: { entity: Entity }) => (
<Grid container spacing={3}>
<Grid item md={6}>
<AboutCard entity={entity} />
</Grid>
</Grid>
);
const ApiDefinitionContent = ({ entity }: { entity: ApiEntity }) => (
<Grid container spacing={3}>
<Grid item xs={12}>
<ApiDefinitionCard apiEntity={entity} />
</Grid>
</Grid>
);
const ApiEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<ApiOverviewContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/definition/*"
title="Definition"
element={<ApiDefinitionContent entity={entity as ApiEntity} />}
/>
</EntityPageLayout>
);
export const EntityPage = () => {
const { entity } = useEntity();
switch (entity?.kind?.toLowerCase()) {
case 'component':
return <ComponentEntityPage entity={entity} />;
case 'api':
return <ApiEntityPage entity={entity} />;
default:
return <DefaultEntityPage entity={entity} />;
}
};
+7
View File
@@ -22,9 +22,16 @@ import {
samlAuthApiRef,
microsoftAuthApiRef,
oneloginAuthApiRef,
oidcAuthApiRef,
} from '@backstage/core';
export const providers = [
{
id: 'oidc-auth-provider',
title: 'Oidc',
message: 'Sign In using OpenId Connect',
apiRef: oidcAuthApiRef,
},
{
id: 'google-auth-provider',
title: 'Google',
+3 -1
View File
@@ -37,6 +37,8 @@ export { plugin as Kubernetes } from '@backstage/plugin-kubernetes';
export { plugin as Cloudbuild } from '@backstage/plugin-cloudbuild';
export { plugin as CostInsights } from '@backstage/plugin-cost-insights';
export { plugin as GitHubInsights } from '@roadiehq/backstage-plugin-github-insights';
export { plugin as CatalogImport } from '@backstage/plugin-catalog-import';
export { plugin as UserSettings } from '@backstage/plugin-user-settings';
export { plugin as BuildKite } from '@roadiehq/backstage-plugin-buildkite';
export { plugin as PagerDuty } from '@backstage/plugin-pagerduty';
export { plugin as Buildkite } from '@roadiehq/backstage-plugin-buildkite';
export { plugin as Search } from '@backstage/plugin-search';
+63
View File
@@ -1,5 +1,68 @@
# @backstage/backend-common
## 0.3.3
### Patch Changes
- 612368274: Allow the `backend.listen.port` config to be both a number or a string.
- Updated dependencies [4e7091759]
- Updated dependencies [b4488ddb0]
- @backstage/config-loader@0.4.0
## 0.3.2
### Patch Changes
- 3aa7efb3f: Added support for passing false as a CSP field value, to drop it from the defaults in the backend
- b3d4e4e57: Move the frontend visibility declarations of integrations config from @backstage/backend-common to @backstage/integration
- Updated dependencies [b3d4e4e57]
- @backstage/integration@0.1.2
## 0.3.1
### Patch Changes
- bff3305aa: Added readTree support to AzureUrlReader
- b47dce06f: Make integration host and url configurations visible in the frontend
## 0.3.0
### Minor Changes
- 1722cb53c: Added support for loading and validating configuration schemas, as well as declaring config visibility through schemas.
The new `loadConfigSchema` function exported by `@backstage/config-loader` allows for the collection and merging of configuration schemas from all nearby dependencies of the project.
A configuration schema is declared using the `https://backstage.io/schema/config-v1` JSON Schema meta schema, which is based on draft07. The only difference to the draft07 schema is the custom `visibility` keyword, which is used to indicate whether the given config value should be visible in the frontend or not. The possible values are `frontend`, `backend`, and `secret`, where `backend` is the default. A visibility of `secret` has the same scope at runtime, but it will be treated with more care in certain contexts, and defining both `frontend` and `secret` for the same value in two different schemas will result in an error during schema merging.
Packages that wish to contribute configuration schema should declare it in a root `"configSchema"` field in `package.json`. The field can either contain an inlined JSON schema, or a relative path to a schema file. Schema files can be in either `.json` or `.d.ts` format.
TypeScript configuration schema files should export a single `Config` type, for example:
```ts
export interface Config {
app: {
/**
* Frontend root URL
* @visibility frontend
*/
baseUrl: string;
};
}
```
- 8e2effb53: Refactored UrlReader.readTree to be required and accept (url, options)
### Patch Changes
- 1722cb53c: Added configuration schema
- 7b37e6834: Added the integration package
- Updated dependencies [1722cb53c]
- Updated dependencies [7b37e6834]
- @backstage/config-loader@0.3.0
- @backstage/integration@0.1.1
- @backstage/test-utils@0.1.3
## 0.2.1
### Patch Changes
+31 -10
View File
@@ -29,7 +29,7 @@ export interface Config {
/** Address of the interface that the backend should bind to. */
address?: string;
/** Port that the backend should listen to. */
port?: number;
port?: string | number;
};
/** HTTPS configuration for the backend. If omitted the backend will serve HTTP */
@@ -79,15 +79,24 @@ export interface Config {
optionsSuccessStatus?: number;
};
/** */
csp?: object;
/**
* Content Security Policy options.
*
* The keys are the plain policy ID, e.g. "upgrade-insecure-requests". The
* values are on the format that the helmet library expects them, as an
* array of strings. There is also the special value false, which means to
* remove the default value that Backstage puts in place for that policy.
*/
csp?: { [policyId: string]: string[] | false };
};
/** Configuration for integrations towards various external repository provider systems */
integrations?: {
/** Integration configuration for Azure */
azure?: Array<{
/** The hostname of the given Azure instance */
/**
* The hostname of the given Azure instance
*/
host: string;
/**
* Token used to authenticate requests.
@@ -98,14 +107,18 @@ export interface Config {
/** Integration configuration for BitBucket */
bitbucket?: Array<{
/** The hostname of the given Bitbucket instance */
/**
* The hostname of the given Bitbucket instance
*/
host: string;
/**
* Token used to authenticate requests.
* @visibility secret
*/
token?: string;
/** The base url for the BitBucket API, for example https://api.bitbucket.org/2.0 */
/**
* The base url for the BitBucket API, for example https://api.bitbucket.org/2.0
*/
apiBaseUrl?: string;
/**
* The username to use for authenticated requests.
@@ -121,22 +134,30 @@ export interface Config {
/** Integration configuration for GitHub */
github?: Array<{
/** The hostname of the given GitHub instance */
/**
* The hostname of the given GitHub instance
*/
host: string;
/**
* Token used to authenticate requests.
* @visibility secret
*/
token?: string;
/** The base url for the GitHub API, for example https://api.github.com */
/**
* The base url for the GitHub API, for example https://api.github.com
*/
apiBaseUrl?: string;
/** The base url for GitHub raw resources, for example https://raw.githubusercontent.com */
/**
* The base url for GitHub raw resources, for example https://raw.githubusercontent.com
*/
rawBaseUrl?: string;
}>;
/** Integration configuration for GitLab */
gitlab?: Array<{
/** The hostname of the given GitLab instance */
/**
* The hostname of the given GitLab instance
*/
host: string;
/**
* Token used to authenticate requests.
+9 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
"version": "0.2.1",
"version": "0.3.3",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -31,11 +31,11 @@
"dependencies": {
"@backstage/cli-common": "^0.1.1",
"@backstage/config": "^0.1.1",
"@backstage/config-loader": "^0.2.0",
"@backstage/integration": "^0.1.0",
"@backstage/test-utils": "^0.1.2",
"@backstage/config-loader": "^0.4.0",
"@backstage/integration": "^0.1.2",
"@types/cors": "^2.8.6",
"@types/express": "^4.17.6",
"archiver": "^5.0.2",
"compression": "^1.7.4",
"concat-stream": "^2.0.0",
"cors": "^2.8.5",
@@ -55,6 +55,7 @@
"selfsigned": "^1.10.7",
"stoppable": "^1.1.0",
"tar": "^6.0.5",
"unzipper": "^0.10.11",
"winston": "^3.2.1"
},
"peerDependencies": {
@@ -66,7 +67,9 @@
}
},
"devDependencies": {
"@backstage/cli": "^0.2.0",
"@backstage/cli": "^0.4.0",
"@backstage/test-utils": "^0.1.4",
"@types/archiver": "^3.1.1",
"@types/compression": "^1.7.0",
"@types/concat-stream": "^1.6.0",
"@types/fs-extra": "^9.0.3",
@@ -78,6 +81,7 @@
"@types/stoppable": "^1.1.0",
"@types/supertest": "^2.0.8",
"@types/tar": "^4.0.3",
"@types/unzipper": "^0.10.3",
"@types/webpack-env": "^1.15.2",
"@types/yaml": "^1.9.7",
"get-port": "^5.1.1",
@@ -14,11 +14,13 @@
* limitations under the License.
*/
import fs from 'fs';
import path from 'path';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { ConfigReader } from '@backstage/config';
import { getVoidLogger } from '../logging';
import { AzureUrlReader } from './AzureUrlReader';
import { AzureUrlReader, getDownloadUrl } from './AzureUrlReader';
import { msw } from '@backstage/test-utils';
import { ReadTreeResponseFactory } from './tree';
@@ -32,104 +34,165 @@ describe('AzureUrlReader', () => {
const worker = setupServer();
msw.setupDefaultHandlers(worker);
beforeEach(() => {
worker.use(
rest.get('*', (req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
url: req.url.toString(),
headers: req.headers.getAllHeaders(),
}),
describe('read', () => {
beforeEach(() => {
worker.use(
rest.get('*', (req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
url: req.url.toString(),
headers: req.headers.getAllHeaders(),
}),
),
),
),
);
});
const createConfig = (token?: string) =>
new ConfigReader(
{
integrations: { azure: [{ host: 'dev.azure.com', token }] },
},
'test-config',
);
it.each([
{
url:
'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster',
config: createConfig(),
response: expect.objectContaining({
url:
'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml&version=master',
}),
},
{
url:
'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml',
config: createConfig(),
response: expect.objectContaining({
url:
'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml',
}),
},
{
url: 'https://dev.azure.com/a/b/_git/repo-name?path=my-template.yaml',
config: createConfig('0123456789'),
response: expect.objectContaining({
headers: expect.objectContaining({
authorization: 'Basic OjAxMjM0NTY3ODk=',
}),
}),
},
{
url: 'https://dev.azure.com/a/b/_git/repo-name?path=my-template.yaml',
config: createConfig(undefined),
response: expect.objectContaining({
headers: expect.not.objectContaining({
authorization: expect.anything(),
}),
}),
},
])('should handle happy path %#', async ({ url, config, response }) => {
const [{ reader }] = AzureUrlReader.factory({
config,
logger,
treeResponseFactory,
);
});
const data = await reader.read(url);
const res = await JSON.parse(data.toString('utf-8'));
expect(res).toEqual(response);
});
const createConfig = (token?: string) =>
new ConfigReader(
{
integrations: { azure: [{ host: 'dev.azure.com', token }] },
},
'test-config',
);
it.each([
{
url: 'https://api.com/a/b/blob/master/path/to/c.yaml',
config: createConfig(),
error:
'Incorrect url: https://api.com/a/b/blob/master/path/to/c.yaml, Error: Wrong Azure Devops URL or Invalid file path',
},
{
url: 'com/a/b/blob/master/path/to/c.yaml',
config: createConfig(),
error:
'Incorrect url: com/a/b/blob/master/path/to/c.yaml, TypeError: Invalid URL: com/a/b/blob/master/path/to/c.yaml',
},
{
url: '',
config: createConfig(''),
error:
"Invalid type in config for key 'integrations.azure[0].token' in 'test-config', got empty-string, wanted string",
},
])('should handle error path %#', async ({ url, config, error }) => {
await expect(async () => {
it.each([
{
url:
'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster',
config: createConfig(),
response: expect.objectContaining({
url:
'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml&version=master',
}),
},
{
url:
'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml',
config: createConfig(),
response: expect.objectContaining({
url:
'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml',
}),
},
{
url: 'https://dev.azure.com/a/b/_git/repo-name?path=my-template.yaml',
config: createConfig('0123456789'),
response: expect.objectContaining({
headers: expect.objectContaining({
authorization: 'Basic OjAxMjM0NTY3ODk=',
}),
}),
},
{
url: 'https://dev.azure.com/a/b/_git/repo-name?path=my-template.yaml',
config: createConfig(undefined),
response: expect.objectContaining({
headers: expect.not.objectContaining({
authorization: expect.anything(),
}),
}),
},
])('should handle happy path %#', async ({ url, config, response }) => {
const [{ reader }] = AzureUrlReader.factory({
config,
logger,
treeResponseFactory,
});
await reader.read(url);
}).rejects.toThrow(error);
const data = await reader.read(url);
const res = await JSON.parse(data.toString('utf-8'));
expect(res).toEqual(response);
});
it.each([
{
url: 'https://api.com/a/b/blob/master/path/to/c.yaml',
config: createConfig(),
error:
'Incorrect url: https://api.com/a/b/blob/master/path/to/c.yaml, Error: Wrong Azure Devops URL or Invalid file path',
},
{
url: 'com/a/b/blob/master/path/to/c.yaml',
config: createConfig(),
error:
'Incorrect url: com/a/b/blob/master/path/to/c.yaml, TypeError: Invalid URL: com/a/b/blob/master/path/to/c.yaml',
},
{
url: '',
config: createConfig(''),
error:
"Invalid type in config for key 'integrations.azure[0].token' in 'test-config', got empty-string, wanted string",
},
])('should handle error path %#', async ({ url, config, error }) => {
await expect(async () => {
const [{ reader }] = AzureUrlReader.factory({
config,
logger,
treeResponseFactory,
});
await reader.read(url);
}).rejects.toThrow(error);
});
});
describe('readTree', () => {
const repoBuffer = fs.readFileSync(
path.resolve('src', 'reading', '__fixtures__', 'repo.zip'),
);
beforeEach(() => {
worker.use(
rest.get(
'https://dev.azure.com/organization/project/_apis/git/repositories/repository/items',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/zip'),
ctx.body(repoBuffer),
),
),
);
});
it('returns the wanted files from an archive', async () => {
const processor = new AzureUrlReader(
{
host: 'dev.azure.com',
},
{ treeResponseFactory },
);
const response = await processor.readTree(
'https://dev.azure.com/organization/project/_git/repository',
);
const files = await response.files();
expect(files.length).toBe(2);
const mkDocsFile = await files[1].content();
const indexMarkdownFile = await files[0].content();
expect(mkDocsFile.toString()).toBe('site_name: Test\n');
expect(indexMarkdownFile.toString()).toBe('# Test\n');
});
});
describe('getDownloadUrl', () => {
it('do not add scopePath if no path is specified', async () => {
const result = getDownloadUrl(
'https://dev.azure.com/organization/project/_git/repository',
);
expect(result.searchParams.get('scopePath')).toBeNull();
});
it('add scopePath if a path is specified', async () => {
const result = getDownloadUrl(
'https://dev.azure.com/organization/project/_git/repository?path=%2Fdocs',
);
expect(result.searchParams.get('scopePath')).toEqual('docs');
});
});
});
@@ -19,22 +19,55 @@ import {
readAzureIntegrationConfigs,
} from '@backstage/integration';
import fetch from 'cross-fetch';
import { Readable } from 'stream';
import parseGitUri from 'git-url-parse';
import { NotFoundError } from '../errors';
import { ReaderFactory, ReadTreeResponse, UrlReader } from './types';
import {
ReaderFactory,
ReadTreeOptions,
ReadTreeResponse,
UrlReader,
} from './types';
import { ReadTreeResponseFactory } from './tree';
export function getDownloadUrl(url: string): URL {
const {
name: repoName,
owner: project,
organization,
protocol,
resource,
filepath,
} = parseGitUri(url);
// scopePath will limit the downloaded content
// /docs will only download the docs folder and everything below it
// /docs/index.md will only download index.md but put it in the root of the archive
const scopePath = filepath
? `&scopePath=${encodeURIComponent(filepath)}`
: '';
return new URL(
`${protocol}://${resource}/${organization}/${project}/_apis/git/repositories/${repoName}/items?recursionLevel=full&download=true&api-version=6.0${scopePath}`,
);
}
export class AzureUrlReader implements UrlReader {
static factory: ReaderFactory = ({ config }) => {
static factory: ReaderFactory = ({ config, treeResponseFactory }) => {
const configs = readAzureIntegrationConfigs(
config.getOptionalConfigArray('integrations.azure') ?? [],
);
return configs.map(options => {
const reader = new AzureUrlReader(options);
const reader = new AzureUrlReader(options, { treeResponseFactory });
const predicate = (url: URL) => url.host === options.host;
return { reader, predicate };
});
};
constructor(private readonly options: AzureIntegrationConfig) {
constructor(
private readonly options: AzureIntegrationConfig,
private readonly deps: { treeResponseFactory: ReadTreeResponseFactory },
) {
if (options.host !== 'dev.azure.com') {
throw Error(
`Azure integration currently only supports 'dev.azure.com', tried to use host '${options.host}'`,
@@ -64,8 +97,26 @@ export class AzureUrlReader implements UrlReader {
throw new Error(message);
}
readTree(): Promise<ReadTreeResponse> {
throw new Error('AzureUrlReader does not implement readTree');
async readTree(
url: string,
options?: ReadTreeOptions,
): Promise<ReadTreeResponse> {
const response = await fetch(
getDownloadUrl(url).toString(),
this.getRequestOptions({ Accept: 'application/zip' }),
);
if (!response.ok) {
const message = `Failed to read tree from ${url}, ${response.status} ${response.statusText}`;
if (response.status === 404) {
throw new NotFoundError(message);
}
throw new Error(message);
}
return this.deps.treeResponseFactory.fromZipArchive({
stream: (response.body as unknown) as Readable,
filter: options?.filter,
});
}
// Converts
@@ -127,8 +178,10 @@ export class AzureUrlReader implements UrlReader {
}
}
private getRequestOptions(): RequestInit {
const headers: HeadersInit = {};
private getRequestOptions(additionalHeaders?: {
[key: string]: string;
}): RequestInit {
const headers: HeadersInit = additionalHeaders ?? {};
if (this.options.token) {
headers.Authorization = `Basic ${Buffer.from(
@@ -235,6 +235,40 @@ describe('GithubUrlReader', () => {
expect(indexMarkdownFile.toString()).toBe('# Test\n');
});
it('includes the subdomain in the github url', async () => {
worker.resetHandlers();
worker.use(
rest.get(
'https://ghe.github.com/backstage/mock/archive/repo.tar.gz',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/x-gzip'),
ctx.body(repoBuffer),
),
),
);
const processor = new GithubUrlReader(
{
host: 'ghe.github.com',
apiBaseUrl: 'https://api.github.com',
},
{ treeResponseFactory },
);
const response = await processor.readTree(
'https://ghe.github.com/backstage/mock/tree/repo/docs',
);
const files = await response.files();
expect(files.length).toBe(1);
const indexMarkdownFile = await files[0].content();
expect(indexMarkdownFile.toString()).toBe('# Test\n');
});
it('must specify a branch', async () => {
const processor = new GithubUrlReader(
{
@@ -179,7 +179,7 @@ export class GithubUrlReader implements UrlReader {
name: repoName,
ref,
protocol,
source,
resource,
full_name,
filepath,
} = parseGitUri(url);
@@ -194,8 +194,9 @@ export class GithubUrlReader implements UrlReader {
// TODO(Rugvip): use API to fetch URL instead
const response = await fetch(
new URL(
`${protocol}://${source}/${full_name}/archive/${ref}.tar.gz`,
`${protocol}://${resource}/${full_name}/archive/${ref}.tar.gz`,
).toString(),
getRawRequestOptions(this.config),
);
if (!response.ok) {
const message = `Failed to read tree from ${url}, ${response.status} ${response.statusText}`;
@@ -207,7 +208,7 @@ export class GithubUrlReader implements UrlReader {
const path = `${repoName}-${ref}/${filepath}`;
return this.deps.treeResponseFactory.fromArchive({
return 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: (response.body as unknown) as Readable,
@@ -18,7 +18,8 @@ import os from 'os';
import { Readable } from 'stream';
import { Config } from '@backstage/config';
import { ReadTreeResponse } from '../types';
import { ArchiveResponse } from './ArchiveResponse';
import { TarArchiveResponse } from './TarArchiveResponse';
import { ZipArchiveResponse } from './ZipArchiveResponse';
type FromArchiveOptions = {
// A binary stream of a tar archive.
@@ -39,8 +40,17 @@ export class ReadTreeResponseFactory {
constructor(private readonly workDir: string) {}
async fromArchive(options: FromArchiveOptions): Promise<ReadTreeResponse> {
return new ArchiveResponse(
async fromTarArchive(options: FromArchiveOptions): Promise<ReadTreeResponse> {
return new TarArchiveResponse(
options.stream,
options.path ?? '',
this.workDir,
options.filter,
);
}
async fromZipArchive(options: FromArchiveOptions): Promise<ReadTreeResponse> {
return new ZipArchiveResponse(
options.stream,
options.path ?? '',
this.workDir,
@@ -17,13 +17,13 @@
import fs from 'fs-extra';
import mockFs from 'mock-fs';
import { resolve as resolvePath } from 'path';
import { ArchiveResponse } from './ArchiveResponse';
import { TarArchiveResponse } from './TarArchiveResponse';
const archiveData = fs.readFileSync(
resolvePath(__filename, '../../__fixtures__/repo.tar.gz'),
);
describe('ArchiveResponse', () => {
describe('TarArchiveResponse', () => {
beforeEach(() => {
mockFs({
'/test-archive.tar.gz': archiveData,
@@ -38,7 +38,7 @@ describe('ArchiveResponse', () => {
it('should read files', async () => {
const stream = fs.createReadStream('/test-archive.tar.gz');
const res = new ArchiveResponse(stream, 'mock-repo/', '/tmp');
const res = new TarArchiveResponse(stream, 'mock-repo/', '/tmp');
const files = await res.files();
expect(files).toEqual([
@@ -61,7 +61,7 @@ describe('ArchiveResponse', () => {
it('should read files with filter', async () => {
const stream = fs.createReadStream('/test-archive.tar.gz');
const res = new ArchiveResponse(stream, 'mock-repo/', '/tmp', path =>
const res = new TarArchiveResponse(stream, 'mock-repo/', '/tmp', path =>
path.endsWith('.yml'),
);
const files = await res.files();
@@ -79,14 +79,14 @@ describe('ArchiveResponse', () => {
it('should read as archive and files', async () => {
const stream = fs.createReadStream('/test-archive.tar.gz');
const res = new ArchiveResponse(stream, 'mock-repo/', '/tmp');
const res = new TarArchiveResponse(stream, 'mock-repo/', '/tmp');
const buffer = await res.archive();
await expect(res.archive()).rejects.toThrow(
'Response has already been read',
);
const res2 = new ArchiveResponse(buffer, '', '/tmp');
const res2 = new TarArchiveResponse(buffer, '', '/tmp');
const files = await res2.files();
expect(files).toEqual([
@@ -109,7 +109,7 @@ describe('ArchiveResponse', () => {
it('should extract entire archive into directory', async () => {
const stream = fs.createReadStream('/test-archive.tar.gz');
const res = new ArchiveResponse(stream, '', '/tmp');
const res = new TarArchiveResponse(stream, '', '/tmp');
const dir = await res.dir();
await expect(
@@ -123,10 +123,10 @@ describe('ArchiveResponse', () => {
it('should extract archive into directory with a subpath', async () => {
const stream = fs.createReadStream('/test-archive.tar.gz');
const res = new ArchiveResponse(stream, 'mock-repo/docs/', '/tmp');
const res = new TarArchiveResponse(stream, 'mock-repo/docs/', '/tmp');
const dir = await res.dir();
expect(dir).toMatch(/^\/tmp\/.*$/);
expect(dir).toMatch(/^[\/\\]tmp[\/\\].*$/);
await expect(
fs.readFile(resolvePath(dir, 'index.md'), 'utf8'),
).resolves.toBe('# Test\n');
@@ -135,7 +135,7 @@ describe('ArchiveResponse', () => {
it('should extract archive into directory with a subpath and filter', async () => {
const stream = fs.createReadStream('/test-archive.tar.gz');
const res = new ArchiveResponse(stream, 'mock-repo/', '/tmp', path =>
const res = new TarArchiveResponse(stream, 'mock-repo/', '/tmp', path =>
path.endsWith('.yml'),
);
const dir = await res.dir({ targetDir: '/tmp' });
@@ -34,7 +34,7 @@ const pipeline = promisify(pipelineCb);
/**
* Wraps a tar archive stream into a tree response reader.
*/
export class ArchiveResponse implements ReadTreeResponse {
export class TarArchiveResponse implements ReadTreeResponse {
private read = false;
constructor(
@@ -49,7 +49,7 @@ export class ArchiveResponse implements ReadTreeResponse {
}
if (subPath.startsWith('/')) {
throw new TypeError(
`ArchiveResponse subPath must not start with a /, got '${subPath}'`,
`TarArchiveResponse subPath must not start with a /, got '${subPath}'`,
);
}
}
@@ -0,0 +1,151 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs-extra';
import mockFs from 'mock-fs';
import { resolve as resolvePath } from 'path';
import { ZipArchiveResponse } from './ZipArchiveResponse';
const archiveData = fs.readFileSync(
resolvePath(__filename, '../../__fixtures__/repo.zip'),
);
describe('ZipArchiveResponse', () => {
beforeEach(() => {
mockFs({
'/test-archive.zip': archiveData,
'/tmp': mockFs.directory(),
});
});
afterEach(() => {
mockFs.restore();
});
it('should read files', async () => {
const stream = fs.createReadStream('/test-archive.zip');
const res = new ZipArchiveResponse(stream, 'mock-repo/', '/tmp');
const files = await res.files();
expect(files).toEqual([
{
path: 'docs/index.md',
content: expect.any(Function),
},
{
path: 'mkdocs.yml',
content: expect.any(Function),
},
]);
const contents = await Promise.all(files.map(f => f.content()));
expect(contents.map(c => c.toString('utf8').trim())).toEqual([
'# Test',
'site_name: Test',
]);
});
it('should read files with filter', async () => {
const stream = fs.createReadStream('/test-archive.zip');
const res = new ZipArchiveResponse(stream, 'mock-repo/', '/tmp', path =>
path.endsWith('.yml'),
);
const files = await res.files();
expect(files).toEqual([
{
path: 'mkdocs.yml',
content: expect.any(Function),
},
]);
const content = await files[0].content();
expect(content.toString('utf8').trim()).toEqual('site_name: Test');
});
it('should read as archive and files', async () => {
const stream = fs.createReadStream('/test-archive.zip');
const res = new ZipArchiveResponse(stream, 'mock-repo/', '/tmp');
const buffer = await res.archive();
await expect(res.archive()).rejects.toThrow(
'Response has already been read',
);
const res2 = new ZipArchiveResponse(buffer, '', '/tmp');
const files = await res2.files();
expect(files).toEqual([
{
path: 'docs/index.md',
content: expect.any(Function),
},
{
path: 'mkdocs.yml',
content: expect.any(Function),
},
]);
const contents = await Promise.all(files.map(f => f.content()));
expect(contents.map(c => c.toString('utf8').trim())).toEqual([
'# Test',
'site_name: Test',
]);
});
it('should extract entire archive into directory', async () => {
const stream = fs.createReadStream('/test-archive.zip');
const res = new ZipArchiveResponse(stream, '', '/tmp');
const dir = await res.dir();
await expect(
fs.readFile(resolvePath(dir, 'mock-repo/mkdocs.yml'), 'utf8'),
).resolves.toBe('site_name: Test\n');
await expect(
fs.readFile(resolvePath(dir, 'mock-repo/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-repo/docs/', '/tmp');
const dir = await res.dir();
expect(dir).toMatch(/^[\/\\]tmp[\/\\].*$/);
await expect(
fs.readFile(resolvePath(dir, 'index.md'), 'utf8'),
).resolves.toBe('# Test\n');
});
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-repo/', '/tmp', path =>
path.endsWith('.yml'),
);
const dir = await res.dir({ targetDir: '/tmp' });
expect(dir).toBe('/tmp');
await expect(fs.pathExists(resolvePath(dir, 'mkdocs.yml'))).resolves.toBe(
true,
);
await expect(
fs.pathExists(resolvePath(dir, 'docs/index.md')),
).resolves.toBe(false);
});
});
@@ -0,0 +1,153 @@
/*
* 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 path from 'path';
import fs from 'fs-extra';
import unzipper, { Entry } from 'unzipper';
import archiver from 'archiver';
import { Readable } from 'stream';
import {
ReadTreeResponse,
ReadTreeResponseFile,
ReadTreeResponseDirOptions,
} from '../types';
/**
* Wraps a zip archive stream into a tree response reader.
*/
export class ZipArchiveResponse implements ReadTreeResponse {
private read = false;
constructor(
private readonly stream: Readable,
private readonly subPath: string,
private readonly workDir: string,
private readonly filter?: (path: string) => boolean,
) {
if (subPath) {
if (!subPath.endsWith('/')) {
this.subPath += '/';
}
if (subPath.startsWith('/')) {
throw new TypeError(
`ZipArchiveResponse subPath must not start with a /, got '${subPath}'`,
);
}
}
}
// Make sure the input stream is only read once
private onlyOnce() {
if (this.read) {
throw new Error('Response has already been read');
}
this.read = true;
}
private getPath(entry: Entry): string {
return entry.path.slice(this.subPath.length);
}
private shouldBeIncluded(entry: Entry): boolean {
if (this.subPath) {
if (!entry.path.startsWith(this.subPath)) {
return false;
}
}
if (this.filter) {
return this.filter(this.getPath(entry));
}
return true;
}
async files(): Promise<ReadTreeResponseFile[]> {
this.onlyOnce();
const files = Array<ReadTreeResponseFile>();
await this.stream
.pipe(unzipper.Parse())
.on('entry', (entry: Entry) => {
if (entry.type === 'Directory') {
entry.resume();
return;
}
if (this.shouldBeIncluded(entry)) {
files.push({
path: this.getPath(entry),
content: () => entry.buffer(),
});
} else {
entry.autodrain();
}
})
.promise();
return files;
}
async archive(): Promise<Readable> {
this.onlyOnce();
if (!this.subPath) {
return this.stream;
}
const archive = archiver('zip');
await this.stream
.pipe(unzipper.Parse())
.on('entry', (entry: Entry) => {
if (entry.type === 'File' && this.shouldBeIncluded(entry)) {
archive.append(entry, { name: this.getPath(entry) });
} else {
entry.autodrain();
}
})
.promise();
archive.finalize();
return archive;
}
async dir(options?: ReadTreeResponseDirOptions): Promise<string> {
this.onlyOnce();
const dir =
options?.targetDir ??
(await fs.mkdtemp(path.join(this.workDir, 'backstage-')));
await this.stream
.pipe(unzipper.Parse())
.on('entry', async (entry: Entry) => {
// 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);
if (dirname) {
await fs.mkdirp(path.join(dir, dirname));
}
entry.pipe(fs.createWriteStream(path.join(dir, entryPath)));
} else {
entry.autodrain();
}
})
.promise();
return dir;
}
}
@@ -0,0 +1,36 @@
/*
* 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 { applyCspDirectives } from './ServiceBuilderImpl';
describe('ServiceBuilderImpl', () => {
describe('applyCspDirectives', () => {
it('copies actual values', () => {
const result = applyCspDirectives({ key: ['value'] });
expect(result).toEqual(
expect.objectContaining({
'default-src': ["'self'"],
key: ['value'],
}),
);
});
it('removes false value keys', () => {
const result = applyCspDirectives({ 'upgrade-insecure-requests': false });
expect(result!['upgrade-insecure-requests']).toBeUndefined();
});
});
});
@@ -18,7 +18,7 @@ import { Config } from '@backstage/config';
import compression from 'compression';
import cors from 'cors';
import express, { Router } from 'express';
import helmet from 'helmet';
import helmet, { HelmetOptions } from 'helmet';
import * as http from 'http';
import stoppable from 'stoppable';
import { Logger } from 'winston';
@@ -56,7 +56,7 @@ const DEFAULT_CSP = {
'script-src': ["'self'"],
'script-src-attr': ["'none'"],
'style-src': ["'self'", 'https:', "'unsafe-inline'"],
'upgrade-insecure-requests': [],
'upgrade-insecure-requests': [] as string[],
};
export class ServiceBuilderImpl implements ServiceBuilder {
@@ -64,7 +64,7 @@ export class ServiceBuilderImpl implements ServiceBuilder {
private host: string | undefined;
private logger: Logger | undefined;
private corsOptions: cors.CorsOptions | undefined;
private cspOptions: CspOptions | undefined;
private cspOptions: Record<string, string[] | false> | undefined;
private httpsSettings: HttpsSettings | undefined;
private enableMetrics: boolean = true;
private routers: [string, Router][];
@@ -85,7 +85,10 @@ export class ServiceBuilderImpl implements ServiceBuilder {
const baseOptions = readBaseOptions(backendConfig);
if (baseOptions.listenPort) {
this.port = baseOptions.listenPort;
this.port =
typeof baseOptions.listenPort === 'string'
? parseInt(baseOptions.listenPort, 10)
: baseOptions.listenPort;
}
if (baseOptions.listenHost) {
this.host = baseOptions.listenHost;
@@ -154,20 +157,11 @@ export class ServiceBuilderImpl implements ServiceBuilder {
host,
logger,
corsOptions,
cspOptions,
httpsSettings,
helmetOptions,
} = this.getOptions();
app.use(
helmet({
contentSecurityPolicy: {
directives: {
...DEFAULT_CSP,
...cspOptions,
},
},
}),
);
app.use(helmet(helmetOptions));
if (corsOptions) {
app.use(cors(corsOptions));
}
@@ -214,16 +208,38 @@ export class ServiceBuilderImpl implements ServiceBuilder {
host: string;
logger: Logger;
corsOptions?: cors.CorsOptions;
cspOptions?: CspOptions;
httpsSettings?: HttpsSettings;
helmetOptions: HelmetOptions;
} {
return {
port: this.port ?? DEFAULT_PORT,
host: this.host ?? DEFAULT_HOST,
logger: this.logger ?? getRootLogger(),
corsOptions: this.corsOptions,
cspOptions: this.cspOptions,
httpsSettings: this.httpsSettings,
helmetOptions: {
contentSecurityPolicy: {
directives: applyCspDirectives(this.cspOptions),
},
},
};
}
}
export function applyCspDirectives(
directives: Record<string, string[] | false> | undefined,
): CspOptions | undefined {
const result: CspOptions = { ...DEFAULT_CSP };
if (directives) {
for (const [key, value] of Object.entries(directives)) {
if (value === false) {
delete result[key];
} else {
result[key] = value;
}
}
}
return result;
}
@@ -0,0 +1,51 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConfigReader } from '@backstage/config';
import { readCspOptions } from './config';
describe('config', () => {
describe('readCspOptions', () => {
it('reads valid values', () => {
const config = ConfigReader.fromConfigs([
{ context: '', data: { csp: { key: ['value'] } } },
]);
expect(readCspOptions(config)).toEqual(
expect.objectContaining({
key: ['value'],
}),
);
});
it('accepts false', () => {
const config = ConfigReader.fromConfigs([
{ context: '', data: { csp: { key: false } } },
]);
expect(readCspOptions(config)).toEqual(
expect.objectContaining({
key: false,
}),
);
});
it('rejects invalid value types', () => {
const config = ConfigReader.fromConfigs([
{ context: '', data: { csp: { key: [4] } } },
]);
expect(() => readCspOptions(config)).toThrow(/wanted string-array/);
});
});
});

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