Merge branch 'master' into ebarriosjr/Creating-gcp-projects-plugin

This commit is contained in:
Esteban Barrios
2020-09-03 17:59:59 +02:00
committed by GitHub
267 changed files with 5449 additions and 2169 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: 'Feature Request'
about: 'Suggest new features and changes'
labels: help wanted
labels: enhancement
---
<!--- Provide a general summary of the feature request in the Title above -->
+8
View File
@@ -74,3 +74,11 @@ jobs:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
package_root: "packages/core"
tag_prefix: "v"
- name: Discord notification
if: ${{ failure() }}
uses: Ilshidur/action-discord@0.2.0
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
with:
args: 'Master build failed https://github.com/{{GITHUB_REPOSITORY}}/actions/runs/{{GITHUB_RUN_ID}}'
+1
View File
@@ -92,6 +92,7 @@ dist
# Microsite build output
microsite/build
microsite/i18n
# Gatsby files
.cache/
+1
View File
@@ -12,3 +12,4 @@
| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. |
| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D |
| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling |
| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling |
+9
View File
@@ -8,6 +8,15 @@ If you encounter issues while upgrading to a newer version, don't hesitate to re
> Collect changes for the next release below
- The backend plugin
[service builder](https://github.com/spotify/backstage/blob/master/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts)
no longer adds `express.json()` automatically to all routes. While convenient
in a lot of cases, it also led to problems where for example the proxy
middleware could hang because the body had already been altered and could not
be streamed. Also, plugins that rather wanted to handle e.g. form encoded data
still had to cater to that manually. We therefore decided to let plugins add
`express.json()` themselves if they happen to deal with JSON data.
## v0.1.1-alpha.20
- Includes https://github.com/spotify/backstage/pull/2097 to resolve issues with create-plugin command.
+2 -5
View File
@@ -1,7 +1,4 @@
---
id: CONTRIBUTING
title: Contributing
---
# Contributing to Backstage
Our vision for Backstage is for it to become the trusted standard toolbox (read: UX layer) for the open source infrastructure landscape. Think of it like Kubernetes for developer experience. We realize this is an ambitious goal. We cant do it alone.
@@ -31,7 +28,7 @@ What kind of plugins should/could be created? Some inspiration from the 120+ plu
## Suggesting a plugin
If you start developing a plugin that you aim to release as open source, we suggest that you create a new [new Issue](https://github.com/spotify/backstage/issues/new?template=plugin_template.md). This helps the community know what plugins are in development.
If you start developing a plugin that you aim to release as open source, we suggest that you create a new [new Issue](https://github.com/spotify/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME). This helps the community know what plugins are in development.
You can also use this process if you have an idea for a good plugin but you hope that someone else will pick up the work.
+1 -1
View File
@@ -61,7 +61,7 @@ Take a look at the [Getting Started](https://backstage.io/docs/getting-started/i
## Documentation
- [Main documentation](https://backstage.io/docs/overview/what-is-backstage)
- [Main documentation](https://backstage.io/docs)
- [Service Catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview)
- [Architecture](https://backstage.io/docs/overview/architecture-terminology) ([Decisions](https://backstage.io/docs/architecture-decisions/adrs-overview))
- [Designing for Backstage](https://backstage.io/docs/dls/design)
+30 -9
View File
@@ -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
@@ -50,7 +44,32 @@ newrelic:
baseUrl: 'https://api.newrelic.com/v2'
key: NEW_RELIC_REST_API_KEY
lighthouse:
baseUrl: http://localhost:3003
catalog:
rules:
- allow: [Component, API, Group, Template, Location]
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
@@ -66,6 +85,8 @@ catalog:
- 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
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/hello-world-api.yaml
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/streetlights-api.yaml
auth:
providers:
@@ -0,0 +1,58 @@
---
id: software-catalog-configuration
title: Catalog Configuration
---
## Static Location Configuration
To enable declarative catalog setups, it is possible to add locations to the
catalog via [static configuration](../../conf/index.md). Locations are added to
the catalog under the `catalog.locations` key, for example:
```yaml
catalog:
locations:
- type: github
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml
```
The locations added through static configuration can not be removed through the
catalog locations API. To remove the locations, you have to remove them from the
configuration.
## Catalog Rules
By default the catalog will only allow ingestion of entities with the kind
`Component`, `API` and `Location`. In order to allow entities of other kinds to
be added, you need to add rules to the catalog. Rules are added either in a
separate `catalog.rules` key, or added to statically configured locations.
For example, given the following configuration:
```yaml
catalog:
rules:
- allow: [Component, API, Location, Template]
locations:
- type: github
target: https://github.com/org/example/blob/master/org-data.yaml
rules:
- allow: [Group]
```
We are able to add entities of kind `Component`, `API`, `Location`, or
`Template` from any location, and `Group` entities from the `org-data.yaml`,
which will also be read as statically configured location.
Note that if the `catalog.rules` key is present it will replace the default
value, meaning that you need to add rules for the default kinds if you want
those to still be allowed.
The following configuration will reject any kind of entities from being added to
the catalog:
```yaml
catalog:
rules: []
```
+16
View File
@@ -79,6 +79,22 @@ All software created through the
[Backstage Software Templates](../software-templates/index.md) are automatically
registered in the catalog.
### Static catalog configuration
In addition to manually registering components, it is also possible to register
components though [static configuration](../../conf/index.md). For example, the
above example can be added using the following configuration:
```yaml
catalog:
locations:
- type: github
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml
```
More information about catalog configuration can be found
[here](configuration.md).
### Updating component metadata
Teams owning the components are responsible for maintaining the metadata about
@@ -58,7 +58,30 @@ 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
You can add the template files to the catalog through
[static location configuration](../software-catalog/configuration.md#static-location-configuration),
for example
```yaml
catalog:
locations:
- type: github
target: https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml
rules:
- allow: [Template]
```
Templates can also be added by posting the to the catalog directly. Note that if
you're doing this, you need to configure the catalog to allow template entities
to be ingested from any source, for example:
```yaml
catalog:
rules:
- allow: [Component, API, Template]
```
For loading from a file, the following command should work when the backend is
running:
```sh
@@ -69,7 +92,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 +106,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.
+6 -6
View File
@@ -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.
![Enter some variables](../../assets/software-templates/template-picked.png)
After filling in these variables, you'll get some more fields to fill out which
are required for backstage usage. The owner, which is a `user` in the backstage
system, 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`.
![Enter backstage vars](../../assets/software-templates/template-picked-2.png)
@@ -51,13 +51,13 @@ It shouldn't take too long, and you'll have a success screen!
![Templating Complete](../../assets/software-templates/complete.png)
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.
![Templating failed](../../assets/software-templates/failed.png)
### 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:
![Catalog](../../assets/software-templates/go-to-catalog.png)
+15 -2
View File
@@ -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
+18 -2
View File
@@ -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_ 🙃
+15 -6
View File
@@ -79,18 +79,26 @@ guidelines to get started.
- Further improvements to platform documentation
### Plugins
Building and maintaining [plugins](https://backstage.io/plugins) is the work of
the entire Backstage community.
A list of plugins that are in development is
[available here](https://github.com/spotify/backstage/issues?q=is%3Aissue+is%3Aopen+label%3Aplugin+sort%3Areactions-%2B1-desc).
We strongly recommend to upvote 👍 plugins you are interested in. This helps us
and the community prioritize what plugins to build.
Are you missing a plugin for your favorite tool? Please
[suggest a new one](https://github.com/spotify/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME).
Chances are that someone will jump in and help build it.
### Future work 🔮
- **[Backstage platform is stable](https://github.com/spotify/backstage/milestone/19)** -
The platform APIs and features are stable and can be depended on for
production use. After this plugins will require little to no maintenance.
- **[Plugin marketplace](https://github.com/spotify/backstage/issues/2009)** -
As the ecosystem of Backstage plugins continues to grow it is becoming
increasingly hard to keep track of what plugins are available. To solve this
we imagine a "Plugin marketplace" that helps with discovery and installation
of plugins.
- **Deploy a product demo at `demo.backstage.io`** - Deploy a typical Backstage
deployment available publicly so that people can click around and get a feel
for the product without having to install anything.
@@ -111,6 +119,7 @@ guidelines to get started.
### Completed milestones ✅
- [Plugin marketplace](https://backstage.io/plugins)
- [Improved and move documentation to backstage.io](https://backstage.io/docs/overview/what-is-backstage)
- [Backstage Service Catalog (alpha)](https://backstage.io/blog/2020/06/22/backstage-service-catalog-alpha)
- [Backstage Software Templates (alpha)](https://backstage.io/blog/2020/08/05/announcing-backstage-software-templates)
+25
View File
@@ -0,0 +1,25 @@
---
id: add-to-marketplace
title: Add to Marketplace
---
## Adding a Plugin to the Marketplace
To add a new plugin to the [plugin marketplace](https://backstage.io/plugins)
create a file in
[`microsite/data/plugins`](https://github.com/spotify/backstage/tree/master/microsite/data/plugins)
with your plugin's information. Example:
```yaml
---
title: Your Plugin
author: Your Name
authorUrl: # A link to information about the author E.g. Company url, github user profile, etc
category: Monitoring # A single category e.g. CI, Machine Learning, Services, Monitoring
description: A brief description of the plugin. # Max 170 characters
documentation: # A link to your documentation E.g. Your github README
iconUrl: # Used as the src attribute for your logo.
# You can provide an external url or add your logo under static/img and provide a path
# relative to static/ e.g. img/my-logo.png
npmPackageName: # Your npm package name E.g. '@backstage/plugin-<etc>' quotes are required
```
+3 -8
View File
@@ -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
+8 -1
View File
@@ -21,8 +21,15 @@ To create a plugin, follow the steps outlined [here](create-a-plugin.md).
If you start developing a plugin that you aim to release as open source, we
suggest that you create a new
[new Issue](https://github.com/spotify/backstage/issues/new?template=plugin_template.md).
[new Issue](https://github.com/spotify/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME).
This helps the community know what plugins are in development.
You can also use this process if you have an idea for a good plugin but you hope
that someone else will pick up the work.
## Integrate into the Service Catalog
If your plugin isn't supposed to live as a standalone page, but rather needs to
be presented as a part of a Service Catalog (e.g. a separate tab or a card on an
"Overview" tab), then check out
[the instruction](integrating-plugin-into-service-catalog.md). on how to do it.
@@ -0,0 +1,124 @@
---
id: integrating-plugin-into-service-catalog
title: Integrate into the Service Catalog
---
> This is an advanced use case and currently is an experimental feature. Expect
> API to change over time
## Steps
1. [Create a plugin](#create-a-plugin)
1. [Export a router with relative routes](#export-a-router)
1. [Import and use router in the APP](#import-and-use-router-in-the-app)
### Create a plugin
Follow the [same process](create-a-plugin.md) as for standalone plugin. You
should have a separate package in a folder, which represents your plugin.
Example:
```
$ yarn create-plugin
> ? Enter an ID for the plugin [required] my-plugin
> ? Enter the owner(s) of the plugin. If specified, this will be added to CODEOWNERS for the plugin path. [optional]
Creating the plugin...
```
### Export a router
Now in the plugin you have a `Router.tsx` file in the `src` folder. By default
it contains only one example route. Create a routing structure needed for your
plugin, keeping in mind that the whole set of routes defined here are going to
be mounted under some different route in the App.
Example:
`my-plugin` consists of 2 different views - `/me` and `/about`. I envision
people integrating it into plugin catalog as a tab named "MyPlugin". Then, my
`Routes.tsx` for the plugin is going to look like:
```tsx
<Routes>
<Route path="/me" element={<MePage />} />
<Route path="/about" element={<AboutPage />} />
</Routes>
```
(where MePage and AboutPage are 2 components defined in your plugin and imported
accordingly inside `Router.tsx`)
> Pay attention, if your `MePage` references the `AboutPage` it needs to do it
> through link to `about`, not `/about`. This allows react-router v6 to enable
> its relative routing mechanism. Read more -
> https://reacttraining.com/blog/react-router-v6-pre/#relative-route-path-and-link-to
### Import and use router in the APP
In the `app/src/components/catalog/EntityPage.tsx` (app === your folder,
containing backstage app) import your created Router:
```tsx
import { Router as MyPluginRouter } from '@backstage/plugin-my-plugin;
```
Now, you need to mount `MyPluginRouter` onto some route, for example if you had:
```tsx
const DefaultEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayout.Content
path="/"
title="Overview"
element={<OverviewPage entity={entity} />}
/>
</EntityPageLayout>
);
```
after you add your code it becomes:
```tsx
const DefaultEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayout.Content
path="/"
title="Overview"
element={<OverviewPage entity={entity} />}
/>
<EntityPageLayout.Content
path="/my-plugin"
title="My Plugin"
element={<MyPluginRouter entity={entity} />}
/>
</EntityPageLayout>
);
```
All of magic happens thanks to the `EntityPageLayout` component, which comes as
an export from `@backstage/plugin-catalog` package.
```tsx
type EntityPageLayoutContentProps = {
/**
* Going to be transformed into react-router v6
* path under the hood. Read more at https://reacttraining.com/blog/react-router-v6-pre
*/
path: string;
/**
* Gets transformed into the title for the tab
*/
title: string;
/**
* Element that is rendered when the location
* matches the path provided
*/
element: JSX.Element;
};
```
> You can either pass the entity from App to the plugin's router as a prop or
> use `useEntity` hook from `@backstage/plugin-catalog` directly inside your
> plugin.
+70 -1
View File
@@ -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`.
+14
View File
@@ -39,4 +39,18 @@ $ git push origin -u new-release
And then create a PR. Once the PR is approved and merged into master, the master
build will publish new versions of all bumped packages.
### Include new changes in existing release PR
If you want to include some last minute changes to an existing release PR,
follow these instructions:
```sh
$ git checkout master
$ git pull
$ git checkout new-release
$ git reset --hard master
$ yarn release
$ git push --force
```
[Back to Docs](../README.md)
+2 -2
View File
@@ -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 | `{}` |
+3
View File
@@ -45,6 +45,9 @@ class Footer extends React.Component {
<div>
<h5>Community</h5>
<a href="https://discord.gg/MUpMjP2">Support chatroom</a>
<a href="https://github.com/spotify/backstage/blob/master/CONTRIBUTING.md">
Contributing
</a>
<a href="https://mailchi.mp/spotify/backstage-community">
Subscribe to our newsletter
</a>
+9
View File
@@ -0,0 +1,9 @@
---
title: API Docs
author: SDA SE
authorUrl: https://sda.se/
category: Discovery
description: Components to discover and display API entities as an extension to the catalog plugin.
documentation: https://github.com/spotify/backstage/blob/master/plugins/api-docs/README.md
iconUrl: https://thecoders.io/wp-content/uploads/2019/11/tech-swagger.svg
npmPackageName: '@backstage/plugin-api-docs'
+12
View File
@@ -0,0 +1,12 @@
---
title: CircleCI
author: Spotify
authorUrl: https://github.com/spotify
category: CI
description: Automate your development process with CI hosted in the cloud or on a private server.
documentation: https://github.com/spotify/backstage/tree/master/plugins/circleci
iconUrl: https://www.saaves.com/storage/brochure/logo-circleci-icon1583764538.png
npmPackageName: '@backstage/plugin-circleci'
tags:
- ci
- cd
@@ -0,0 +1,13 @@
---
title: GitHub Actions
author: Spotify
authorUrl: https://github.com/spotify
category: CI
description: GitHub Actions makes it easy to automate all your software workflows, now with world-class CI/CD. Build, test, and deploy your code right from GitHub.
documentation: https://github.com/spotify/backstage/tree/master/plugins/github-actions
iconUrl: https://avatars2.githubusercontent.com/u/44036562?s=400&v=4
npmPackageName: '@backstage/plugin-github-actions'
tags:
- ci
- cd
- github
@@ -0,0 +1,10 @@
---
title: GitHub Pull Requests
author: roadie.io
authorUrl: https://roadie.io/
category: CI
description: View GitHub pull requests for your service in Backstage.
documentation: https://roadie.io/backstage/plugins/github-pull-requests
iconUrl: https://roadie.io/static/7f13bb8d861d8dedc5112fb939d215f9/351f2/GitHub-Mark-Light-120px-plus.png
npmPackageName: '@roadiehq/backstage-plugin-github-pull-requests'
@@ -0,0 +1,14 @@
---
title: GitOps Clusters
author: Weaveworks
authorUrl: https://www.weave.works/
category: Kubernetes
description: Create GitOps-managed Kubernetes clusters. Currently, it supports provisioning EKS clusters on GitHub via GitHub Actions.
documentation: https://github.com/spotify/backstage/tree/master/plugins/gitops-profiles
iconUrl: https://res-5.cloudinary.com/crunchbase-production/image/upload/c_lpad,h_256,w_256,f_auto,q_auto:eco/v1462316670/i9d3delzvx1erzjhmcws.png
npmPackageName: '@backstage/plugin-gitops-profiles'
tags:
- kubernetes
- gitops
- github
- eks
+14
View File
@@ -0,0 +1,14 @@
---
title: GraphiQL
author: Spotify
authorUrl: https://github.com/spotify
category: Debugging
description: Integrates GraphiQL as a tool to browse GraphiQL endpoints inside Backstage.
documentation: https://github.com/spotify/backstage/tree/master/plugins/lighthouse
iconUrl: https://upload.wikimedia.org/wikipedia/commons/thumb/1/17/GraphQL_Logo.svg/1024px-GraphQL_Logo.svg.png
npmPackageName: '@backstage/plugin-graphiql'
tags:
- graphql
- github
- gitlab
- api
+12
View File
@@ -0,0 +1,12 @@
---
title: Jenkins
author: '@timja'
authorUrl: https://github.com/timja
category: CI
description: Jenkins offers a simple way to set up a continuous integration and continuous delivery environment.
documentation: https://github.com/spotify/backstage/tree/master/plugins/jenkins
iconUrl: https://img.icons8.com/color/1600/jenkins.png
npmPackageName: '@backstage/plugin-jenkins'
tags:
- ci
- cd
+14
View File
@@ -0,0 +1,14 @@
---
title: Lighthouse
author: Spotify
authorUrl: https://github.com/spotify
category: Accessibility
description: Google's Lighthouse tool is a great resource for benchmarking and improving the accessibility, performance, SEO, and best practices of your website.
documentation: https://github.com/spotify/backstage/tree/master/plugins/lighthouse
iconUrl: https://seeklogo.com/images/G/google-lighthouse-logo-1C7FA08580-seeklogo.com.png
npmPackageName: '@backstage/plugin-lighthouse'
tags:
- web
- seo
- accessibility
- performance
+14
View File
@@ -0,0 +1,14 @@
---
title: New Relic
author: '@timwheelercom'
authorUrl: https://github.com/timwheelercom
category: Monitoring
description: Observability platform built to help engineers create and monitor their software.
documentation: https://github.com/spotify/backstage/tree/master/plugins/newrelic
iconUrl: https://www.mulesoft.com/sites/default/files/2018-10/New_relic.png
npmPackageName: '@backstage/plugin-newrelic'
tags:
- performance
- monitoring
- errors
- alerting
+10
View File
@@ -0,0 +1,10 @@
---
title: Rollbar
author: '@andrewthauer'
authorUrl: https://github.com/andrewthauer
category: Monitoring
description: View Rollbar errors for your services in Backstage.
documentation: https://github.com/spotify/backstage/tree/master/plugins/rollbar
iconUrl: https://rollbar.com/assets/media/rollbar-mark-color.png
npmPackageName: '@backstage/plugin-rollbar'
+9
View File
@@ -0,0 +1,9 @@
---
title: Sentry
author: Spotify
authorUrl: https://github.com/spotify
category: Monitoring
description: View Sentry issues in Backstage.
documentation: https://github.com/spotify/backstage/tree/master/plugins/sentry
iconUrl: https://sentry-brand.storage.googleapis.com/sentry-glyph-white.png
npmPackageName: '@backstage/plugin-sentry'
+9
View File
@@ -0,0 +1,9 @@
---
title: Tech Radar
author: Spotify
authorUrl: https://github.com/spotify
category: Discovery
description: Visualize the your company's official guidelines of different areas of software development.
documentation: https://github.com/spotify/backstage/tree/master/plugins/tech-radar
iconUrl: https://www.materialui.co/materialIcons/action/track_changes_white_192x192.png
npmPackageName: '@backstage/plugin-tech-radar'
+10
View File
@@ -0,0 +1,10 @@
---
title: Travis CI
author: roadie.io
authorUrl: https://roadie.io/
category: CI
description: View Travis CI builds for your service in Backstage.
documentation: https://roadie.io/backstage/plugins/travis-ci
iconUrl: https://roadie.io/static/af2941eaf0af675facb281d566f42e14/45f2b/travis-ci-mascot-200x200.png
npmPackageName: '@roadiehq/backstage-plugin-travis-ci'
-321
View File
@@ -1,321 +0,0 @@
{
"_comment": "This file is auto-generated by write-translations.js",
"localized-strings": {
"next": "Next",
"previous": "Previous",
"tagline": "An open platform for building developer portals",
"docs": {
"api/backend": {
"title": "Backend"
},
"api/utility-apis": {
"title": "Utility APIs"
},
"architecture-decisions/adrs-adr001": {
"title": "ADR001: Architecture Decision Record (ADR) log",
"sidebar_label": "ADR001"
},
"architecture-decisions/adrs-adr002": {
"title": "ADR002: Default Software Catalog File Format",
"sidebar_label": "ADR002"
},
"architecture-decisions/adrs-adr003": {
"title": "ADR003: Avoid Default Exports and Prefer Named Exports",
"sidebar_label": "ADR003"
},
"architecture-decisions/adrs-adr004": {
"title": "ADR004: Module Export Structure",
"sidebar_label": "ADR004"
},
"architecture-decisions/adrs-adr005": {
"title": "ADR005: Catalog Core Entities",
"sidebar_label": "ADR005"
},
"architecture-decisions/adrs-adr006": {
"title": "ADR006: Avoid React.FC and React.SFC",
"sidebar_label": "ADR006"
},
"architecture-decisions/adrs-adr007": {
"title": "ADR007: Use MSW to mock http requests",
"sidebar_label": "ADR007"
},
"architecture-decisions/adrs-adr008": {
"title": "ADR008: Default Catalog File Name",
"sidebar_label": "ADR008"
},
"architecture-decisions/adrs-overview": {
"title": "Architecture Decision Records (ADR)",
"sidebar_label": "Overview"
},
"auth/add-auth-provider": {
"title": "Adding authentication providers"
},
"auth/auth-backend-classes": {
"title": "Auth backend classes"
},
"auth/auth-backend": {
"title": "Auth backend"
},
"auth/glossary": {
"title": "Glossary"
},
"auth/index": {
"title": "User Authentication and Authorization in Backstage"
},
"auth/oauth": {
"title": "OAuth and OpenID Connect"
},
"conf/defining": {
"title": "Defining Configuration for your Plugin"
},
"conf/index": {
"title": "Static Configuration in Backstage"
},
"conf/reading": {
"title": "Reading Backstage Configuration"
},
"conf/writing": {
"title": "Writing Backstage Configuration Files"
},
"dls/contributing-to-storybook": {
"title": "Contributing to Storybook"
},
"dls/design": {
"title": "Design"
},
"dls/figma": {
"title": "Figma"
},
"FAQ": {
"title": "FAQ"
},
"features/software-catalog/software-catalog-api": {
"title": "API"
},
"features/software-catalog/descriptor-format": {
"title": "Descriptor Format of Catalog Entities",
"sidebar_label": "YAML File Format"
},
"features/software-catalog/extending-the-model": {
"title": "Extending the model"
},
"features/software-catalog/external-integrations": {
"title": "External integrations"
},
"features/software-catalog/software-catalog-overview": {
"title": "Backstage Service Catalog (alpha)",
"sidebar_label": "Backstage Service Catalog"
},
"features/software-catalog/installation": {
"title": "features/software-catalog/installation"
},
"features/software-catalog/system-model": {
"title": "System Model"
},
"features/software-templates/adding-templates": {
"title": "Adding your own Templates"
},
"features/software-templates/extending/extending-preparer": {
"title": "Create your own Preparer"
},
"features/software-templates/extending/extending-publisher": {
"title": "Create your own Publisher"
},
"features/software-templates/extending/extending-templater": {
"title": "Creating your own Templater"
},
"features/software-templates/extending/extending-index": {
"title": "Extending the Scaffolder"
},
"features/software-templates/software-templates-index": {
"title": "Software Templates"
},
"features/software-templates/installation": {
"title": "features/software-templates/installation"
},
"features/techdocs/concepts": {
"title": "Concepts"
},
"features/techdocs/creating-and-publishing": {
"title": "Creating and publishing your docs",
"sidebar_label": "Creating and Publishing Documentation"
},
"features/techdocs/faqs": {
"title": "TechDocs FAQ",
"sidebar_label": "FAQ"
},
"features/techdocs/getting-started": {
"title": "Getting Started"
},
"features/techdocs/techdocs-overview": {
"title": "TechDocs Documentation",
"sidebar_label": "Overview"
},
"getting-started/app-custom-theme": {
"title": "Customize the look-and-feel of your App"
},
"getting-started/configure-app-with-plugins": {
"title": "Configuring App with plugins"
},
"getting-started/create-an-app": {
"title": "Create an App"
},
"getting-started/deployment-k8s": {
"title": "Kubernetes"
},
"getting-started/deployment-other": {
"title": "Other"
},
"getting-started/development-environment": {
"title": "Development Environment"
},
"getting-started/index": {
"title": "Running Backstage Locally"
},
"getting-started/installation": {
"title": "Installation"
},
"overview/adopting": {
"title": "Strategies for adopting"
},
"overview/architecture-overview": {
"title": "Architecture overview"
},
"overview/architecture-terminology": {
"title": "Architecture terminology"
},
"overview/background": {
"title": "The Spotify Story"
},
"overview/roadmap": {
"title": "Project roadmap"
},
"overview/support": {
"title": "Support and community"
},
"overview/vision": {
"title": "Vision"
},
"overview/what-is-backstage": {
"title": "What is Backstage?"
},
"plugins/backend-plugin": {
"title": "Backend plugin"
},
"plugins/call-existing-api": {
"title": "Call Existing API"
},
"plugins/create-a-plugin": {
"title": "Create a Backstage Plugin"
},
"plugins/existing-plugins": {
"title": "Existing plugins"
},
"plugins/index": {
"title": "Intro to plugins"
},
"plugins/plugin-development": {
"title": "Plugin Development"
},
"plugins/proxying": {
"title": "Proxying"
},
"plugins/publish-private": {
"title": "Publish private"
},
"plugins/publishing": {
"title": "Publishing"
},
"plugins/structure-of-a-plugin": {
"title": "Structure of a Plugin"
},
"plugins/testing": {
"title": "Testing with Jest"
},
"README": {
"title": "README"
},
"reference/createPlugin-feature-flags": {
"title": "createPlugin - feature flags"
},
"reference/createPlugin-router": {
"title": "createPlugin - router"
},
"reference/createPlugin": {
"title": "createPlugin"
},
"reference/utility-apis/AlertApi": {
"title": "reference/utility-apis/AlertApi"
},
"reference/utility-apis/AppThemeApi": {
"title": "reference/utility-apis/AppThemeApi"
},
"reference/utility-apis/BackstageIdentityApi": {
"title": "reference/utility-apis/BackstageIdentityApi"
},
"reference/utility-apis/Config": {
"title": "reference/utility-apis/Config"
},
"reference/utility-apis/ErrorApi": {
"title": "reference/utility-apis/ErrorApi"
},
"reference/utility-apis/FeatureFlagsApi": {
"title": "reference/utility-apis/FeatureFlagsApi"
},
"reference/utility-apis/IdentityApi": {
"title": "reference/utility-apis/IdentityApi"
},
"reference/utility-apis/OAuthApi": {
"title": "reference/utility-apis/OAuthApi"
},
"reference/utility-apis/OAuthRequestApi": {
"title": "reference/utility-apis/OAuthRequestApi"
},
"reference/utility-apis/OpenIdConnectApi": {
"title": "reference/utility-apis/OpenIdConnectApi"
},
"reference/utility-apis/ProfileInfoApi": {
"title": "reference/utility-apis/ProfileInfoApi"
},
"reference/utility-apis/README": {
"title": "Utility API References"
},
"reference/utility-apis/SessionStateApi": {
"title": "reference/utility-apis/SessionStateApi"
},
"reference/utility-apis/StorageApi": {
"title": "reference/utility-apis/StorageApi"
},
"tutorials/journey": {
"title": "Future developer journey"
}
},
"links": {
"GitHub": "GitHub",
"Docs": "Docs",
"Blog": "Blog",
"Demos": "Demos",
"Newsletter": "Newsletter"
},
"categories": {
"Overview": "Overview",
"Getting Started": "Getting Started",
"Features": "Features",
"Plugins": "Plugins",
"Configuration": "Configuration",
"Auth and identity": "Auth and identity",
"Designing for Backstage": "Designing for Backstage",
"API references": "API references",
"Tutorials": "Tutorials",
"Architecture Decision Records (ADRs)": "Architecture Decision Records (ADRs)",
"Contribute": "Contribute",
"Support": "Support",
"FAQ": "FAQ"
}
},
"pages-strings": {
"Help Translate|recruit community translators for your project": "Help Translate",
"Edit this Doc|recruitment message asking to edit the doc source": "Edit",
"Translate this Doc|recruitment message asking to translate the docs": "Translate"
}
}
+3 -2
View File
@@ -1,5 +1,5 @@
{
"version": "0.1.1-alpha.20",
"version": "0.1.1-alpha.21",
"name": "backstage-microsite",
"license": "Apache-2.0",
"private": true,
@@ -13,6 +13,7 @@
"rename-version": "docusaurus-rename-version"
},
"devDependencies": {
"docusaurus": "^2.0.0-alpha.61"
"docusaurus": "^2.0.0-alpha.61",
"js-yaml": "^3.14.0"
}
}
+3 -7
View File
@@ -455,12 +455,8 @@ class Index extends React.Component {
Share with the community
</Block.SmallTitle>
<Block.Paragraph>
Building{' '}
<a href="https://github.com/spotify/backstage/blob/master/docs/FAQ.md#how-do-i-find-out-if-a-plugin-already-exists">
open source plugins
</a>{' '}
contributes to the entire Backstage ecosystem, which benefits
everyone
Building <a href="/plugins">open source plugins</a> contributes
to the entire Backstage ecosystem, which benefits everyone
</Block.Paragraph>
</Block.TextBox>
@@ -472,7 +468,7 @@ class Index extends React.Component {
<ActionBlock className="stripe-top bg-teal">
<ActionBlock.Title>Build a plugin</ActionBlock.Title>
<ActionBlock.Link href="https://github.com/spotify/backstage/blob/master/docs/plugins/create-a-plugin.md">
<ActionBlock.Link href="/docs/plugins/create-a-plugin">
Contribute
</ActionBlock.Link>
</ActionBlock>
+119
View File
@@ -0,0 +1,119 @@
/**
* 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 fs = require('fs');
const yaml = require('js-yaml');
const React = require('react');
const Components = require(`${process.cwd()}/core/Components.js`);
const {
Block: { Container },
BulletLine,
} = Components;
const pluginsDirectory = require('path').join(process.cwd(), 'data/plugins');
const pluginMetadata = fs
.readdirSync(pluginsDirectory)
.map(file =>
yaml.safeLoad(fs.readFileSync(`./data/plugins/${file}`, 'utf8')),
);
const truncate = text =>
text.length > 170 ? text.substr(0, 170) + '...' : text;
const addPluginDocsLink = '/docs/plugins/add-to-marketplace';
const defaultIconUrl = 'img/logo-gradient-on-dark.svg';
const Plugins = () => (
<main className="MainContent">
<div className="PluginPageLayout">
<div className="PluginPageHeader">
<h2>Plugin marketplace</h2>
<p>
Open source plugins that you can add to your Backstage deployment.
Learn how to build a <a href="/docs/plugins">plugin</a>.
</p>
<span>
<a
className="PluginAddNewButton ButtonFilled"
href={addPluginDocsLink}
>
<b>Add to marketplace</b>
</a>
</span>
</div>
<BulletLine style={{ width: '100% ' }} />
<Container wrapped className="grid">
{pluginMetadata.map(
({
iconUrl,
title,
description,
author,
authorUrl,
documentation,
category,
}) => (
<div className="PluginCard">
<div className="PluginCardHeader">
<img src={iconUrl || defaultIconUrl} alt={title} />
<h2 className="PluginCardTitle">{title}</h2>
<p className="Author">
by <a href={authorUrl}>{author}</a>
</p>
<span className="ChipOutlined">{category}</span>
</div>
<div className="PluginCardBody">
<p>{truncate(description)}</p>
</div>
<Container className="PluginCardFooter">
<span>
<a
className="PluginCardLink ButtonFilled"
href={documentation}
>
Explore
</a>
</span>
</Container>
</div>
),
)}
<div className="PluginCard" id="add-plugin-card">
<div className="PluginCardBody">
<p>
Do you have an existing plugin that you want to add to the
Marketplace?
</p>
<p
style={{
marginTop: '20px',
textAlign: 'center',
}}
>
<a className="ButtonFilled" href={addPluginDocsLink}>
<b>Add to marketplace</b>
</a>
</p>
</div>
<Container className="PluginCardFooter">
<p>
See what plugins are already{' '}
<a href="https://github.com/spotify/backstage/issues?q=is%3Aissue+is%3Aopen+label%3Aplugin+sort%3Areactions-%2B1-desc">
in progress
</a>{' '}
and 👍. Missing a plugin for your favorite tool? Please{' '}
<a href="https://github.com/spotify/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME">
suggest
</a>{' '}
a new one.
</p>
</Container>
</div>
</Container>
</div>
</main>
);
module.exports = Plugins;
+2 -1
View File
@@ -74,6 +74,7 @@
"plugins/create-a-plugin",
"plugins/plugin-development",
"plugins/structure-of-a-plugin",
"plugins/integrating-plugin-into-service-catalog",
{
"type": "subcategory",
"label": "Backends and APIs",
@@ -91,7 +92,7 @@
{
"type": "subcategory",
"label": "Publishing",
"ids": ["plugins/publishing", "plugins/publish-private"]
"ids": ["plugins/publishing", "plugins/publish-private", "plugins/add-to-marketplace"]
}
],
"Configuration": [
+5 -1
View File
@@ -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',
@@ -38,6 +38,10 @@ const siteConfig = {
href: '/docs',
label: 'Docs',
},
{
page: 'plugins',
label: 'Plugins',
},
{
page: 'blog',
blog: true,
+1 -1
View File
@@ -216,7 +216,7 @@ td {
code {
font-family: IBM Plex Mono, Menlo, Monaco, Consolas, Courier New, monospace;
font-weight: 500;
background-color: #0e0e0e;
background-color: #272822;
}
/* .stripe {
+116
View File
@@ -0,0 +1,116 @@
.PluginCard {
background-color: #272822;
height: 100%;
padding: 16px;
display: flex;
flex-direction: column;
}
.grid {
display: grid;
grid-gap: 1rem;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 1fr;
padding-top: 32px;
}
@media (max-width: 1200px) {
.grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media only screen and (max-width: 815px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
.PluginCard img {
float: left;
margin: 0px 16px 8px 0px;
height: 80px;
width: 80px;
}
.PluginCardHeader {
max-height: fit-content;
min-height: fit-content;
}
.PluginCardTitle {
color: white;
vertical-align: top;
margin: 8px 0px 0px 16px;
}
.PluginAddNewButton {
position: absolute;
bottom: 16px;
right: 0px;
}
.ButtonFilled {
padding: 4px 8px;
border-radius: 4px;
background-color: #36baa2;
color: white;
margin-top: 36px;
}
.ButtonFilled:hover {
border: 1px solid #36baa2;
background-color: transparent;
}
.ChipOutlined {
font-size: small;
border-radius: 16px;
padding: 2px 8px;
border: 1px solid #36baa2;
color: #36baa2;
}
.PluginCardLink {
padding: 2px 8px;
position: absolute;
bottom: 0;
right: 0;
}
.PluginPageLayout {
margin: auto;
max-width: 1430px;
padding: 20px;
}
.PluginPageHeader {
position: relative;
}
.PluginPageHeader h2 {
display: inline-block;
}
.PluginCardBody {
padding-top: 8px;
}
.PluginCardFooter {
position: relative;
min-height: 2em;
}
.Author,
.Author a {
margin-bottom: 0.25em;
color: rgba(255, 255, 255, 0.6);
}
.Author a:hover {
color: white;
}
#add-plugin-card {
border: 1px solid #36baa2;
}
+1
View File
@@ -28,6 +28,7 @@ nav:
- Overview: 'features/software-catalog/index.md'
- System model: 'features/software-catalog/system-model.md'
- YAML File Format: 'features/software-catalog/descriptor-format.md'
- Configuration: 'features/software-catalog/configuration.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'
+24 -23
View File
@@ -1,30 +1,31 @@
{
"name": "example-app",
"version": "0.1.1-alpha.20",
"version": "0.1.1-alpha.21",
"private": true,
"bundled": true,
"dependencies": {
"@backstage/cli": "^0.1.1-alpha.20",
"@backstage/core": "^0.1.1-alpha.20",
"@backstage/plugin-api-docs": "^0.1.1-alpha.20",
"@backstage/plugin-catalog": "^0.1.1-alpha.20",
"@backstage/plugin-circleci": "^0.1.1-alpha.20",
"@backstage/plugin-explore": "^0.1.1-alpha.20",
"@backstage/plugin-gcp-projects": "^0.1.1-alpha.20",
"@backstage/plugin-github-actions": "^0.1.1-alpha.20",
"@backstage/plugin-gitops-profiles": "^0.1.1-alpha.20",
"@backstage/plugin-graphiql": "^0.1.1-alpha.20",
"@backstage/plugin-jenkins": "^0.1.1-alpha.20",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.20",
"@backstage/plugin-newrelic": "^0.1.1-alpha.20",
"@backstage/plugin-register-component": "^0.1.1-alpha.20",
"@backstage/plugin-rollbar": "^0.1.1-alpha.20",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.20",
"@backstage/plugin-sentry": "^0.1.1-alpha.20",
"@backstage/plugin-tech-radar": "^0.1.1-alpha.20",
"@backstage/plugin-techdocs": "^0.1.1-alpha.20",
"@backstage/plugin-welcome": "^0.1.1-alpha.20",
"@backstage/test-utils": "^0.1.1-alpha.20",
"@backstage/theme": "^0.1.1-alpha.20",
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/plugin-api-docs": "^0.1.1-alpha.21",
"@backstage/plugin-catalog": "^0.1.1-alpha.21",
"@backstage/plugin-circleci": "^0.1.1-alpha.21",
"@backstage/plugin-explore": "^0.1.1-alpha.21",
"@backstage/plugin-github-actions": "^0.1.1-alpha.21",
"@backstage/plugin-gitops-profiles": "^0.1.1-alpha.21",
"@backstage/plugin-graphiql": "^0.1.1-alpha.21",
"@backstage/plugin-jenkins": "^0.1.1-alpha.21",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.21",
"@backstage/plugin-newrelic": "^0.1.1-alpha.21",
"@backstage/plugin-register-component": "^0.1.1-alpha.21",
"@backstage/plugin-rollbar": "^0.1.1-alpha.21",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.21",
"@backstage/plugin-sentry": "^0.1.1-alpha.21",
"@backstage/plugin-tech-radar": "^0.1.1-alpha.21",
"@backstage/plugin-techdocs": "^0.1.1-alpha.21",
"@backstage/plugin-welcome": "^0.1.1-alpha.21",
"@backstage/test-utils": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"@octokit/rest": "^18.0.0",
+3
View File
@@ -27,6 +27,9 @@ describe('App', () => {
data: {
app: { title: 'Test' },
backend: { baseUrl: 'http://localhost:7000' },
lighthouse: {
baseUrl: 'http://localhost:3003',
},
techdocs: {
storageUrl: 'http://localhost:7000/techdocs/static/docs',
},
+16 -1
View File
@@ -26,6 +26,10 @@ import * as plugins from './plugins';
import { apis } from './apis';
import { hot } from 'react-hot-loader/root';
import { providers } from './identityProviders';
import { Router as CatalogRouter } from '@backstage/plugin-catalog';
import { Route, Routes, Navigate } from 'react-router';
import { EntityPage } from './components/catalog/EntityPage';
const app = createApp({
apis,
@@ -46,7 +50,18 @@ const app = createApp({
const AppProvider = app.getProvider();
const AppRouter = app.getRouter();
const AppRoutes = app.getRoutes();
const deprecatedAppRoutes = app.getRoutes();
const AppRoutes = () => (
<Routes>
<Route
path="/catalog/*"
element={<CatalogRouter EntityPage={EntityPage} />}
/>
<Navigate key="/" to="/catalog" />
{...deprecatedAppRoutes}
</Routes>
);
const App: FC<{}> = () => (
<AppProvider>
+1 -1
View File
@@ -118,7 +118,7 @@ export const apis = (config: ConfigApi) => {
builder.add(featureFlagsApiRef, new FeatureFlags());
builder.add(lighthouseApiRef, new LighthouseRestApi('http://localhost:3003'));
builder.add(lighthouseApiRef, LighthouseRestApi.fromConfig(config));
builder.add(travisCIApiRef, new TravisCIApi());
builder.add(githubPullRequestsApiRef, new GithubPullRequestsClient());
+1 -1
View File
@@ -90,7 +90,7 @@ const Root: FC<{}> = ({ children }) => (
<SidebarSearchField onSearch={handleSearch} />
<SidebarDivider />
{/* Global nav, not org-specific */}
<SidebarItem icon={HomeIcon} to="./" text="Home" />
<SidebarItem icon={HomeIcon} to="/catalog" text="Home" />
<SidebarItem icon={ExploreIcon} to="explore" text="Explore" />
<SidebarItem icon={ExtensionIcon} to="api-docs" text="APIs" />
<SidebarItem icon={LibraryBooks} to="docs" text="Docs" />
@@ -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 { Router as GitHubActionsRouter } from '@backstage/plugin-github-actions';
import React from 'react';
import {
EntityPageLayout,
useEntity,
AboutCard,
} from '@backstage/plugin-catalog';
import { Entity } from '@backstage/catalog-model';
import { Grid } from '@material-ui/core';
const OverviewContent = ({ entity }: { entity: Entity }) => (
<Grid container spacing={3}>
<Grid item>
<AboutCard entity={entity} />
</Grid>
</Grid>
);
const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayout.Content
path="/"
title="Overview"
element={<OverviewContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/ci-cd/*"
title="CI/CD"
element={<GitHubActionsRouter entity={entity} />}
/>
</EntityPageLayout>
);
const WebsiteEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayout.Content
path="/"
title="Overview"
element={<OverviewContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/ci-cd/*"
title="CI/CD"
element={<GitHubActionsRouter entity={entity} />}
/>
</EntityPageLayout>
);
const DefaultEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<OverviewContent entity={entity} />}
/>
</EntityPageLayout>
);
export const EntityPage = () => {
const { entity } = useEntity();
switch (entity?.spec?.type) {
case 'service':
return <ServiceEntityPage entity={entity} />;
case 'website':
return <WebsiteEntityPage entity={entity} />;
default:
return <DefaultEntityPage entity={entity} />;
}
};
+7 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
"version": "0.1.1-alpha.20",
"version": "0.1.1-alpha.21",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -29,9 +29,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/cli-common": "^0.1.1-alpha.20",
"@backstage/config": "^0.1.1-alpha.20",
"@backstage/config-loader": "^0.1.1-alpha.20",
"@backstage/cli-common": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/config-loader": "^0.1.1-alpha.21",
"@types/cors": "^2.8.6",
"@types/express": "^4.17.6",
"compression": "^1.7.4",
@@ -46,7 +46,8 @@
"prom-client": "^12.0.0",
"selfsigned": "^1.10.7",
"stoppable": "^1.1.0",
"winston": "^3.2.1"
"winston": "^3.2.1",
"logform": "^2.1.1"
},
"peerDependencies": {
"pg-connection-string": "^2.3.0"
@@ -57,7 +58,7 @@
}
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.20",
"@backstage/cli": "^0.1.1-alpha.21",
"@types/compression": "^1.7.0",
"@types/http-errors": "^1.6.3",
"@types/morgan": "^1.9.0",
@@ -15,7 +15,7 @@
*/
import { ConfigReader } from '@backstage/config';
import { createDatabase } from './connection';
import { createDatabaseClient } from './connection';
describe('database connection', () => {
const createConfig = (data: any) =>
@@ -26,10 +26,10 @@ describe('database connection', () => {
},
]);
describe(createDatabase, () => {
describe(createDatabaseClient, () => {
it('returns a postgres connection', () => {
expect(
createDatabase(
createDatabaseClient(
createConfig({
client: 'pg',
connection: {
@@ -45,7 +45,7 @@ describe('database connection', () => {
it('returns an sqlite connection', () => {
expect(
createDatabase(
createDatabaseClient(
createConfig({
client: 'sqlite3',
connection: ':memory:',
@@ -56,7 +56,7 @@ describe('database connection', () => {
it('tries to create a mysql connection as a passthrough', () => {
expect(() =>
createDatabase(
createDatabaseClient(
createConfig({
client: 'mysql',
connection: {
@@ -72,7 +72,7 @@ describe('database connection', () => {
it('accepts overrides', () => {
expect(
createDatabase(
createDatabaseClient(
createConfig({
client: 'pg',
connection: {
@@ -93,7 +93,7 @@ describe('database connection', () => {
it('throws an error without a client', () => {
expect(() =>
createDatabase(
createDatabaseClient(
createConfig({
connection: '',
}),
@@ -103,7 +103,7 @@ describe('database connection', () => {
it('throws an error without a connection', () => {
expect(() =>
createDatabase(
createDatabaseClient(
createConfig({
client: 'pg',
}),
@@ -15,30 +15,36 @@
*/
import knex from 'knex';
import { ConfigReader } from '@backstage/config';
import { Config } from '@backstage/config';
import { mergeDatabaseConfig } from './config';
import { createPgDatabase } from './postgres';
import { createSqlite3Database } from './sqlite3';
import { createPgDatabaseClient } from './postgres';
import { createSqliteDatabaseClient } from './sqlite3';
type DatabaseClient = 'pg' | 'sqlite3' | string;
/**
* Creates a knex database connection
*
* @param config The database config
* @param dbConfig The database config
* @param overrides Additional options to merge with the config
*/
export function createDatabase(
config: ConfigReader,
export function createDatabaseClient(
dbConfig: Config,
overrides?: Partial<knex.Config>,
) {
const client: DatabaseClient = config.getString('client');
const client: DatabaseClient = dbConfig.getString('client');
if (client === 'pg') {
return createPgDatabase(config, overrides);
return createPgDatabaseClient(dbConfig, overrides);
} else if (client === 'sqlite3') {
return createSqlite3Database(config);
return createSqliteDatabaseClient(dbConfig);
}
return knex(mergeDatabaseConfig(config.get(), overrides));
return knex(mergeDatabaseConfig(dbConfig.get(), overrides));
}
/**
* Alias for createDatabaseClient
* @deprecated Use createDatabaseClient instead
*/
export const createDatabase = createDatabaseClient;
@@ -14,15 +14,26 @@
* limitations under the License.
*/
import { ConfigReader } from '@backstage/config';
import { Config, ConfigReader } from '@backstage/config';
import {
parsePgConnectionString,
createPgDatabaseClient,
buildPgDatabaseConfig,
createPgDatabase,
getPgConnectionConfig,
parsePgConnectionString,
} from './postgres';
describe('postgres', () => {
const createConfig = (connection: any) =>
const createMockConnection = () => ({
host: 'acme',
user: 'foo',
password: 'bar',
database: 'foodb',
});
const createMockConnectionString = () =>
'postgresql://foo:bar@acme:5432/foodb';
const createConfig = (connection: any): Config =>
ConfigReader.fromConfigs([
{
context: '',
@@ -35,86 +46,58 @@ describe('postgres', () => {
describe(buildPgDatabaseConfig, () => {
it('builds a postgres config', () => {
expect(
buildPgDatabaseConfig(
createConfig({
host: 'acme',
user: 'foo',
password: 'bar',
port: '5432',
database: 'foodb',
}),
),
).toEqual({
const mockConnection = createMockConnection();
expect(buildPgDatabaseConfig(createConfig(mockConnection))).toEqual({
client: 'pg',
connection: {
host: 'acme',
user: 'foo',
password: 'bar',
port: '5432',
database: 'foodb',
},
connection: mockConnection,
useNullAsDefault: true,
});
});
it('builds a connection string config', () => {
expect(
buildPgDatabaseConfig(
createConfig('postgresql://foo:bar@acme:5432/foodb'),
),
).toEqual({
client: 'pg',
connection: 'postgresql://foo:bar@acme:5432/foodb',
useNullAsDefault: true,
});
const mockConnectionString = createMockConnectionString();
expect(buildPgDatabaseConfig(createConfig(mockConnectionString))).toEqual(
{
client: 'pg',
connection: mockConnectionString,
useNullAsDefault: true,
},
);
});
it('overrides the database name', () => {
const mockConnection = createMockConnection();
expect(
buildPgDatabaseConfig(
createConfig({
host: 'somehost',
user: 'postgres',
password: 'pass',
database: 'foo',
}),
{ connection: { database: 'foodb' } },
),
buildPgDatabaseConfig(createConfig(mockConnection), {
connection: { database: 'other_db' },
}),
).toEqual({
client: 'pg',
connection: {
host: 'somehost',
user: 'postgres',
password: 'pass',
database: 'foodb',
...mockConnection,
database: 'other_db',
},
useNullAsDefault: true,
});
});
it('adds additional config settings', () => {
const mockConnection = createMockConnection();
expect(
buildPgDatabaseConfig(
createConfig({
host: 'somehost',
user: 'postgres',
password: 'pass',
database: 'foo',
}),
{
connection: { database: 'foodb' },
pool: { min: 0, max: 7 },
debug: true,
},
),
buildPgDatabaseConfig(createConfig(mockConnection), {
connection: { database: 'other_db' },
pool: { min: 0, max: 7 },
debug: true,
}),
).toEqual({
client: 'pg',
connection: {
host: 'somehost',
user: 'postgres',
password: 'pass',
database: 'foodb',
...mockConnection,
database: 'other_db',
},
useNullAsDefault: true,
pool: { min: 0, max: 7 },
@@ -123,37 +106,72 @@ describe('postgres', () => {
});
it('overrides the database from connection string', () => {
const mockConnectionString = createMockConnectionString();
const mockConnection = createMockConnection();
expect(
buildPgDatabaseConfig(
createConfig('postgresql://postgres:pass@localhost:5432/dbname'),
{ connection: { database: 'foodb' } },
),
buildPgDatabaseConfig(createConfig(mockConnectionString), {
connection: { database: 'other_db' },
}),
).toEqual({
client: 'pg',
connection: {
host: 'localhost',
user: 'postgres',
password: 'pass',
...mockConnection,
port: '5432',
database: 'foodb',
database: 'other_db',
},
useNullAsDefault: true,
});
});
});
describe(createPgDatabase, () => {
describe(getPgConnectionConfig, () => {
it('returns the connection object back', () => {
const mockConnection = createMockConnection();
const config = createConfig(mockConnection);
expect(getPgConnectionConfig(config)).toEqual(mockConnection);
});
it('does not parse the connection string', () => {
const mockConnection = createMockConnection();
const config = createConfig(mockConnection);
expect(getPgConnectionConfig(config, true)).toEqual(mockConnection);
});
it('automatically parses the connection string', () => {
const mockConnection = createMockConnection();
const mockConnectionString = createMockConnectionString();
const config = createConfig(mockConnectionString);
expect(getPgConnectionConfig(config)).toEqual({
...mockConnection,
port: '5432',
});
});
it('parses the connection string', () => {
const mockConnection = createMockConnection();
const mockConnectionString = createMockConnectionString();
const config = createConfig(mockConnectionString);
expect(getPgConnectionConfig(config, true)).toEqual({
...mockConnection,
port: '5432',
});
});
});
describe(createPgDatabaseClient, () => {
it('creates a postgres knex instance', () => {
expect(
createPgDatabase(
createPgDatabaseClient(
createConfig({
client: 'pg',
connection: {
host: 'acme',
user: 'foo',
password: 'bar',
database: 'foodb',
},
host: 'acme',
user: 'foo',
password: 'bar',
database: 'foodb',
}),
),
).toBeTruthy();
@@ -161,7 +179,7 @@ describe('postgres', () => {
it('attempts to read an ssl cert', () => {
expect(() =>
createPgDatabase(
createPgDatabaseClient(
createConfig(
'postgresql://postgres:pass@localhost:5432/dbname?sslrootcert=/path/to/file',
),
@@ -14,18 +14,18 @@
* limitations under the License.
*/
import knex from 'knex';
import { ConfigReader } from '@backstage/config';
import knex, { PgConnectionConfig } from 'knex';
import { Config } from '@backstage/config';
import { mergeDatabaseConfig } from './config';
/**
* Creates a knex sqlite3 database connection
* Creates a knex postgres database connection
*
* @param dbConfig The database config
* @param overrides Additional options to merge with the config
*/
export function createPgDatabase(
dbConfig: ConfigReader,
export function createPgDatabaseClient(
dbConfig: Config,
overrides?: knex.Config,
) {
const knexConfig = buildPgDatabaseConfig(dbConfig, overrides);
@@ -40,26 +40,43 @@ export function createPgDatabase(
* @param overrides Additional options to merge with the config
*/
export function buildPgDatabaseConfig(
dbConfig: ConfigReader,
dbConfig: Config,
overrides?: knex.Config,
) {
const connection = dbConfig.get('connection') as any;
return mergeDatabaseConfig(
dbConfig.get(),
{
// Only parse the connection string when overrides are provided
connection:
overrides &&
(typeof connection === 'string' || connection instanceof String)
? parsePgConnectionString(connection as string)
: connection,
connection: getPgConnectionConfig(dbConfig, !!overrides),
useNullAsDefault: true,
},
overrides,
);
}
/**
* Gets the postgres connection config
*
* @param dbConfig The database config
* @param parseConnectionString Flag to explictly control connection string parsing
*/
export function getPgConnectionConfig(
dbConfig: Config,
parseConnectionString?: boolean,
): PgConnectionConfig | string {
const connection = dbConfig.get('connection') as any;
const isConnectionString =
typeof connection === 'string' || connection instanceof String;
const autoParse = typeof parseConnectionString !== 'boolean';
const shouldParseConnectionString = autoParse
? isConnectionString
: parseConnectionString && isConnectionString;
return shouldParseConnectionString
? parsePgConnectionString(connection as string)
: connection;
}
/**
* Parses a connection string using pg-connection-string
*
@@ -15,7 +15,10 @@
*/
import { ConfigReader } from '@backstage/config';
import { buildSqlite3DatabaseConfig, createSqlite3Database } from './sqlite3';
import {
buildSqliteDatabaseConfig,
createSqliteDatabaseClient,
} from './sqlite3';
describe('sqlite3', () => {
const createConfig = (connection: any) =>
@@ -29,9 +32,9 @@ describe('sqlite3', () => {
},
]);
describe(buildSqlite3DatabaseConfig, () => {
describe(buildSqliteDatabaseConfig, () => {
it('buidls a string connection', () => {
expect(buildSqlite3DatabaseConfig(createConfig(':memory:'))).toEqual({
expect(buildSqliteDatabaseConfig(createConfig(':memory:'))).toEqual({
client: 'sqlite3',
connection: ':memory:',
useNullAsDefault: true,
@@ -40,7 +43,7 @@ describe('sqlite3', () => {
it('builds a filename connection', () => {
expect(
buildSqlite3DatabaseConfig(
buildSqliteDatabaseConfig(
createConfig({
filename: '/path/to/foo',
}),
@@ -56,7 +59,7 @@ describe('sqlite3', () => {
it('replaces the connection with an override', () => {
expect(
buildSqlite3DatabaseConfig(createConfig(':memory:'), {
buildSqliteDatabaseConfig(createConfig(':memory:'), {
connection: { filename: '/path/to/foo' },
}),
).toEqual({
@@ -69,10 +72,10 @@ describe('sqlite3', () => {
});
});
describe(createSqlite3Database, () => {
describe(createSqliteDatabaseClient, () => {
it('creates an in memory knex instance', () => {
expect(
createSqlite3Database(
createSqliteDatabaseClient(
createConfig({
client: 'sqlite3',
connection: ':memory:',
@@ -15,7 +15,7 @@
*/
import knex from 'knex';
import { ConfigReader } from '@backstage/config';
import { Config } from '@backstage/config';
import { mergeDatabaseConfig } from './config';
/**
@@ -24,11 +24,11 @@ import { mergeDatabaseConfig } from './config';
* @param dbConfig The database config
* @param overrides Additional options to merge with the config
*/
export function createSqlite3Database(
dbConfig: ConfigReader,
export function createSqliteDatabaseClient(
dbConfig: Config,
overrides?: knex.Config,
) {
const knexConfig = buildSqlite3DatabaseConfig(dbConfig, overrides);
const knexConfig = buildSqliteDatabaseConfig(dbConfig, overrides);
const database = knex(knexConfig);
database.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
@@ -44,8 +44,8 @@ export function createSqlite3Database(
* @param dbConfig The database config
* @param overrides Additional options to merge with the config
*/
export function buildSqlite3DatabaseConfig(
dbConfig: ConfigReader,
export function buildSqliteDatabaseConfig(
dbConfig: Config,
overrides?: knex.Config,
) {
return mergeDatabaseConfig(
+1
View File
@@ -20,4 +20,5 @@ export * from './errors';
export * from './logging';
export * from './middleware';
export * from './service';
export * from './paths';
export * from './hot';
@@ -0,0 +1,35 @@
/*
* 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 * as winston from 'winston';
import { TransformableInfo } from 'logform';
const coloredTemplate = (info: TransformableInfo) => {
const { timestamp, level, message, plugin, service } = info;
const colorizer = winston.format.colorize();
const prefix = plugin || service;
const timestampColor = colorizer.colorize('timestamp', timestamp);
const prefixColor = colorizer.colorize('prefix', prefix);
return `${timestampColor} ${prefixColor} ${level} ${message}`;
};
export const coloredFormat = winston.format.combine(
winston.format.timestamp(),
winston.format.colorize({
colors: { timestamp: 'dim', prefix: 'blue' },
}),
winston.format.printf(coloredTemplate),
);
@@ -14,17 +14,14 @@
* limitations under the License.
*/
import * as winston from 'winston';
import { coloredFormat } from './formats';
let rootLogger: winston.Logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format:
process.env.NODE_ENV === 'production'
? winston.format.json()
: winston.format.combine(
winston.format.colorize(),
winston.format.timestamp(),
winston.format.simple(),
),
: coloredFormat,
defaultMeta: { service: 'backstage' },
transports: [
new winston.transports.Console({
+36
View File
@@ -0,0 +1,36 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/camelcase */
import { resolve as resolvePath } from 'path';
/**
* Resolve a path relative to the root of a package directory.
* Additional path arguments are resolved relative to the package dir.
*
* This is particularly useful when you want to access assets shipped with
* your backend plugin package. When doing so, do not forget to include the assets
* in your published package by adding them to `files` in your `package.json`.
*/
export function resolvePackagePath(name: string, ...paths: string[]) {
const req =
typeof __non_webpack_require__ === 'undefined'
? require
: __non_webpack_require__;
return resolvePath(req.resolve(`${name}/package.json`), '..', ...paths);
}
@@ -135,7 +135,6 @@ export class ServiceBuilderImpl implements ServiceBuilder {
app.use(cors(corsOptions));
}
app.use(compression());
app.use(express.json());
if (this.enableMetrics) {
app.use(metricsHandler());
}
+1
View File
@@ -37,6 +37,7 @@ yarn start
Substitute `x` for actual values, or leave them as
dummy values just to try out the backend without using the auth or sentry features.
You can also, instead of using dummy values for a huge number of environment variables, remove those config directly from app-config.yaml file located in the root folder.
The backend starts up on port 7000 per default.
+17 -15
View File
@@ -1,6 +1,6 @@
{
"name": "example-backend",
"version": "0.1.1-alpha.20",
"version": "0.1.1-alpha.21",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"private": true,
@@ -18,20 +18,22 @@
"migrate:create": "knex migrate:make -x ts"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.20",
"@backstage/catalog-model": "^0.1.1-alpha.20",
"@backstage/config": "^0.1.1-alpha.20",
"@backstage/plugin-auth-backend": "^0.1.1-alpha.20",
"@backstage/plugin-catalog-backend": "^0.1.1-alpha.20",
"@backstage/plugin-graphql-backend": "^0.1.1-alpha.20",
"@backstage/plugin-identity-backend": "^0.1.1-alpha.20",
"@backstage/plugin-proxy-backend": "^0.1.1-alpha.20",
"@backstage/plugin-rollbar-backend": "^0.1.1-alpha.20",
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.20",
"@backstage/plugin-sentry-backend": "^0.1.1-alpha.20",
"@backstage/plugin-techdocs-backend": "^0.1.1-alpha.20",
"@backstage/backend-common": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/plugin-app-backend": "^0.1.1-alpha.21",
"@backstage/plugin-auth-backend": "^0.1.1-alpha.21",
"@backstage/plugin-catalog-backend": "^0.1.1-alpha.21",
"@backstage/plugin-graphql-backend": "^0.1.1-alpha.21",
"@backstage/plugin-identity-backend": "^0.1.1-alpha.21",
"@backstage/plugin-proxy-backend": "^0.1.1-alpha.21",
"@backstage/plugin-rollbar-backend": "^0.1.1-alpha.21",
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.21",
"@backstage/plugin-sentry-backend": "^0.1.1-alpha.21",
"@backstage/plugin-techdocs-backend": "^0.1.1-alpha.21",
"@octokit/rest": "^18.0.0",
"dockerode": "^3.2.0",
"example-app": "^0.1.1-alpha.21",
"express": "^4.17.1",
"knex": "^0.21.1",
"pg": "^8.3.0",
@@ -40,10 +42,10 @@
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.20",
"@backstage/cli": "^0.1.1-alpha.21",
"@types/dockerode": "^2.5.32",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",
"@types/helmet": "^0.0.47"
"@types/helmet": "^0.0.48"
}
}
+13 -7
View File
@@ -23,7 +23,7 @@
*/
import {
createDatabase,
createDatabaseClient,
createServiceBuilder,
loadBackendConfig,
getRootLogger,
@@ -40,6 +40,7 @@ import sentry from './plugins/sentry';
import proxy from './plugins/proxy';
import techdocs from './plugins/techdocs';
import graphql from './plugins/graphql';
import app from './plugins/app';
import { PluginEnvironment } from './types';
function makeCreateEnv(loadedConfigs: AppConfig[]) {
@@ -47,11 +48,14 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) {
return (plugin: string): PluginEnvironment => {
const logger = getRootLogger().child({ type: 'plugin', plugin });
const database = createDatabase(config.getConfig('backend.database'), {
connection: {
database: `backstage_plugin_${plugin}`,
const database = createDatabaseClient(
config.getConfig('backend.database'),
{
connection: {
database: `backstage_plugin_${plugin}`,
},
},
});
);
return { logger, database, config };
};
}
@@ -71,6 +75,7 @@ async function main() {
const sentryEnv = useHotMemoize(module, () => createEnv('sentry'));
const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
const graphqlEnv = useHotMemoize(module, () => createEnv('graphql'));
const appEnv = useHotMemoize(module, () => createEnv('app'));
const service = createServiceBuilder(module)
.loadConfig(configReader)
@@ -82,8 +87,9 @@ async function main() {
.addRouter('/auth', await auth(authEnv))
.addRouter('/identity', await identity(identityEnv))
.addRouter('/techdocs', await techdocs(techdocsEnv))
.addRouter('/proxy', await proxy(proxyEnv))
.addRouter('/graphql', await graphql(graphqlEnv));
.addRouter('/proxy', await proxy(proxyEnv, '/proxy'))
.addRouter('/graphql', await graphql(graphqlEnv))
.addRouter('', await app(appEnv));
await service.start().catch(err => {
console.log(err);
@@ -14,16 +14,12 @@
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { InfoCard, StructuredMetadataTable } from '@backstage/core';
import React, { FC } from 'react';
import { createRouter } from '@backstage/plugin-app-backend';
import { PluginEnvironment } from '../types';
type Props = {
entity: Entity;
};
export const EntityMetadataCard: FC<Props> = ({ entity }) => (
<InfoCard title="Information">
<StructuredMetadataTable metadata={entity.metadata} />
</InfoCard>
);
export default async function createPlugin({ logger }: PluginEnvironment) {
return await createRouter({
logger,
appPackageName: 'example-app',
});
}
+6 -5
View File
@@ -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 });
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/catalog-model",
"version": "0.1.1-alpha.20",
"version": "0.1.1-alpha.21",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,7 +20,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.20",
"@backstage/config": "^0.1.1-alpha.21",
"@types/json-schema": "^7.0.5",
"@types/yup": "^0.28.2",
"json-schema": "^0.2.5",
@@ -29,7 +29,7 @@
"yup": "^0.29.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.20",
"@backstage/cli": "^0.1.1-alpha.21",
"@types/express": "^4.17.6",
"@types/jest": "^26.0.7",
"@types/lodash": "^4.14.151",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli-common",
"description": "Common functionality used by cli, backend, and create-app",
"version": "0.1.1-alpha.20",
"version": "0.1.1-alpha.21",
"private": false,
"main": "src/index.ts",
"types": "src/index.ts",
+3
View File
@@ -28,6 +28,9 @@ module.exports = {
env: {
jest: true,
},
globals: {
__non_webpack_require__: 'readonly',
},
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
+6 -1
View File
@@ -56,7 +56,12 @@ module.exports = {
'@typescript-eslint/no-unused-expressions': 'error',
'@typescript-eslint/no-unused-vars': [
'warn',
{ vars: 'all', args: 'after-used', ignoreRestSiblings: true },
{
vars: 'all',
args: 'after-used',
ignoreRestSiblings: true,
argsIgnorePattern: '^_',
},
],
'no-restricted-imports': [
2,
+8 -8
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
"version": "0.1.1-alpha.20",
"version": "0.1.1-alpha.21",
"private": false,
"publishConfig": {
"access": "public"
@@ -28,9 +28,9 @@
"backstage-cli": "bin/backstage-cli"
},
"dependencies": {
"@backstage/cli-common": "^0.1.1-alpha.20",
"@backstage/config": "^0.1.1-alpha.20",
"@backstage/config-loader": "^0.1.1-alpha.20",
"@backstage/cli-common": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/config-loader": "^0.1.1-alpha.21",
"@hot-loader/react-dom": "^16.13.0",
"@lerna/package-graph": "^3.18.5",
"@lerna/project": "^3.18.0",
@@ -40,17 +40,17 @@
"@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",
"@svgr/webpack": "5.4.x",
"@types/start-server-webpack-plugin": "^2.2.0",
"@types/webpack-env": "^1.15.2",
"@types/webpack-node-externals": "^2.5.0",
"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",
@@ -102,7 +102,7 @@
"@types/fs-extra": "^9.0.1",
"@types/html-webpack-plugin": "^3.2.2",
"@types/http-proxy": "^1.17.4",
"@types/inquirer": "^6.5.0",
"@types/inquirer": "^7.3.1",
"@types/mini-css-extract-plugin": "^0.9.1",
"@types/node": "^13.7.2",
"@types/ora": "^3.2.0",
@@ -28,6 +28,7 @@ import {
} from '../../lib/codeowners';
import { paths } from '../../lib/paths';
import { Task, templatingTask } from '../../lib/tasks';
import { version as backstageVersion } from '../../lib/version';
const exec = promisify(execCb);
@@ -239,7 +240,11 @@ export default async () => {
await createTemporaryPluginFolder(tempDir);
Task.section('Preparing files');
await templatingTask(templateDir, tempDir, { ...answers, version });
await templatingTask(templateDir, tempDir, {
...answers,
version,
backstageVersion,
});
Task.section('Moving to final location');
await movePlugin(tempDir, pluginDir, answers.id);
+4 -1
View File
@@ -25,7 +25,7 @@ import {
yesPromptFunc,
} from '../../lib/diff';
import { paths } from '../../lib/paths';
import { version } from '../../lib/version';
import { version as backstageVersion } from '../../lib/version';
export type PluginData = {
id: string;
@@ -62,9 +62,12 @@ export default async (cmd: Command) => {
promptFunc = yesPromptFunc;
}
const { version } = await fs.readJson(paths.resolveTargetRoot('lerna.json'));
const data = await readPluginData();
const templateFiles = await diffTemplateFiles('default-plugin', {
version,
backstageVersion,
...data,
});
await handleAllFiles(fileHandlers, templateFiles, promptFunc);
+6
View File
@@ -57,6 +57,12 @@ export default async (cmd: Command) => {
}
}
// This is the only thing that is not implemented by jest.run(), so we do it here instead
// https://github.com/facebook/jest/blob/cd8828f7bbec6e55b4df5e41e853a5133c4a3ee1/packages/jest-cli/bin/jest.js#L12
if (!process.env.NODE_ENV) {
(process.env as any).NODE_ENV = 'test';
}
// eslint-disable-next-line jest/no-jest-import
await require('jest').run(args);
};
+14 -4
View File
@@ -88,15 +88,25 @@ export const makeConfigs = async (
}),
resolve({ mainFields }),
commonjs({
include: ['node_modules/**', '../../node_modules/**'],
exclude: ['**/*.stories.*', '**/*.test.*'],
include: /node_modules/,
exclude: [/\/[^/]+\.(?:stories|test)\.[^/]+$/],
}),
postcss(),
imageFiles({ exclude: '**/*.icon.svg' }),
imageFiles({
exclude: /\.icon\.svg$/,
include: [
/\.css$/,
/\.svg$/,
/\.png$/,
/\.gif$/,
/\.jpg$/,
/\.jpeg$/,
],
}),
json(),
yaml(),
svgr({
include: '**/*.icon.svg',
include: /\.icon\.svg$/,
template: svgrTemplate,
}),
esbuild({
+1 -1
View File
@@ -25,7 +25,7 @@ export async function serveBackend(
},
) {
const paths = resolveBundlingPaths(options);
const config = createBackendConfig(paths, {
const config = await createBackendConfig(paths, {
...options,
isDev: true,
});
+1 -1
View File
@@ -36,7 +36,7 @@ export async function buildBundle(options: BuildOptions) {
const { statsJsonEnabled } = options;
const paths = resolveBundlingPaths(options);
const config = createConfig(paths, {
const config = await createConfig(paths, {
...options,
checksEnabled: false,
isDev: false,
+58 -10
View File
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin';
@@ -25,6 +27,9 @@ import { Config } from '@backstage/config';
import { BundlingPaths } from './paths';
import { transforms } from './transforms';
import { BundlingOptions, BackendBundlingOptions } from './types';
import { version } from '../../lib/version';
import { paths as cliPaths } from '../../lib/paths';
import { runPlain } from '../run';
export function resolveBaseUrl(config: Config): URL {
const baseUrl = config.getString('app.baseUrl');
@@ -35,10 +40,40 @@ export function resolveBaseUrl(config: Config): URL {
}
}
export function createConfig(
async function readBuildInfo() {
const timestamp = Date.now();
let commit = 'unknown';
try {
commit = await runPlain('git', 'rev-parse', 'HEAD');
} catch (error) {
console.warn(`WARNING: Failed to read git commit, ${error}`);
}
let gitVersion = 'unknown';
try {
gitVersion = await runPlain('git', 'describe', '--always');
} catch (error) {
console.warn(`WARNING: Failed to describe git version, ${error}`);
}
const { version: packageVersion } = await fs.readJson(
cliPaths.resolveTarget('package.json'),
);
return {
cliVersion: version,
gitVersion,
packageVersion,
timestamp,
commit,
};
}
export async function createConfig(
paths: BundlingPaths,
options: BundlingOptions,
): webpack.Configuration {
): Promise<webpack.Configuration> {
const { checksEnabled, isDev } = options;
const { plugins, loaders } = transforms(options);
@@ -81,6 +116,13 @@ export function createConfig(
}),
);
const buildInfo = await readBuildInfo();
plugins.push(
new webpack.DefinePlugin({
'process.env.BUILD_INFO': JSON.stringify(buildInfo),
}),
);
return {
mode: isDev ? 'development' : 'production',
profile: false,
@@ -130,14 +172,23 @@ export function createConfig(
};
}
export function createBackendConfig(
export async function createBackendConfig(
paths: BundlingPaths,
options: BackendBundlingOptions,
): webpack.Configuration {
): Promise<webpack.Configuration> {
const { checksEnabled, isDev } = options;
const { loaders } = transforms(options);
// Find all local monorepo packages and their node_modules, and mark them as external.
const LernaProject = require('@lerna/project');
const project = new LernaProject(cliPaths.targetDir);
const packages = await project.getPackages();
const localPackageNames = packages.map((p: any) => p.name);
const moduleDirs = packages.map((p: any) =>
resolvePath(p.location, 'node_modules'),
);
return {
mode: isDev ? 'development' : 'production',
profile: false,
@@ -152,11 +203,8 @@ export function createBackendConfig(
externals: [
nodeExternals({
modulesDir: paths.rootNodeModules,
allowlist: ['webpack/hot/poll?100', /\@backstage\/.*/],
}),
nodeExternals({
modulesDir: paths.targetNodeModules,
allowlist: ['webpack/hot/poll?100', /\@backstage\/.*/],
additionalModuleDirs: moduleDirs,
allowlist: ['webpack/hot/poll?100', ...localPackageNames],
}),
],
target: 'node' as const,
@@ -178,7 +226,7 @@ export function createBackendConfig(
resolve: {
extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'],
mainFields: ['browser', 'module', 'main'],
modules: [paths.targetNodeModules, paths.rootNodeModules],
modules: [paths.rootNodeModules, ...moduleDirs],
plugins: [
new ModuleScopePlugin(
[paths.targetSrc, paths.targetDev],
-1
View File
@@ -63,7 +63,6 @@ export function resolveBundlingPaths(options: BundlingPathsOptions) {
targetDev: paths.resolveTarget('dev'),
targetEntry: resolveTargetModule(entry),
targetTsConfig: paths.resolveTargetRoot('tsconfig.json'),
targetNodeModules: paths.resolveTarget('node_modules'),
targetPackageJson: paths.resolveTarget('package.json'),
rootNodeModules: paths.resolveTargetRoot('node_modules'),
root: paths.targetRoot,
+1 -1
View File
@@ -30,7 +30,7 @@ export async function serveBundle(options: ServeOptions) {
const paths = resolveBundlingPaths(options);
const pkgPath = paths.targetPackageJson;
const pkg = await fs.readJson(pkgPath);
const config = createConfig(paths, {
const config = await createConfig(paths, {
...options,
isDev: true,
baseUrl: url,
+21
View File
@@ -26,6 +26,7 @@ type LernaPackage = {
private: boolean;
location: string;
scripts: Record<string, string>;
get(key: string): any;
};
type FileEntry =
@@ -107,6 +108,26 @@ async function moveToDistWorkspace(
strip: 1,
});
await fs.remove(archivePath);
// We remove the dependencies from package.json of packages that are marked
// as bundled, so that yarn doesn't try to install them.
if (target.get('bundled')) {
const pkgJson = await fs.readJson(
resolvePath(absoluteOutputPath, 'package.json'),
);
delete pkgJson.dependencies;
delete pkgJson.devDependencies;
delete pkgJson.peerDependencies;
delete pkgJson.optionalDependencies;
await fs.writeJson(
resolvePath(absoluteOutputPath, 'package.json'),
pkgJson,
{
spaces: 2,
},
);
}
}),
);
}
@@ -21,8 +21,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core": "^{{version}}",
"@backstage/theme": "^{{version}}",
"@backstage/core": "^{{backstageVersion}}",
"@backstage/theme": "^{{backstageVersion}}",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -31,8 +31,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^{{version}}",
"@backstage/dev-utils": "^{{version}}",
"@backstage/cli": "^{{backstageVersion}}",
"@backstage/dev-utils": "^{{backstageVersion}}",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
@@ -2,13 +2,13 @@ import { createPlugin, createRouteRef } from '@backstage/core';
import ExampleComponent from './components/ExampleComponent';
export const rootRouteRef = createRouteRef({
path: '/{{ id }}',
title: '{{ id }}',
path: '/{{ id }}',
title: '{{ id }}',
});
export const plugin = createPlugin({
id: '{{ id }}',
register({ router }) {
router.addRoute(rootRouteRef, ExampleComponent);
},
id: '{{ id }}',
register({ router }) {
router.addRoute(rootRouteRef, ExampleComponent);
},
});
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/config-loader",
"description": "Config loading functionality used by Backstage backend, and CLI",
"version": "0.1.1-alpha.20",
"version": "0.1.1-alpha.21",
"private": false,
"publishConfig": {
"access": "public",
@@ -30,7 +30,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.20",
"@backstage/config": "^0.1.1-alpha.21",
"fs-extra": "^9.0.0",
"yaml": "^1.9.2",
"yup": "^0.29.1"
+1
View File
@@ -14,5 +14,6 @@
* limitations under the License.
*/
export { readEnvConfig } from './lib';
export { loadConfig } from './loader';
export type { LoadConfigOptions } from './loader';
+21 -21
View File
@@ -14,16 +14,16 @@
* limitations under the License.
*/
import { readEnv } from './env';
import { readEnvConfig } from './env';
describe('readEnv', () => {
describe('readEnvConfig', () => {
it('should return empty config for empty env', () => {
expect(readEnv({})).toEqual([]);
expect(readEnvConfig({})).toEqual([]);
});
it('should return empty config for no matching keys', () => {
expect(
readEnv({
readEnvConfig({
NODE_ENV: 'production',
NOPE_ENV: 'development',
APP_CONFIG: 'foo',
@@ -34,7 +34,7 @@ describe('readEnv', () => {
it('should create config from env', () => {
expect(
readEnv({
readEnvConfig({
NODE_ENV: 'production',
APP_CONFIG_foo: '"bar"',
APP_CONFIG_numbers_a: '1',
@@ -57,22 +57,22 @@ describe('readEnv', () => {
});
it('should accept string values', () => {
expect(readEnv({ APP_CONFIG_foo: '"abc"', APP_CONFIG_bar: 'xyz' })).toEqual(
[
{
data: {
foo: 'abc',
bar: 'xyz',
},
context: 'env',
expect(
readEnvConfig({ APP_CONFIG_foo: '"abc"', APP_CONFIG_bar: 'xyz' }),
).toEqual([
{
data: {
foo: 'abc',
bar: 'xyz',
},
],
);
context: 'env',
},
]);
});
it('should accept complex objects', () => {
expect(
readEnv({
readEnvConfig({
APP_CONFIG_foo: '{ "a": 123, "b": "123", "c": [] }',
APP_CONFIG_bar: '[123, "abc", {}]',
}),
@@ -95,7 +95,7 @@ describe('readEnv', () => {
['APP_CONFIG_fo o'],
['APP_CONFIG_foo_(foo)_foo'],
])('should reject invalid key %p', key => {
expect(() => readEnv({ [key]: '0' })).toThrow(
expect(() => readEnvConfig({ [key]: '0' })).toThrow(
`Invalid env config key '${key.replace('APP_CONFIG_', '')}'`,
);
});
@@ -103,7 +103,7 @@ describe('readEnv', () => {
it.each([['hello'], ['"hello'], ['{'], ['}']])(
'should fallback to string when invalid json value %p',
value => {
expect(readEnv({ APP_CONFIG_foo: value })).toEqual([
expect(readEnvConfig({ APP_CONFIG_foo: value })).toEqual([
{
data: {
foo: value,
@@ -116,7 +116,7 @@ describe('readEnv', () => {
it('should not allow null as a value', () => {
expect(() =>
readEnv({
readEnvConfig({
APP_CONFIG_foo: 'null',
}),
).toThrow(
@@ -126,7 +126,7 @@ describe('readEnv', () => {
it('should not allow duplicate values', () => {
expect(() =>
readEnv({
readEnvConfig({
APP_CONFIG_foo_bar: '1',
APP_CONFIG_foo_bar_baz: '2',
}),
@@ -137,7 +137,7 @@ describe('readEnv', () => {
it('should not allow mixing of objects and other values', () => {
expect(() =>
readEnv({
readEnvConfig({
APP_CONFIG_nested_foo: '1',
APP_CONFIG_nested: '2',
}),
+1 -1
View File
@@ -39,7 +39,7 @@ const CONFIG_KEY_PART_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z][a-z0-9]*)*$/i;
*
* APP_CONFIG_app_title='"My Title"'
*/
export function readEnv(env: {
export function readEnvConfig(env: {
[name: string]: string | undefined;
}): AppConfig[] {
let data: JsonObject | undefined = undefined;
+1 -1
View File
@@ -16,5 +16,5 @@
export { resolveStaticConfig } from './resolver';
export { readConfigFile } from './reader';
export { readEnv } from './env';
export { readEnvConfig } from './env';
export { readSecret } from './secrets';
@@ -30,6 +30,14 @@ jest.mock('fs-extra', () => {
sessionKey: development-key
`,
'/root/secrets/session-key.txt': 'abc123',
'/secret-port/app-config.yaml': `
backend:
listen:
port:
$secret:
file: secrets/port.txt
`,
'/secret-port/secrets/port.txt': '12345',
};
return {
+2 -2
View File
@@ -20,7 +20,7 @@ import { AppConfig, JsonObject } from '@backstage/config';
import {
resolveStaticConfig,
readConfigFile,
readEnv,
readEnvConfig,
readSecret,
} from './lib';
@@ -102,7 +102,7 @@ export async function loadConfig(
);
}
configs.push(...readEnv(process.env));
configs.push(...readEnvConfig(process.env));
return configs;
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/config",
"description": "Config API used by Backstage core, backend, and CLI",
"version": "0.1.1-alpha.20",
"version": "0.1.1-alpha.21",
"private": false,
"publishConfig": {
"access": "public",
+14 -1
View File
@@ -88,7 +88,7 @@ function expectInvalidValues(config: ConfigReader) {
"Invalid type in config for key 'string' in 'ctx', got string, wanted boolean",
);
expect(() => config.getNumber('string')).toThrow(
"Invalid type in config for key 'string' in 'ctx', got string, wanted number",
"Unable to convert config value for key 'string' in 'ctx' to a number",
);
expect(() => config.getString('one')).toThrow(
"Invalid type in config for key 'one' in 'ctx', got number, wanted string",
@@ -580,4 +580,17 @@ describe('ConfigReader.get()', () => {
},
});
});
it('coerces number strings to numbers', () => {
const config = ConfigReader.fromConfigs([
{
data: {
port: '123',
},
context: '1',
},
]);
expect(config.getNumber('port')).toEqual(123);
});
});
+17 -2
View File
@@ -49,6 +49,9 @@ const errors = {
missing(key: string) {
return `Missing required config value at '${key}'`;
},
convert(key: string, context: string, expected: string) {
return `Unable to convert config value for key '${key}' in '${context}' to a ${expected}`;
},
};
export class ConfigReader implements Config {
@@ -183,10 +186,22 @@ export class ConfigReader implements Config {
}
getOptionalNumber(key: string): number | undefined {
return this.readConfigValue(
const value = this.readConfigValue<string | number>(
key,
value => typeof value === 'number' || { expected: 'number' },
val =>
typeof val === 'number' ||
typeof val === 'string' || { expected: 'number' },
);
if (typeof value === 'number' || value === undefined) {
return value;
}
const number = Number(value);
if (!Number.isFinite(number)) {
throw new Error(
errors.convert(this.fullKey(key), this.context, 'number'),
);
}
return number;
}
getBoolean(key: string): boolean {
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-api",
"description": "Internal Core API used by Backstage plugins and apps",
"version": "0.1.1-alpha.20",
"version": "0.1.1-alpha.21",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,8 +29,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.20",
"@backstage/theme": "^0.1.1-alpha.20",
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"@types/react": "^16.9",
@@ -41,8 +41,8 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.20",
"@backstage/test-utils-core": "^0.1.1-alpha.20",
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/test-utils-core": "^0.1.1-alpha.21",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+3 -8
View File
@@ -136,7 +136,7 @@ export class PrivateAppImpl implements BackstageApp {
return this.icons[key];
}
getRoutes(): ComponentType<{}> {
getRoutes(): JSX.Element[] {
const routes = new Array<JSX.Element>();
const registeredFeatureFlags = new Array<FeatureFlagsRegistryItem>();
@@ -191,14 +191,9 @@ export class PrivateAppImpl implements BackstageApp {
FeatureFlags.registeredFeatureFlags = registeredFeatureFlags;
}
const rendered = (
<Routes>
{routes}
<Route element={<NotFoundErrorPage />} />
</Routes>
);
routes.push(<Route element={<NotFoundErrorPage />} />);
return () => rendered;
return routes;
}
getProvider(): ComponentType<{}> {

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