Merge branch 'master' into new-release-31-aug-20

This commit is contained in:
Patrik Oldsberg
2020-09-02 16:17:40 +02:00
committed by GitHub
101 changed files with 2379 additions and 1145 deletions
+8
View File
@@ -74,3 +74,11 @@ jobs:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
package_root: "packages/core"
tag_prefix: "v"
- name: Discord notification
if: ${{ failure() }}
uses: Ilshidur/action-discord@0.2.0
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
with:
args: 'Master build failed https://github.com/{{GITHUB_REPOSITORY}}/actions/runs/{{GITHUB_RUN_ID}}'
+1
View File
@@ -92,6 +92,7 @@ dist
# Microsite build output
microsite/build
microsite/i18n
# Gatsby files
.cache/
+1
View File
@@ -12,3 +12,4 @@
| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. |
| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D |
| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling |
| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling |
+9
View File
@@ -8,6 +8,15 @@ If you encounter issues while upgrading to a newer version, don't hesitate to re
> Collect changes for the next release below
- The backend plugin
[service builder](https://github.com/spotify/backstage/blob/master/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts)
no longer adds `express.json()` automatically to all routes. While convenient
in a lot of cases, it also led to problems where for example the proxy
middleware could hang because the body had already been altered and could not
be streamed. Also, plugins that rather wanted to handle e.g. form encoded data
still had to cater to that manually. We therefore decided to let plugins add
`express.json()` themselves if they happen to deal with JSON data.
## v0.1.1-alpha.20
- Includes https://github.com/spotify/backstage/pull/2097 to resolve issues with create-plugin command.
+2 -5
View File
@@ -1,7 +1,4 @@
---
id: CONTRIBUTING
title: Contributing
---
# Contributing to Backstage
Our vision for Backstage is for it to become the trusted standard toolbox (read: UX layer) for the open source infrastructure landscape. Think of it like Kubernetes for developer experience. We realize this is an ambitious goal. We cant do it alone.
@@ -31,7 +28,7 @@ What kind of plugins should/could be created? Some inspiration from the 120+ plu
## Suggesting a plugin
If you start developing a plugin that you aim to release as open source, we suggest that you create a new [new Issue](https://github.com/spotify/backstage/issues/new?template=plugin_template.md). This helps the community know what plugins are in development.
If you start developing a plugin that you aim to release as open source, we suggest that you create a new [new Issue](https://github.com/spotify/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME). This helps the community know what plugins are in development.
You can also use this process if you have an idea for a good plugin but you hope that someone else will pick up the work.
+1 -1
View File
@@ -61,7 +61,7 @@ Take a look at the [Getting Started](https://backstage.io/docs/getting-started/i
## Documentation
- [Main documentation](https://backstage.io/docs/overview/what-is-backstage)
- [Main documentation](https://backstage.io/docs)
- [Service Catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview)
- [Architecture](https://backstage.io/docs/overview/architecture-terminology) ([Decisions](https://backstage.io/docs/architecture-decisions/adrs-overview))
- [Designing for Backstage](https://backstage.io/docs/dls/design)
+2
View File
@@ -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: []
```
+16
View File
@@ -79,6 +79,22 @@ All software created through the
[Backstage Software Templates](../software-templates/index.md) are automatically
registered in the catalog.
### Static catalog configuration
In addition to manually registering components, it is also possible to register
components though [static configuration](../../conf/index.md). For example, the
above example can be added using the following configuration:
```yaml
catalog:
locations:
- type: github
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml
```
More information about catalog configuration can be found
[here](configuration.md).
### Updating component metadata
Teams owning the components are responsible for maintaining the metadata about
@@ -58,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:
+15 -6
View File
@@ -79,18 +79,26 @@ guidelines to get started.
- Further improvements to platform documentation
### Plugins
Building and maintaining [plugins](https://backstage.io/plugins) is the work of
the entire Backstage community.
A list of plugins that are in development is
[available here](https://github.com/spotify/backstage/issues?q=is%3Aissue+is%3Aopen+label%3Aplugin+sort%3Areactions-%2B1-desc).
We strongly recommend to upvote 👍 plugins you are interested in. This helps us
and the community prioritize what plugins to build.
Are you missing a plugin for your favorite tool? Please
[suggest a new one](https://github.com/spotify/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME).
Chances are that someone will jump in and help build it.
### Future work 🔮
- **[Backstage platform is stable](https://github.com/spotify/backstage/milestone/19)** -
The platform APIs and features are stable and can be depended on for
production use. After this plugins will require little to no maintenance.
- **[Plugin marketplace](https://github.com/spotify/backstage/issues/2009)** -
As the ecosystem of Backstage plugins continues to grow it is becoming
increasingly hard to keep track of what plugins are available. To solve this
we imagine a "Plugin marketplace" that helps with discovery and installation
of plugins.
- **Deploy a product demo at `demo.backstage.io`** - Deploy a typical Backstage
deployment available publicly so that people can click around and get a feel
for the product without having to install anything.
@@ -111,6 +119,7 @@ guidelines to get started.
### Completed milestones ✅
- [Plugin marketplace](https://backstage.io/plugins)
- [Improved and move documentation to backstage.io](https://backstage.io/docs/overview/what-is-backstage)
- [Backstage Service Catalog (alpha)](https://backstage.io/blog/2020/06/22/backstage-service-catalog-alpha)
- [Backstage Software Templates (alpha)](https://backstage.io/blog/2020/08/05/announcing-backstage-software-templates)
+3 -1
View File
@@ -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
---
+14
View File
@@ -39,4 +39,18 @@ $ git push origin -u new-release
And then create a PR. Once the PR is approved and merged into master, the master
build will publish new versions of all bumped packages.
### Include new changes in existing release PR
If you want to include some last minute changes to an existing release PR,
follow these instructions:
```sh
$ git checkout master
$ git pull
$ git checkout new-release
$ git reset --hard master
$ yarn release
$ git push --force
```
[Back to Docs](../README.md)
+3
View File
@@ -45,6 +45,9 @@ class Footer extends React.Component {
<div>
<h5>Community</h5>
<a href="https://discord.gg/MUpMjP2">Support chatroom</a>
<a href="https://github.com/spotify/backstage/blob/master/CONTRIBUTING.md">
Contributing
</a>
<a href="https://mailchi.mp/spotify/backstage-community">
Subscribe to our newsletter
</a>
+9
View File
@@ -0,0 +1,9 @@
---
title: API Docs
author: SDA SE
authorUrl: https://sda.se/
category: Discovery
description: Components to discover and display API entities as an extension to the catalog plugin.
documentation: https://github.com/spotify/backstage/blob/master/plugins/api-docs/README.md
iconUrl: https://thecoders.io/wp-content/uploads/2019/11/tech-swagger.svg
npmPackageName: '@backstage/plugin-api-docs'
+9
View File
@@ -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
+13
View File
@@ -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
+14
View File
@@ -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
+14
View File
@@ -0,0 +1,14 @@
---
title: New Relic
author: '@timwheelercom'
authorUrl: https://github.com/timwheelercom
category: Monitoring
description: Observability platform built to help engineers create and monitor their software.
documentation: https://github.com/spotify/backstage/tree/master/plugins/newrelic
iconUrl: https://www.mulesoft.com/sites/default/files/2018-10/New_relic.png
npmPackageName: '@backstage/plugin-newrelic'
tags:
- performance
- monitoring
- errors
- alerting
+9
View File
@@ -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'
-325
View File
@@ -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"
}
}
+3 -7
View File
@@ -455,12 +455,8 @@ class Index extends React.Component {
Share with the community
</Block.SmallTitle>
<Block.Paragraph>
Building{' '}
<a href="https://github.com/spotify/backstage/blob/master/docs/FAQ.md#how-do-i-find-out-if-a-plugin-already-exists">
open source plugins
</a>{' '}
contributes to the entire Backstage ecosystem, which benefits
everyone
Building <a href="/plugins">open source plugins</a> contributes
to the entire Backstage ecosystem, which benefits everyone
</Block.Paragraph>
</Block.TextBox>
@@ -472,7 +468,7 @@ class Index extends React.Component {
<ActionBlock className="stripe-top bg-teal">
<ActionBlock.Title>Build a plugin</ActionBlock.Title>
<ActionBlock.Link href="https://github.com/spotify/backstage/blob/master/docs/plugins/create-a-plugin.md">
<ActionBlock.Link href="/docs/plugins/create-a-plugin">
Contribute
</ActionBlock.Link>
</ActionBlock>
+35 -3
View File
@@ -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>
+1 -1
View File
@@ -216,7 +216,7 @@ td {
code {
font-family: IBM Plex Mono, Menlo, Monaco, Consolas, Courier New, monospace;
font-weight: 500;
background-color: #0e0e0e;
background-color: #272822;
}
/* .stripe {
+58 -57
View File
@@ -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;
}
+1
View File
@@ -28,6 +28,7 @@ nav:
- Overview: 'features/software-catalog/index.md'
- System model: 'features/software-catalog/system-model.md'
- YAML File Format: 'features/software-catalog/descriptor-format.md'
- Configuration: 'features/software-catalog/configuration.md'
- Extending the model: 'features/software-catalog/extending-the-model.md'
- External integrations: 'features/software-catalog/external-integrations.md'
- API: 'features/software-catalog/api.md'
+1
View File
@@ -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",
+2 -1
View File
@@ -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(
+1
View File
@@ -20,4 +20,5 @@ export * from './errors';
export * from './logging';
export * from './middleware';
export * from './service';
export * from './paths';
export * from './hot';
@@ -0,0 +1,35 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as winston from 'winston';
import { TransformableInfo } from 'logform';
const coloredTemplate = (info: TransformableInfo) => {
const { timestamp, level, message, plugin, service } = info;
const colorizer = winston.format.colorize();
const prefix = plugin || service;
const timestampColor = colorizer.colorize('timestamp', timestamp);
const prefixColor = colorizer.colorize('prefix', prefix);
return `${timestampColor} ${prefixColor} ${level} ${message}`;
};
export const coloredFormat = winston.format.combine(
winston.format.timestamp(),
winston.format.colorize({
colors: { timestamp: 'dim', prefix: 'blue' },
}),
winston.format.printf(coloredTemplate),
);
@@ -14,17 +14,14 @@
* limitations under the License.
*/
import * as winston from 'winston';
import { coloredFormat } from './formats';
let rootLogger: winston.Logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format:
process.env.NODE_ENV === 'production'
? winston.format.json()
: winston.format.combine(
winston.format.colorize(),
winston.format.timestamp(),
winston.format.simple(),
),
: coloredFormat,
defaultMeta: { service: 'backstage' },
transports: [
new winston.transports.Console({
+36
View File
@@ -0,0 +1,36 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/camelcase */
import { resolve as resolvePath } from 'path';
/**
* Resolve a path relative to the root of a package directory.
* Additional path arguments are resolved relative to the package dir.
*
* This is particularly useful when you want to access assets shipped with
* your backend plugin package. When doing so, do not forget to include the assets
* in your published package by adding them to `files` in your `package.json`.
*/
export function resolvePackagePath(name: string, ...paths: string[]) {
const req =
typeof __non_webpack_require__ === 'undefined'
? require
: __non_webpack_require__;
return resolvePath(req.resolve(`${name}/package.json`), '..', ...paths);
}
@@ -135,7 +135,6 @@ export class ServiceBuilderImpl implements ServiceBuilder {
app.use(cors(corsOptions));
}
app.use(compression());
app.use(express.json());
if (this.enableMetrics) {
app.use(metricsHandler());
}
+1 -1
View File
@@ -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"
}
}
+8 -5
View File
@@ -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 };
};
}
+1 -1
View File
@@ -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);
+4 -1
View File
@@ -25,7 +25,7 @@ import {
yesPromptFunc,
} from '../../lib/diff';
import { paths } from '../../lib/paths';
import { version } from '../../lib/version';
import { version as backstageVersion } from '../../lib/version';
export type PluginData = {
id: string;
@@ -62,9 +62,12 @@ export default async (cmd: Command) => {
promptFunc = yesPromptFunc;
}
const { version } = await fs.readJson(paths.resolveTargetRoot('lerna.json'));
const data = await readPluginData();
const templateFiles = await diffTemplateFiles('default-plugin', {
version,
backstageVersion,
...data,
});
await handleAllFiles(fileHandlers, templateFiles, promptFunc);
+6
View File
@@ -57,6 +57,12 @@ export default async (cmd: Command) => {
}
}
// This is the only thing that is not implemented by jest.run(), so we do it here instead
// https://github.com/facebook/jest/blob/cd8828f7bbec6e55b4df5e41e853a5133c4a3ee1/packages/jest-cli/bin/jest.js#L12
if (!process.env.NODE_ENV) {
(process.env as any).NODE_ENV = 'test';
}
// eslint-disable-next-line jest/no-jest-import
await require('jest').run(args);
};
+14 -4
View File
@@ -88,15 +88,25 @@ export const makeConfigs = async (
}),
resolve({ mainFields }),
commonjs({
include: ['node_modules/**', '../../node_modules/**'],
exclude: ['**/*.stories.*', '**/*.test.*'],
include: /node_modules/,
exclude: [/\/[^/]+\.(?:stories|test)\.[^/]+$/],
}),
postcss(),
imageFiles({ exclude: '**/*.icon.svg' }),
imageFiles({
exclude: /\.icon\.svg$/,
include: [
/\.css$/,
/\.svg$/,
/\.png$/,
/\.gif$/,
/\.jpg$/,
/\.jpeg$/,
],
}),
json(),
yaml(),
svgr({
include: '**/*.icon.svg',
include: /\.icon\.svg$/,
template: svgrTemplate,
}),
esbuild({
+1 -1
View File
@@ -25,7 +25,7 @@ export async function serveBackend(
},
) {
const paths = resolveBundlingPaths(options);
const config = createBackendConfig(paths, {
const config = await createBackendConfig(paths, {
...options,
isDev: true,
});
+1 -1
View File
@@ -36,7 +36,7 @@ export async function buildBundle(options: BuildOptions) {
const { statsJsonEnabled } = options;
const paths = resolveBundlingPaths(options);
const config = createConfig(paths, {
const config = await createConfig(paths, {
...options,
checksEnabled: false,
isDev: false,
+58 -10
View File
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin';
@@ -25,6 +27,9 @@ import { Config } from '@backstage/config';
import { BundlingPaths } from './paths';
import { transforms } from './transforms';
import { BundlingOptions, BackendBundlingOptions } from './types';
import { version } from '../../lib/version';
import { paths as cliPaths } from '../../lib/paths';
import { runPlain } from '../run';
export function resolveBaseUrl(config: Config): URL {
const baseUrl = config.getString('app.baseUrl');
@@ -35,10 +40,40 @@ export function resolveBaseUrl(config: Config): URL {
}
}
export function createConfig(
async function readBuildInfo() {
const timestamp = Date.now();
let commit = 'unknown';
try {
commit = await runPlain('git', 'rev-parse', 'HEAD');
} catch (error) {
console.warn(`WARNING: Failed to read git commit, ${error}`);
}
let gitVersion = 'unknown';
try {
gitVersion = await runPlain('git', 'describe', '--always');
} catch (error) {
console.warn(`WARNING: Failed to describe git version, ${error}`);
}
const { version: packageVersion } = await fs.readJson(
cliPaths.resolveTarget('package.json'),
);
return {
cliVersion: version,
gitVersion,
packageVersion,
timestamp,
commit,
};
}
export async function createConfig(
paths: BundlingPaths,
options: BundlingOptions,
): webpack.Configuration {
): Promise<webpack.Configuration> {
const { checksEnabled, isDev } = options;
const { plugins, loaders } = transforms(options);
@@ -81,6 +116,13 @@ export function createConfig(
}),
);
const buildInfo = await readBuildInfo();
plugins.push(
new webpack.DefinePlugin({
'process.env.BUILD_INFO': JSON.stringify(buildInfo),
}),
);
return {
mode: isDev ? 'development' : 'production',
profile: false,
@@ -130,14 +172,23 @@ export function createConfig(
};
}
export function createBackendConfig(
export async function createBackendConfig(
paths: BundlingPaths,
options: BackendBundlingOptions,
): webpack.Configuration {
): Promise<webpack.Configuration> {
const { checksEnabled, isDev } = options;
const { loaders } = transforms(options);
// Find all local monorepo packages and their node_modules, and mark them as external.
const LernaProject = require('@lerna/project');
const project = new LernaProject(cliPaths.targetDir);
const packages = await project.getPackages();
const localPackageNames = packages.map((p: any) => p.name);
const moduleDirs = packages.map((p: any) =>
resolvePath(p.location, 'node_modules'),
);
return {
mode: isDev ? 'development' : 'production',
profile: false,
@@ -152,11 +203,8 @@ export function createBackendConfig(
externals: [
nodeExternals({
modulesDir: paths.rootNodeModules,
allowlist: ['webpack/hot/poll?100', /\@backstage\/.*/],
}),
nodeExternals({
modulesDir: paths.targetNodeModules,
allowlist: ['webpack/hot/poll?100', /\@backstage\/.*/],
additionalModuleDirs: moduleDirs,
allowlist: ['webpack/hot/poll?100', ...localPackageNames],
}),
],
target: 'node' as const,
@@ -178,7 +226,7 @@ export function createBackendConfig(
resolve: {
extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'],
mainFields: ['browser', 'module', 'main'],
modules: [paths.targetNodeModules, paths.rootNodeModules],
modules: [paths.rootNodeModules, ...moduleDirs],
plugins: [
new ModuleScopePlugin(
[paths.targetSrc, paths.targetDev],
-1
View File
@@ -63,7 +63,6 @@ export function resolveBundlingPaths(options: BundlingPathsOptions) {
targetDev: paths.resolveTarget('dev'),
targetEntry: resolveTargetModule(entry),
targetTsConfig: paths.resolveTargetRoot('tsconfig.json'),
targetNodeModules: paths.resolveTarget('node_modules'),
targetPackageJson: paths.resolveTarget('package.json'),
rootNodeModules: paths.resolveTargetRoot('node_modules'),
root: paths.targetRoot,
+1 -1
View File
@@ -30,7 +30,7 @@ export async function serveBundle(options: ServeOptions) {
const paths = resolveBundlingPaths(options);
const pkgPath = paths.targetPackageJson;
const pkg = await fs.readJson(pkgPath);
const config = createConfig(paths, {
const config = await createConfig(paths, {
...options,
isDev: true,
baseUrl: url,
+21
View File
@@ -26,6 +26,7 @@ type LernaPackage = {
private: boolean;
location: string;
scripts: Record<string, string>;
get(key: string): any;
};
type FileEntry =
@@ -107,6 +108,26 @@ async function moveToDistWorkspace(
strip: 1,
});
await fs.remove(archivePath);
// We remove the dependencies from package.json of packages that are marked
// as bundled, so that yarn doesn't try to install them.
if (target.get('bundled')) {
const pkgJson = await fs.readJson(
resolvePath(absoluteOutputPath, 'package.json'),
);
delete pkgJson.dependencies;
delete pkgJson.devDependencies;
delete pkgJson.peerDependencies;
delete pkgJson.optionalDependencies;
await fs.writeJson(
resolvePath(absoluteOutputPath, 'package.json'),
pkgJson,
{
spaces: 2,
},
);
}
}),
);
}
@@ -21,8 +21,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core": "^{{version}}",
"@backstage/theme": "^{{version}}",
"@backstage/core": "^{{backstageVersion}}",
"@backstage/theme": "^{{backstageVersion}}",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -31,8 +31,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^{{version}}",
"@backstage/dev-utils": "^{{version}}",
"@backstage/cli": "^{{backstageVersion}}",
"@backstage/dev-utils": "^{{backstageVersion}}",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
@@ -2,13 +2,13 @@ import { createPlugin, createRouteRef } from '@backstage/core';
import ExampleComponent from './components/ExampleComponent';
export const rootRouteRef = createRouteRef({
path: '/{{ id }}',
title: '{{ id }}',
path: '/{{ id }}',
title: '{{ id }}',
});
export const plugin = createPlugin({
id: '{{ id }}',
register({ router }) {
router.addRoute(rootRouteRef, ExampleComponent);
},
id: '{{ id }}',
register({ router }) {
router.addRoute(rootRouteRef, ExampleComponent);
},
});
+1 -1
View File
@@ -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]
@@ -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 };
};
}
+11 -2
View File
@@ -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
View File
@@ -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:
+4 -9
View File
@@ -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({
-1
View File
@@ -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';
+1 -3
View File
@@ -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;
+2 -3
View File
@@ -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({
-1
View File
@@ -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>
);
}
@@ -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';
@@ -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>
);
+6 -7
View File
@@ -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;
};
+10 -1
View File
@@ -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