Merge branch 'master' into new-release-31-aug-20
This commit is contained in:
@@ -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}}'
|
||||
|
||||
@@ -92,6 +92,7 @@ dist
|
||||
|
||||
# Microsite build output
|
||||
microsite/build
|
||||
microsite/i18n
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
@@ -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 can’t 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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -48,6 +48,8 @@ lighthouse:
|
||||
baseUrl: http://localhost:3003
|
||||
|
||||
catalog:
|
||||
rules:
|
||||
- allow: [Component, API, Group, Template, Location]
|
||||
processors:
|
||||
githubApi:
|
||||
privateToken:
|
||||
|
||||
@@ -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: []
|
||||
```
|
||||
@@ -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,6 +58,29 @@ 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.
|
||||
|
||||
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:
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -6,7 +6,9 @@ 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 `data/plugins` with your plugin's information. Example:
|
||||
create a file in
|
||||
[`microsite/data/plugins`](https://github.com/spotify/backstage/tree/master/microsite/data/plugins)
|
||||
with your plugin's information. Example:
|
||||
|
||||
```yaml
|
||||
---
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: CircleCI
|
||||
author: Spotify
|
||||
authorUrl: https://www.spotify.com/
|
||||
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'
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
title: GitHub Actions
|
||||
author: Spotify
|
||||
authorUrl: https://www.spotify.com/
|
||||
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
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
title: GraphiQL
|
||||
author: Spotify
|
||||
authorUrl: https://www.spotify.com/
|
||||
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
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: Lighthouse
|
||||
author: Spotify
|
||||
authorUrl: https://www.spotify.com/
|
||||
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
|
||||
@@ -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
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: Tech Radar
|
||||
author: Spotify
|
||||
authorUrl: https://www.spotify.com/
|
||||
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://img.icons8.com/officel/2x/radar.png
|
||||
npmPackageName: '@backstage/plugin-tech-radar'
|
||||
@@ -1,325 +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/add-to-marketplace": {
|
||||
"title": "Add to Marketplace"
|
||||
},
|
||||
"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",
|
||||
"Plugins": "Plugins",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -29,13 +29,14 @@ const Plugins = () => (
|
||||
<main className="MainContent">
|
||||
<div className="PluginPageLayout">
|
||||
<div className="PluginPageHeader">
|
||||
<h2>Plugins</h2>
|
||||
<h2>Plugin marketplace</h2>
|
||||
<p>Open source plugins that you can add to your Backstage deployment</p>
|
||||
<span>
|
||||
<a
|
||||
className="PluginAddNewButton ButtonFilled"
|
||||
href={addPluginDocsLink}
|
||||
>
|
||||
<b>Add Plugin</b>
|
||||
<b>Add to marketplace</b>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
@@ -69,13 +70,44 @@ const Plugins = () => (
|
||||
className="PluginCardLink ButtonFilled"
|
||||
href={documentation}
|
||||
>
|
||||
docs
|
||||
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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,111 +1,112 @@
|
||||
.PluginCard {
|
||||
background-color: #272822;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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;
|
||||
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);
|
||||
}
|
||||
.grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 815px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.PluginCard img {
|
||||
float: left;
|
||||
margin: 0px 16px 8px 0px;
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
float: left;
|
||||
margin: 0px 16px 8px 0px;
|
||||
height: 80px;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.PluginCardHeader {
|
||||
max-height: fit-content;
|
||||
min-height: fit-content;
|
||||
max-height: fit-content;
|
||||
min-height: fit-content;
|
||||
}
|
||||
|
||||
.PluginCardTitle {
|
||||
color: white;
|
||||
vertical-align: top;
|
||||
margin: 8px 0px 0px 16px;
|
||||
color: white;
|
||||
vertical-align: top;
|
||||
margin: 8px 0px 0px 16px;
|
||||
}
|
||||
|
||||
.PluginAddNewButton {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
right: 0px;
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.ButtonFilled {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
background-color: #36BAA2;
|
||||
color: white;
|
||||
margin-top: 36px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
background-color: #36baa2;
|
||||
color: white;
|
||||
margin-top: 36px;
|
||||
}
|
||||
|
||||
.ButtonFilled:hover {
|
||||
border: 1px solid #36BAA2;
|
||||
background-color: transparent;
|
||||
border: 1px solid #36baa2;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.ChipOutlined {
|
||||
font-size: small;
|
||||
border-radius: 16px;
|
||||
padding: 2px 8px;
|
||||
border: 1px solid #36BAA2;
|
||||
color: #36BAA2;
|
||||
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;
|
||||
padding: 2px 8px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.PluginPageLayout {
|
||||
margin: auto;
|
||||
max-width: 1430px;
|
||||
padding: 20px;
|
||||
margin: auto;
|
||||
max-width: 1430px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.PluginPageHeader {
|
||||
position: relative;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.PluginPageHeader h2 {
|
||||
display: inline-block;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.PluginCardBody {
|
||||
padding-top: 8px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.PluginCardFooter {
|
||||
position: relative;
|
||||
min-height: 2em;
|
||||
position: relative;
|
||||
min-height: 2em;
|
||||
}
|
||||
|
||||
.Author, .Author a {
|
||||
margin-bottom: 0.25em;
|
||||
color: rgba(255,255,255, 0.6);
|
||||
.Author,
|
||||
.Author a {
|
||||
margin-bottom: 0.25em;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.Author a:hover {
|
||||
color: white;
|
||||
.Author a:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "example-app",
|
||||
"version": "0.1.1-alpha.21",
|
||||
"private": true,
|
||||
"bundled": true,
|
||||
"dependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.21",
|
||||
"@backstage/core": "^0.1.1-alpha.21",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -46,6 +46,6 @@
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
createDatabase,
|
||||
createDatabaseClient,
|
||||
createServiceBuilder,
|
||||
loadBackendConfig,
|
||||
getRootLogger,
|
||||
@@ -48,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 };
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"@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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -25,7 +25,7 @@ export async function serveBackend(
|
||||
},
|
||||
) {
|
||||
const paths = resolveBundlingPaths(options);
|
||||
const config = createBackendConfig(paths, {
|
||||
const config = await createBackendConfig(paths, {
|
||||
...options,
|
||||
isDev: true,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ const useStyles = makeStyles<BackstageTheme, { backgroundImage: string }>(
|
||||
alignItems: 'center',
|
||||
backgroundImage: props => props.backgroundImage,
|
||||
backgroundPosition: 'center',
|
||||
backgroundSize: '100% 400px',
|
||||
backgroundSize: 'cover',
|
||||
},
|
||||
leftItemsBox: {
|
||||
flex: '1 1 auto',
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
import {
|
||||
Header,
|
||||
Page,
|
||||
HeaderLabel,
|
||||
ContentHeader,
|
||||
Content,
|
||||
pageTheme,
|
||||
} from '../';
|
||||
import { SupportButton, Table, StatusOK, TableColumn } from '../../components';
|
||||
import { Box, Typography, Link, Chip, Button } from '@material-ui/core';
|
||||
|
||||
export default {
|
||||
title: 'Example Plugin',
|
||||
component: Page,
|
||||
};
|
||||
|
||||
interface TableData {
|
||||
id: number;
|
||||
branch: string;
|
||||
hash: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
const generateTestData = (rows = 10) => {
|
||||
const data: Array<TableData> = [];
|
||||
while (data.length <= rows) {
|
||||
data.push({
|
||||
id: data.length + 18534,
|
||||
branch: 'techdocs: modify documentation header',
|
||||
hash: 'techdocs/docs-header 5749c98e3f61f8bb116e5cb87b0e4e1 ',
|
||||
status: 'Success',
|
||||
});
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
{
|
||||
title: 'ID',
|
||||
field: 'id',
|
||||
highlight: true,
|
||||
type: 'numeric',
|
||||
width: '80px',
|
||||
},
|
||||
{
|
||||
title: 'Message/Source',
|
||||
highlight: true,
|
||||
render: (row: Partial<TableData>) => (
|
||||
<>
|
||||
<Link>{row.branch}</Link>
|
||||
<Typography variant="body2">{row.hash}</Typography>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
render: (row: Partial<TableData>) => (
|
||||
<Box display="flex" alignItems="center">
|
||||
<StatusOK />
|
||||
<Typography variant="body2">{row.status}</Typography>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
render: () => <Chip label="Tag Name" />,
|
||||
width: '10%',
|
||||
},
|
||||
];
|
||||
|
||||
export const PluginWithTable = () => {
|
||||
return (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header title="Example" subtitle="This an example plugin">
|
||||
<HeaderLabel label="Owner" value="Owner" />
|
||||
<HeaderLabel label="Lifecycle" value="Lifecycle" />
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentHeader title="Header">
|
||||
<Button color="primary" variant="contained">
|
||||
Settings
|
||||
</Button>
|
||||
<SupportButton>
|
||||
This Plugin is an example. This text could provide usefull
|
||||
information for the user.
|
||||
</SupportButton>
|
||||
</ContentHeader>
|
||||
<Table
|
||||
options={{ paging: true, padding: 'dense' }}
|
||||
data={generateTestData(10)}
|
||||
columns={columns}
|
||||
title="Example Content"
|
||||
/>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -44,6 +44,9 @@ proxy:
|
||||
techdocs:
|
||||
storageUrl: http://localhost:7000/techdocs/static/docs
|
||||
|
||||
lighthouse:
|
||||
baseUrl: http://localhost:3003
|
||||
|
||||
auth:
|
||||
providers: {}
|
||||
|
||||
@@ -69,11 +72,21 @@ catalog:
|
||||
# Backstage example templates
|
||||
- type: github
|
||||
target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml
|
||||
rules:
|
||||
- allow: [Template]
|
||||
- type: github
|
||||
target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/template.yaml
|
||||
rules:
|
||||
- allow: [Template]
|
||||
- type: github
|
||||
target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/create-react-app/template.yaml
|
||||
rules:
|
||||
- allow: [Template]
|
||||
- type: github
|
||||
target: https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml
|
||||
rules:
|
||||
- allow: [Template]
|
||||
- type: github
|
||||
target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml
|
||||
rules:
|
||||
- allow: [Template]
|
||||
|
||||
+1
-1
@@ -2,5 +2,5 @@
|
||||
"packages": ["packages/*", "plugins/*"],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "{{version}}"
|
||||
"version": "0.1.0"
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "app",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"bundled": true,
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
@@ -11,6 +12,11 @@
|
||||
"@backstage/plugin-register-component": "^{{version}}",
|
||||
"@backstage/plugin-scaffolder": "^{{version}}",
|
||||
"@backstage/plugin-techdocs": "^{{version}}",
|
||||
"@backstage/plugin-circleci": "^{{version}}",
|
||||
"@backstage/plugin-explore": "^{{version}}",
|
||||
"@backstage/plugin-lighthouse": "^{{version}}",
|
||||
"@backstage/plugin-tech-radar": "^{{version}}",
|
||||
"@backstage/plugin-github-actions": "^{{version}}",
|
||||
"@backstage/test-utils": "^{{version}}",
|
||||
"@backstage/theme": "^{{version}}",
|
||||
"history": "^5.0.0",
|
||||
|
||||
@@ -14,15 +14,30 @@ import {
|
||||
WebStorage,
|
||||
} from '@backstage/core';
|
||||
|
||||
import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog';
|
||||
import {
|
||||
lighthouseApiRef,
|
||||
LighthouseRestApi,
|
||||
} from '@backstage/plugin-lighthouse';
|
||||
|
||||
import { scaffolderApiRef, ScaffolderApi } from '@backstage/plugin-scaffolder';
|
||||
import {
|
||||
GithubActionsClient,
|
||||
githubActionsApiRef,
|
||||
} from '@backstage/plugin-github-actions';
|
||||
|
||||
import {
|
||||
techdocsStorageApiRef,
|
||||
TechDocsStorageApi,
|
||||
} from '@backstage/plugin-techdocs';
|
||||
|
||||
import { techRadarApiRef, TechRadar } from '@backstage/plugin-tech-radar';
|
||||
|
||||
import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog';
|
||||
import { CircleCIApi, circleCIApiRef } from '@backstage/plugin-circleci';
|
||||
|
||||
import { scaffolderApiRef, ScaffolderApi } from '@backstage/plugin-scaffolder';
|
||||
|
||||
|
||||
|
||||
export const apis = (config: ConfigApi) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Creating APIs for ${config.getString('app.title')}`);
|
||||
@@ -46,9 +61,25 @@ export const apis = (config: ConfigApi) => {
|
||||
builder.add(oauthRequestApiRef, new OAuthRequestManager());
|
||||
|
||||
builder.add(catalogApiRef, new CatalogClient({ discoveryApi }));
|
||||
builder.add(githubActionsApiRef, new GithubActionsClient());
|
||||
|
||||
builder.add(lighthouseApiRef, new LighthouseRestApi('http://localhost:3003'));
|
||||
|
||||
builder.add(
|
||||
circleCIApiRef,
|
||||
new CircleCIApi(`${backendUrl}/proxy/circleci/api`),
|
||||
);
|
||||
|
||||
builder.add(scaffolderApiRef, new ScaffolderApi({ discoveryApi }));
|
||||
|
||||
builder.add(
|
||||
techRadarApiRef,
|
||||
new TechRadar({
|
||||
width: 1500,
|
||||
height: 800,
|
||||
}),
|
||||
);
|
||||
|
||||
builder.add(
|
||||
techdocsStorageApiRef,
|
||||
new TechDocsStorageApi({ apiOrigin: techdocsStorageUrl }),
|
||||
|
||||
@@ -2,3 +2,8 @@ export { plugin as CatalogPlugin } from '@backstage/plugin-catalog';
|
||||
export { plugin as RegisterComponent } from '@backstage/plugin-register-component';
|
||||
export { plugin as ScaffolderPlugin } from '@backstage/plugin-scaffolder';
|
||||
export { plugin as TechDocsPlugin } from '@backstage/plugin-techdocs';
|
||||
export { plugin as Explore } from '@backstage/plugin-explore';
|
||||
export { plugin as Circleci } from '@backstage/plugin-circleci';
|
||||
export { plugin as LighthousePlugin } from '@backstage/plugin-lighthouse';
|
||||
export { plugin as TechRadar } from '@backstage/plugin-tech-radar';
|
||||
export { plugin as GithubActions } from '@backstage/plugin-github-actions';
|
||||
|
||||
@@ -2,6 +2,11 @@ import React from 'react';
|
||||
import HomeIcon from '@material-ui/icons/Home';
|
||||
import LibraryBooks from '@material-ui/icons/LibraryBooks';
|
||||
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
|
||||
import ExploreIcon from '@material-ui/icons/Explore';
|
||||
import BuildIcon from '@material-ui/icons/BuildRounded';
|
||||
import RuleIcon from '@material-ui/icons/AssignmentTurnedIn';
|
||||
import MapIcon from '@material-ui/icons/MyLocation';
|
||||
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarItem,
|
||||
@@ -18,8 +23,13 @@ export const AppSidebar = () => (
|
||||
<SidebarDivider />
|
||||
{/* Global nav, not org-specific */}
|
||||
<SidebarItem icon={HomeIcon} to="./" text="Home" />
|
||||
<SidebarItem icon={ExploreIcon} to="explore" text="Explore" />
|
||||
<SidebarItem icon={LibraryBooks} to="/docs" text="Docs" />
|
||||
<SidebarItem icon={CreateComponentIcon} to="create" text="Create..." />
|
||||
<SidebarDivider />
|
||||
<SidebarItem icon={MapIcon} to="tech-radar" text="Tech Radar" />
|
||||
<SidebarItem icon={RuleIcon} to="lighthouse" text="Lighthouse" />
|
||||
<SidebarItem icon={BuildIcon} to="circleci" text="CircleCI" />
|
||||
{/* End global nav */}
|
||||
<SidebarDivider />
|
||||
<SidebarSpace />
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
"@backstage/plugin-proxy-backend": "^{{version}}",
|
||||
"@backstage/plugin-rollbar-backend": "^{{version}}",
|
||||
"@backstage/plugin-scaffolder-backend": "^{{version}}",
|
||||
"@backstage/plugin-sentry-backend": "^{{version}}",
|
||||
"@backstage/plugin-techdocs-backend": "^{{version}}",
|
||||
"@octokit/rest": "^18.0.0",
|
||||
"dockerode": "^3.2.0",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
createDatabase,
|
||||
createDatabaseClient,
|
||||
createServiceBuilder,
|
||||
loadBackendConfig,
|
||||
getRootLogger,
|
||||
@@ -27,11 +27,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 };
|
||||
};
|
||||
}
|
||||
|
||||
@@ -199,8 +199,17 @@ class DevAppBuilder {
|
||||
|
||||
for (const plugin of plugins) {
|
||||
for (const output of plugin.output()) {
|
||||
if (output.type === 'legacy-route') {
|
||||
paths.push(output.path);
|
||||
switch (output.type) {
|
||||
case 'legacy-route': {
|
||||
paths.push(output.path);
|
||||
break;
|
||||
}
|
||||
case 'route': {
|
||||
paths.push(output.target.path);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
This is the Docker container that powers the creation of static documentation sites that are supported by [TechDocs](https://github.com/spotify/backstage/blob/master/plugins/techdocs).
|
||||
|
||||
**WIP: This is a work in progress. It is not ready for use yet. Follow our progress on [the Backstage Discord](https://discord.gg/MUpMjP2) under #docs-like-code or on [our GitHub Milestone](https://github.com/spotify/backstage/milestone/15).**
|
||||
|
||||
## Getting Started
|
||||
|
||||
Using the TechDocs CLI, we can invoke the latest version of `techdocs-container` via Docker Hub:
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { resolve as resolvePath, dirname } from 'path';
|
||||
import { notFoundHandler } from '@backstage/backend-common';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { notFoundHandler, resolvePackagePath } from '@backstage/backend-common';
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { Logger } from 'winston';
|
||||
@@ -23,19 +23,14 @@ import { injectEnvConfig } from '../lib/config';
|
||||
|
||||
export interface RouterOptions {
|
||||
logger: Logger;
|
||||
appPackageName?: string;
|
||||
appPackageName: string;
|
||||
staticFallbackHandler?: express.Handler;
|
||||
}
|
||||
|
||||
export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
const appDistDir = resolvePath(
|
||||
dirname(
|
||||
__non_webpack_require__.resolve(`${options.appPackageName}/package.json`),
|
||||
),
|
||||
'dist',
|
||||
);
|
||||
const appDistDir = resolvePackagePath(options.appPackageName, 'dist');
|
||||
options.logger.info(`Serving static app content from ${appDistDir}`);
|
||||
|
||||
await injectEnvConfig({
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
"@backstage/backend-common": "^0.1.1-alpha.21",
|
||||
"@backstage/config": "^0.1.1-alpha.21",
|
||||
"@types/express": "^4.17.6",
|
||||
"body-parser": "^1.19.0",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"cors": "^2.8.5",
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
*/
|
||||
|
||||
import Knex from 'knex';
|
||||
import path from 'path';
|
||||
import { utc } from 'moment';
|
||||
import { resolvePackagePath } from '@backstage/backend-common';
|
||||
import { AnyJWK, KeyStore, StoredKey } from './types';
|
||||
|
||||
const migrationsDir = path.resolve(
|
||||
require.resolve('@backstage/plugin-auth-backend/package.json'),
|
||||
'../migrations',
|
||||
const migrationsDir = resolvePackagePath(
|
||||
'@backstage/plugin-auth-backend',
|
||||
'migrations',
|
||||
);
|
||||
|
||||
const TABLE = 'signing_keys';
|
||||
|
||||
@@ -18,8 +18,6 @@ import express from 'express';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../identity';
|
||||
import { Config } from '@backstage/config';
|
||||
import { OAuthProvider } from '../lib/OAuthProvider';
|
||||
import { SamlAuthProvider } from './saml/provider';
|
||||
|
||||
export type OAuthProviderOptions = {
|
||||
/**
|
||||
@@ -174,7 +172,7 @@ export type AuthProviderFactory = (
|
||||
envConfig: Config,
|
||||
logger: Logger,
|
||||
issuer: TokenIssuer,
|
||||
) => OAuthProvider | SamlAuthProvider | undefined;
|
||||
) => AuthProviderRouteHandlers | undefined;
|
||||
|
||||
export type AuthResponse<ProviderInfo> = {
|
||||
providerInfo: ProviderInfo;
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import bodyParser from 'body-parser';
|
||||
import Knex from 'knex';
|
||||
import { Logger } from 'winston';
|
||||
import { createAuthProviderRouter } from '../providers';
|
||||
@@ -53,8 +52,8 @@ export async function createRouter(
|
||||
});
|
||||
|
||||
router.use(cookieParser());
|
||||
router.use(bodyParser.urlencoded({ extended: false }));
|
||||
router.use(bodyParser.json());
|
||||
router.use(express.urlencoded({ extended: false }));
|
||||
router.use(express.json());
|
||||
|
||||
const providersConfig = options.config.getConfig('auth.providers');
|
||||
const providers = providersConfig.keys();
|
||||
|
||||
@@ -14,17 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { getVoidLogger, resolvePackagePath } from '@backstage/backend-common';
|
||||
import { makeValidator } from '@backstage/catalog-model';
|
||||
import Knex from 'knex';
|
||||
import path from 'path';
|
||||
import { Logger } from 'winston';
|
||||
import { CommonDatabase } from './CommonDatabase';
|
||||
import { Database } from './types';
|
||||
|
||||
const migrationsDir = path.resolve(
|
||||
require.resolve('@backstage/plugin-catalog-backend/package.json'),
|
||||
'../migrations',
|
||||
const migrationsDir = resolvePackagePath(
|
||||
'@backstage/plugin-catalog-backend',
|
||||
'migrations',
|
||||
);
|
||||
|
||||
export type CreateDatabaseOptions = {
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* 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 { LocationSpec, Entity } from '@backstage/catalog-model';
|
||||
import { CatalogRulesEnforcer } from './CatalogRules';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
|
||||
const entity = {
|
||||
user: {
|
||||
kind: 'User',
|
||||
} as Entity,
|
||||
group: {
|
||||
kind: 'Group',
|
||||
} as Entity,
|
||||
component: {
|
||||
kind: 'component',
|
||||
} as Entity,
|
||||
location: {
|
||||
kind: 'Location',
|
||||
} as Entity,
|
||||
};
|
||||
|
||||
const location: Record<string, LocationSpec> = {
|
||||
x: {
|
||||
type: 'github',
|
||||
target: 'https://github.com/a/b/blob/master/x.yaml',
|
||||
},
|
||||
y: {
|
||||
type: 'github',
|
||||
target: 'https://github.com/a/b/blob/master/y.yaml',
|
||||
},
|
||||
z: {
|
||||
type: 'file',
|
||||
target: '/root/z.yaml',
|
||||
},
|
||||
};
|
||||
|
||||
describe('CatalogRulesEnforcer', () => {
|
||||
it('should deny by default', () => {
|
||||
const enforcer = new CatalogRulesEnforcer([]);
|
||||
expect(enforcer.isAllowed(entity.user, location.x)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.group, location.y)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.component, location.z)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.location, location.z)).toBe(false);
|
||||
});
|
||||
|
||||
it('should deny all', () => {
|
||||
const enforcer = new CatalogRulesEnforcer([{ allow: [] }]);
|
||||
expect(enforcer.isAllowed(entity.user, location.x)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.group, location.y)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.component, location.z)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.location, location.z)).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow all', () => {
|
||||
const enforcer = new CatalogRulesEnforcer([
|
||||
{
|
||||
allow: ['User', 'Group', 'Component', 'Location'].map(kind => ({
|
||||
kind,
|
||||
})),
|
||||
},
|
||||
]);
|
||||
expect(enforcer.isAllowed(entity.user, location.x)).toBe(true);
|
||||
expect(enforcer.isAllowed(entity.group, location.y)).toBe(true);
|
||||
expect(enforcer.isAllowed(entity.component, location.z)).toBe(true);
|
||||
expect(enforcer.isAllowed(entity.location, location.z)).toBe(true);
|
||||
});
|
||||
|
||||
it('should deny groups', () => {
|
||||
const enforcer = new CatalogRulesEnforcer([
|
||||
{ allow: [{ kind: 'User' }, { kind: 'Component' }] },
|
||||
]);
|
||||
expect(enforcer.isAllowed(entity.user, location.x)).toBe(true);
|
||||
expect(enforcer.isAllowed(entity.group, location.x)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.group, location.y)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.group, location.z)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.component, location.z)).toBe(true);
|
||||
});
|
||||
|
||||
it('should deny groups from github', () => {
|
||||
const enforcer = new CatalogRulesEnforcer([
|
||||
{ allow: [{ kind: 'User' }, { kind: 'Component' }] },
|
||||
{ allow: [{ kind: 'Group' }], locations: [{ type: 'file' }] },
|
||||
]);
|
||||
expect(enforcer.isAllowed(entity.user, location.x)).toBe(true);
|
||||
expect(enforcer.isAllowed(entity.group, location.x)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.group, location.y)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.group, location.z)).toBe(true);
|
||||
expect(enforcer.isAllowed(entity.component, location.z)).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow groups from files', () => {
|
||||
const enforcer = new CatalogRulesEnforcer([
|
||||
{ allow: [{ kind: 'Group' }], locations: [{ type: 'file' }] },
|
||||
]);
|
||||
expect(enforcer.isAllowed(entity.user, location.x)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.group, location.x)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.group, location.y)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.group, location.z)).toBe(true);
|
||||
expect(enforcer.isAllowed(entity.component, location.z)).toBe(false);
|
||||
});
|
||||
|
||||
it('should not be sensitive to kind case', () => {
|
||||
const enforcer = new CatalogRulesEnforcer([
|
||||
{ allow: [{ kind: 'group' }] },
|
||||
{ allow: [{ kind: 'Component' }] },
|
||||
]);
|
||||
expect(enforcer.isAllowed(entity.user, location.x)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.group, location.x)).toBe(true);
|
||||
expect(enforcer.isAllowed(entity.group, location.y)).toBe(true);
|
||||
expect(enforcer.isAllowed(entity.group, location.z)).toBe(true);
|
||||
expect(enforcer.isAllowed(entity.component, location.z)).toBe(true);
|
||||
});
|
||||
|
||||
describe('fromConfig', () => {
|
||||
it('should allow components by default', () => {
|
||||
const enforcer = CatalogRulesEnforcer.fromConfig(new ConfigReader({}));
|
||||
expect(enforcer.isAllowed(entity.user, location.x)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.group, location.y)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.component, location.z)).toBe(true);
|
||||
expect(enforcer.isAllowed(entity.location, location.z)).toBe(true);
|
||||
});
|
||||
|
||||
it('should deny all', () => {
|
||||
const enforcer = CatalogRulesEnforcer.fromConfig(
|
||||
new ConfigReader({ catalog: { rules: [] } }),
|
||||
);
|
||||
expect(enforcer.isAllowed(entity.user, location.x)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.group, location.y)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.component, location.z)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.location, location.z)).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow all', () => {
|
||||
const enforcer = CatalogRulesEnforcer.fromConfig(
|
||||
new ConfigReader({
|
||||
catalog: {
|
||||
rules: [{ allow: ['User', 'Group'] }, { allow: ['Component'] }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(enforcer.isAllowed(entity.user, location.x)).toBe(true);
|
||||
expect(enforcer.isAllowed(entity.group, location.y)).toBe(true);
|
||||
expect(enforcer.isAllowed(entity.component, location.z)).toBe(true);
|
||||
});
|
||||
|
||||
it('should deny groups', () => {
|
||||
const enforcer = CatalogRulesEnforcer.fromConfig(
|
||||
new ConfigReader({
|
||||
catalog: { rules: [{ allow: ['User'] }, { allow: ['Component'] }] },
|
||||
}),
|
||||
);
|
||||
expect(enforcer.isAllowed(entity.user, location.x)).toBe(true);
|
||||
expect(enforcer.isAllowed(entity.group, location.x)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.group, location.y)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.group, location.z)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.component, location.z)).toBe(true);
|
||||
expect(enforcer.isAllowed(entity.location, location.z)).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow groups from a specific github location', () => {
|
||||
const enforcer = CatalogRulesEnforcer.fromConfig(
|
||||
new ConfigReader({
|
||||
catalog: {
|
||||
rules: [{ allow: ['user'] }],
|
||||
locations: [
|
||||
{
|
||||
type: 'github',
|
||||
target: 'https://github.com/a/b/blob/master/x.yaml',
|
||||
rules: [
|
||||
{
|
||||
allow: ['Group'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(enforcer.isAllowed(entity.user, location.x)).toBe(true);
|
||||
expect(enforcer.isAllowed(entity.group, location.x)).toBe(true);
|
||||
expect(enforcer.isAllowed(entity.group, location.y)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.group, location.z)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.component, location.z)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.location, location.z)).toBe(false);
|
||||
});
|
||||
|
||||
it('should not care about location configuration in catalog.rules', () => {
|
||||
const enforcer = CatalogRulesEnforcer.fromConfig(
|
||||
new ConfigReader({
|
||||
catalog: {
|
||||
rules: [{ allow: ['Group'], locations: [{ type: 'github' }] }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(enforcer.isAllowed(entity.user, location.x)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.group, location.x)).toBe(true);
|
||||
expect(enforcer.isAllowed(entity.group, location.y)).toBe(true);
|
||||
expect(enforcer.isAllowed(entity.group, location.z)).toBe(true);
|
||||
expect(enforcer.isAllowed(entity.component, location.z)).toBe(false);
|
||||
expect(enforcer.isAllowed(entity.location, location.z)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* 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 { Config } from '@backstage/config';
|
||||
import { LocationSpec, Entity } from '@backstage/catalog-model';
|
||||
|
||||
/**
|
||||
* A structure for matching entities to a given rule.
|
||||
*/
|
||||
type EntityMatcher = {
|
||||
kind: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A structure for matching locations to a given rule.
|
||||
*/
|
||||
type LocationMatcher = {
|
||||
target?: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Rules to apply to catalog entities
|
||||
*
|
||||
* An undefined list of matchers means match all, an empty list of matchers means match none
|
||||
*/
|
||||
type CatalogRule = {
|
||||
allow: EntityMatcher[];
|
||||
locations?: LocationMatcher[];
|
||||
};
|
||||
|
||||
export class CatalogRulesEnforcer {
|
||||
/**
|
||||
* Default rules used by the catalog.
|
||||
*
|
||||
* Denies any location from specifying user or group entities.
|
||||
*/
|
||||
static readonly defaultRules: CatalogRule[] = [
|
||||
{
|
||||
allow: ['Component', 'API', 'Location'].map(kind => ({ kind })),
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Loads catalog rules from config.
|
||||
*
|
||||
* This reads `catalog.rules` and defaults to the default rules if no value is present.
|
||||
* The value of the config should be a list of config objects, each with a single `allow`
|
||||
* field which in turn is a list of entity kinds to allow.
|
||||
*
|
||||
* If there is no matching rule to allow an ingested entity, it will be rejected by the catalog.
|
||||
*
|
||||
* It also reads in rules from `catalog.locations`, where each location can have a list
|
||||
* of rules for that specific location, specified in a `rules` field.
|
||||
*
|
||||
* For example:
|
||||
*
|
||||
* ```yaml
|
||||
* catalog:
|
||||
* rules:
|
||||
* - allow: [Component, API]
|
||||
*
|
||||
* locations:
|
||||
* - type: github
|
||||
* target: https://github.com/org/repo/blob/master/users.yaml
|
||||
* rules:
|
||||
* - allow: [User, Group]
|
||||
* - type: github
|
||||
* target: https://github.com/org/repo/blob/master/systems.yaml
|
||||
* rules:
|
||||
* - allow: [System]
|
||||
* ```
|
||||
*/
|
||||
static fromConfig(config: Config) {
|
||||
const rules = new Array<CatalogRule>();
|
||||
|
||||
if (config.has('catalog.rules')) {
|
||||
const globalRules = config.getConfigArray('catalog.rules').map(sub => ({
|
||||
allow: sub.getStringArray('allow').map(kind => ({ kind })),
|
||||
}));
|
||||
rules.push(...globalRules);
|
||||
} else {
|
||||
rules.push(...CatalogRulesEnforcer.defaultRules);
|
||||
}
|
||||
|
||||
if (config.has('catalog.locations')) {
|
||||
const locationRules = config
|
||||
.getConfigArray('catalog.locations')
|
||||
.flatMap(locConf => {
|
||||
if (!locConf.has('rules')) {
|
||||
return [];
|
||||
}
|
||||
const type = locConf.getString('type');
|
||||
const target = locConf.getString('target');
|
||||
|
||||
return locConf.getConfigArray('rules').map(ruleConf => ({
|
||||
allow: ruleConf.getStringArray('allow').map(kind => ({ kind })),
|
||||
locations: [{ type, target }],
|
||||
}));
|
||||
});
|
||||
|
||||
rules.push(...locationRules);
|
||||
}
|
||||
|
||||
return new CatalogRulesEnforcer(rules);
|
||||
}
|
||||
|
||||
constructor(private readonly rules: CatalogRule[]) {}
|
||||
|
||||
/**
|
||||
* Checks wether a specific entity/location combination is allowed
|
||||
* according to the configured rules.
|
||||
*/
|
||||
isAllowed(entity: Entity, location: LocationSpec) {
|
||||
for (const rule of this.rules) {
|
||||
if (!this.matchLocation(location, rule.locations)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.matchEntity(entity, rule.allow)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private matchLocation(
|
||||
location: LocationSpec,
|
||||
matchers?: LocationMatcher[],
|
||||
): boolean {
|
||||
if (!matchers) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const matcher of matchers) {
|
||||
if (matcher.type !== location.type) {
|
||||
continue;
|
||||
}
|
||||
if (matcher.target && matcher.target !== location.target) {
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private matchEntity(entity: Entity, matchers?: EntityMatcher[]): boolean {
|
||||
if (!matchers) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const matcher of matchers) {
|
||||
if (entity.kind.toLowerCase() !== matcher.kind.toLowerCase()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
} from './processors/types';
|
||||
import { YamlProcessor } from './processors/YamlProcessor';
|
||||
import { LocationReader, ReadLocationResult } from './types';
|
||||
import { CatalogRulesEnforcer } from './CatalogRules';
|
||||
|
||||
// The max amount of nesting depth of generated work items
|
||||
const MAX_DEPTH = 10;
|
||||
@@ -63,6 +64,7 @@ type Options = {
|
||||
export class LocationReaders implements LocationReader {
|
||||
private readonly logger: Logger;
|
||||
private readonly processors: LocationProcessor[];
|
||||
private readonly rulesEnforcer: CatalogRulesEnforcer;
|
||||
|
||||
static defaultProcessors(options: {
|
||||
config?: Config;
|
||||
@@ -96,6 +98,9 @@ export class LocationReaders implements LocationReader {
|
||||
}: Options) {
|
||||
this.logger = logger;
|
||||
this.processors = processors;
|
||||
this.rulesEnforcer = config
|
||||
? CatalogRulesEnforcer.fromConfig(config)
|
||||
: new CatalogRulesEnforcer(CatalogRulesEnforcer.defaultRules);
|
||||
}
|
||||
|
||||
async read(location: LocationSpec): Promise<ReadLocationResult> {
|
||||
@@ -112,11 +117,20 @@ export class LocationReaders implements LocationReader {
|
||||
} else if (item.type === 'data') {
|
||||
await this.handleData(item, emit);
|
||||
} else if (item.type === 'entity') {
|
||||
const entity = await this.handleEntity(item, emit);
|
||||
output.entities.push({
|
||||
entity,
|
||||
location: item.location,
|
||||
});
|
||||
if (this.rulesEnforcer.isAllowed(item.entity, item.location)) {
|
||||
const entity = await this.handleEntity(item, emit);
|
||||
output.entities.push({
|
||||
entity,
|
||||
location: item.location,
|
||||
});
|
||||
} else {
|
||||
output.errors.push({
|
||||
location: item.location,
|
||||
error: new Error(
|
||||
`Entity of kind ${item.entity.kind} is not allowed from location ${item.location.target}:${item.location.type}`,
|
||||
),
|
||||
});
|
||||
}
|
||||
} else if (item.type === 'error') {
|
||||
await this.handleError(item, emit);
|
||||
output.errors.push({
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"@backstage/plugin-github-actions": "^0.1.1-alpha.21",
|
||||
"@backstage/plugin-jenkins": "^0.1.1-alpha.21",
|
||||
"@backstage/plugin-scaffolder": "^0.1.1-alpha.21",
|
||||
"@backstage/plugin-sentry": "^0.1.1-alpha.21",
|
||||
"@backstage/plugin-techdocs": "^0.1.1-alpha.21",
|
||||
"@backstage/theme": "^0.1.1-alpha.21",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { AboutCard } from './AboutCard';
|
||||
|
||||
describe('<AboutCard />', () => {
|
||||
it('renders info and "view source" link', () => {
|
||||
const entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'software',
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'github:https://github.com/spotify/backstage/blob/master/software.yaml',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
owner: 'guest',
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
},
|
||||
};
|
||||
const { getByText } = render(<AboutCard entity={entity} />);
|
||||
expect(getByText('service')).toBeInTheDocument();
|
||||
expect(getByText('View Source').closest('a')).toHaveAttribute(
|
||||
'href',
|
||||
'https://github.com/spotify/backstage/blob/master/software.yaml',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
import {
|
||||
Grid,
|
||||
Typography,
|
||||
makeStyles,
|
||||
Chip,
|
||||
IconButton,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Divider,
|
||||
} from '@material-ui/core';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
import GitHubIcon from '@material-ui/icons/GitHub';
|
||||
import { IconLinkVertical } from './IconLinkVertical';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import DocsIcon from '@material-ui/icons/Description';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
links: {
|
||||
margin: theme.spacing(2, 0),
|
||||
display: 'grid',
|
||||
gridAutoFlow: 'column',
|
||||
gridAutoColumns: 'min-content',
|
||||
gridGap: theme.spacing(3),
|
||||
},
|
||||
label: {
|
||||
color: theme.palette.text.secondary,
|
||||
textTransform: 'uppercase',
|
||||
fontSize: '10px',
|
||||
fontWeight: 'bold',
|
||||
letterSpacing: 0.5,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
value: {
|
||||
fontWeight: 'bold',
|
||||
overflow: 'hidden',
|
||||
lineHeight: '24px',
|
||||
wordBreak: 'break-word',
|
||||
},
|
||||
description: {
|
||||
wordBreak: 'break-word',
|
||||
},
|
||||
}));
|
||||
|
||||
const iconMap: Record<string, React.ReactNode> = {
|
||||
github: <GitHubIcon />,
|
||||
};
|
||||
|
||||
type CodeLinkInfo = { icon?: React.ReactNode; href?: string };
|
||||
|
||||
function getCodeLinkInfo(entity: Entity): CodeLinkInfo {
|
||||
const location =
|
||||
entity?.metadata?.annotations?.['backstage.io/managed-by-location'];
|
||||
|
||||
if (location) {
|
||||
// split by first `:`
|
||||
// e.g. "github:https://github.com/spotify/backstage/blob/master/software.yaml"
|
||||
const [type, target] = location.split(/:(.+)/);
|
||||
|
||||
return { icon: iconMap[type], href: target };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
type AboutCardProps = {
|
||||
entity: Entity;
|
||||
};
|
||||
|
||||
export function AboutCard({ entity }: AboutCardProps) {
|
||||
const classes = useStyles();
|
||||
const codeLink = getCodeLinkInfo(entity);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="About"
|
||||
action={
|
||||
<IconButton href={codeLink.href || '#'} aria-label="Edit">
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
}
|
||||
subheader={
|
||||
<nav className={classes.links}>
|
||||
<IconLinkVertical label="View Source" {...codeLink} />
|
||||
<IconLinkVertical
|
||||
label="View Techdocs"
|
||||
icon={<DocsIcon />}
|
||||
href={`/docs/${''}`}
|
||||
/>
|
||||
</nav>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
<CardContent>
|
||||
<Grid container>
|
||||
<AboutField label="Description" gridSizes={{ xs: 12 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
paragraph
|
||||
className={classes.description}
|
||||
>
|
||||
{entity?.metadata?.description || 'No description'}
|
||||
</Typography>
|
||||
</AboutField>
|
||||
<AboutField
|
||||
label="Owner"
|
||||
value={entity?.spec?.owner as string}
|
||||
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
|
||||
/>
|
||||
<AboutField
|
||||
label="Type"
|
||||
value={entity?.spec?.type as string}
|
||||
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
|
||||
/>
|
||||
<AboutField
|
||||
label="Lifecycle"
|
||||
value={entity?.spec?.lifecycle as string}
|
||||
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
|
||||
/>
|
||||
<AboutField
|
||||
label="Tags"
|
||||
value="No Tags"
|
||||
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
|
||||
>
|
||||
{(entity?.metadata?.tags || []).map(t => (
|
||||
<Chip key={t} size="small" label={t} />
|
||||
))}
|
||||
</AboutField>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function AboutField({
|
||||
label,
|
||||
value,
|
||||
gridSizes,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
value?: string;
|
||||
gridSizes?: Record<string, number>;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
|
||||
// Content is either children or a string prop `value`
|
||||
const content = React.Children.count(children) ? (
|
||||
children
|
||||
) : (
|
||||
<Typography variant="body2" className={classes.value}>
|
||||
{value || `unknown`}
|
||||
</Typography>
|
||||
);
|
||||
return (
|
||||
<Grid item {...gridSizes}>
|
||||
<Typography variant="subtitle2" className={classes.label}>
|
||||
{label}
|
||||
</Typography>
|
||||
{content}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
import { makeStyles, Link } from '@material-ui/core';
|
||||
import LinkIcon from '@material-ui/icons/Link';
|
||||
|
||||
export type IconLinkVerticalProps = {
|
||||
icon?: React.ReactNode;
|
||||
href?: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const useIconStyles = makeStyles({
|
||||
link: {
|
||||
display: 'grid',
|
||||
justifyItems: 'center',
|
||||
gridGap: 4,
|
||||
textAlign: 'center',
|
||||
},
|
||||
label: {
|
||||
fontSize: '0.7rem',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: 600,
|
||||
letterSpacing: 1.2,
|
||||
},
|
||||
});
|
||||
|
||||
export function IconLinkVertical({
|
||||
icon = <LinkIcon />,
|
||||
href = '#',
|
||||
...props
|
||||
}: IconLinkVerticalProps) {
|
||||
const classes = useIconStyles();
|
||||
return (
|
||||
<Link className={classes.link} href={href} {...props}>
|
||||
{icon}
|
||||
<span className={classes.label}>{props.label}</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
+1
-13
@@ -14,16 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { InfoCard, StructuredMetadataTable } from '@backstage/core';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
type Props = {
|
||||
entity: Entity;
|
||||
};
|
||||
|
||||
export const EntityMetadataCard: FC<Props> = ({ entity }) => (
|
||||
<InfoCard title="Information">
|
||||
<StructuredMetadataTable metadata={entity.metadata} />
|
||||
</InfoCard>
|
||||
);
|
||||
export { IconLinkVertical } from './IconLinkVertical';
|
||||
+1
-16
@@ -14,19 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { EntityMetadataCard } from './EntityMetadataCard';
|
||||
|
||||
describe('EntityMetadataCard component', () => {
|
||||
it('should display entity name if provided', async () => {
|
||||
const testEntity: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: { name: 'test' },
|
||||
};
|
||||
const rendered = await render(<EntityMetadataCard entity={testEntity} />);
|
||||
expect(await rendered.findByText('test')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
export { AboutCard } from './AboutCard';
|
||||
@@ -75,12 +75,7 @@ const columns: TableColumn<Entity>[] = [
|
||||
<>
|
||||
{entity.metadata.tags &&
|
||||
entity.metadata.tags.map(t => (
|
||||
<Chip
|
||||
key={t}
|
||||
label={t}
|
||||
color="secondary"
|
||||
style={{ marginBottom: '0px' }}
|
||||
/>
|
||||
<Chip key={t} label={t} style={{ marginBottom: '0px' }} />
|
||||
))}
|
||||
</>
|
||||
),
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Content } from '@backstage/core';
|
||||
import { SentryIssuesWidget } from '@backstage/plugin-sentry';
|
||||
import { Widget as GithubActionsWidget } from '@backstage/plugin-github-actions';
|
||||
import {
|
||||
JenkinsBuildsWidget,
|
||||
@@ -24,14 +23,14 @@ import {
|
||||
} from '@backstage/plugin-jenkins';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import React, { FC } from 'react';
|
||||
import { EntityMetadataCard } from '../EntityMetadataCard/EntityMetadataCard';
|
||||
import { AboutCard } from '../AboutCard';
|
||||
|
||||
export const EntityPageOverview: FC<{ entity: Entity }> = ({ entity }) => {
|
||||
return (
|
||||
<Content>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item sm={4}>
|
||||
<EntityMetadataCard entity={entity} />
|
||||
<AboutCard entity={entity} />
|
||||
</Grid>
|
||||
{entity.metadata?.annotations?.[
|
||||
'backstage.io/jenkins-github-folder'
|
||||
@@ -52,12 +51,6 @@ export const EntityPageOverview: FC<{ entity: Entity }> = ({ entity }) => {
|
||||
<GithubActionsWidget entity={entity} branch="master" />
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item sm={8}>
|
||||
<SentryIssuesWidget
|
||||
sentryProjectId="sample-sentry-project-id"
|
||||
statsFor="24h"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
);
|
||||
|
||||
@@ -14,17 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { errorHandler } from '@backstage/backend-common';
|
||||
import { errorHandler, resolvePackagePath } from '@backstage/backend-common';
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { Logger } from 'winston';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { ApolloServer } from 'apollo-server-express';
|
||||
|
||||
const schemaPath = path.resolve(
|
||||
require.resolve('@backstage/plugin-graphql-backend/package.json'),
|
||||
'../schema.gql',
|
||||
const schemaPath = resolvePackagePath(
|
||||
'@backstage/plugin-graphql-backend',
|
||||
'schema.gql',
|
||||
);
|
||||
|
||||
export interface RouterOptions {
|
||||
@@ -39,8 +38,8 @@ export async function createRouter(
|
||||
const server = new ApolloServer({ typeDefs, logger: options.logger });
|
||||
const router = Router();
|
||||
|
||||
const apolloMiddlware = server.getMiddleware({ path: '/' });
|
||||
router.use(apolloMiddlware);
|
||||
const apolloMiddleware = server.getMiddleware({ path: '/' });
|
||||
router.use(apolloMiddleware);
|
||||
|
||||
router.get('/health', (_, response) => {
|
||||
response.send({ status: 'ok' });
|
||||
|
||||
@@ -27,13 +27,15 @@ export interface RouterOptions {
|
||||
|
||||
const makeRouter = (adapter: IdentityApi): express.Router => {
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
|
||||
router.get('/users/:user/groups', async (req, res) => {
|
||||
const user = req.params.user;
|
||||
const type = req.query.type?.toString() ?? '';
|
||||
|
||||
const response = await adapter.getUserGroups({ user, type });
|
||||
res.send(response);
|
||||
});
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ export interface RouterOptions {
|
||||
// given config.
|
||||
function buildMiddleware(
|
||||
pathPrefix: string,
|
||||
logger: Logger,
|
||||
route: string,
|
||||
config: string | ProxyConfig,
|
||||
): Proxy {
|
||||
@@ -54,6 +55,9 @@ function buildMiddleware(
|
||||
fullConfig.changeOrigin = true;
|
||||
}
|
||||
|
||||
// Attach the logger to the proxy config
|
||||
fullConfig.logProvider = () => logger;
|
||||
|
||||
return createProxyMiddleware(fullConfig);
|
||||
}
|
||||
|
||||
@@ -66,7 +70,12 @@ export async function createRouter(
|
||||
Object.entries(proxyConfig).forEach(([route, proxyRouteConfig]) => {
|
||||
router.use(
|
||||
route,
|
||||
buildMiddleware(options.pathPrefix, route, proxyRouteConfig),
|
||||
buildMiddleware(
|
||||
options.pathPrefix,
|
||||
options.logger,
|
||||
route,
|
||||
proxyRouteConfig,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
const router = Router();
|
||||
|
||||
const logger = options.logger.child({ plugin: 'rollbar' });
|
||||
const config = options.config.getConfig('rollbar');
|
||||
const accessToken = !options.rollbarApi
|
||||
|
||||
@@ -42,6 +42,7 @@ export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
|
||||
const {
|
||||
preparers,
|
||||
|
||||
@@ -64,7 +64,7 @@ export const ScaffolderPage: React.FC<{}> = () => {
|
||||
}, [error, errorApi]);
|
||||
|
||||
return (
|
||||
<Page theme={pageTheme.other}>
|
||||
<Page theme={pageTheme.home}>
|
||||
<Header
|
||||
pageTitleOverride="Create a new component"
|
||||
title={
|
||||
@@ -95,7 +95,7 @@ export const ScaffolderPage: React.FC<{}> = () => {
|
||||
<Typography variant="body2">
|
||||
Shoot! Looks like you don't have any templates. Check out the
|
||||
documentation{' '}
|
||||
<Link href="docs/backstage/features/software-templates/adding-templates">
|
||||
<Link href="https://backstage.io/docs/features/software-templates/adding-templates">
|
||||
here!
|
||||
</Link>
|
||||
</Typography>
|
||||
|
||||
@@ -136,7 +136,7 @@ export const TemplatePage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Page theme={pageTheme.other}>
|
||||
<Page theme={pageTheme.home}>
|
||||
<Header
|
||||
pageTitleOverride="Create a new component"
|
||||
title={
|
||||
|
||||
@@ -20,6 +20,8 @@ import { getSentryApiForwarder } from './sentry-api';
|
||||
|
||||
export async function createRouter(logger: Logger): Promise<express.Router> {
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
|
||||
const SENTRY_TOKEN = process.env.SENTRY_TOKEN;
|
||||
if (!SENTRY_TOKEN) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
|
||||
@@ -19,7 +19,6 @@ import express from 'express';
|
||||
import Knex from 'knex';
|
||||
import fetch from 'node-fetch';
|
||||
import { Config } from '@backstage/config';
|
||||
import path from 'path';
|
||||
import Docker from 'dockerode';
|
||||
import {
|
||||
GeneratorBuilder,
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
PublisherBase,
|
||||
LocalPublish,
|
||||
} from '../techdocs';
|
||||
import { resolvePackagePath } from '@backstage/backend-common';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
type RouterOptions = {
|
||||
@@ -39,6 +39,11 @@ type RouterOptions = {
|
||||
dockerClient: Docker;
|
||||
};
|
||||
|
||||
const staticDocsDir = resolvePackagePath(
|
||||
'@backstage/plugin-techdocs-backend',
|
||||
'static/docs',
|
||||
);
|
||||
|
||||
export async function createRouter({
|
||||
preparers,
|
||||
generators,
|
||||
@@ -102,10 +107,7 @@ export async function createRouter({
|
||||
});
|
||||
|
||||
if (publisher instanceof LocalPublish) {
|
||||
router.use(
|
||||
'/static/docs/',
|
||||
express.static(path.resolve(__dirname, `../../static/docs`)),
|
||||
);
|
||||
router.use('/static/docs/', express.static(staticDocsDir));
|
||||
router.use(
|
||||
'/static/docs/:kind/:namespace/:name',
|
||||
async (req, res, next) => {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Generators, TechdocsGenerator } from './';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
|
||||
const logger = getVoidLogger();
|
||||
|
||||
const mockEntity = {
|
||||
apiVersion: 'version',
|
||||
kind: 'TestKind',
|
||||
metadata: {
|
||||
name: 'testName',
|
||||
},
|
||||
};
|
||||
|
||||
describe('generators', () => {
|
||||
it('should return error if no generator is registered', async () => {
|
||||
const generators = new Generators();
|
||||
|
||||
expect(() => generators.get(mockEntity)).toThrowError(
|
||||
'No generator registered for entity: "techdocs"',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return correct registered generator', async () => {
|
||||
const generators = new Generators();
|
||||
const techdocs = new TechdocsGenerator(logger);
|
||||
|
||||
generators.register('techdocs', techdocs);
|
||||
|
||||
expect(generators.get(mockEntity)).toBe(techdocs);
|
||||
});
|
||||
});
|
||||
@@ -15,32 +15,29 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
GeneratorBase,
|
||||
SupportedGeneratorKey,
|
||||
GeneratorBuilder,
|
||||
} from './types';
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { getGeneratorKey } from './helpers';
|
||||
|
||||
export class Generators implements GeneratorBuilder {
|
||||
private generatorMap = new Map<SupportedGeneratorKey, GeneratorBase>();
|
||||
|
||||
register(templaterKey: SupportedGeneratorKey, templater: GeneratorBase) {
|
||||
this.generatorMap.set(templaterKey, templater);
|
||||
}
|
||||
|
||||
get(entity: Entity): GeneratorBase {
|
||||
const generatorKey = getGeneratorKey(entity);
|
||||
const generator = this.generatorMap.get(generatorKey);
|
||||
|
||||
if (!generator) {
|
||||
throw new Error(
|
||||
`No generator registered for entity: "${generatorKey}"`,
|
||||
);
|
||||
}
|
||||
|
||||
return generator;
|
||||
}
|
||||
GeneratorBase,
|
||||
SupportedGeneratorKey,
|
||||
GeneratorBuilder,
|
||||
} from './types';
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { getGeneratorKey } from './helpers';
|
||||
|
||||
export class Generators implements GeneratorBuilder {
|
||||
private generatorMap = new Map<SupportedGeneratorKey, GeneratorBase>();
|
||||
|
||||
register(generatorKey: SupportedGeneratorKey, generator: GeneratorBase) {
|
||||
this.generatorMap.set(generatorKey, generator);
|
||||
}
|
||||
|
||||
|
||||
get(entity: Entity): GeneratorBase {
|
||||
const generatorKey = getGeneratorKey(entity);
|
||||
const generator = this.generatorMap.get(generatorKey);
|
||||
|
||||
if (!generator) {
|
||||
throw new Error(`No generator registered for entity: "${generatorKey}"`);
|
||||
}
|
||||
|
||||
return generator;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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 Stream, { PassThrough } from 'stream';
|
||||
import os from 'os';
|
||||
import Docker from 'dockerode';
|
||||
import { runDockerContainer, getGeneratorKey } from './helpers';
|
||||
|
||||
const mockEntity = {
|
||||
apiVersion: 'version',
|
||||
kind: 'TestKind',
|
||||
metadata: {
|
||||
name: 'testName',
|
||||
},
|
||||
};
|
||||
|
||||
const mockDocker = new Docker() as jest.Mocked<Docker>;
|
||||
|
||||
describe('helpers', () => {
|
||||
describe('getGeneratorKey', () => {
|
||||
it('should return techdocs as the only generator key', () => {
|
||||
const key = getGeneratorKey(mockEntity);
|
||||
expect(key).toBe('techdocs');
|
||||
});
|
||||
});
|
||||
|
||||
describe('runDockerContainer', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(mockDocker, 'pull').mockImplementation((async (
|
||||
_image: string,
|
||||
_something: any,
|
||||
handler: (err: Error | undefined, stream: PassThrough) => void,
|
||||
) => {
|
||||
const mockStream = new PassThrough();
|
||||
handler(undefined, mockStream);
|
||||
mockStream.end();
|
||||
}) as any);
|
||||
|
||||
jest
|
||||
.spyOn(mockDocker, 'run')
|
||||
.mockResolvedValue([{ Error: null, StatusCode: 0 }]);
|
||||
});
|
||||
|
||||
const imageName = 'spotify/techdocs';
|
||||
const args = ['build', '-d', '/result'];
|
||||
const docsDir = os.tmpdir();
|
||||
const resultDir = os.tmpdir();
|
||||
|
||||
it('should pull the techdocs docker container', async () => {
|
||||
await runDockerContainer({
|
||||
imageName,
|
||||
args,
|
||||
docsDir,
|
||||
resultDir,
|
||||
dockerClient: mockDocker,
|
||||
});
|
||||
|
||||
expect(mockDocker.pull).toHaveBeenCalledWith(
|
||||
imageName,
|
||||
{},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should run the techdocs docker container', async () => {
|
||||
await runDockerContainer({
|
||||
imageName,
|
||||
args,
|
||||
docsDir,
|
||||
resultDir,
|
||||
dockerClient: mockDocker,
|
||||
});
|
||||
|
||||
expect(mockDocker.run).toHaveBeenCalledWith(
|
||||
imageName,
|
||||
args,
|
||||
expect.any(Stream),
|
||||
{
|
||||
Volumes: {
|
||||
'/content': {},
|
||||
'/result': {},
|
||||
},
|
||||
WorkingDir: '/content',
|
||||
HostConfig: {
|
||||
Binds: [`${docsDir}:/content`, `${resultDir}:/result`],
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -79,9 +79,10 @@ export const checkoutGitRepository = async (
|
||||
|
||||
if (fs.existsSync(repositoryTmpPath)) {
|
||||
const repository = await Repository.open(repositoryTmpPath);
|
||||
const currentBranchName = (await repository.getCurrentBranch()).shorthand();
|
||||
await repository.mergeBranches(
|
||||
parsedGitLocation.ref,
|
||||
`origin/${parsedGitLocation.ref}`,
|
||||
currentBranchName,
|
||||
`origin/${currentBranchName}`,
|
||||
);
|
||||
return repositoryTmpPath;
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { Logger } from 'winston';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { PublisherBase } from './types';
|
||||
import { resolvePackagePath } from '@backstage/backend-common';
|
||||
|
||||
export class LocalPublish implements PublisherBase {
|
||||
private readonly logger: Logger;
|
||||
@@ -39,9 +39,9 @@ export class LocalPublish implements PublisherBase {
|
||||
| { remoteUrl: string } {
|
||||
const entityNamespace = entity.metadata.namespace ?? 'default';
|
||||
|
||||
const publishDir = path.resolve(
|
||||
__dirname,
|
||||
'../../../../static/docs/',
|
||||
const publishDir = resolvePackagePath(
|
||||
'@backstage/plugin-techdocs-backend',
|
||||
'static/docs',
|
||||
entity.kind,
|
||||
entityNamespace,
|
||||
entity.metadata.name,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user