Merge branch 'master' into rugvip/bapp
@@ -33,19 +33,11 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
registry-url: https://registry.npmjs.org/ # Needed for auth
|
||||
- name: cache all node_modules
|
||||
id: cache-modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
|
||||
- name: find location of global yarn cache
|
||||
id: yarn-cache
|
||||
if: steps.cache-modules.outputs.cache-hit != 'true'
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- name: cache global yarn cache
|
||||
uses: actions/cache@v2
|
||||
if: steps.cache-modules.outputs.cache-hit != 'true'
|
||||
with:
|
||||
path: ${{ steps.yarn-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
|
||||
|
||||
@@ -25,19 +25,11 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
registry-url: https://registry.npmjs.org/ # Needed for auth
|
||||
- name: cache all node_modules
|
||||
id: cache-modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
|
||||
- name: find location of global yarn cache
|
||||
id: yarn-cache
|
||||
if: steps.cache-modules.outputs.cache-hit != 'true'
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- name: cache global yarn cache
|
||||
uses: actions/cache@v2
|
||||
if: steps.cache-modules.outputs.cache-hit != 'true'
|
||||
with:
|
||||
path: ${{ steps.yarn-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
|
||||
|
||||
@@ -14,21 +14,15 @@ backend:
|
||||
client: sqlite3
|
||||
connection: ':memory:'
|
||||
|
||||
# See README.md in the proxy-backend plugin for information on the configuration format
|
||||
proxy:
|
||||
'/circleci/api':
|
||||
target: 'https://circleci.com/api/v1.1'
|
||||
changeOrigin: true
|
||||
pathRewrite:
|
||||
'^/proxy/circleci/api/': '/'
|
||||
'/circleci/api': https://circleci.com/api/v1.1
|
||||
'/jenkins/api':
|
||||
target: 'http://localhost:8080'
|
||||
changeOrigin: true
|
||||
target: http://localhost:8080
|
||||
headers:
|
||||
Authorization:
|
||||
$secret:
|
||||
env: JENKINS_BASIC_AUTH_HEADER
|
||||
pathRewrite:
|
||||
'^/proxy/jenkins/api/': '/'
|
||||
|
||||
organization:
|
||||
name: Spotify
|
||||
@@ -51,6 +45,26 @@ newrelic:
|
||||
key: NEW_RELIC_REST_API_KEY
|
||||
|
||||
catalog:
|
||||
processors:
|
||||
githubApi:
|
||||
privateToken:
|
||||
$secret:
|
||||
env: GITHUB_PRIVATE_TOKEN
|
||||
bitbucketApi:
|
||||
username:
|
||||
$secret:
|
||||
env: BITBUCKET_USERNAME
|
||||
appPassword:
|
||||
$secret:
|
||||
env: BITBUCKET_APP_PASSWORD
|
||||
gitlabApi:
|
||||
privateToken:
|
||||
$secret:
|
||||
env: GITLAB_PRIVATE_TOKEN
|
||||
azureApi:
|
||||
privateToken:
|
||||
$secret:
|
||||
env: AZURE_PRIVATE_TOKEN
|
||||
exampleEntityLocations:
|
||||
github:
|
||||
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml
|
||||
@@ -61,6 +75,11 @@ catalog:
|
||||
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-lib-component.yaml
|
||||
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/www-artist-component.yaml
|
||||
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/shuffle-api-component.yaml
|
||||
- https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml
|
||||
- https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/template.yaml
|
||||
- https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/create-react-app/template.yaml
|
||||
- https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml
|
||||
- https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml
|
||||
|
||||
auth:
|
||||
providers:
|
||||
@@ -136,8 +155,6 @@ auth:
|
||||
env: AUTH_AUTH0_DOMAIN
|
||||
microsoft:
|
||||
development:
|
||||
appOrigin: 'http://localhost:3000/'
|
||||
secure: false
|
||||
clientId:
|
||||
$secret:
|
||||
env: AUTH_MICROSOFT_CLIENT_ID
|
||||
|
||||
@@ -3,7 +3,7 @@ id: FAQ
|
||||
title: FAQ
|
||||
---
|
||||
|
||||
## Product FAQ:
|
||||
## Product FAQ
|
||||
|
||||
### Can we call Backstage something different? So that it fits our company better?
|
||||
|
||||
@@ -67,7 +67,7 @@ valuable as you grow.
|
||||
Yes! The Backstage UI is built using Material-UI. With the theming capabilities
|
||||
of Material-UI, you are able to adapt the interface to your brand guidelines.
|
||||
|
||||
## Technical FAQ:
|
||||
## Technical FAQ
|
||||
|
||||
### Why Material-UI?
|
||||
|
||||
|
||||
@@ -1,105 +1,3 @@
|
||||
# Documentation structure
|
||||
# Documentation
|
||||
|
||||
**Note!** This documentation structure is very much work in progress. If (when,
|
||||
really 😆) you find broken links or missing content, please create an issue or,
|
||||
better yet, a pull request.
|
||||
|
||||
# Plugins
|
||||
|
||||
- Overview
|
||||
- [What is Backstage?](overview/what-is-backstage.md)
|
||||
- [Backstage architecture](overview/architecture-overview.md)
|
||||
- [Architecture and terminology](overview/architecture-terminology.md)
|
||||
- [Roadmap](overview/roadmap.md)
|
||||
- [Vision](overview/vision.md)
|
||||
- [Strategies for adopting](overview/adopting.md)
|
||||
- Getting started
|
||||
- [Running Backstage locally](getting-started/index.md)
|
||||
- [Installation](getting-started/installation.md)
|
||||
- [Local development](getting-started/development-environment.md)
|
||||
- [Demo deployment](https://backstage-demo.roadie.io)
|
||||
- Production deployments
|
||||
- [Create an App](getting-started/create-an-app.md)
|
||||
- App configuration
|
||||
- [Configuring App with plugins](getting-started/configure-app-with-plugins.md)
|
||||
- [Customize the look-and-feel of your App](getting-started/app-custom-theme.md)
|
||||
- Deployment scenarios
|
||||
- [Kubernetes](getting-started/deployment-k8s.md)
|
||||
- [Other](getting-started/deployment-other.md)
|
||||
- Features
|
||||
- Software Catalog
|
||||
- [Overview](features/software-catalog/index.md)
|
||||
- [System model](features/software-catalog/system-model.md)
|
||||
- [YAML File Format](features/software-catalog/descriptor-format.md)
|
||||
- [Extending the model](features/software-catalog/extending-the-model.md)
|
||||
- [External integrations](features/software-catalog/external-integrations.md)
|
||||
- [API](features/software-catalog/api.md)
|
||||
- Software creation templates
|
||||
- [Overview](features/software-templates/index.md)
|
||||
- [Adding templates](features/software-templates/adding-templates.md)
|
||||
- Extending the Scaffolder:
|
||||
- [Overview](features/software-templates/extending/index.md)
|
||||
- [Create your own Templater](features/software-templates/extending/create-your-own-templater.md)
|
||||
- [Create your own Publisher](features/software-templates/extending/create-your-own-publisher.md)
|
||||
- [Create your own Preparer](features/software-templates/extending/create-your-own-preparer.md)
|
||||
- Docs-like-code
|
||||
- [Overview](features/techdocs/README.md)
|
||||
- [Getting Started](features/techdocs/getting-started.md)
|
||||
- [Concepts](features/techdocs/concepts.md)
|
||||
- [Creating and Publishing Documentation](features/techdocs/creating-and-publishing.md)
|
||||
- [FAQ](features/techdocs/FAQ.md)
|
||||
- Plugins
|
||||
- [Overview](plugins/index.md)
|
||||
- [Existing plugins](plugins/existing-plugins.md)
|
||||
- [Creating a new plugin](plugins/create-a-plugin.md)
|
||||
- [Developing a plugin](plugins/plugin-development.md)
|
||||
- [Structure of a plugin](plugins/structure-of-a-plugin.md)
|
||||
- Backends and APIs
|
||||
- [Proxying](plugins/proxying.md)
|
||||
- [Backstage backend plugin](plugins/backend-plugin.md)
|
||||
- [Call existing API](plugins/call-existing-api.md)
|
||||
- Testing
|
||||
- [Overview](plugins/testing.md)
|
||||
- Publishing
|
||||
- [Open source and NPM](plugins/publishing.md)
|
||||
- [Private/internal (non-open source)](plugins/publish-private.md)
|
||||
- Configuration
|
||||
- [Overview](conf/index.md)
|
||||
- [Reading Configuration](conf/reading.md)
|
||||
- [Writing Configuration](conf/writing.md)
|
||||
- [Defining Configuration](conf/defining.md)
|
||||
- Authentication and identity
|
||||
- [Overview](auth/index.md)
|
||||
- [Add auth provider](auth/add-auth-provider.md)
|
||||
- [Auth backend](auth/auth-backend.md)
|
||||
- [OAuth](auth/oauth.md)
|
||||
- [Glossary](auth/glossary.md)
|
||||
- Designing for Backstage
|
||||
- [Backstage Design Language System (DLS)](dls/design.md)
|
||||
- [Storybook -- reusable UI components](http://backstage.io/storybook)
|
||||
- [Contributing to Storybook](dls/contributing-to-storybook.md)
|
||||
- [Figma resources](dls/figma.md)
|
||||
- API references
|
||||
- TypeScript API
|
||||
- [Utility APIs](api/utility-apis.md)
|
||||
- [Utility API References](reference/utility-apis/README.md)
|
||||
- [createPlugin](reference/createPlugin.md)
|
||||
- [createPlugin-feature-flags](reference/createPlugin-feature-flags.md)
|
||||
- [createPlugin-router](reference/createPlugin-router.md)
|
||||
- Backend APIs
|
||||
- [Backend](api/backend.md)
|
||||
- Tutorials
|
||||
- [Overview](tutorials/index.md)
|
||||
- Architecture Decision Records (ADRs)
|
||||
- [Overview](architecture-decisions/index.md)
|
||||
- [ADR001 - Architecture Decision Record (ADR) log](architecture-decisions/adr001-add-adr-log.md)
|
||||
- [ADR002 - Default Software Catalog File Format](architecture-decisions/adr002-default-catalog-file-format.md)
|
||||
- [ADR003 - Avoid Default Exports and Prefer Named Exports](architecture-decisions/adr003-avoid-default-exports.md)
|
||||
- [ADR004 - Module Export Structure](architecture-decisions/adr004-module-export-structure.md)
|
||||
- [ADR005 - Catalog Core Entities](architecture-decisions/adr005-catalog-core-entities.md)
|
||||
- [ADR006 - Avoid React.FC and React.SFC](architecture-decisions/adr006-avoid-react-fc.md)
|
||||
- [ADR007 - Use MSW for Mocking Network Requests](architecture-decisions/adr007-use-msw-to-mock-service-requests.md)
|
||||
- [ADR008 - Default Catalog File Name](architecture-decisions/adr008-default-catalog-file-name.md)
|
||||
- [Contribute](../CONTRIBUTING.md)
|
||||
- [Support](overview/support.md)
|
||||
- [FAQ](FAQ.md)
|
||||
The Backstage documentation is available at https://backstage.io/docs
|
||||
|
||||
@@ -4,8 +4,6 @@ title: Architecture Decision Records (ADR)
|
||||
sidebar_label: Overview
|
||||
---
|
||||
|
||||
#
|
||||
|
||||
The substantial architecture decisions made in the Backstage project lives here.
|
||||
For more information about ADRs, when to write them, and why, please see
|
||||
[this blog post](https://engineering.atspotify.com/2020/04/14/when-should-i-write-an-architecture-decision-record/).
|
||||
@@ -25,7 +23,10 @@ Records should be stored under the `architecture-decisions` directory.
|
||||
- Submit a pull request
|
||||
- Address and integrate feedback from the community
|
||||
- Eventually, assign a number
|
||||
- Add the full path of the ADR to the [`mkdocs.yml`](/mkdocs.yml)
|
||||
- Add the path of the ADR to the microsite sidebar in
|
||||
[`sidebars.json`](https://github.com/spotify/backstage/blob/master/microsite/sidebars.json)
|
||||
- Add the path of the ADR to the
|
||||
[`mkdocs.yml`](https://github.com/spotify/backstage/blob/master/mkdocs.yml)
|
||||
- Merge the pull request
|
||||
|
||||
## Superseding an ADR
|
||||
|
||||
|
Before Width: | Height: | Size: 303 KiB After Width: | Height: | Size: 303 KiB |
|
Before Width: | Height: | Size: 269 KiB After Width: | Height: | Size: 269 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 414 KiB After Width: | Height: | Size: 414 KiB |
|
Before Width: | Height: | Size: 234 KiB After Width: | Height: | Size: 234 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 134 KiB |
@@ -1,4 +1,7 @@
|
||||
# Authentication Backend Classes Layout and Description
|
||||
---
|
||||
id: auth-backend-classes
|
||||
title: Auth backend classes
|
||||
---
|
||||
|
||||
## How Does Authentication Work?
|
||||
|
||||
|
||||
@@ -104,6 +104,8 @@ request an access token.
|
||||
|
||||
The following diagram visualizes the flow described in the previous section.
|
||||
|
||||

|
||||
|
||||
<!--
|
||||
@startuml oauth-popup-flow
|
||||
|
||||
@@ -152,5 +154,3 @@ Browser <- Backend: Tokens and info
|
||||
|
||||
@enduml
|
||||
-->
|
||||
|
||||

|
||||
|
||||
@@ -12,7 +12,7 @@ Backstage natively supports tracking of the following component
|
||||
- Documentation
|
||||
- Other
|
||||
|
||||

|
||||

|
||||
|
||||
Since these types are likely not the only kind of software you will want to
|
||||
track in Backstage, it is possible to
|
||||
@@ -31,9 +31,10 @@ catalog.
|
||||
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 advice against this; firstly,
|
||||
we have found that it is preferred to match the conceptual model that your
|
||||
engineers have when describing your sofware. Secondly, Backstage helps your
|
||||
engineers manage their software by integrating the infratrucure tooling through
|
||||
plugins. Different plugins are used for managing different types of components.
|
||||
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/spotify/backstage/tree/master/plugins/lighthouse)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
id: software-catalog-overview
|
||||
title: Backstage Service Catalog (alpha)
|
||||
sidebar_label: Backstage Service Catalog
|
||||
---
|
||||
|
||||
## What is a Service Catalog?
|
||||
@@ -54,18 +55,18 @@ There are 3 ways to add components to the catalog:
|
||||
Users can register new components by going to `/create` and clicking the
|
||||
**REGISTER EXISTING COMPONENT** button:
|
||||
|
||||

|
||||

|
||||
|
||||
Backstage expects the full URL to the YAML in your source control. Example:
|
||||
|
||||
```
|
||||
```bash
|
||||
https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml
|
||||
```
|
||||
|
||||
_More examples can be found
|
||||
[here](https://github.com/spotify/backstage/tree/master/packages/catalog-model/examples)._
|
||||
|
||||

|
||||

|
||||
|
||||
It is important to note that any kind of software can be registered in
|
||||
Backstage. Even if the software is not maintained by your company (SaaS
|
||||
@@ -83,7 +84,7 @@ registered in the catalog.
|
||||
Teams owning the components are responsible for maintaining the metadata about
|
||||
them, and do so using their normal Git workflow.
|
||||
|
||||

|
||||

|
||||
|
||||
Once the change has been merged, Backstage will automatically show the updated
|
||||
metadata in the service catalog after a short while.
|
||||
@@ -92,25 +93,25 @@ metadata in the service catalog after a short while.
|
||||
|
||||
By default the service catalog shows components owned by the team of the logged
|
||||
in user. But you can also switch to _All_ to see all the components across your
|
||||
companie's software ecosystem. Basic inline _search_ and _column filtering_
|
||||
makes it easy to browse a big set of components.
|
||||
company's software ecosystem. Basic inline _search_ and _column filtering_ makes
|
||||
it easy to browse a big set of components.
|
||||
|
||||

|
||||

|
||||
|
||||
## Starring components
|
||||
|
||||
For easy and quick access to components you visit frequently, Backstage supports
|
||||
_starring_ of components:
|
||||
|
||||

|
||||

|
||||
|
||||
## Integrated tooling through plugins
|
||||
|
||||
The service catalog is a great way to organise the infrastructure tools you use
|
||||
The service catalog is a great way to organize the infrastructure tools you use
|
||||
to manage the software. This is how Backstage creates one developer portal for
|
||||
all your tools. Rather than asking teams to jump between different
|
||||
infrastructure UI’s (and incurring additional cognitive overhead each time they
|
||||
make a context switch), most of these tools can be organised around the entities
|
||||
infrastructure UIs (and incurring additional cognitive overhead each time they
|
||||
make a context switch), most of these tools can be organized around the entities
|
||||
in the catalog.
|
||||
|
||||

|
||||
|
||||
@@ -16,11 +16,13 @@ We model software in the Backstage catalogue using these three core entities
|
||||
(further explained below):
|
||||
|
||||
- **Components** are individual pieces of software
|
||||
|
||||
- **APIs** are the boundaries between different components
|
||||
|
||||
- **Resources** are physical or virtual infrastructure needed to operate a
|
||||
component
|
||||
|
||||

|
||||

|
||||
|
||||
### Component
|
||||
|
||||
@@ -29,22 +31,22 @@ backend service or data pipeline (list not exhaustive). A component can be
|
||||
tracked in source control, or use some existing open source or commercial
|
||||
software.
|
||||
|
||||
A component can implement APIs for other components to consume. In turn it
|
||||
might depend on APIs implemented by other components, or resources that are
|
||||
attached to it at runtime.
|
||||
A component can implement APIs for other components to consume. In turn it might
|
||||
depend on APIs implemented by other components, or resources that are attached
|
||||
to it at runtime.
|
||||
|
||||
### API
|
||||
|
||||
APIs form an important (maybe the most important) abstraction that allows large
|
||||
software ecosystems to scale. Thus, APIs are a first class citizen in the
|
||||
Backstage model and the primary way to discover existing functionality in
|
||||
the ecosystem.
|
||||
Backstage model and the primary way to discover existing functionality in the
|
||||
ecosystem.
|
||||
|
||||
APIs are implemented by components and form boundaries between components. They
|
||||
might be defined using an RPC IDL (eg Protobuf, GraphQL, ...), a data schema
|
||||
(eg Avro, TFRecord, ...), or as code interfaces. In any case, APIs exposed by
|
||||
components need to be in a known machine-readable format so we can
|
||||
build further tooling and analysis on top.
|
||||
might be defined using an RPC IDL (eg Protobuf, GraphQL, ...), a data schema (eg
|
||||
Avro, TFRecord, ...), or as code interfaces. In any case, APIs exposed by
|
||||
components need to be in a known machine-readable format so we can build further
|
||||
tooling and analysis on top.
|
||||
|
||||
APIs have a visibility: they are either public (making them available for any
|
||||
other component to consume), restricted (only available to a whitelisted set of
|
||||
@@ -62,12 +64,13 @@ footprint, and create tooling around them.
|
||||
|
||||
## Ecosystem Modeling
|
||||
|
||||
A large catalogue of components, APIs and resources can be highly granular
|
||||
and hard to understand as a whole. It might thus be convenient to further
|
||||
categorize these entities using the following (optional) concepts:
|
||||
* **Systems** are a collection of entities that cooperate to perform some
|
||||
function
|
||||
* **Domains** relate entities and systems to part of the business
|
||||
A large catalogue of components, APIs and resources can be highly granular and
|
||||
hard to understand as a whole. It might thus be convenient to further categorize
|
||||
these entities using the following (optional) concepts:
|
||||
|
||||
- **Systems** are a collection of entities that cooperate to perform some
|
||||
function
|
||||
- **Domains** relate entities and systems to part of the business
|
||||
|
||||
### System
|
||||
|
||||
@@ -82,15 +85,16 @@ exposes one or several public APIs. The main benefit of modelling a system is
|
||||
that it hides its resources and private APIs between the components for any
|
||||
consumers. This means that as the owner, you can evolve the implementation, in
|
||||
terms of components and resources, without your consumers being able to notice.
|
||||
Typically, a system will consist of at most a handful of components (see
|
||||
Domain for a grouping of systems).
|
||||
Typically, a system will consist of at most a handful of components (see Domain
|
||||
for a grouping of systems).
|
||||
|
||||
For example, a playlist management system might encapsulate a backend service
|
||||
to update playlists, a backend service to query them, and a database to store
|
||||
them. It could expose an RPC API, a daily snapshots dataset, and an event
|
||||
stream of playlist updates.
|
||||
For example, a playlist management system might encapsulate a backend service to
|
||||
update playlists, a backend service to query them, and a database to store them.
|
||||
It could expose an RPC API, a daily snapshots dataset, and an event stream of
|
||||
playlist updates.
|
||||
|
||||
### Domain
|
||||
|
||||
While systems are the basic level of encapsulation for related entities, it is
|
||||
often useful to group a collection of systems that share terminology, domain
|
||||
models, metrics, KPIs, business purpose, or documentation, i.e. they form a
|
||||
|
||||
@@ -58,7 +58,7 @@ 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.
|
||||
|
||||
For loading from a file the following command should work when the backend is
|
||||
For loading from a file, the following command should work when the backend is
|
||||
running:
|
||||
|
||||
```sh
|
||||
@@ -69,7 +69,7 @@ curl \
|
||||
--data-raw "{\"type\": \"file\", \"target\": \"${YOUR PATH HERE}/template.yaml\"}"
|
||||
```
|
||||
|
||||
If loading from a git location, you can run the following
|
||||
If loading from a Git location, you can run the following
|
||||
|
||||
```sh
|
||||
curl \
|
||||
@@ -83,7 +83,7 @@ This should then have added the catalog, and also should now be listed under the
|
||||
create page at http://localhost:3000/create.
|
||||
|
||||
Alternatively, if you want to get setup with some mock templates that are
|
||||
already provided for you, you can run the following to load those templates:
|
||||
already provided, run the following to load those templates:
|
||||
|
||||
```
|
||||
yarn lerna run mock-data
|
||||
|
||||
@@ -7,8 +7,8 @@ Publishers are responsible for pushing and storing the templated skeleton after
|
||||
the values have been templated by the `Templater`. See
|
||||
[Create your own templater](./create-your-own-templater.md) for more info.
|
||||
|
||||
They recieve a directory or location where the templater has sucessfully run on,
|
||||
and is now ready to store somewhere. They also get given some other options
|
||||
They receive a directory or location where the templater has sucessfully run
|
||||
and is now ready to store somewhere. They also are given some other options
|
||||
which are sent from the frontend, such as the `storePath` which is a string of
|
||||
where the frontend thinks we should save this templated folder.
|
||||
|
||||
@@ -17,7 +17,7 @@ Currently we provide the following `publishers`:
|
||||
- `github`
|
||||
|
||||
This publisher is passed through to the `createRouter` function of the
|
||||
`@spotify/plugin-scaffolder-backend`. Currently only one publisher is supported,
|
||||
`@spotify/plugin-scaffolder-backend`. Currently, only one publisher is supported,
|
||||
but PR's are always welcome.
|
||||
|
||||
An full example backend can be found
|
||||
|
||||
@@ -8,14 +8,14 @@ returned by the preparers, and then executing the templating command on top of
|
||||
the file and returning the completed template path. This may or may not be the
|
||||
same directory as the input directory.
|
||||
|
||||
They also recieve additional values from the frontend, which can be used to
|
||||
They also receive additional values from the frontend, which can be used to
|
||||
interpolate into the skeleton files.
|
||||
|
||||
Currently we provide the following templaters:
|
||||
|
||||
- `cookiecutter`
|
||||
|
||||
This templater is added the `TemplaterBuilder` and then passed into the
|
||||
This templater is added to the `TemplaterBuilder` and then passed into the
|
||||
`createRouter` function of the `@spotify/plugin-scaffolder-backend`
|
||||
|
||||
An full example backend can be found
|
||||
@@ -48,7 +48,7 @@ This `TemplaterKey` is used to select the correct templater from the
|
||||
`spec.templater` in the
|
||||
[Template Entity](../../software-catalog/descriptor-format.md#kind-template).
|
||||
|
||||
If you wish to add a new templater you'll need to register it with the
|
||||
If you wish to add a new templater, you'll need to register it with the
|
||||
`TemplaterBuilder`.
|
||||
|
||||
### Creating your own Templater to add to the `TemplaterBuilder`
|
||||
@@ -83,10 +83,10 @@ follows:
|
||||
- `dockerClient` - a [dockerode](https://github.com/apocas/dockerode) client to
|
||||
be able to run docker containers.
|
||||
|
||||
_note_ currently the templaters that we provide are basically docker action
|
||||
_note_ Currently the templaters that we provide are basically Docker action
|
||||
containers that are run on top of the skeleton folder. This keeps dependencies
|
||||
to a minimal for running backstage scaffolder, but you don't /have/ to use
|
||||
docker. You could create your own templater that spins up an EC2 instance and
|
||||
Docker. You could create your own templater that spins up an EC2 instance and
|
||||
downloads the folder and does everything using an AMI if you want. It's entirely
|
||||
up to you!
|
||||
|
||||
@@ -138,7 +138,7 @@ spec:
|
||||
description: Description of the component
|
||||
```
|
||||
|
||||
You see that the `spec.templater` is set as `handlebars`, you'll need to
|
||||
You see that the `spec.templater` is set as `handlebars`, so you'll need to
|
||||
register this with the `TemplaterBuilder` like so:
|
||||
|
||||
```ts
|
||||
|
||||
@@ -5,11 +5,11 @@ title: Extending the Scaffolder
|
||||
|
||||
Welcome. Take a seat. You're at the Scaffolder Documentation.
|
||||
|
||||
So - You wanna create stuff inside your company from some prebaked templates?
|
||||
So, you want to create stuff inside your company from some prebaked templates?
|
||||
You're at the right place.
|
||||
|
||||
This guide is gonna take you through how the Scaffolder in Backstage works.
|
||||
We'll dive into some jargon and run through whats going on in the backend to be
|
||||
This guide is going to take you through how the Scaffolder in Backstage works.
|
||||
We'll dive into some jargon and run through what's going on in the backend to be
|
||||
able to create these templates. There's also more guides that you might find
|
||||
useful at the bottom of this document. At it's core, theres 3 simple stages.
|
||||
|
||||
@@ -25,9 +25,9 @@ scaffolder that you will need to know:
|
||||
3. Publish
|
||||
|
||||
Each of these steps can be configured for your own use case, but we provide some
|
||||
sensible defaults too.
|
||||
sensible defaults, too.
|
||||
|
||||
Lets dive a little deeper into these phases.
|
||||
Let's dive a little deeper into these phases.
|
||||
|
||||
### Glossary and Jargon
|
||||
|
||||
@@ -38,8 +38,8 @@ the router to pick the correct `Preparer` to run for the `Template` entity.
|
||||
|
||||
**Templater** - The templater is responsible for actually running the chosen
|
||||
templater on top of the previously returned temporary directory from the
|
||||
**Preprarer**. We advise making these docker containers as it can keep all
|
||||
dependencies, for example Cookiecutter, self contained and not a dependency on
|
||||
**Preprarer**. We advise making these Docker containers as it can keep all
|
||||
dependencies--for example Cookiecutter--self contained and not a dependency on
|
||||
the host machine.
|
||||
|
||||
**Publisher** - The publisher is responsible for taking the finished directory,
|
||||
@@ -50,11 +50,11 @@ passed through to the scaffolder backend.
|
||||
|
||||
### How it works
|
||||
|
||||
The main of the heavy lifting is done in the
|
||||
Most of the heavy lifting is done in the
|
||||
[router.ts](https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/src/service/router.ts#L93)
|
||||
file in the `scaffolder-backend` plugin.
|
||||
|
||||
There are 2 routes defined in the router. `POST /v1/jobs` and
|
||||
There are two routes defined in the router: `POST /v1/jobs` and
|
||||
`GET /v1/job/:jobId`
|
||||
|
||||
To create a scaffolding job, a JSON object containing the
|
||||
@@ -78,7 +78,7 @@ additional templating values must be posted as the post body.
|
||||
The values should represent something that is valid with the `schema` part of
|
||||
the [Template Entity](../../software-catalog/descriptor-format.md#kind-template)
|
||||
|
||||
Once that has been posted, a job will be setup with different stages. And the
|
||||
Once that has been posted, a job will be setup with different stages, and the
|
||||
job processor will complete each stage before moving onto the next stage, whilst
|
||||
collecting logs and mutating the running job.
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ title: Software Templates
|
||||
---
|
||||
|
||||
The Software Templates part of Backstage is a tool that can help you create
|
||||
Components inside Backstage. It by default has the ability to load skeletons of
|
||||
code, template in some variables and then publish the template to some location
|
||||
Components inside Backstage. By default, it has the ability to load skeletons of
|
||||
code, template in some variables, and then publish the template to some location
|
||||
like GitHub.
|
||||
|
||||
<video width="100%" height="100%" controls>
|
||||
@@ -33,8 +33,8 @@ internally.
|
||||

|
||||
|
||||
After filling in these variables, you'll get some more fields to fill out which
|
||||
are required for backstage usage. The owner, which is a `user` in the backstage
|
||||
system, and the `storePath` which right now must be a GitHub Organisation and a
|
||||
are required for backstage usage: the owner, (which is a `user` in the backstage
|
||||
system), the `storePath` (which right now must be a GitHub Organisation), and a
|
||||
non-existing github repository name in the format `organisation/reponame`.
|
||||
|
||||

|
||||
@@ -51,13 +51,13 @@ It shouldn't take too long, and you'll have a success screen!
|
||||

|
||||
|
||||
If it fails, you'll be able to click on each section to get the log from the
|
||||
step that failed which can be helpful to debug.
|
||||
step that failed which can be helpful in debugging.
|
||||
|
||||

|
||||
|
||||
### View Component in Catalog
|
||||
|
||||
When it's been created you'll see the `View in Catalog` button, which will take
|
||||
When it's been created, you'll see the `View in Catalog` button, which will take
|
||||
you to the registered component in the catalog:
|
||||
|
||||

|
||||
|
||||
@@ -101,3 +101,7 @@ more to come...
|
||||
https://github.com/spotify/backstage/blob/master/packages/techdocs-container
|
||||
[techdocs/cli]:
|
||||
https://github.com/spotify/backstage/blob/master/packages/techdocs-cli
|
||||
|
||||
## TechDocs Big Picture
|
||||
|
||||

|
||||
|
||||
@@ -53,4 +53,4 @@ Reader. The reason why transformers were introduced was to provide a way to
|
||||
transform the HTML content on pre and post render (e.g. rewrite docs links or
|
||||
modify css).
|
||||
|
||||
[Transformers API docs](https://github.com/spotify/backstage/blob/master/plugins/techdocs/src/reader/transformers/README.md)
|
||||
[Transformers API docs](https://github.com/spotify/backstage/blob/master/plugins/techdocs/src/reader/README.md)
|
||||
|
||||
@@ -30,6 +30,11 @@ the documentation template.
|
||||
Create an entity from the documentation template and you will get the needed
|
||||
setup for free.
|
||||
|
||||
!!! warning Currently the Backstage Software Templates are limited to create repositories
|
||||
inside GitHub organizations. You also need to generate an personal access token
|
||||
and use as an environment variable. Read more about this
|
||||
[here](../software-templates/installation.md#runtime-dependencies).
|
||||
|
||||
### Manually add documentation setup to already existing repository
|
||||
|
||||
Prerequisities:
|
||||
|
||||
@@ -85,7 +85,3 @@ yarn create-plugin # Create a new plugin
|
||||
> See
|
||||
> [package.json](https://github.com/spotify/backstage/blob/master/package.json)
|
||||
> for other yarn commands/options.
|
||||
|
||||
[Next Step - Create a Backstage plugin](../plugins/create-a-plugin.md)
|
||||
|
||||
[Back to Docs](../README.md)
|
||||
|
||||
@@ -3,9 +3,22 @@ id: index
|
||||
title: Running Backstage Locally
|
||||
---
|
||||
|
||||
First make sure you are using NodeJS with an Active LTS Release, currently v12.
|
||||
This is made easy with a version manager such as nvm which allows for version switching.
|
||||
|
||||
```bash
|
||||
# Checking your version
|
||||
node --version
|
||||
> v14.7.0
|
||||
|
||||
# Adding a second node version
|
||||
nvm install 12
|
||||
> Downloading and installing node v12.18.3...
|
||||
> Now using node v12.18.3 (npm v6.14.6)
|
||||
```
|
||||
|
||||
To get up and running with a local Backstage to evaluate it, let's clone it off
|
||||
of GitHub and run an initial build. First make sure that you have at least node
|
||||
version 12 installed locally.
|
||||
of GitHub and run an initial build.
|
||||
|
||||
```bash
|
||||
# Start from your local development folder
|
||||
|
||||
@@ -87,14 +87,17 @@ Example of tactics we have used to evangelize Backstage internally:
|
||||
- Pro-actively identify new plugins. Reach out to teams that own internal UIs or
|
||||
platforms that you think would make sense to consolidate into Backstage.
|
||||
|
||||
## Metrics
|
||||
## KPIs and metrics
|
||||
|
||||
These are some of the metrics that you can use to verify if Backstage has a
|
||||
successful impact on your software development process:
|
||||
|
||||
- **Onboarding time** Time until new engineers are productive. At Spotify we
|
||||
measure this as the time until the employee has merged their 10th PR (this
|
||||
metric was down 55% two years after deploying Backstage).
|
||||
metric was down 55% two years after deploying Backstage). Even though you may
|
||||
not be onboarding engineers at a rapid pace, this metric is a great proxy for
|
||||
the overall complexity of your ecosystem. Reducing it will therefore benefit
|
||||
your whole engineering organization, not just new joiners.
|
||||
|
||||
- **Number of merges per developer/day** Less time spent jumping between
|
||||
different tools and looking for information means more time to focus on
|
||||
@@ -142,3 +145,16 @@ Backstage as _the_ platform:
|
||||
|
||||
- % of contributions coming from outside the central Backstage team (currently
|
||||
85% inside Spotify)
|
||||
|
||||
- Traditional metrics such as visits (MAU, DAU, etc) and page views. Currently
|
||||
~50% of all Spotifiers use Backstage on a monthly basis, even though the
|
||||
percentage of engineers is below 50%. Most engineers actually use Backstage on
|
||||
a daily basis.
|
||||
|
||||
Again, any feedback is appreciated. Please use the Edit button at the top of the
|
||||
page to make a suggestion.
|
||||
|
||||
_**Note!** It might be tempting to try to optimize Backstage usage and
|
||||
"engagement". Even though you want to consolidate all your tooling and technical
|
||||
documentation in Backstage, it is important to remember that time spent in
|
||||
Backstage is time not spent writing code_ 🙃
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
id: background
|
||||
title: The Spotify Story
|
||||
---
|
||||
|
||||
Backstage was born out of necessity at Spotify. We found that as we grew, our
|
||||
infrastructure was becoming more fragmented, our engineers less productive.
|
||||
|
||||
Instead of building and testing code, teams were spending more time looking for
|
||||
the right information just to get started. “Where’s the API for that service
|
||||
we’re all supposed to be using?” “What version of that framework is everyone
|
||||
on?” “This service isn’t responding, who owns it?” “I can’t find documentation
|
||||
for anything!”
|
||||
|
||||
Context switching and cognitive overload were dragging engineers down, day by
|
||||
day. We needed to make it easier for our engineers to do their work without
|
||||
having to become an expert in every aspect of infrastructure tooling.
|
||||
|
||||
Our idea was to centralize and simplify end-to-end software development with an
|
||||
abstraction layer that sits on top of all of our infrastructure and developer
|
||||
tooling. That’s Backstage.
|
||||
|
||||
It’s a developer portal powered by a centralized service catalog — with a plugin
|
||||
architecture that makes it endlessly extensible and customizable.
|
||||
|
||||
Manage all your services, software, tooling, and testing in Backstage. Start
|
||||
building a new microservice using an automated template in Backstage. Create,
|
||||
maintain, and find the documentation for all that software in Backstage.
|
||||
|
||||
One place for everything. Accessible to everyone.
|
||||
@@ -71,11 +71,7 @@ Example:
|
||||
```yaml
|
||||
# In app-config.yaml
|
||||
proxy:
|
||||
'/frobs':
|
||||
target: 'http://api.frobsco.com/v1'
|
||||
changeOrigin: true
|
||||
pathRewrite:
|
||||
'^/proxy/frobs/': '/'
|
||||
'/frobs': http://api.frobsco.com/v1
|
||||
```
|
||||
|
||||
```ts
|
||||
@@ -86,9 +82,8 @@ fetch(`${backendUrl}/proxy/frobs/list`)
|
||||
.then(payload => setFrobs(payload as Frob[]));
|
||||
```
|
||||
|
||||
The proxy is powered by the `http-proxy-middleware` package, and supports all of
|
||||
its
|
||||
[configuration options](https://github.com/chimurai/http-proxy-middleware#options).
|
||||
The proxy is powered by the `http-proxy-middleware` package. See
|
||||
[Proxying](proxying.md) for a full description of its configuration options.
|
||||
|
||||
Internally at Spotify, the proxy option has been the overwhelmingly most popular
|
||||
choice for plugin makers. Since we have DNS based service discovery in place and
|
||||
|
||||
@@ -15,20 +15,16 @@ dependencies, then run the following on your command line (invoking the
|
||||
yarn create-plugin
|
||||
```
|
||||
|
||||
<p align='center'>
|
||||
<img src='https://github.com/spotify/backstage/raw/master/docs/getting-started/create-plugin_output.png' width='600' alt='create plugin'>
|
||||
</p>
|
||||

|
||||
|
||||
This will create a new Backstage Plugin based on the ID that was provided. It
|
||||
will be built and added to the Backstage App automatically.
|
||||
|
||||
_If `yarn start` is already running you should be able to see the default page
|
||||
for your new plugin directly by navigating to
|
||||
`http://localhost:3000/my-plugin`._
|
||||
> If `yarn start` is already running you should be able to see the default page
|
||||
> for your new plugin directly by navigating to
|
||||
> `http://localhost:3000/my-plugin`.
|
||||
|
||||
<p align='center'>
|
||||
<img src='https://github.com/spotify/backstage/raw/master/docs/plugins/my-plugin_screenshot.png' width='600' alt='my plugin'>
|
||||
</p>
|
||||

|
||||
|
||||
You can also serve the plugin in isolation by running `yarn start` in the plugin
|
||||
directory. Or by using the yarn workspace command, for example:
|
||||
@@ -40,7 +36,3 @@ yarn workspace @backstage/plugin-welcome start # Also supports --check
|
||||
This method of serving the plugin provides quicker iteration speed and a faster
|
||||
startup and hot reloads. It is only meant for local development, and the setup
|
||||
for it can be found inside the plugin's `dev/` directory.
|
||||
|
||||
[Next Step - Structure of a plugin](structure-of-a-plugin.md)
|
||||
|
||||
[Back to Getting Started](../README.md)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
id: plugin-development
|
||||
title: Plugin Development in Backstage
|
||||
title: Plugin Development
|
||||
---
|
||||
|
||||
Backstage plugins provide features to a Backstage App.
|
||||
@@ -10,28 +10,12 @@ type of content. Plugins all use a common set of platform APIs and reusable UI
|
||||
components. Plugins can fetch data from external sources using the regular
|
||||
browser APIs or by depending on external modules to do the work.
|
||||
|
||||
<!-- MOVED TO create-a-plugin.md ## Creating a new plugin
|
||||
On your command line, invoke the `backstage-cli` to create a new plugin:
|
||||
```bash
|
||||
yarn create-plugin
|
||||
```
|
||||
|
||||

|
||||
|
||||
This will create a new Backstage Plugin based on the ID that was provided. It will be built and
|
||||
added to the Backstage App automatically.
|
||||
|
||||
*If `yarn start` is already running you should be able to see the default page for your new
|
||||
plugin directly by navigating to `http://localhost:3000/my-plugin`.*
|
||||
|
||||
 -->
|
||||
|
||||
## Developing guidelines
|
||||
|
||||
- Consider writing plugins in `TypeScript`.
|
||||
- Plan the directory structure of your plugin so that it becomes easy to manage.
|
||||
- Prefer using the Backstage components, otherwise go with
|
||||
[Material-UI](https://material-ui.com/).
|
||||
- Prefer using the [Backstage components](https://backstage.io/storybook),
|
||||
otherwise go with [Material-UI](https://material-ui.com/).
|
||||
- Check out the shared Backstage APIs before building a new one.
|
||||
|
||||
## Plugin concepts / API
|
||||
|
||||
@@ -3,4 +3,73 @@ id: proxying
|
||||
title: Proxying
|
||||
---
|
||||
|
||||
## TODO
|
||||
## Overview
|
||||
|
||||
The Backstage backend comes packaged with a basic HTTP proxy, that can aid in
|
||||
reaching backend service APIs from frontend plugin code. See
|
||||
[Call Existing API](call-existing-api.md) for a description of when the proxy
|
||||
can be the best choice for communicating with an API.
|
||||
|
||||
## Getting Started
|
||||
|
||||
The plugin is already added to a default Backstage project.
|
||||
|
||||
In `packages/backend/src/index.ts`:
|
||||
|
||||
```ts
|
||||
const proxyEnv = useHotMemoize(module, () => createEnv('proxy'));
|
||||
|
||||
const service = createServiceBuilder(module)
|
||||
.loadConfig(configReader)
|
||||
/** ... other routers ... */
|
||||
.addRouter('/proxy', await proxy(proxyEnv, '/proxy'));
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration for the proxy plugin lives under a `proxy` root key of your
|
||||
`app-config.yaml` file.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
# in app-config.yaml
|
||||
proxy:
|
||||
'/simple-example': http://simple.example.com:8080
|
||||
'/larger-example/v1':
|
||||
target: http://larger.example.com:8080/svc.v1
|
||||
headers:
|
||||
Authorization:
|
||||
$secret:
|
||||
env: EXAMPLE_AUTH_HEADER
|
||||
```
|
||||
|
||||
Each key under the proxy configuration entry is a route to match, below the
|
||||
prefix that the proxy plugin is mounted on. It must start with a slash. For
|
||||
example, if the backend mounts the proxy plugin as `/proxy`, the above
|
||||
configuration will lead to the proxy acting on backend requests to
|
||||
`/proxy/simple-example/...` and `/proxy/larger-example/v1/...`.
|
||||
|
||||
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).
|
||||
|
||||
If the value is a string, it is assumed to correspond to:
|
||||
|
||||
```yaml
|
||||
target: <the string>
|
||||
changeOrigin: true
|
||||
pathRewrite:
|
||||
'^<url prefix><the string>/': '/'
|
||||
```
|
||||
|
||||
When the target is an object, it is given verbatim to `http-proxy-middleware`
|
||||
except with the following caveats for convenience:
|
||||
|
||||
- If `changeOrigin` is not specified, it is set to `true`. This is the most
|
||||
commonly useful value.
|
||||
- If `pathRewrite` is not specified, it is set to a single rewrite that removes
|
||||
the entire prefix and route. In the above example, a rewrite of
|
||||
`'^/proxy/larger-example/v1/': '/'` is added. That means that a request to
|
||||
`/proxy/larger-example/v1/some/path` will be translated to a request to
|
||||
`http://larger.example.com:8080/svc.v1/some/path`.
|
||||
|
||||
@@ -37,5 +37,3 @@ const myPluginRouteRef = createRouteRef({
|
||||
title: 'My Plugin',
|
||||
});
|
||||
```
|
||||
|
||||
[Back to References](../README.md)
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
id: index
|
||||
title: Overview
|
||||
---
|
||||
|
||||
## Coming soon!
|
||||
@@ -1,12 +1,15 @@
|
||||
# Purpose
|
||||
---
|
||||
id: journey
|
||||
title: Future developer journey
|
||||
---
|
||||
|
||||
This RFC describes a possible journey of a future Backstage plugin developer as
|
||||
they build a plugin that touches many different aspects of a Backstage. The
|
||||
story invents many new things that are not part of Backstage today, but are
|
||||
things that I'm suggesting we should add as long term or north star goals. The
|
||||
idea is to discuss what parts of the story makes sense to aim for, and what we'd
|
||||
want to do differently or not at all. The "chapters" are numbered to make it a
|
||||
bit easier to comment on parts of the story.
|
||||
> This document describes a possible journey of a **_future_** Backstage plugin
|
||||
> developer as they build a plugin that touches many different aspects of a
|
||||
> Backstage. The story invents many new things that are not part of Backstage
|
||||
> today, but are things that I'm suggesting we should add as long term or north
|
||||
> star goals. The idea is to discuss what parts of the story makes sense to aim
|
||||
> for, and what we'd want to do differently or not at all. The "chapters" are
|
||||
> numbered to make it a bit easier to comment on parts of the story.
|
||||
|
||||
# The Protagonist
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
| app.resources | Kubernetes Pod resource requests/limits | `{}` |
|
||||
| app.nodeSelector | Node selectors for scheduling app/frontend pods | `{}` |
|
||||
| app.tolerations | Tolerations for scheduling app/frontend pods | `{}` |
|
||||
| app.affinity | Affinity setttings for scheduling app/frontend pods | `{}` |
|
||||
| app.affinity | Affinity settings for scheduling app/frontend pods | `{}` |
|
||||
|
||||
## Backend Values
|
||||
|
||||
@@ -48,4 +48,4 @@
|
||||
| backend.resources | Kubernetes Pod resource requests/limits | `{}` |
|
||||
| backend.nodeSelector | Node selectors for scheduling backend pods | `{}` |
|
||||
| backend.tolerations | Tolerations for scheduling backend pods | `{}` |
|
||||
| backend.affinity | Affinity setttings for scheduling backend pods | `{}` |
|
||||
| backend.affinity | Affinity settings for scheduling backend pods | `{}` |
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"title": "Adding authentication providers"
|
||||
},
|
||||
"auth/auth-backend-classes": {
|
||||
"title": "auth/auth-backend-classes"
|
||||
"title": "Auth backend classes"
|
||||
},
|
||||
"auth/auth-backend": {
|
||||
"title": "Auth backend"
|
||||
@@ -103,7 +103,8 @@
|
||||
"title": "External integrations"
|
||||
},
|
||||
"features/software-catalog/software-catalog-overview": {
|
||||
"title": "Backstage Service Catalog (alpha)"
|
||||
"title": "Backstage Service Catalog (alpha)",
|
||||
"sidebar_label": "Backstage Service Catalog"
|
||||
},
|
||||
"features/software-catalog/installation": {
|
||||
"title": "features/software-catalog/installation"
|
||||
@@ -174,9 +175,6 @@
|
||||
"getting-started/installation": {
|
||||
"title": "Installation"
|
||||
},
|
||||
"journey": {
|
||||
"title": "journey"
|
||||
},
|
||||
"overview/adopting": {
|
||||
"title": "Strategies for adopting"
|
||||
},
|
||||
@@ -186,6 +184,9 @@
|
||||
"overview/architecture-terminology": {
|
||||
"title": "Architecture terminology"
|
||||
},
|
||||
"overview/background": {
|
||||
"title": "The Spotify Story"
|
||||
},
|
||||
"overview/roadmap": {
|
||||
"title": "Project roadmap"
|
||||
},
|
||||
@@ -214,7 +215,7 @@
|
||||
"title": "Intro to plugins"
|
||||
},
|
||||
"plugins/plugin-development": {
|
||||
"title": "Plugin Development in Backstage"
|
||||
"title": "Plugin Development"
|
||||
},
|
||||
"plugins/proxying": {
|
||||
"title": "Proxying"
|
||||
@@ -285,8 +286,8 @@
|
||||
"reference/utility-apis/StorageApi": {
|
||||
"title": "reference/utility-apis/StorageApi"
|
||||
},
|
||||
"tutorials/index": {
|
||||
"title": "Overview"
|
||||
"tutorials/journey": {
|
||||
"title": "Future developer journey"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
@@ -294,7 +295,6 @@
|
||||
"Docs": "Docs",
|
||||
"Blog": "Blog",
|
||||
"Demos": "Demos",
|
||||
"The Spotify story": "The Spotify story",
|
||||
"Newsletter": "Newsletter"
|
||||
},
|
||||
"categories": {
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
const React = require('react');
|
||||
const Components = require(`${process.cwd()}/core/Components.js`);
|
||||
const Block = Components.Block;
|
||||
const Breakpoint = Components.Breakpoint;
|
||||
|
||||
const Background = props => {
|
||||
const { config: siteConfig } = props;
|
||||
const { baseUrl } = siteConfig;
|
||||
return (
|
||||
<div className="mainWrapper">
|
||||
<Block>
|
||||
<Block.Container column>
|
||||
<Block.TitleBox story>The Spotify Story</Block.TitleBox>
|
||||
<Block.TextBox>
|
||||
<Block.Paragraph>
|
||||
Backstage was born out of necessity at Spotify. We found that as
|
||||
we grew, our infrastructure was becoming more fragmented, our
|
||||
engineers less productive.{' '}
|
||||
</Block.Paragraph>
|
||||
|
||||
<Block.Paragraph>
|
||||
Instead of building and testing code, teams were spending more
|
||||
time looking for the right information just to get started.
|
||||
“Where’s the API for that service we’re all supposed to be using?”
|
||||
“What version of that framework is everyone on?” “This service
|
||||
isn’t responding, who owns it?” “I can’t find documentation for
|
||||
anything!”{' '}
|
||||
</Block.Paragraph>
|
||||
<Breakpoint
|
||||
narrow={
|
||||
<Block small>
|
||||
<Block.QuoteContainer>
|
||||
<Block.Divider quote />
|
||||
|
||||
<Block.Quote>
|
||||
One place for everything. Accessible to everyone.
|
||||
</Block.Quote>
|
||||
</Block.QuoteContainer>
|
||||
</Block>
|
||||
}
|
||||
></Breakpoint>
|
||||
<Block.Paragraph>
|
||||
Context switching and cognitive overload were dragging engineers
|
||||
down, day by day. We needed to make it easier for our engineers to
|
||||
do their work without having to become an expert in every aspect
|
||||
of infrastructure tooling.
|
||||
</Block.Paragraph>
|
||||
|
||||
<Block.Paragraph>
|
||||
Our idea was to centralize and simplify end-to-end software
|
||||
development with an abstraction layer that sits on top of all of
|
||||
our infrastructure and developer tooling. That’s Backstage.
|
||||
</Block.Paragraph>
|
||||
|
||||
<Block.Paragraph>
|
||||
It’s a developer portal powered by a centralized service catalog —
|
||||
with a plugin architecture that makes it endlessly extensible and
|
||||
customizable.
|
||||
</Block.Paragraph>
|
||||
|
||||
<Block.Paragraph>
|
||||
Manage all your services, software, tooling, and testing in
|
||||
Backstage. Start building a new microservice using an automated
|
||||
template in Backstage. Create, maintain, and find the
|
||||
documentation for all that software in Backstage.{' '}
|
||||
</Block.Paragraph>
|
||||
|
||||
<Block.Paragraph>
|
||||
One place for everything. Accessible to everyone.
|
||||
</Block.Paragraph>
|
||||
<Breakpoint
|
||||
narrow={
|
||||
<Block small>
|
||||
<Block.LinkButton stretch href={'https://backstage.io/'}>
|
||||
Explore Features
|
||||
</Block.LinkButton>
|
||||
</Block>
|
||||
}
|
||||
></Breakpoint>
|
||||
</Block.TextBox>
|
||||
</Block.Container>
|
||||
</Block>
|
||||
<Breakpoint
|
||||
wide={
|
||||
<Block small>
|
||||
<Block.QuoteContainer>
|
||||
<Block.Divider quote />
|
||||
|
||||
<Block.Quote>
|
||||
One place for everything. Accessible to everyone.
|
||||
</Block.Quote>
|
||||
<Block.LinkButton stretch href={'https://backstage.io/'}>
|
||||
Explore Features
|
||||
</Block.LinkButton>
|
||||
</Block.QuoteContainer>
|
||||
</Block>
|
||||
}
|
||||
></Breakpoint>
|
||||
|
||||
<div
|
||||
style={{
|
||||
zIndex: -1,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundImage: `linear-gradient( to bottom, rgb(18, 18, 18), rgba(0, 0, 0, 0) ), url(../img/dot.svg)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = Background;
|
||||
@@ -6,6 +6,7 @@
|
||||
"overview/architecture-terminology",
|
||||
"overview/roadmap",
|
||||
"overview/vision",
|
||||
"overview/background",
|
||||
"overview/adopting"
|
||||
],
|
||||
"Getting Started": [
|
||||
@@ -38,7 +39,6 @@
|
||||
"features/software-catalog/software-catalog-overview",
|
||||
"features/software-catalog/system-model",
|
||||
"features/software-catalog/descriptor-format",
|
||||
"features/software-catalog/populating-catalog",
|
||||
"features/software-catalog/extending-the-model",
|
||||
"features/software-catalog/external-integrations",
|
||||
"features/software-catalog/software-catalog-api"
|
||||
@@ -132,7 +132,7 @@
|
||||
"ids": ["api/backend"]
|
||||
}
|
||||
],
|
||||
"Tutorials": ["tutorials/index"],
|
||||
"Tutorials": ["tutorials/journey"],
|
||||
"Architecture Decision Records (ADRs)": [
|
||||
"architecture-decisions/adrs-overview",
|
||||
"architecture-decisions/adrs-adr001",
|
||||
|
||||
@@ -17,7 +17,7 @@ const siteConfig = {
|
||||
url: 'https://backstage.io', // Your website URL
|
||||
cname: 'backstage.io',
|
||||
baseUrl: '/', // Base URL for your project */
|
||||
editUrl: 'https://github.com/spotify/backstage/tree/master/docs/',
|
||||
editUrl: 'https://github.com/spotify/backstage/edit/master/docs/',
|
||||
|
||||
// Used for publishing and more
|
||||
projectName: 'backstage',
|
||||
@@ -47,10 +47,6 @@ const siteConfig = {
|
||||
page: 'demos',
|
||||
label: 'Demos',
|
||||
},
|
||||
{
|
||||
page: 'background',
|
||||
label: 'The Spotify story',
|
||||
},
|
||||
{
|
||||
href: 'https://mailchi.mp/spotify/backstage-community',
|
||||
label: 'Newsletter',
|
||||
@@ -114,6 +110,12 @@ const siteConfig = {
|
||||
stylesheets: [
|
||||
'https://fonts.googleapis.com/css?family=IBM+Plex+Mono:500,700&display=swap',
|
||||
],
|
||||
|
||||
algolia: {
|
||||
apiKey: '8d115c9875ba0f4feaee95bab55a1645',
|
||||
indexName: 'backstage',
|
||||
searchParameters: {}, // Optional (if provided by Algolia)
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = siteConfig;
|
||||
|
||||
@@ -8,6 +8,7 @@ nav:
|
||||
- Architecture and terminology: 'overview/architecture-terminology.md'
|
||||
- Roadmap: 'overview/roadmap.md'
|
||||
- Vision: 'overview/vision.md'
|
||||
- The Spotify story: 'overview/background.md'
|
||||
- Strategies for adopting: 'overview/adopting.md'
|
||||
- Getting started:
|
||||
- Running Backstage locally: 'getting-started/index.md'
|
||||
|
||||
@@ -60,5 +60,10 @@
|
||||
"*.{json,md}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"jest": {
|
||||
"transformModules": [
|
||||
"@kyma-project/asyncapi-react"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
ErrorAlerter,
|
||||
featureFlagsApiRef,
|
||||
FeatureFlags,
|
||||
discoveryApiRef,
|
||||
UrlPatternDiscovery,
|
||||
GoogleAuth,
|
||||
GithubAuth,
|
||||
OAuth2,
|
||||
@@ -90,6 +92,10 @@ export const apis = (config: ConfigApi) => {
|
||||
|
||||
const builder = ApiRegistry.builder();
|
||||
|
||||
const discoveryApi = builder.add(
|
||||
discoveryApiRef,
|
||||
UrlPatternDiscovery.compile(`${backendUrl}/{{ pluginId }}`),
|
||||
);
|
||||
const alertApi = builder.add(alertApiRef, new AlertApiForwarder());
|
||||
const errorApi = builder.add(
|
||||
errorApiRef,
|
||||
@@ -121,8 +127,7 @@ export const apis = (config: ConfigApi) => {
|
||||
builder.add(
|
||||
googleAuthApiRef,
|
||||
GoogleAuth.create({
|
||||
backendUrl,
|
||||
basePath: '/auth/',
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
);
|
||||
@@ -130,8 +135,7 @@ export const apis = (config: ConfigApi) => {
|
||||
builder.add(
|
||||
microsoftAuthApiRef,
|
||||
MicrosoftAuth.create({
|
||||
backendUrl,
|
||||
basePath: '/auth/',
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
);
|
||||
@@ -139,8 +143,7 @@ export const apis = (config: ConfigApi) => {
|
||||
const githubAuthApi = builder.add(
|
||||
githubAuthApiRef,
|
||||
GithubAuth.create({
|
||||
backendUrl,
|
||||
basePath: '/auth/',
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
);
|
||||
@@ -148,8 +151,7 @@ export const apis = (config: ConfigApi) => {
|
||||
builder.add(
|
||||
oktaAuthApiRef,
|
||||
OktaAuth.create({
|
||||
backendUrl,
|
||||
basePath: '/auth/',
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
);
|
||||
@@ -157,8 +159,7 @@ export const apis = (config: ConfigApi) => {
|
||||
builder.add(
|
||||
gitlabAuthApiRef,
|
||||
GitlabAuth.create({
|
||||
backendUrl,
|
||||
basePath: '/auth/',
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
);
|
||||
@@ -166,8 +167,7 @@ export const apis = (config: ConfigApi) => {
|
||||
builder.add(
|
||||
auth0AuthApiRef,
|
||||
Auth0Auth.create({
|
||||
backendUrl,
|
||||
basePath: '/auth/',
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
);
|
||||
@@ -175,8 +175,7 @@ export const apis = (config: ConfigApi) => {
|
||||
builder.add(
|
||||
oauth2ApiRef,
|
||||
OAuth2.create({
|
||||
backendUrl,
|
||||
basePath: '/auth/',
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
);
|
||||
@@ -189,21 +188,9 @@ export const apis = (config: ConfigApi) => {
|
||||
}),
|
||||
);
|
||||
|
||||
builder.add(
|
||||
catalogApiRef,
|
||||
new CatalogClient({
|
||||
apiOrigin: backendUrl,
|
||||
basePath: '/catalog',
|
||||
}),
|
||||
);
|
||||
builder.add(catalogApiRef, new CatalogClient({ discoveryApi }));
|
||||
|
||||
builder.add(
|
||||
scaffolderApiRef,
|
||||
new ScaffolderApi({
|
||||
apiOrigin: backendUrl,
|
||||
basePath: '/scaffolder/v1',
|
||||
}),
|
||||
);
|
||||
builder.add(scaffolderApiRef, new ScaffolderApi({ discoveryApi }));
|
||||
|
||||
builder.add(gitOpsApiRef, new GitOpsRestApi('http://localhost:3008'));
|
||||
|
||||
@@ -224,13 +211,7 @@ export const apis = (config: ConfigApi) => {
|
||||
]),
|
||||
);
|
||||
|
||||
builder.add(
|
||||
rollbarApiRef,
|
||||
new RollbarClient({
|
||||
apiOrigin: backendUrl,
|
||||
basePath: '/rollbar',
|
||||
}),
|
||||
);
|
||||
builder.add(rollbarApiRef, new RollbarClient({ discoveryApi }));
|
||||
|
||||
builder.add(
|
||||
techdocsStorageApiRef,
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
SidebarUserSettings,
|
||||
SidebarThemeToggle,
|
||||
SidebarPinButton,
|
||||
DefaultProviderSettings,
|
||||
} from '@backstage/core';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { graphiQLRouteRef } from '@backstage/plugin-graphiql';
|
||||
@@ -107,7 +108,7 @@ const Root: FC<{}> = ({ children }) => (
|
||||
<SidebarSpace />
|
||||
<SidebarDivider />
|
||||
<SidebarThemeToggle />
|
||||
<SidebarUserSettings />
|
||||
<SidebarUserSettings providerSettings={<DefaultProviderSettings />} />
|
||||
<SidebarPinButton />
|
||||
</Sidebar>
|
||||
{children}
|
||||
|
||||
@@ -37,11 +37,13 @@
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
"express-prom-bundle": "^6.1.0",
|
||||
"express-promise-router": "^3.0.3",
|
||||
"helmet": "^4.0.0",
|
||||
"knex": "^0.21.1",
|
||||
"lodash": "^4.17.15",
|
||||
"morgan": "^1.10.0",
|
||||
"prom-client": "^12.0.0",
|
||||
"selfsigned": "^1.10.7",
|
||||
"stoppable": "^1.1.0",
|
||||
"winston": "^3.2.1"
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
HttpsSettings,
|
||||
} from './config';
|
||||
import { createHttpServer, createHttpsServer } from './hostFactory';
|
||||
import { metricsHandler } from './metrics';
|
||||
|
||||
const DEFAULT_PORT = 7000;
|
||||
// '' is express default, which listens to all interfaces
|
||||
@@ -48,6 +49,7 @@ export class ServiceBuilderImpl implements ServiceBuilder {
|
||||
private logger: Logger | undefined;
|
||||
private corsOptions: cors.CorsOptions | undefined;
|
||||
private httpsSettings: HttpsSettings | undefined;
|
||||
private enableMetrics: boolean = true;
|
||||
private routers: [string, Router][];
|
||||
// Reference to the module where builder is created - needed for hot module
|
||||
// reloading
|
||||
@@ -82,6 +84,9 @@ export class ServiceBuilderImpl implements ServiceBuilder {
|
||||
this.httpsSettings = httpsSettings;
|
||||
}
|
||||
|
||||
// For now, configuration of metrics is a simple boolean and active by default
|
||||
this.enableMetrics = backendConfig.getOptionalBoolean('metrics') !== false;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -131,6 +136,9 @@ export class ServiceBuilderImpl implements ServiceBuilder {
|
||||
}
|
||||
app.use(compression());
|
||||
app.use(express.json());
|
||||
if (this.enableMetrics) {
|
||||
app.use(metricsHandler());
|
||||
}
|
||||
app.use(requestLoggingHandler());
|
||||
for (const [root, route] of this.routers) {
|
||||
app.use(root, route);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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 prom from 'prom-client';
|
||||
import promBundle from 'express-prom-bundle';
|
||||
import { RequestHandler } from 'express';
|
||||
|
||||
/**
|
||||
* Adds a /metrics endpoint, register default runtime metrics and instrument the router.
|
||||
*/
|
||||
export function metricsHandler(): RequestHandler {
|
||||
// We can only initialize the metrics once and have to clean them up between hot reloads
|
||||
prom.register.clear();
|
||||
|
||||
return promBundle({
|
||||
includeMethod: true,
|
||||
includePath: true,
|
||||
// Using includePath alone is problematic, as it will include path labels with high
|
||||
// cardinality (e.g. path params). Instead we would have to template them. However, this
|
||||
// is difficult, as every backend plugin might use different routes. Instead we only take
|
||||
// the first directory of the path, to have at least an idea how each plugin performs:
|
||||
normalizePath: [['^/([^/]*)/.*', '/$1']],
|
||||
promClient: { collectDefaultMetrics: {} },
|
||||
});
|
||||
}
|
||||
@@ -84,7 +84,7 @@ async function main() {
|
||||
.addRouter('/auth', await auth(authEnv))
|
||||
.addRouter('/identity', await identity(identityEnv))
|
||||
.addRouter('/techdocs', await techdocs(techdocsEnv))
|
||||
.addRouter('/proxy', await proxy(proxyEnv))
|
||||
.addRouter('/proxy', await proxy(proxyEnv, '/proxy'))
|
||||
.addRouter('/graphql', await graphql(graphqlEnv))
|
||||
.addRouter('', await app(appEnv));
|
||||
|
||||
|
||||
@@ -13,13 +13,14 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
import { createRouter } from '@backstage/plugin-proxy-backend';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
config,
|
||||
}: PluginEnvironment) {
|
||||
return await createRouter({ logger, config });
|
||||
export default async function createPlugin(
|
||||
{ logger, config }: PluginEnvironment,
|
||||
pathPrefix: string,
|
||||
) {
|
||||
return await createRouter({ logger, config, pathPrefix });
|
||||
}
|
||||
|
||||
@@ -25,43 +25,6 @@ async function getConfig() {
|
||||
return require(path.resolve('jest.config.ts'));
|
||||
}
|
||||
|
||||
const options = {
|
||||
rootDir: path.resolve('src'),
|
||||
coverageDirectory: path.resolve('coverage'),
|
||||
collectCoverageFrom: ['**/*.{js,jsx,ts,tsx}', '!**/*.d.ts'],
|
||||
moduleNameMapper: {
|
||||
'\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'),
|
||||
},
|
||||
|
||||
// We build .esm.js files with plugin:build, so to be able to load these in tests they need to be transformed
|
||||
// TODO: jest is working on module support, it's possible that we can remove this in the future
|
||||
transform: {
|
||||
'\\.esm\\.js$': require.resolve('jest-esm-transformer'),
|
||||
'\\.(js|jsx|ts|tsx)$': [
|
||||
require.resolve('ts-jest'),
|
||||
{ isolatedModules: true },
|
||||
],
|
||||
'\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg)$': require.resolve(
|
||||
'./jestFileTransform.js',
|
||||
),
|
||||
},
|
||||
|
||||
// A bit more opinionated
|
||||
testMatch: ['**/?(*.)test.{js,jsx,mjs,ts,tsx}'],
|
||||
|
||||
// Default behaviour is to not apply transforms for node_modules, but we still want
|
||||
// to apply the esm-transformer to .esm.js files, since that's what we use in backstage packages.
|
||||
// The @kyma-project/asyncapi-react library needs to be transformed.
|
||||
transformIgnorePatterns: [
|
||||
'/node_modules/(?!@kyma-project/asyncapi-react/)(?!.*\\.(?:esm\\.js|bmp|gif|jpg|jpeg|png|frag|xml|svg)$)',
|
||||
],
|
||||
};
|
||||
|
||||
// Use src/setupTests.ts as the default location for configuring test env
|
||||
if (fs.existsSync('src/setupTests.ts')) {
|
||||
options.setupFilesAfterEnv = ['<rootDir>/setupTests.ts'];
|
||||
}
|
||||
|
||||
// We read all "jest" config fields in package.json files all the way to the filesystem root.
|
||||
// All configs are merged together to create the final config, with longer paths taking precedence.
|
||||
// The merging of the configs is shallow, meaning e.g. all transforms are replaced if new ones are defined.
|
||||
@@ -92,6 +55,56 @@ async function getConfig() {
|
||||
currentPath = newPath;
|
||||
}
|
||||
|
||||
// We add an additional Jest config parameter only known by the Backstage CLI
|
||||
// called `transformModules`. It's a list of modules that we want to apply
|
||||
// our configured jest transformations for.
|
||||
// This is useful when packages are published in untranspiled ESM or TS form.
|
||||
const transformModules = pkgJsonConfigs
|
||||
.flatMap(conf => {
|
||||
const modules = conf.transformModules || [];
|
||||
delete conf.transformModules;
|
||||
return modules;
|
||||
})
|
||||
.map(name => `${name}/`)
|
||||
.join('|');
|
||||
const transformModulePattern = transformModules && `(?!${transformModules})`;
|
||||
|
||||
const options = {
|
||||
rootDir: path.resolve('src'),
|
||||
coverageDirectory: path.resolve('coverage'),
|
||||
collectCoverageFrom: ['**/*.{js,jsx,ts,tsx}', '!**/*.d.ts'],
|
||||
moduleNameMapper: {
|
||||
'\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'),
|
||||
},
|
||||
|
||||
// We build .esm.js files with plugin:build, so to be able to load these in tests they need to be transformed
|
||||
// TODO: jest is working on module support, it's possible that we can remove this in the future
|
||||
transform: {
|
||||
'\\.esm\\.js$': require.resolve('jest-esm-transformer'),
|
||||
'\\.(js|jsx|ts|tsx)$': [
|
||||
require.resolve('ts-jest'),
|
||||
{ isolatedModules: true },
|
||||
],
|
||||
'\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg)$': require.resolve(
|
||||
'./jestFileTransform.js',
|
||||
),
|
||||
},
|
||||
|
||||
// A bit more opinionated
|
||||
testMatch: ['**/?(*.)test.{js,jsx,mjs,ts,tsx}'],
|
||||
|
||||
// Default behaviour is to not apply transforms for node_modules, but we still want
|
||||
// to apply the esm-transformer to .esm.js files, since that's what we use in backstage packages.
|
||||
transformIgnorePatterns: [
|
||||
`/node_modules/${transformModulePattern}(?:(?!\\.esm).)*\\.(?:js|json)$`,
|
||||
],
|
||||
};
|
||||
|
||||
// Use src/setupTests.ts as the default location for configuring test env
|
||||
if (fs.existsSync('src/setupTests.ts')) {
|
||||
options.setupFilesAfterEnv = ['<rootDir>/setupTests.ts'];
|
||||
}
|
||||
|
||||
return Object.assign(options, ...pkgJsonConfigs);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"@rollup/plugin-yaml": "^2.1.1",
|
||||
"@spotify/eslint-config": "^7.0.1",
|
||||
"@sucrase/webpack-loader": "^2.0.0",
|
||||
"@svgr/plugin-jsx": "4.3.x",
|
||||
"@svgr/plugin-jsx": "5.4.x",
|
||||
"@svgr/plugin-svgo": "4.3.x",
|
||||
"@svgr/rollup": "5.4.x",
|
||||
"@svgr/webpack": "4.3.x",
|
||||
@@ -50,7 +50,7 @@
|
||||
"bfj": "^7.0.2",
|
||||
"chalk": "^4.0.0",
|
||||
"chokidar": "^3.3.1",
|
||||
"commander": "^4.1.1",
|
||||
"commander": "^6.1.0",
|
||||
"css-loader": "^3.5.3",
|
||||
"dashify": "^2.0.0",
|
||||
"diff": "^4.0.2",
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 { createApiRef } from '../ApiRef';
|
||||
|
||||
/**
|
||||
* The discovery API is used to provide a mechanism for plugins to
|
||||
* discover the endpoint to use to talk to their backend counterpart.
|
||||
*
|
||||
* The purpose of the discovery API is to allow for many different deployment
|
||||
* setups and routing methods through a central configuration, instead
|
||||
* of letting each individual plugin manage that configuration.
|
||||
*
|
||||
* Implementations of the discovery API can be a simple as a URL pattern
|
||||
* using the pluginId, but could also have overrides for individual plugins,
|
||||
* or query a separate discovery service.
|
||||
*/
|
||||
export type DiscoveryApi = {
|
||||
/**
|
||||
* Returns the HTTP base backend URL for a given plugin, without a trailing slash.
|
||||
*
|
||||
* This method must always be called just before making a request. as opposed to
|
||||
* fetching the URL when constructing an API client. That is to ensure that more
|
||||
* flexible routing patterns can be supported.
|
||||
*
|
||||
* For example, asking for the URL for `auth` may return something
|
||||
* like `https://backstage.example.com/api/auth`
|
||||
*/
|
||||
getBaseUrl(pluginId: string): Promise<string>;
|
||||
};
|
||||
|
||||
export const discoveryApiRef = createApiRef<DiscoveryApi>({
|
||||
id: 'core.discovery',
|
||||
description: 'Provides service discovery of backend plugins',
|
||||
});
|
||||
@@ -27,6 +27,7 @@ export * from './AppThemeApi';
|
||||
export * from './ConfigApi';
|
||||
export * from './ErrorApi';
|
||||
export * from './FeatureFlagsApi';
|
||||
export * from './DiscoveryApi';
|
||||
export * from './IdentityApi';
|
||||
export * from './OAuthRequestApi';
|
||||
export * from './StorageApi';
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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 { UrlPatternDiscovery } from './UrlPatternDiscovery';
|
||||
|
||||
describe('UrlPatternDiscovery', () => {
|
||||
it('should not require interpolation', async () => {
|
||||
const discoveryApi = UrlPatternDiscovery.compile('http://example.com');
|
||||
await expect(discoveryApi.getBaseUrl('my-plugin')).resolves.toBe(
|
||||
'http://example.com',
|
||||
);
|
||||
});
|
||||
|
||||
it('should use a plain pattern', async () => {
|
||||
const discoveryApi = UrlPatternDiscovery.compile(
|
||||
'http://localhost:7000/{{ pluginId }}',
|
||||
);
|
||||
await expect(discoveryApi.getBaseUrl('my-plugin')).resolves.toBe(
|
||||
'http://localhost:7000/my-plugin',
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow for multiple interpolation points', async () => {
|
||||
const discoveryApi = UrlPatternDiscovery.compile(
|
||||
'https://{{pluginId }}.example.com/api/{{ pluginId}}',
|
||||
);
|
||||
await expect(discoveryApi.getBaseUrl('my-plugin')).resolves.toBe(
|
||||
'https://my-plugin.example.com/api/my-plugin',
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate that the pattern is a valid URL', () => {
|
||||
expect(() => {
|
||||
UrlPatternDiscovery.compile('example.com');
|
||||
}).toThrow('Invalid discovery URL pattern, Invalid URL: example.com');
|
||||
|
||||
expect(() => {
|
||||
UrlPatternDiscovery.compile('http://');
|
||||
}).toThrow('Invalid discovery URL pattern, Invalid URL: http://');
|
||||
|
||||
expect(() => {
|
||||
UrlPatternDiscovery.compile('abc123');
|
||||
}).toThrow('Invalid discovery URL pattern, Invalid URL: abc123');
|
||||
|
||||
expect(() => {
|
||||
UrlPatternDiscovery.compile('http://example.com:{{pluginId}}');
|
||||
}).toThrow(
|
||||
'Invalid discovery URL pattern, Invalid URL: http://example.com:pluginId',
|
||||
);
|
||||
|
||||
expect(() => {
|
||||
UrlPatternDiscovery.compile('/{{pluginId}}');
|
||||
}).toThrow('Invalid discovery URL pattern, Invalid URL: /pluginId');
|
||||
|
||||
expect(() => {
|
||||
UrlPatternDiscovery.compile('http://localhost/{{pluginId}}?forbidden');
|
||||
}).toThrow('Invalid discovery URL pattern, URL must not have a query');
|
||||
|
||||
expect(() => {
|
||||
UrlPatternDiscovery.compile('http://localhost/{{pluginId}}#forbidden');
|
||||
}).toThrow('Invalid discovery URL pattern, URL must not have a hash');
|
||||
|
||||
expect(() => {
|
||||
UrlPatternDiscovery.compile('http://localhost/{{pluginId}}/');
|
||||
}).toThrow('Invalid discovery URL pattern, URL must not end with a slash');
|
||||
|
||||
expect(() => {
|
||||
UrlPatternDiscovery.compile('http://localhost/');
|
||||
}).toThrow('Invalid discovery URL pattern, URL must not end with a slash');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DiscoveryApi } from '../../definitions/DiscoveryApi';
|
||||
|
||||
/**
|
||||
* UrlPatternDiscovery is a lightweight DiscoveryApi implementation.
|
||||
* It uses a single template string to construct URLs for each plugin.
|
||||
*/
|
||||
export class UrlPatternDiscovery implements DiscoveryApi {
|
||||
/**
|
||||
* Creates a new UrlPatternDiscovery given a template. The the only
|
||||
* interpolation done for the template is to replace instances of `{{pluginId}}`
|
||||
* with the ID of the plugin being requested.
|
||||
*
|
||||
* Example pattern: `http://localhost:7000/api/{{ pluginId }}`
|
||||
*/
|
||||
static compile(pattern: string): UrlPatternDiscovery {
|
||||
const parts = pattern.split(/\{\{\s*pluginId\s*\}\}/);
|
||||
|
||||
try {
|
||||
const urlStr = parts.join('pluginId');
|
||||
const url = new URL(urlStr);
|
||||
if (url.hash) {
|
||||
throw new Error('URL must not have a hash');
|
||||
}
|
||||
if (url.search) {
|
||||
throw new Error('URL must not have a query');
|
||||
}
|
||||
if (urlStr.endsWith('/')) {
|
||||
throw new Error('URL must not end with a slash');
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid discovery URL pattern, ${error.message}`);
|
||||
}
|
||||
|
||||
return new UrlPatternDiscovery(parts);
|
||||
}
|
||||
|
||||
private constructor(private readonly parts: string[]) {}
|
||||
|
||||
async getBaseUrl(pluginId: string): Promise<string> {
|
||||
return this.parts.join(pluginId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// This folder contains implementations for all core APIs.
|
||||
//
|
||||
// Plugins should rely on these APIs for functionality as much as possible.
|
||||
|
||||
export { UrlPatternDiscovery } from './UrlPatternDiscovery';
|
||||
@@ -27,16 +27,17 @@ import {
|
||||
AuthRequestOptions,
|
||||
BackstageIdentity,
|
||||
} from '../../../definitions/auth';
|
||||
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
|
||||
import {
|
||||
OAuthRequestApi,
|
||||
AuthProvider,
|
||||
DiscoveryApi,
|
||||
} from '../../../definitions';
|
||||
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
|
||||
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
|
||||
import { Observable } from '../../../../types';
|
||||
|
||||
type CreateOptions = {
|
||||
// TODO(Following the words of Rugvip): These two should be grabbed from global config when available, they're not unique to Auth0Auth
|
||||
backendUrl: string;
|
||||
basePath: string;
|
||||
|
||||
discoveryApi: DiscoveryApi;
|
||||
oauthRequestApi: OAuthRequestApi;
|
||||
|
||||
environment?: string;
|
||||
@@ -67,15 +68,13 @@ class Auth0Auth
|
||||
BackstageIdentityApi,
|
||||
SessionStateApi {
|
||||
static create({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment = 'development',
|
||||
provider = DEFAULT_PROVIDER,
|
||||
oauthRequestApi,
|
||||
}: CreateOptions) {
|
||||
const connector = new DefaultAuthConnector({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment,
|
||||
provider,
|
||||
oauthRequestApi: oauthRequestApi,
|
||||
|
||||
@@ -25,7 +25,11 @@ import {
|
||||
BackstageIdentity,
|
||||
AuthRequestOptions,
|
||||
} from '../../../definitions/auth';
|
||||
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
|
||||
import {
|
||||
OAuthRequestApi,
|
||||
AuthProvider,
|
||||
DiscoveryApi,
|
||||
} from '../../../definitions';
|
||||
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
|
||||
import {
|
||||
AuthSessionStore,
|
||||
@@ -34,10 +38,7 @@ import {
|
||||
import { Observable } from '../../../../types';
|
||||
|
||||
type CreateOptions = {
|
||||
// TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GithubAuth
|
||||
backendUrl: string;
|
||||
basePath: string;
|
||||
|
||||
discoveryApi: DiscoveryApi;
|
||||
oauthRequestApi: OAuthRequestApi;
|
||||
|
||||
environment?: string;
|
||||
@@ -62,15 +63,13 @@ const DEFAULT_PROVIDER = {
|
||||
|
||||
class GithubAuth implements OAuthApi, SessionStateApi {
|
||||
static create({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment = 'development',
|
||||
provider = DEFAULT_PROVIDER,
|
||||
oauthRequestApi,
|
||||
}: CreateOptions) {
|
||||
const connector = new DefaultAuthConnector({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment,
|
||||
provider,
|
||||
oauthRequestApi: oauthRequestApi,
|
||||
|
||||
@@ -25,15 +25,17 @@ import {
|
||||
BackstageIdentity,
|
||||
AuthRequestOptions,
|
||||
} from '../../../definitions/auth';
|
||||
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
|
||||
import {
|
||||
OAuthRequestApi,
|
||||
AuthProvider,
|
||||
DiscoveryApi,
|
||||
} from '../../../definitions';
|
||||
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
|
||||
import { StaticAuthSessionManager } from '../../../../lib/AuthSessionManager';
|
||||
import { Observable } from '../../../../types';
|
||||
|
||||
type CreateOptions = {
|
||||
backendUrl: string;
|
||||
basePath: string;
|
||||
|
||||
discoveryApi: DiscoveryApi;
|
||||
oauthRequestApi: OAuthRequestApi;
|
||||
|
||||
environment?: string;
|
||||
@@ -58,15 +60,13 @@ const DEFAULT_PROVIDER = {
|
||||
|
||||
class GitlabAuth implements OAuthApi, SessionStateApi {
|
||||
static create({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment = 'development',
|
||||
provider = DEFAULT_PROVIDER,
|
||||
oauthRequestApi,
|
||||
}: CreateOptions) {
|
||||
const connector = new DefaultAuthConnector({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment,
|
||||
provider,
|
||||
oauthRequestApi,
|
||||
|
||||
@@ -28,16 +28,17 @@ import {
|
||||
AuthRequestOptions,
|
||||
BackstageIdentity,
|
||||
} from '../../../definitions/auth';
|
||||
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
|
||||
import {
|
||||
OAuthRequestApi,
|
||||
AuthProvider,
|
||||
DiscoveryApi,
|
||||
} from '../../../definitions';
|
||||
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
|
||||
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
|
||||
import { Observable } from '../../../../types';
|
||||
|
||||
type CreateOptions = {
|
||||
// TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GoogleAuth
|
||||
backendUrl: string;
|
||||
basePath: string;
|
||||
|
||||
discoveryApi: DiscoveryApi;
|
||||
oauthRequestApi: OAuthRequestApi;
|
||||
|
||||
environment?: string;
|
||||
@@ -71,15 +72,13 @@ class GoogleAuth
|
||||
BackstageIdentityApi,
|
||||
SessionStateApi {
|
||||
static create({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment = 'development',
|
||||
provider = DEFAULT_PROVIDER,
|
||||
oauthRequestApi,
|
||||
}: CreateOptions) {
|
||||
const connector = new DefaultAuthConnector({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment,
|
||||
provider,
|
||||
oauthRequestApi: oauthRequestApi,
|
||||
|
||||
@@ -30,15 +30,17 @@ import {
|
||||
BackstageIdentity,
|
||||
} from '../../../definitions/auth';
|
||||
|
||||
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
|
||||
import {
|
||||
OAuthRequestApi,
|
||||
AuthProvider,
|
||||
DiscoveryApi,
|
||||
} from '../../../definitions';
|
||||
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
|
||||
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
|
||||
import { Observable } from '../../../../types';
|
||||
|
||||
type CreateOptions = {
|
||||
backendUrl: string;
|
||||
basePath: string;
|
||||
|
||||
discoveryApi: DiscoveryApi;
|
||||
oauthRequestApi: OAuthRequestApi;
|
||||
|
||||
environment?: string;
|
||||
@@ -70,15 +72,13 @@ class MicrosoftAuth
|
||||
BackstageIdentityApi,
|
||||
SessionStateApi {
|
||||
static create({
|
||||
backendUrl,
|
||||
basePath,
|
||||
environment = 'development',
|
||||
provider = DEFAULT_PROVIDER,
|
||||
oauthRequestApi,
|
||||
discoveryApi,
|
||||
}: CreateOptions) {
|
||||
const connector = new DefaultAuthConnector({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment,
|
||||
provider,
|
||||
oauthRequestApi: oauthRequestApi,
|
||||
|
||||
@@ -19,7 +19,11 @@ import { DefaultAuthConnector } from '../../../../lib/AuthConnector';
|
||||
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
|
||||
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
|
||||
import { Observable } from '../../../../types';
|
||||
import { AuthProvider, OAuthRequestApi } from '../../../definitions';
|
||||
import {
|
||||
AuthProvider,
|
||||
OAuthRequestApi,
|
||||
DiscoveryApi,
|
||||
} from '../../../definitions';
|
||||
import {
|
||||
AuthRequestOptions,
|
||||
BackstageIdentity,
|
||||
@@ -33,9 +37,7 @@ import {
|
||||
import { OAuth2Session } from './types';
|
||||
|
||||
type CreateOptions = {
|
||||
backendUrl: string;
|
||||
basePath: string;
|
||||
|
||||
discoveryApi: DiscoveryApi;
|
||||
oauthRequestApi: OAuthRequestApi;
|
||||
|
||||
environment?: string;
|
||||
@@ -64,15 +66,13 @@ const SCOPE_PREFIX = '';
|
||||
class OAuth2
|
||||
implements OAuthApi, OpenIdConnectApi, ProfileInfoApi, SessionStateApi {
|
||||
static create({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment = 'development',
|
||||
provider = DEFAULT_PROVIDER,
|
||||
oauthRequestApi,
|
||||
}: CreateOptions) {
|
||||
const connector = new DefaultAuthConnector({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment,
|
||||
provider,
|
||||
oauthRequestApi: oauthRequestApi,
|
||||
|
||||
@@ -28,15 +28,17 @@ import {
|
||||
AuthRequestOptions,
|
||||
BackstageIdentity,
|
||||
} from '../../../definitions/auth';
|
||||
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
|
||||
import {
|
||||
OAuthRequestApi,
|
||||
AuthProvider,
|
||||
DiscoveryApi,
|
||||
} from '../../../definitions';
|
||||
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
|
||||
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
|
||||
import { Observable } from '../../../../types';
|
||||
|
||||
type CreateOptions = {
|
||||
backendUrl: string;
|
||||
basePath: string;
|
||||
|
||||
discoveryApi: DiscoveryApi;
|
||||
oauthRequestApi: OAuthRequestApi;
|
||||
|
||||
environment?: string;
|
||||
@@ -80,15 +82,13 @@ class OktaAuth
|
||||
BackstageIdentityApi,
|
||||
SessionStateApi {
|
||||
static create({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment = 'development',
|
||||
provider = DEFAULT_PROVIDER,
|
||||
oauthRequestApi,
|
||||
}: CreateOptions) {
|
||||
const connector = new DefaultAuthConnector({
|
||||
backendUrl,
|
||||
basePath,
|
||||
discoveryApi,
|
||||
environment,
|
||||
provider,
|
||||
oauthRequestApi: oauthRequestApi,
|
||||
|
||||
@@ -24,5 +24,6 @@ export * from './AlertApi';
|
||||
export * from './AppThemeApi';
|
||||
export * from './ConfigApi';
|
||||
export * from './ErrorApi';
|
||||
export * from './DiscoveryApi';
|
||||
export * from './OAuthRequestApi';
|
||||
export * from './StorageApi';
|
||||
|
||||
@@ -18,11 +18,12 @@ import ProviderIcon from '@material-ui/icons/AcUnit';
|
||||
import { DefaultAuthConnector } from './DefaultAuthConnector';
|
||||
import MockOAuthApi from '../../apis/implementations/OAuthRequestApi/MockOAuthApi';
|
||||
import * as loginPopup from '../loginPopup';
|
||||
import { UrlPatternDiscovery } from '../../apis';
|
||||
|
||||
const anyFetch = fetch as any;
|
||||
|
||||
const defaultOptions = {
|
||||
backendUrl: 'http://my-origin',
|
||||
discoveryApi: UrlPatternDiscovery.compile('http://my-host/api/{{pluginId}}'),
|
||||
environment: 'production',
|
||||
provider: {
|
||||
id: 'my-provider',
|
||||
@@ -115,7 +116,7 @@ describe('DefaultAuthConnector', () => {
|
||||
expect(popupSpy).toBeCalledTimes(1);
|
||||
expect(popupSpy.mock.calls[0][0]).toMatchObject({
|
||||
url:
|
||||
'http://my-origin/api/auth/my-provider/start?scope=a%20b&env=production',
|
||||
'http://my-host/api/auth/my-provider/start?scope=a%20b&env=production',
|
||||
});
|
||||
|
||||
await expect(sessionPromise).resolves.toEqual({
|
||||
@@ -141,9 +142,9 @@ describe('DefaultAuthConnector', () => {
|
||||
instantPopup: true,
|
||||
});
|
||||
|
||||
expect(popupSpy).toBeCalledTimes(1);
|
||||
|
||||
await expect(sessionPromise).resolves.toBe('my-session');
|
||||
|
||||
expect(popupSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should use join func to join scopes', async () => {
|
||||
@@ -164,7 +165,7 @@ describe('DefaultAuthConnector', () => {
|
||||
expect(popupSpy).toBeCalledTimes(1);
|
||||
expect(popupSpy.mock.calls[0][0]).toMatchObject({
|
||||
url:
|
||||
'http://my-origin/api/auth/my-provider/start?scope=-ab-&env=production',
|
||||
'http://my-host/api/auth/my-provider/start?scope=-ab-&env=production',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,21 +15,19 @@
|
||||
*/
|
||||
|
||||
import { AuthRequester } from '../../apis';
|
||||
import { OAuthRequestApi, AuthProvider } from '../../apis/definitions';
|
||||
import {
|
||||
OAuthRequestApi,
|
||||
AuthProvider,
|
||||
DiscoveryApi,
|
||||
} from '../../apis/definitions';
|
||||
import { showLoginPopup } from '../loginPopup';
|
||||
import { AuthConnector, CreateSessionOptions } from './types';
|
||||
|
||||
const DEFAULT_BASE_PATH = '/api/auth/';
|
||||
|
||||
type Options<AuthSession> = {
|
||||
/**
|
||||
* The base URL of the auth backend.
|
||||
* DiscoveryApi instance used to locate the auth backend endpoint.
|
||||
*/
|
||||
backendUrl?: string;
|
||||
/**
|
||||
* Base path of the auth requests, defaults to /api/auth/
|
||||
*/
|
||||
basePath?: string;
|
||||
discoveryApi: DiscoveryApi;
|
||||
/**
|
||||
* Environment hint passed on to auth backend, for example 'production' or 'development'
|
||||
*/
|
||||
@@ -64,8 +62,7 @@ function defaultJoinScopes(scopes: Set<string>) {
|
||||
*/
|
||||
export class DefaultAuthConnector<AuthSession>
|
||||
implements AuthConnector<AuthSession> {
|
||||
private readonly backendUrl: string;
|
||||
private readonly basePath: string;
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly environment: string;
|
||||
private readonly provider: AuthProvider & { id: string };
|
||||
private readonly joinScopesFunc: (scopes: Set<string>) => string;
|
||||
@@ -74,8 +71,7 @@ export class DefaultAuthConnector<AuthSession>
|
||||
|
||||
constructor(options: Options<AuthSession>) {
|
||||
const {
|
||||
backendUrl = window.location.origin,
|
||||
basePath = DEFAULT_BASE_PATH,
|
||||
discoveryApi,
|
||||
environment,
|
||||
provider,
|
||||
joinScopes = defaultJoinScopes,
|
||||
@@ -88,8 +84,7 @@ export class DefaultAuthConnector<AuthSession>
|
||||
onAuthRequest: scopes => this.showPopup(scopes),
|
||||
});
|
||||
|
||||
this.backendUrl = backendUrl;
|
||||
this.basePath = basePath;
|
||||
this.discoveryApi = discoveryApi;
|
||||
this.environment = environment;
|
||||
this.provider = provider;
|
||||
this.joinScopesFunc = joinScopes;
|
||||
@@ -104,12 +99,15 @@ export class DefaultAuthConnector<AuthSession>
|
||||
}
|
||||
|
||||
async refreshSession(): Promise<any> {
|
||||
const res = await fetch(this.buildUrl('/refresh', { optional: true }), {
|
||||
headers: {
|
||||
'x-requested-with': 'XMLHttpRequest',
|
||||
const res = await fetch(
|
||||
await this.buildUrl('/refresh', { optional: true }),
|
||||
{
|
||||
headers: {
|
||||
'x-requested-with': 'XMLHttpRequest',
|
||||
},
|
||||
credentials: 'include',
|
||||
},
|
||||
credentials: 'include',
|
||||
}).catch(error => {
|
||||
).catch(error => {
|
||||
throw new Error(`Auth refresh request failed, ${error}`);
|
||||
});
|
||||
|
||||
@@ -134,7 +132,7 @@ export class DefaultAuthConnector<AuthSession>
|
||||
}
|
||||
|
||||
async removeSession(): Promise<void> {
|
||||
const res = await fetch(this.buildUrl('/logout'), {
|
||||
const res = await fetch(await this.buildUrl('/logout'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-requested-with': 'XMLHttpRequest',
|
||||
@@ -153,13 +151,12 @@ export class DefaultAuthConnector<AuthSession>
|
||||
|
||||
private async showPopup(scopes: Set<string>): Promise<AuthSession> {
|
||||
const scope = this.joinScopesFunc(scopes);
|
||||
const popupUrl = this.buildUrl('/start', { scope });
|
||||
const { origin } = new URL(this.backendUrl);
|
||||
const popupUrl = await this.buildUrl('/start', { scope });
|
||||
|
||||
const payload = await showLoginPopup({
|
||||
url: popupUrl,
|
||||
name: `${this.provider.title} Login`,
|
||||
origin,
|
||||
origin: new URL(popupUrl).origin,
|
||||
width: 450,
|
||||
height: 730,
|
||||
});
|
||||
@@ -167,16 +164,17 @@ export class DefaultAuthConnector<AuthSession>
|
||||
return await this.sessionTransform(payload);
|
||||
}
|
||||
|
||||
private buildUrl(
|
||||
private async buildUrl(
|
||||
path: string,
|
||||
query?: { [key: string]: string | boolean | undefined },
|
||||
): string {
|
||||
): Promise<string> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('auth');
|
||||
const queryString = this.buildQueryString({
|
||||
...query,
|
||||
env: this.environment,
|
||||
});
|
||||
|
||||
return `${this.backendUrl}${this.basePath}${this.provider.id}${path}${queryString}`;
|
||||
return `${baseUrl}/${this.provider.id}${path}${queryString}`;
|
||||
}
|
||||
|
||||
private buildQueryString(query?: {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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 {
|
||||
configApiRef,
|
||||
githubAuthApiRef,
|
||||
gitlabAuthApiRef,
|
||||
googleAuthApiRef,
|
||||
oauth2ApiRef,
|
||||
oktaAuthApiRef,
|
||||
microsoftAuthApiRef,
|
||||
useApi,
|
||||
} from '@backstage/core-api';
|
||||
import Star from '@material-ui/icons/Star';
|
||||
import React from 'react';
|
||||
import { OAuthProviderSettings, OIDCProviderSettings } from './Settings';
|
||||
|
||||
export const DefaultProviderSettings = () => {
|
||||
const configApi = useApi(configApiRef);
|
||||
const providersConfig = configApi.getOptionalConfig('auth.providers');
|
||||
const providers = providersConfig?.keys() ?? [];
|
||||
|
||||
return (
|
||||
<>
|
||||
{providers.includes('google') && (
|
||||
<OIDCProviderSettings
|
||||
title="Google"
|
||||
apiRef={googleAuthApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
)}
|
||||
{providers.includes('microsoft') && (
|
||||
<OIDCProviderSettings
|
||||
title="Microsoft"
|
||||
apiRef={microsoftAuthApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
)}
|
||||
{providers.includes('github') && (
|
||||
<OAuthProviderSettings
|
||||
title="Github"
|
||||
apiRef={githubAuthApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
)}
|
||||
{providers.includes('gitlab') && (
|
||||
<OAuthProviderSettings
|
||||
title="Gitlab"
|
||||
apiRef={gitlabAuthApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
)}
|
||||
{providers.includes('okta') && (
|
||||
<OIDCProviderSettings
|
||||
title="Okta"
|
||||
apiRef={oktaAuthApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
)}
|
||||
{providers.includes('oauth2') && (
|
||||
<OIDCProviderSettings
|
||||
title="YourOrg"
|
||||
apiRef={oauth2ApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -23,10 +23,15 @@ import {
|
||||
SidebarSearchField,
|
||||
SidebarSpace,
|
||||
SidebarUserSettings,
|
||||
OAuthProviderSettings,
|
||||
} from '.';
|
||||
import HomeOutlinedIcon from '@material-ui/icons/HomeOutlined';
|
||||
import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline';
|
||||
import Star from '@material-ui/icons/Star';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import {
|
||||
githubAuthApiRef,
|
||||
} from '@backstage/core-api';
|
||||
|
||||
export default {
|
||||
title: 'Sidebar',
|
||||
@@ -55,6 +60,12 @@ export const SampleSidebar = () => (
|
||||
<SidebarIntro />
|
||||
<SidebarSpace />
|
||||
<SidebarDivider />
|
||||
<SidebarUserSettings />
|
||||
<SidebarUserSettings providerSettings={
|
||||
<OAuthProviderSettings
|
||||
title="Github"
|
||||
apiRef={githubAuthApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
} />
|
||||
</Sidebar>
|
||||
);
|
||||
|
||||
@@ -14,36 +14,22 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
githubAuthApiRef,
|
||||
gitlabAuthApiRef,
|
||||
googleAuthApiRef,
|
||||
identityApiRef,
|
||||
oauth2ApiRef,
|
||||
oktaAuthApiRef,
|
||||
microsoftAuthApiRef,
|
||||
useApi,
|
||||
configApiRef,
|
||||
} from '@backstage/core-api';
|
||||
import { identityApiRef, useApi } from '@backstage/core-api';
|
||||
import Collapse from '@material-ui/core/Collapse';
|
||||
import SignOutIcon from '@material-ui/icons/MeetingRoom';
|
||||
import Star from '@material-ui/icons/Star';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { SidebarContext } from './config';
|
||||
import { SidebarItem } from './Items';
|
||||
import {
|
||||
OAuthProviderSettings,
|
||||
OIDCProviderSettings,
|
||||
UserProfile as SidebarUserProfile,
|
||||
} from './Settings';
|
||||
import { UserProfile as SidebarUserProfile } from './Settings';
|
||||
|
||||
export function SidebarUserSettings() {
|
||||
type SidebarUserSettingsProps = { providerSettings?: React.ReactNode };
|
||||
|
||||
export function SidebarUserSettings({
|
||||
providerSettings,
|
||||
}: SidebarUserSettingsProps) {
|
||||
const { isOpen: sidebarOpen } = useContext(SidebarContext);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const identityApi = useApi(identityApiRef);
|
||||
const configApi = useApi(configApiRef);
|
||||
const providersConfig = configApi.getOptionalConfig('auth.providers');
|
||||
const providers = providersConfig?.keys() ?? [];
|
||||
|
||||
// Close the provider list when sidebar collapse
|
||||
useEffect(() => {
|
||||
@@ -54,48 +40,8 @@ export function SidebarUserSettings() {
|
||||
<>
|
||||
<SidebarUserProfile open={open} setOpen={setOpen} />
|
||||
<Collapse in={open} timeout="auto">
|
||||
{providers.includes('google') && (
|
||||
<OIDCProviderSettings
|
||||
title="Google"
|
||||
apiRef={googleAuthApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
)}
|
||||
{providers.includes('microsoft') && (
|
||||
<OIDCProviderSettings
|
||||
title="Microsoft"
|
||||
apiRef={microsoftAuthApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
)}
|
||||
{providers.includes('github') && (
|
||||
<OAuthProviderSettings
|
||||
title="GitHub"
|
||||
apiRef={githubAuthApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
)}
|
||||
{providers.includes('gitlab') && (
|
||||
<OAuthProviderSettings
|
||||
title="GitLab"
|
||||
apiRef={gitlabAuthApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
)}
|
||||
{providers.includes('okta') && (
|
||||
<OIDCProviderSettings
|
||||
title="Okta"
|
||||
apiRef={oktaAuthApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
)}
|
||||
{providers.includes('oauth2') && (
|
||||
<OIDCProviderSettings
|
||||
title="YourOrg"
|
||||
apiRef={oauth2ApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
)}
|
||||
{providerSettings}
|
||||
|
||||
<SidebarItem
|
||||
icon={SignOutIcon}
|
||||
text="Sign Out"
|
||||
|
||||
@@ -34,4 +34,5 @@ export {
|
||||
export type { SidebarContextType } from './config';
|
||||
export { SidebarThemeToggle } from './SidebarThemeToggle';
|
||||
export { SidebarUserSettings } from './UserSettings';
|
||||
export { DefaultProviderSettings } from './DefaultProviderSettings';
|
||||
export * from './Settings';
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"dependencies": {
|
||||
"@backstage/cli-common": "^0.1.1-alpha.20",
|
||||
"chalk": "^4.0.0",
|
||||
"commander": "^4.1.1",
|
||||
"commander": "^6.1.0",
|
||||
"fs-extra": "^9.0.0",
|
||||
"handlebars": "^4.7.3",
|
||||
"inquirer": "^7.0.4",
|
||||
|
||||
@@ -13,6 +13,28 @@ backend:
|
||||
origin: http://localhost:3000
|
||||
methods: [GET, POST, PUT, DELETE]
|
||||
credentials: true
|
||||
{{#if dbTypeSqlite}}
|
||||
database:
|
||||
client: sqlite3
|
||||
connection: ':memory:'
|
||||
{{/if}}
|
||||
{{#if dbTypePG}}
|
||||
database:
|
||||
client: pg
|
||||
connection:
|
||||
host:
|
||||
$secret:
|
||||
env: POSTGRES_HOST
|
||||
port:
|
||||
$secret:
|
||||
env: POSTGRES_PORT
|
||||
user:
|
||||
$secret:
|
||||
env: POSTGRES_USER
|
||||
password:
|
||||
$secret:
|
||||
env: POSTGRES_PASSWORD
|
||||
{{/if}}
|
||||
|
||||
proxy:
|
||||
'/test':
|
||||
@@ -20,14 +42,14 @@ proxy:
|
||||
changeOrigin: true
|
||||
|
||||
techdocs:
|
||||
storageUrl: https://techdocs-mock-sites.storage.googleapis.com
|
||||
storageUrl: http://localhost:7000/techdocs/static/docs
|
||||
|
||||
auth:
|
||||
providers: {}
|
||||
|
||||
catalog:
|
||||
locations:
|
||||
# Backstage Example Component
|
||||
# Backstage example components
|
||||
- type: github
|
||||
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml
|
||||
- type: github
|
||||
@@ -44,8 +66,7 @@ catalog:
|
||||
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/www-artist-component.yaml
|
||||
- type: github
|
||||
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/shuffle-api-component.yaml
|
||||
|
||||
# Backstage Example Templates
|
||||
# Backstage example templates
|
||||
- type: github
|
||||
target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml
|
||||
- type: github
|
||||
@@ -42,5 +42,10 @@
|
||||
"*.{json,md}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"jest": {
|
||||
"transformModules": [
|
||||
"@kyma-project/asyncapi-react"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"@backstage/plugin-catalog": "^{{version}}",
|
||||
"@backstage/plugin-register-component": "^{{version}}",
|
||||
"@backstage/plugin-scaffolder": "^{{version}}",
|
||||
"@backstage/plugin-techdocs": "^{{version}}",
|
||||
"@backstage/test-utils": "^{{version}}",
|
||||
"@backstage/theme": "^{{version}}",
|
||||
"history": "^5.0.0",
|
||||
|
||||
@@ -11,6 +11,9 @@ describe('App', () => {
|
||||
data: {
|
||||
app: { title: 'Test' },
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
techdocs: {
|
||||
storageUrl: 'http://localhost:7000/techdocs/static/docs',
|
||||
},
|
||||
},
|
||||
context: 'test',
|
||||
},
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
ConfigApi,
|
||||
ErrorApiForwarder,
|
||||
ErrorAlerter,
|
||||
discoveryApiRef,
|
||||
UrlPatternDiscovery,
|
||||
oauthRequestApiRef,
|
||||
OAuthRequestManager,
|
||||
storageApiRef,
|
||||
@@ -16,14 +18,24 @@ import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog';
|
||||
|
||||
import { scaffolderApiRef, ScaffolderApi } from '@backstage/plugin-scaffolder';
|
||||
|
||||
import {
|
||||
techdocsStorageApiRef,
|
||||
TechDocsStorageApi,
|
||||
} from '@backstage/plugin-techdocs';
|
||||
|
||||
export const apis = (config: ConfigApi) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Creating APIs for ${config.getString('app.title')}`);
|
||||
|
||||
const backendUrl = config.getString('backend.baseUrl');
|
||||
const techdocsStorageUrl = config.getString('techdocs.storageUrl');
|
||||
|
||||
const builder = ApiRegistry.builder();
|
||||
|
||||
const discoveryApi = builder.add(
|
||||
discoveryApiRef,
|
||||
UrlPatternDiscovery.compile(`${backendUrl}/{{ pluginId }}`),
|
||||
);
|
||||
const alertApi = builder.add(alertApiRef, new AlertApiForwarder());
|
||||
const errorApi = builder.add(
|
||||
errorApiRef,
|
||||
@@ -33,20 +45,13 @@ export const apis = (config: ConfigApi) => {
|
||||
builder.add(storageApiRef, WebStorage.create({ errorApi }));
|
||||
builder.add(oauthRequestApiRef, new OAuthRequestManager());
|
||||
|
||||
builder.add(
|
||||
catalogApiRef,
|
||||
new CatalogClient({
|
||||
apiOrigin: backendUrl,
|
||||
basePath: '/catalog',
|
||||
}),
|
||||
);
|
||||
builder.add(catalogApiRef, new CatalogClient({ discoveryApi }));
|
||||
|
||||
builder.add(scaffolderApiRef, new ScaffolderApi({ discoveryApi }));
|
||||
|
||||
builder.add(
|
||||
scaffolderApiRef,
|
||||
new ScaffolderApi({
|
||||
apiOrigin: backendUrl,
|
||||
basePath: '/scaffolder/v1',
|
||||
}),
|
||||
techdocsStorageApiRef,
|
||||
new TechDocsStorageApi({ apiOrigin: techdocsStorageUrl }),
|
||||
);
|
||||
|
||||
return builder.build();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { plugin as CatalogPlugin } from '@backstage/plugin-catalog';
|
||||
export { plugin as RegisterComponent } from '@backstage/plugin-register-component';
|
||||
export { plugin as ScaffolderPlugin } from '@backstage/plugin-scaffolder';
|
||||
export { plugin as TechDocsPlugin } from '@backstage/plugin-techdocs';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import HomeIcon from '@material-ui/icons/Home';
|
||||
import LibraryBooks from '@material-ui/icons/LibraryBooks';
|
||||
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
SidebarUserSettings,
|
||||
SidebarThemeToggle,
|
||||
SidebarPinButton,
|
||||
DefaultProviderSettings,
|
||||
} from '@backstage/core';
|
||||
|
||||
export const AppSidebar = () => (
|
||||
@@ -16,13 +18,14 @@ export const AppSidebar = () => (
|
||||
<SidebarDivider />
|
||||
{/* Global nav, not org-specific */}
|
||||
<SidebarItem icon={HomeIcon} to="./" text="Home" />
|
||||
<SidebarItem icon={LibraryBooks} to="/docs" text="Docs" />
|
||||
<SidebarItem icon={CreateComponentIcon} to="create" text="Create..." />
|
||||
{/* End global nav */}
|
||||
<SidebarDivider />
|
||||
<SidebarSpace />
|
||||
<SidebarDivider />
|
||||
<SidebarThemeToggle />
|
||||
<SidebarUserSettings />
|
||||
<SidebarUserSettings providerSettings={<DefaultProviderSettings />} />
|
||||
<SidebarPinButton />
|
||||
</Sidebar>
|
||||
);
|
||||
|
||||
@@ -7,18 +7,13 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
createDatabase,
|
||||
createServiceBuilder,
|
||||
loadBackendConfig,
|
||||
getRootLogger,
|
||||
useHotMemoize,
|
||||
} from '@backstage/backend-common';
|
||||
import { ConfigReader, AppConfig } from '@backstage/config';
|
||||
{{#if dbTypePG}}
|
||||
import knex, { PgConnectionConfig } from 'knex';
|
||||
{{/if}}
|
||||
{{#if dbTypeSqlite}}
|
||||
import knex from 'knex';
|
||||
{{/if}}
|
||||
import auth from './plugins/auth';
|
||||
import catalog from './plugins/catalog';
|
||||
import identity from './plugins/identity';
|
||||
@@ -32,30 +27,10 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) {
|
||||
|
||||
return (plugin: string): PluginEnvironment => {
|
||||
const logger = getRootLogger().child({ type: 'plugin', plugin });
|
||||
|
||||
{{#if dbTypePG}}
|
||||
const knexConfig = {
|
||||
client: 'pg',
|
||||
useNullAsDefault: true,
|
||||
const database = createDatabase(config.getConfig('backend.database'), {
|
||||
connection: {
|
||||
port: process.env.POSTGRES_PORT,
|
||||
host: process.env.POSTGRES_HOST,
|
||||
user: process.env.POSTGRES_USER,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
database: `backstage_plugin_${plugin}`,
|
||||
} as PgConnectionConfig,
|
||||
};
|
||||
{{/if}}
|
||||
{{#if dbTypeSqlite}}
|
||||
const knexConfig = {
|
||||
client: 'sqlite3',
|
||||
connection: ':memory:',
|
||||
useNullAsDefault: true,
|
||||
};
|
||||
{{/if}}
|
||||
const database = knex(knexConfig);
|
||||
database.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
|
||||
resource.run('PRAGMA foreign_keys = ON', () => {});
|
||||
},
|
||||
});
|
||||
return { logger, database, config };
|
||||
};
|
||||
@@ -80,7 +55,7 @@ async function main() {
|
||||
.addRouter('/auth', await auth(authEnv))
|
||||
.addRouter('/identity', await identity(identityEnv))
|
||||
.addRouter('/techdocs', await techdocs(techdocsEnv))
|
||||
.addRouter('/proxy', await proxy(proxyEnv));
|
||||
.addRouter('/proxy', await proxy(proxyEnv, '/proxy'));
|
||||
|
||||
await service.start().catch(err => {
|
||||
console.log(err);
|
||||
@@ -2,9 +2,9 @@
|
||||
import { createRouter } from '@backstage/plugin-proxy-backend';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
config,
|
||||
}: PluginEnvironment) {
|
||||
return await createRouter({ logger, config });
|
||||
export default async function createPlugin(
|
||||
{ logger, config }: PluginEnvironment,
|
||||
pathPrefix: string,
|
||||
) {
|
||||
return await createRouter({ logger, config, pathPrefix });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
createRouter,
|
||||
DirectoryPreparer,
|
||||
GithubPreparer,
|
||||
Preparers,
|
||||
Generators,
|
||||
LocalPublish,
|
||||
@@ -15,12 +16,15 @@ export default async function createPlugin({
|
||||
}: PluginEnvironment) {
|
||||
const generators = new Generators();
|
||||
const techdocsGenerator = new TechdocsGenerator(logger);
|
||||
|
||||
generators.register('techdocs', techdocsGenerator);
|
||||
|
||||
const directoryPreparer = new DirectoryPreparer(logger);
|
||||
const preparers = new Preparers();
|
||||
const directoryPreparer = new DirectoryPreparer(logger);
|
||||
const githubPreparer = new GithubPreparer(logger);
|
||||
|
||||
preparers.register('dir', directoryPreparer);
|
||||
preparers.register('github', githubPreparer);
|
||||
|
||||
const publisher = new LocalPublish(logger);
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
AlertApiForwarder,
|
||||
oauthRequestApiRef,
|
||||
OAuthRequestManager,
|
||||
UrlPatternDiscovery,
|
||||
discoveryApiRef,
|
||||
GoogleAuth,
|
||||
googleAuthApiRef,
|
||||
GithubAuth,
|
||||
@@ -58,46 +60,49 @@ export const oauthRequestApiFactory = createApiFactory({
|
||||
factory: () => new OAuthRequestManager(),
|
||||
});
|
||||
|
||||
export const discoveryApiFactory = createApiFactory({
|
||||
implements: discoveryApiRef,
|
||||
deps: {},
|
||||
factory: () =>
|
||||
UrlPatternDiscovery.compile(`http://localhost:7000/{{ pluginId }}`),
|
||||
});
|
||||
|
||||
export const googleAuthApiFactory = createApiFactory({
|
||||
implements: googleAuthApiRef,
|
||||
deps: { oauthRequestApi: oauthRequestApiRef },
|
||||
factory: ({ oauthRequestApi }) =>
|
||||
deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef },
|
||||
factory: ({ discoveryApi, oauthRequestApi }) =>
|
||||
GoogleAuth.create({
|
||||
backendUrl: 'http://localhost:7000',
|
||||
basePath: '/auth/',
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
});
|
||||
|
||||
export const githubAuthApiFactory = createApiFactory({
|
||||
implements: githubAuthApiRef,
|
||||
deps: { oauthRequestApi: oauthRequestApiRef },
|
||||
factory: ({ oauthRequestApi }) =>
|
||||
deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef },
|
||||
factory: ({ discoveryApi, oauthRequestApi }) =>
|
||||
GithubAuth.create({
|
||||
backendUrl: 'http://localhost:7000',
|
||||
basePath: '/auth/',
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
});
|
||||
|
||||
export const gitlabAuthApiFactory = createApiFactory({
|
||||
implements: gitlabAuthApiRef,
|
||||
deps: { oauthRequestApi: oauthRequestApiRef },
|
||||
factory: ({ oauthRequestApi }) =>
|
||||
deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef },
|
||||
factory: ({ discoveryApi, oauthRequestApi }) =>
|
||||
GitlabAuth.create({
|
||||
backendUrl: 'http://localhost:7000',
|
||||
basePath: '/auth/',
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
});
|
||||
|
||||
export const auth0AuthApiFactory = createApiFactory({
|
||||
implements: auth0AuthApiRef,
|
||||
deps: { oauthRequestApi: oauthRequestApiRef },
|
||||
factory: ({ oauthRequestApi }) =>
|
||||
deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef },
|
||||
factory: ({ discoveryApi, oauthRequestApi }) =>
|
||||
Auth0Auth.create({
|
||||
backendUrl: 'http://localhost:7000',
|
||||
basePath: '/auth/',
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^4.0.0",
|
||||
"commander": "^4.1.1",
|
||||
"commander": "^6.1.0",
|
||||
"fs-extra": "^9.0.0",
|
||||
"github-slugger": "^1.3.0",
|
||||
"ts-node": "^8.6.2",
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.20",
|
||||
"commander": "^5.1.0",
|
||||
"commander": "^6.1.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
"http-proxy": "^1.18.1",
|
||||
"react-dev-utils": "^10.2.1",
|
||||
|
||||
@@ -54,6 +54,8 @@ export default function mockBreakpoint(initialBreakpoint: Breakpoint = 'xl') {
|
||||
let currentBreakpoint = initialBreakpoint;
|
||||
const queries = Array<Query>();
|
||||
|
||||
const previousMatchMedia: any = (window as any).matchMedia;
|
||||
|
||||
(window as any).matchMedia = (query: string): QueryList => {
|
||||
const listeners = new Set<Listener>();
|
||||
|
||||
@@ -85,7 +87,7 @@ export default function mockBreakpoint(initialBreakpoint: Breakpoint = 'xl') {
|
||||
});
|
||||
},
|
||||
remove() {
|
||||
delete window.matchMedia;
|
||||
(window as any).matchMedia = previousMatchMedia;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"@types/react": "^16.9",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
@@ -45,6 +44,7 @@
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react": "^16.9",
|
||||
"@types/swagger-ui-react": "^3.23.3",
|
||||
"jest-fetch-mock": "^3.0.3"
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Table, TableColumn } from '@backstage/core';
|
||||
import { Link } from '@material-ui/core';
|
||||
|
||||
@@ -16,13 +16,15 @@
|
||||
|
||||
import { ApiEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import { InfoCard } from '@backstage/core';
|
||||
import React, { FC } from 'react';
|
||||
import React from 'react';
|
||||
import { ApiDefinitionWidget } from '../ApiDefinitionWidget/ApiDefinitionWidget';
|
||||
|
||||
export const ApiDefinitionCard: FC<{
|
||||
type Props = {
|
||||
title?: string;
|
||||
apiEntity: ApiEntityV1alpha1;
|
||||
}> = ({ title, apiEntity }) => {
|
||||
};
|
||||
|
||||
export const ApiDefinitionCard = ({ title, apiEntity }: Props) => {
|
||||
const type = apiEntity?.spec?.type || '';
|
||||
const definition = apiEntity?.spec?.definition || '';
|
||||
|
||||
|
||||
@@ -14,15 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React from 'react';
|
||||
import { AsyncApiDefinitionWidget } from '../AsyncApiDefinitionWidget/AsyncApiDefinitionWidget';
|
||||
import { OpenApiDefinitionWidget } from '../OpenApiDefinitionWidget/OpenApiDefinitionWidget';
|
||||
import { PlainApiDefinitionWidget } from '../PlainApiDefinitionWidget/PlainApiDefinitionWidget';
|
||||
|
||||
export const ApiDefinitionWidget: FC<{
|
||||
type Props = {
|
||||
type: string;
|
||||
definition: string;
|
||||
}> = ({ type, definition }) => {
|
||||
};
|
||||
|
||||
export const ApiDefinitionWidget = ({ type, definition }: Props) => {
|
||||
switch (type) {
|
||||
case 'openapi':
|
||||
return <OpenApiDefinitionWidget definition={definition} />;
|
||||
@@ -31,6 +33,8 @@ export const ApiDefinitionWidget: FC<{
|
||||
return <AsyncApiDefinitionWidget definition={definition} />;
|
||||
|
||||
default:
|
||||
return <PlainApiDefinitionWidget definition={definition} />;
|
||||
return (
|
||||
<PlainApiDefinitionWidget definition={definition} language={type} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog';
|
||||
import { Box } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import React, { FC, useEffect } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useAsync } from 'react-use';
|
||||
import { ApiDefinitionCard } from '../ApiDefinitionCard/ApiDefinitionCard';
|
||||
@@ -59,15 +59,18 @@ export const getPageTheme = (entity?: Entity): PageTheme => {
|
||||
return pageTheme[themeKey] ?? pageTheme.home;
|
||||
};
|
||||
|
||||
const EntityPageTitle: FC<{ title: string; entity: Entity | undefined }> = ({
|
||||
title,
|
||||
}) => (
|
||||
type EntityPageTitleProps = {
|
||||
title: string;
|
||||
entity: Entity | undefined;
|
||||
};
|
||||
|
||||
const EntityPageTitle = ({ title }: EntityPageTitleProps) => (
|
||||
<Box display="inline-flex" alignItems="center" height="1em">
|
||||
{title}
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const ApiEntityPage: FC<{}> = () => {
|
||||
export const ApiEntityPage = () => {
|
||||
const { optionalNamespaceAndName } = useParams() as {
|
||||
optionalNamespaceAndName: string;
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import AsyncApi from '@kyma-project/asyncapi-react';
|
||||
import React, { FC } from 'react';
|
||||
import React from 'react';
|
||||
import { makeStyles, fade } from '@material-ui/core/styles';
|
||||
import '@kyma-project/asyncapi-react/lib/styles/fiori.css';
|
||||
|
||||
@@ -135,9 +135,11 @@ const useStyles = makeStyles(theme => ({
|
||||
},
|
||||
}));
|
||||
|
||||
export const AsyncApiDefinitionWidget: FC<{
|
||||
type Props = {
|
||||
definition: any;
|
||||
}> = ({ definition }) => {
|
||||
};
|
||||
|
||||
export const AsyncApiDefinitionWidget = ({ definition }: Props) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import SwaggerUI from 'swagger-ui-react';
|
||||
import 'swagger-ui-react/swagger-ui.css';
|
||||
@@ -37,7 +37,7 @@ const useStyles = makeStyles(theme => ({
|
||||
'& section.models, section.models.is-open h4': {
|
||||
'border-color': theme.palette.divider,
|
||||
},
|
||||
'& .opblock .opblock-summary-description, .parameter__type, table.headers td, .model-title, .model .property.primitive': {
|
||||
'& .opblock .opblock-summary-description, .parameter__type, table.headers td, .model-title, .model .property.primitive, section h3': {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
'& .opblock .opblock-summary-operation-id, .opblock .opblock-summary-path, .opblock .opblock-summary-path__deprecated, .opblock .opblock-section-header h4, .parameter__name, .response-col_status, .response-col_links, .responses-inner h4, .swagger-ui .responses-inner h5, .opblock-section-header .btn, .tab li, .info li, .info p, .info table, section.models h4, .info .title, table.model tr.description, .property-row': {
|
||||
@@ -65,9 +65,11 @@ const useStyles = makeStyles(theme => ({
|
||||
},
|
||||
}));
|
||||
|
||||
export const OpenApiDefinitionWidget: FC<{
|
||||
type Props = {
|
||||
definition: any;
|
||||
}> = ({ definition }) => {
|
||||
};
|
||||
|
||||
export const OpenApiDefinitionWidget = ({ definition }: Props) => {
|
||||
const classes = useStyles();
|
||||
|
||||
// Due to a bug in the swagger-ui-react component, the component needs
|
||||
|
||||
@@ -15,10 +15,13 @@
|
||||
*/
|
||||
|
||||
import { CodeSnippet } from '@backstage/core';
|
||||
import React, { FC } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
export const PlainApiDefinitionWidget: FC<{
|
||||
type Props = {
|
||||
definition: any;
|
||||
}> = ({ definition }) => {
|
||||
return <CodeSnippet text={definition} language="yaml" />;
|
||||
language: string;
|
||||
};
|
||||
|
||||
export const PlainApiDefinitionWidget = ({ definition, language }: Props) => {
|
||||
return <CodeSnippet text={definition} language={language} />;
|
||||
};
|
||||
|
||||
@@ -274,6 +274,13 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
|
||||
|
||||
await this.populateIdentity(response.backstageIdentity);
|
||||
|
||||
if (
|
||||
response.providerInfo.refreshToken &&
|
||||
response.providerInfo.refreshToken !== refreshToken
|
||||
) {
|
||||
this.setRefreshTokenCookie(res, response.providerInfo.refreshToken);
|
||||
}
|
||||
|
||||
res.send(response);
|
||||
} catch (error) {
|
||||
res.status(401).send(`${error.message}`);
|
||||
|
||||
@@ -45,7 +45,6 @@ export const makeProfileInfo = (
|
||||
if ((!email || !picture) && idToken) {
|
||||
try {
|
||||
const decoded: Record<string, string> = jwtDecoder(idToken);
|
||||
|
||||
if (!email && decoded.email) {
|
||||
email = decoded.email;
|
||||
}
|
||||
@@ -133,7 +132,7 @@ export const executeRefreshTokenStrategy = async (
|
||||
(
|
||||
err: Error | null,
|
||||
accessToken: string,
|
||||
_refreshToken: string,
|
||||
newRefreshToken: string,
|
||||
params: any,
|
||||
) => {
|
||||
if (err) {
|
||||
@@ -149,6 +148,7 @@ export const executeRefreshTokenStrategy = async (
|
||||
|
||||
resolve({
|
||||
accessToken,
|
||||
refreshToken: newRefreshToken,
|
||||
params,
|
||||
});
|
||||
},
|
||||
|
||||