Merge branch 'master' of github.com:spotify/backstage into mob/catalog-in-gql

* 'master' of github.com:spotify/backstage: (73 commits)
  chore(gql): graphql should be public
  chore(deps): bump chokidar from 3.4.0 to 3.4.1 (#1818)
  chore(deps-dev): bump @types/nodegit from 0.26.5 to 0.26.7 (#1817)
  chore(deps-dev): bump @testing-library/react-hooks from 3.3.0 to 3.4.1 (#1820)
  Mob/techdocs end to end (#1736)
  chore(scaffolder): updating the wording for the sample templates
  chore(deps): bump react-use from 14.2.0 to 15.3.3
  workflows: run cli checks on changes to create-app
  create-app: sync version
  create-app: fix deps
  create-app: add lint command
  feat(create-app): add missing template backend package
  plugins/graphql: fix schema packaging
  create-app: packaging fixes
  fix(create-app): update entrypoint
  feat: fallback to string value if config value is not json
  yarn.lock: security fix (#1797)
  fix(cli): add create-app import to e2e tests
  cli: just print warning if postpack fails
  broken link to create an app
  ...
This commit is contained in:
blam
2020-08-05 05:07:29 +02:00
200 changed files with 4372 additions and 1081 deletions
+1
View File
@@ -7,6 +7,7 @@ on:
paths:
- '.github/workflows/cli-win.yml'
- 'packages/cli/**'
- 'packages/create-app/**'
jobs:
build:
+18
View File
@@ -5,6 +5,7 @@ on:
paths:
- '.github/workflows/cli.yml'
- 'packages/cli/**'
- 'packages/create-app/**'
- 'packages/core/**'
- 'packages/core-api/**'
- 'yarn.lock'
@@ -13,6 +14,18 @@ jobs:
build:
runs-on: ${{ matrix.os }}
services:
postgres:
image: postgres:latest
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5432/tcp
# needed because the postgres container does not provide a healthcheck
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
strategy:
matrix:
os: [ubuntu-latest]
@@ -44,6 +57,11 @@ jobs:
- run: yarn tsc
- run: yarn build
- name: verify app and plugin creation
env:
POSTGRES_HOST: localhost
POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }}
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
run: |
sudo sysctl fs.inotify.max_user_watches=524288
node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js
+13 -13
View File
@@ -1,13 +1,13 @@
| Organization | Contact | Description of Use |
| ---------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------- |
| [Spotify](https://www.spotify.com) | [@stefanalund](https://github.com/stefanalund) | Main interface towards all of Spotify's infrastructure and technical documentation. |
| [bol.com](https://www.bol.com) | [@RoyJacobs](https://github.com/RoyJacobs) | Initial work being done to unify platform tooling. |
| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. |
| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up |
| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. |
| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. |
| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. |
| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications |
| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. |
| [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
| Organization | Contact | Description of Use |
| --------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| [Spotify](https://www.spotify.com) | [@stefanalund](https://github.com/stefanalund) | Main interface towards all of Spotify's infrastructure and technical documentation. |
| [bol.com](https://www.bol.com) | [@RoyJacobs](https://github.com/RoyJacobs) | Initial work being done to unify platform tooling. |
| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. |
| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up |
| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. |
| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. |
| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. |
| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications |
| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. |
| [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 |
+8
View File
@@ -64,6 +64,14 @@ So...feel ready to jump in? Let's do this. Head over to the [Getting Started gui
If you need help, just jump into our [Discord chatroom](https://discord.gg/MUpMjP2).
# Coding Guidelines
All code is formatted with `prettier` using the configuration in the repo. If possible we recommend configuring your editor to format automatically, but you can also use the `yarn prettier --write <file>` command to format files.
If you're contributing to the backend or CLI tooling, be mindful of cross-platform support. [This](https://shapeshed.com/writing-cross-platform-node/) blog post is a good guide of what to keep in mind when writing cross-platform NodeJS.
Also be sure to skim through our [ADRs](https://github.com/spotify/backstage/tree/master/docs/architecture-decisions) to see if they cover what you're working on. In particular [ADR006: Avoid React.FC and React.SFC](https://github.com/spotify/backstage/blob/master/docs/architecture-decisions/adr006-avoid-react-fc.md) is one to look out for.
# Code of Conduct
This project adheres to the [Spotify FOSS Code of Conduct][code-of-conduct]. By participating, you are expected to honor this code.
+1 -1
View File
@@ -56,7 +56,7 @@ Our vision for Backstage is for it to become the trusted standard toolbox (read:
The Backstage platform consists of a number of different components:
- **app** - Main web application that users interact with. It's built up by a number of different _Plugins_. This repo contains an example implementation of an app (located in `packages/app`) and you can easily get started with your own app by [creating one](docs/create-an-app.md).
- **app** - Main web application that users interact with. It's built up by a number of different _Plugins_. This repo contains an example implementation of an app (located in `packages/app`) and you can easily get started with your own app by [creating one](docs/getting-started/create-an-app.md).
- [**plugins**](https://github.com/spotify/backstage/tree/master/plugins) - Each plugin is treated as a self-contained web app and can include almost any type of content. Plugins all use a common set of platform API's and reusable UI components. Plugins can fetch data either from the _backend_ or through any RESTful API exposed through the _proxy_.
- [**service catalog**](https://github.com/spotify/backstage/tree/master/packages/backend) - Service that holds the model of your software ecosystem, including organisational information and what team owns what software. The backend also has a Plugin model for extending its graph.
- [**proxy**](https://github.com/spotify/backstage/tree/master/plugins/proxy-backend) - Terminates HTTPS and exposes any RESTful API to Plugins.
+4 -1
View File
@@ -21,7 +21,7 @@ organization:
name: Spotify
techdocs:
storageUrl: https://techdocs-mock-sites.storage.googleapis.com
storageUrl: http://localhost:7000/techdocs/static/docs
sentry:
organization: spotify
@@ -48,6 +48,9 @@ auth:
clientSecret:
$secret:
env: AUTH_GITHUB_CLIENT_SECRET
enterpriseInstanceUrl:
$secret:
env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL
gitlab:
development:
appOrigin: "http://localhost:3000/"
+1 -1
View File
@@ -16,7 +16,7 @@ function inject_config() {
with_entries(select(.key | startswith("APP_CONFIG_")) | .key |= sub("APP_CONFIG_"; "")) |
to_entries |
reduce .[] as $item (
{}; setpath($item.key | split("_"); $item.value | fromjson)
{}; setpath($item.key | split("_"); $item.value | try fromjson catch $item.value)
)')"
>&2 echo "Runtime app config: $config"
+1
View File
@@ -94,6 +94,7 @@ better yet, a pull request.
- [ADR004 - Module Export Structure](architecture-decisions/adr004-module-export-structure.md)
- [ADR005 - Catalog Core Entities](architecture-decisions/adr005-catalog-core-entities.md)
- [ADR006 - Avoid React.FC and React.SFC](architecture-decisions/adr006-avoid-react-fc.md)
- [ADR007 - Use MSW for Mocking Network Requests](architecture-decisions/adr007-use-msw-to-mock-service-requests.md)
- [Contribute](../CONTRIBUTING.md)
- [Support](overview/support.md)
- [FAQ](FAQ.md)
@@ -0,0 +1,60 @@
# ADR007: Use MSW to mock http requests
## Context
Network request mocking can be a total pain sometimes, in all different types of
tests, unit tests to e2e tests always have their own implementation of mocking
these requests. There's been traction in the outer community towards using this
library to mock network requests by using an express style declaration for
routes. react-testing-library suggests using this library instead of mocking
fetch directly wether this be in a browser or in node.
https://github.com/mswjs/msw
## Decision
Moving forward, we have decided that any `fetch` or `XMLHTTPRequest` that
happens, should be mocked by using `msw`.
Here is an example:
```ts
import { setupWorker, rest } from 'msw';
const worker = setupWorker(
rest.get('*/user/:userId', (req, res, ctx) => {
return res(
ctx.json({
firstName: 'John',
lastName: 'Maverick',
}),
);
}),
);
// Start the Mock Service Worker
worker.start();
```
and in a more real life scenario, taken from
[CatalogClient.test.ts](https://github.com/spotify/backstage/blob/f3245c4f8f0b6b2625c4a6d5d50161b612fb4757/plugins/catalog/src/api/CatalogClient.test.ts)
```ts
beforeEach(() => {
server.use(
rest.get(`${mockApiOrigin}${mockBasePath}/entities`, (_, res, ctx) => {
return res(ctx.json(defaultResponse));
}),
);
});
it('should entities from correct endpoint', async () => {
const entities = await client.getEntities();
expect(entities).toEqual(defaultResponse);
});
```
## Consequences
- A little more code to write
- Gradually will replace the codebase with `msw`
+8 -9
View File
@@ -1,20 +1,23 @@
# TechDocs FAQ
This page answer frequently asked questions about [TechDocs].
This page answers frequently asked questions about [TechDocs](README.md).
#### Technology
_Got a question that you think others might be interested in knowing the answer to? Edit this file
[here](https://github.com/spotify/backstage/edit/master/docs/features/techdocs/FAQ.md)._
## Technology
- [What static site generator is TechDocs using?](./#what-static-site-generator-is-techdocs-using)
- [What is the mkdocs-techdocs-core plugin?](./#what-is-the-mkdocs-techdocs-core-plugin)
## What static site generator is TechDocs using?
#### What static site generator is TechDocs using?
TechDocs is using [MkDocs](https://www.mkdocs.org/) to build project
doucmentation under the hood. Documentation built with the
documentation under the hood. Documentation built with the
[techdocs-container](https://github.com/spotify/backstage/blob/master/packages/techdocs-container/README.md)
is using the MkDocs Material Theme.
## What is the mkdocs-techdocs-core plugin?
#### What is the mkdocs-techdocs-core plugin?
The
[mkdocs-techdocs-core](https://github.com/spotify/backstage/blob/master/packages/techdocs-container/techdocs-core/README.md)
@@ -23,7 +26,3 @@ plugins (e.g.
[MkDocs Monorepo Plugin](https://github.com/spotify/mkdocs-monorepo-plugin)) as
well as a selection of Python Markdown extensions that TechDocs supports.
_Add a question that you think others might be interested in? Edit the file
[here](https://github.com/spotify/backstage/edit/master/docs/features/techdocs/FAQ.md)._
[techdocs]: README.md
+10 -11
View File
@@ -1,7 +1,6 @@
# Concepts
This page describes concepts that has been introduced with Spotify's
docs-like-code solution in Backstage.
This page describes concepts that are introduced with Spotify's docs-like-code solution in Backstage.
### TechDocs Core Plugin
@@ -20,34 +19,34 @@ MkDocs.
[TechDocs Container](../../../packages/techdocs-container/README.md)
### TechDocs publisher (Coming Soon)
### TechDocs publisher (coming soon)
### TechDocs CLI
The TechDocs CLI was created to make it easy to write, generate and preview
documentation for publishing. Currently it mostly acts as a wrapper around the
TechDocs container and provides a easy to use interface for our docker
TechDocs container and provides an easy-to-use interface for our docker
container.
[TechDocs CLI](../../../packages/techdocs-cli/README.md)
### TechDocs Reader
Documentation generated by TechDocs is generated as static html sites. The
TechDocs Reader was therefore created to be able to integrate pre-generated html
Documentation generated by TechDocs is generated as static HTML sites. The
TechDocs Reader was therefore created to be able to integrate pre-generated HTML
sites with the Backstage UI.
The TechDocs Reader purpose is also to open up the opportunity to integrate
TechDocs widgets for a customized full-featured TechDocs experience.
([Coming Soon V.2](https://github.com/spotify/backstage/milestone/17))
([coming soon V.2](https://github.com/spotify/backstage/milestone/17))
[TechDocs Reader](../../../plugins/techdocs/src/reader/README.md)
### Transformers
Transformers is different pieces of functionality used inside the TechDocs
Reader. The reason to why transformers were introduced is to provide a way to
transform the html content on pre and post render. (e.g. rewrite docs links or
modify css)
Transformers are different pieces of functionality used inside the TechDocs
Reader. The reason why transformers were introduced was to provide a way to
transform the HTML content on pre and post render (e.g. rewrite docs links or
modify css).
[Transformers API docs](../../../plugins/techdocs/src/reader/transformers/README.md)
@@ -5,22 +5,18 @@ This section will guide you through:
- Creating a basic setup for your documentation
- Writing and previewing your documentation in a local Backstage environment
- Creating a build ready for publication
- Publishing your documentation and making your Backstage instance read from
your published docs.
- Publishing your documentation and making your Backstage instance read your published docs.
## Prerequisities
- [Docker](https://docs.docker.com/get-docker/)
- Static file hosting
- A working Backstage instance with TechDocs installed
[TechDocs getting started](getting-started.md)
(see [TechDocs getting started](getting-started.md))
## Create a basic documentation setup
Create a directory that contains your documentation. Inside this directory you
should create a file called `mkdocs.yml`. As an example you can create a
directory called `hello-docs` in your home directory (also known as `~`). Below
is a basic example of how it could look.
In your home directory (also known as `~`), create a directory that contains your documentation (for example, `hello-docs`). Inside this directory, create a file called `mkdocs.yml`. Below is a basic example of how it could look.
The `~/hello-docs/mkdocs.yml` file should have the following content:
@@ -34,7 +30,7 @@ plugins:
- techdocs-core
```
And then the `~/hello-docs/docs/index.md` should have the following content:
The `~/hello-docs/docs/index.md` should have the following content:
```md
# example docs
@@ -58,7 +54,7 @@ npx techdocs-cli serve
## Build production ready documentation
To get a build suitable for publication you can build your docs using the
`spotify/techdocs` container.
`spotify/techdocs` container:
```bash
cd ~/hello-docs/
@@ -69,20 +65,15 @@ You should now have a folder called `~/hello-docs/site/`.
## Deploy to a file server
In order to serve documentation to TechDocs, our Backstage plugin needs to
download the HTML rendered from the
[Create documentation](#create-documentation) step above. This will likely exist
on an external file server, or a storage solution such as Google Cloud Storage.
In order to serve documentation to TechDocs, our Backstage plugin needs to download the HTML rendered from the previous step. This will likely exist on an external file server, or a storage solution such as Google Cloud Storage.
When deploying documentation, it should be deployed on that file server /
storage solution with the following convention: `{id}/{file}`. For example, if
we wanted to upload the `getting-started/index.html` file for the `backstage`
When deploying documentation, it should be deployed on that file server/storage solution with the following convention: `{id}/{file}`. For example, if
you want to upload the `getting-started/index.html` file for the `backstage`
documentation site, we would upload it to our file server as
`backstage/getting-started/index.html`.
To explain further how this would look like for multiple documentation sites,
take a look at this example file tree that would be represented on your file
server:
To explain further what this would look like for multiple documentation sites,
take a look at this example file tree that would be represented on your file server:
```md
/backstage/index.html /backstage/getting-started/index.html
@@ -96,7 +87,7 @@ In this file tree, we have two documentation sites available: `backstage` and
on `http://example.com` as the server URL.
When you configure the TechDocs plugin in Backstage to use `http://example.com`
as the file server / storage solution, it will translate the following URLs to
as the file server/storage solution, it will translate the following URLs to
the file server:
| Backstage URL | File Server URL |
@@ -104,10 +95,8 @@ the file server:
| https://demo.backstage.io/docs/backstage/ | http://example.com/backstage/index.html |
| https://demo.backstage.io/docs/mkdocs/plugin-development/ | http://example.com/mkdocs/plugin-development/index.html |
Then deploying new sites is easy. It's as simple as copying over the `site/`
folder produced in the [Create documentation](#create-documentation) step above
and copying it over to the file server / storage solution under the ID of the
documentation site. It will then become immediately available in Backstage under
Then deploying new sites is easy: simply copy over the `site/`
folder produced in the [Create documentation](#build-production-ready-documentation) step above to the file server/storage solution under the ID of the documentation site. It will then become immediately available in Backstage under
the same ID as you can see in the table above.
So, if the URL to your file server is `http://example.com/`, your
@@ -120,7 +109,7 @@ In order for Backstage to show your documentation, it needs to know where you
uploaded it.
Make sure you have Backstage set up using
[TechDocs getting started](getting-started.md)
[TechDocs getting started](getting-started.md).
To point Backstage to your docs storage, add or change the following lines in
your Backstage `app-config.yaml`:
+26 -32
View File
@@ -1,27 +1,27 @@
# Getting Started
> TechDocs is not feature complete and currently you can't set up a complete
> TechDocs is not yet feature complete - currently you can't set up a complete
> end-to-end working TechDocs plugin without customizing the plugin itself.
> With TechDocs V.0 you can expect a demonstration of how to integrate docs into
> Backstage. Currently it can create docs using
> [mkdocs](https://www.mkdocs.org/), as well as reading published docs. If you
> publish generated docs and passing in a storageUrl in your `app-config.yaml`
> you can view it in Backstage by going to
> What you can expect from TechDocs V.0 is a demonstration of how to integrate docs into
> Backstage. TechDocs can create docs using
> [mkdocs](https://www.mkdocs.org/), as well as read published docs. If you
> publish generated docs and pass in a `storageUrl` in your `app-config.yaml`,
> you can view them in Backstage by going to
> `http://localhost:3000/docs/<remote-folder>`.
Getting started with TechDocs is easy. TechDocs functions as a plugin to
Backstage, why you will need to use Backstage to use TechDocs.
TechDocs functions as a plugin to
Backstage, so you will need to use Backstage to use TechDocs.
## What is Backstage?
Backstage is an open platform for building developer portals. Its based on the
developer portal weve been using internally at Spotify for over four years.
[Read more](https://github.com/spotify/backstage).
[Read more here](https://github.com/spotify/backstage).
## Prerequisities
In order to use Backstage and TechDocs, you will need to have the following
In order to use Backstage and TechDocs, you need to have the following
installed:
- [Node.js](https://nodejs.org) Active LTS (long term support), currently v12
@@ -29,48 +29,41 @@ installed:
## Creating a new Backstage app
> If you have already created a Backstage application for this purpose, jump to
> If you have already created a Backstage application, jump to
> [Installing TechDocs](#installing-techdocs), otherwise complete this step.
To create a new Backstage application for us to set up TechDocs, you will need
to run the following command:
To create a new Backstage application for TechDocs, run the following command:
```bash
npx @backstage/cli create-app
```
You will then be prompted to enter a name for your application. Once you do so,
this will create a new Backstage application for you in a new folder. For
example, if we chose the name `hello-world` for our application, it would create
a new `hello-world` folder containing our new Backstage application.
You will then be prompted to enter a name for your application. Once that's done, a new Backstage application will be created in a new folder. For
example, if you choose the name `hello-world`, a new `hello-world` folder is created containing your new Backstage application.
## Installing TechDocs
Inside of our new Backstage application, TechDocs is not provided by default.
For this reason we will need to manually set up TechDocs. It should take less
TechDocs is not provided with the Backstage application by default, so you will now need to set up TechDocs manually. It should take less
than a minute.
### Adding the package
We will need to add our plugin to your Backstage application. To do so, you can
navigate to your new Backstage application folder and then run a single command
to install TechDocs.
The first step is to add the TechDocs plugin to your Backstage application. Navigate to your new Backstage application folder:
```bash
cd hello-world/
```
Then you need to navigate to your `packages/app` folder to install TechDocs:
Then navigate to your `packages/app` folder to install TechDocs:
```bash
cd packages/app
yarn add @backstage/plugin-techdocs
```
After a short while, it should successfully install the TechDocs plugin. Now we
just need to set up some basic configuration!
After a short while, the TechDocs plugin should be successfully installed.
Enter the following command:
Next, you need to set up some basic configuration. Enter the following command:
```bash
yarn install
@@ -85,7 +78,7 @@ export { plugin as TechDocs } from '@backstage/plugin-techdocs';
### Setting the configuration
TechDocs allows for configuration of the docs storage URL through your
app-config file. The URL provided here is demo docs used to testing.
`app-config` file. The URL provided here is for demo docs to use for testing purposes.
To use the demo docs, add the following lines to `app-config.yaml`:
@@ -94,16 +87,17 @@ techdocs:
storageUrl: https://techdocs-mock-sites.storage.googleapis.com
```
## Run Backstage Locally
## Run Backstage locally
Change folder to your Backstage application root.
Change folder to your Backstage application root and run the following command:
```bash
yarn start
```
Open browser at [http://localhost:3000/docs/](http://localhost:3000/docs/)
Open your browser at [http://localhost:3000/docs/](http://localhost:3000/docs/).
## Extra Reading
## Additional reading
[Back to Docs](README.md)
* [Creating and publishing your docs](creating-and-publishing.md)
* [Back to README](README.md)
+1 -1
View File
@@ -15,7 +15,7 @@ To create a Backstage app, you will need to have
With `npx`:
```bash
npx @backstage/cli create-app
npx @backstage/create-app
```
This will create a new Backstage App inside the current folder. The name of the
+1
View File
@@ -91,6 +91,7 @@ nav:
- ADR004 - Module Export Structure: 'architecture-decisions/adr004-module-export-structure.md'
- ADR005 - Catalog Core Entities: 'architecture-decisions/adr005-catalog-core-entities.md'
- ADR006 - Avoid React.FC and React.SFC: 'architecture-decisions/adr006-avoid-react-fc.md'
- ADR007 - Use MSW for Network Request Mocking: 'architecture-decisions/adr007-use-msw-to-mock-service-requests.md'
- Contribute: '../CONTRIBUTING.md'
- Support: 'overview/support.md'
- FAQ: FAQ.md
+3
View File
@@ -5,7 +5,9 @@
"node": "12"
},
"scripts": {
"dev": "concurrently 'yarn start' 'yarn start-backend'",
"start": "yarn workspace example-app start",
"start-backend": "yarn workspace example-backend start",
"build": "lerna run build",
"tsc": "tsc",
"clean": "backstage-cli clean && lerna run clean",
@@ -35,6 +37,7 @@
"devDependencies": {
"@spotify/eslint-config-oss": "^1.0.1",
"@spotify/prettier-config": "^8.0.0",
"concurrently": "^5.2.0",
"husky": "^4.2.3",
"lerna": "^3.20.2",
"lint-staged": "^10.1.0",
+2 -2
View File
@@ -31,7 +31,7 @@
"react-hot-loader": "^4.12.21",
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^14.2.0",
"react-use": "^15.3.3",
"zen-observable": "^0.8.15"
},
"devDependencies": {
@@ -39,7 +39,7 @@
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^25.2.2",
"@types/jest": "^26.0.7",
"@types/jquery": "^3.3.34",
"@types/node": "^12.0.0",
"@types/react-dom": "^16.9.8",
+2
View File
@@ -2,6 +2,8 @@ FROM node:12
WORKDIR /usr/src/app
# This will copy the contents of the dist-workspace when running the build-image command.
# Do not use this Dockerfile outside of that command, as it will copy in the source code instead.
COPY . .
RUN yarn install --frozen-lockfile --production
+1
View File
@@ -35,6 +35,7 @@
"dockerode": "^3.2.0",
"express": "^4.17.1",
"knex": "^0.21.1",
"pg": "^8.3.0",
"sqlite3": "^4.2.0",
"winston": "^3.2.1"
},
+31 -6
View File
@@ -29,7 +29,7 @@ import {
} from '@backstage/backend-common';
import { ConfigReader, AppConfig } from '@backstage/config';
import { loadConfig } from '@backstage/config-loader';
import knex from 'knex';
import knex, { PgConnectionConfig } from 'knex';
import healthcheck from './plugins/healthcheck';
import auth from './plugins/auth';
import catalog from './plugins/catalog';
@@ -47,11 +47,36 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) {
return (plugin: string): PluginEnvironment => {
const logger = getRootLogger().child({ type: 'plugin', plugin });
const database = knex({
client: 'sqlite3',
connection: ':memory:',
useNullAsDefault: true,
});
// Supported DBs are sqlite and postgres
const isPg = [
'POSTGRES_USER',
'POSTGRES_HOST',
'POSTGRES_PASSWORD',
].every(key => config.getOptional(`backend.${key}`));
let knexConfig;
if (isPg) {
knexConfig = {
client: 'pg',
useNullAsDefault: true,
connection: {
port: config.getOptionalNumber('backend.POSTGRES_PORT'),
host: config.getString('backend.POSTGRES_HOST'),
user: config.getString('backend.POSTGRES_USER'),
password: config.getString('backend.POSTGRES_PASSWORD'),
database: `backstage_plugin_${plugin}`,
} as PgConnectionConfig,
};
} else {
knexConfig = {
client: 'sqlite3',
connection: ':memory:',
useNullAsDefault: true,
};
}
const database = knex(knexConfig);
database.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
resource.run('PRAGMA foreign_keys = ON', () => {});
});
+31 -3
View File
@@ -14,9 +14,37 @@
* limitations under the License.
*/
import { createRouter } from '@backstage/plugin-techdocs-backend';
import {
createRouter,
DirectoryPreparer,
Preparers,
Generators,
LocalPublish,
TechdocsGenerator
} from '@backstage/plugin-techdocs-backend';
import { PluginEnvironment } from '../types';
import Docker from 'dockerode';
export default async function createPlugin({ logger }: PluginEnvironment) {
return await createRouter({ logger });
export default async function createPlugin({ logger, config }: PluginEnvironment) {
const generators = new Generators();
const techdocsGenerator = new TechdocsGenerator();
generators.register('techdocs', techdocsGenerator);
const directoryPreparer = new DirectoryPreparer();
const preparers = new Preparers();
preparers.register('dir', directoryPreparer);
const publisher = new LocalPublish();
const dockerClient = new Docker();
return await createRouter({
preparers,
generators,
publisher,
dockerClient,
logger,
config,
});
}
+1 -1
View File
@@ -31,7 +31,7 @@
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.16",
"@types/express": "^4.17.6",
"@types/jest": "^25.2.2",
"@types/jest": "^26.0.7",
"@types/lodash": "^4.14.151",
"yaml": "^1.9.2"
},
+101 -5
View File
@@ -16,6 +16,7 @@
const os = require('os');
const fs = require('fs-extra');
const fetch = require('node-fetch');
const killTree = require('tree-kill');
const { resolve: resolvePath, join: joinPath } = require('path');
const Browser = require('zombie');
@@ -28,6 +29,7 @@ const {
waitForExit,
print,
} = require('./helpers');
const pgtools = require('pgtools');
async function main() {
const rootDir = await fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-e2e-'));
@@ -36,8 +38,9 @@ async function main() {
print('Building dist workspace');
const workspaceDir = await buildDistWorkspace('workspace', rootDir);
const isPostgres = Boolean(process.env.POSTGRES_USER);
print('Creating a Backstage App');
const appDir = await createApp('test-app', workspaceDir, rootDir);
const appDir = await createApp('test-app', isPostgres, workspaceDir, rootDir);
print('Creating a Backstage Plugin');
const pluginName = await createPlugin('test-plugin', appDir);
@@ -45,6 +48,9 @@ async function main() {
print('Starting the app');
await testAppServe(pluginName, appDir);
print('Testing the backend startup');
await testBackendStart(appDir, isPostgres);
print('All tests successful, removing test dir');
await fs.remove(rootDir);
}
@@ -63,6 +69,7 @@ async function buildDistWorkspace(workspaceName, rootDir) {
'build-workspace',
workspaceDir,
'@backstage/cli',
'@backstage/create-app',
'@backstage/core',
'@backstage/dev-utils',
'@backstage/test-utils',
@@ -97,12 +104,11 @@ async function pinYarnVersion(dir) {
/**
* Creates a new app inside rootDir called test-app, using packages from the workspaceDir
*/
async function createApp(appName, workspaceDir, rootDir) {
async function createApp(appName, isPostgres, workspaceDir, rootDir) {
const child = spawnPiped(
[
'node',
resolvePath(workspaceDir, 'packages/cli/bin/backstage-cli'),
'create-app',
resolvePath(workspaceDir, 'packages/create-app/bin/backstage-create-app'),
'--skip-install',
],
{
@@ -119,6 +125,14 @@ async function createApp(appName, workspaceDir, rootDir) {
await waitFor(() => stdout.includes('Enter a name for the app'));
child.stdin.write(`${appName}\n`);
await waitFor(() => stdout.includes('Select database for the backend'));
if (!isPostgres) {
// Simulate down arrow press
child.stdin.write(`\u001B\u005B\u0042`);
}
child.stdin.write(`\n`);
print('Waiting for app create script to be done');
await waitForExit(child);
@@ -201,7 +215,7 @@ async function createPlugin(pluginName, appDir) {
await waitForExit(child);
const pluginDir = resolvePath(appDir, 'plugins', pluginName);
for (const cmd of [['lint'], ['test', '--no-watch']]) {
for (const cmd of [['tsc'], ['lint'], ['test', '--no-watch']]) {
print(`Running 'yarn ${cmd.join(' ')}' in newly created plugin`);
await runPlain(['yarn', ...cmd], { cwd: pluginDir });
}
@@ -250,5 +264,87 @@ async function testAppServe(pluginName, appDir) {
}
}
/** Creates PG databases (drops if exists before) */
async function createDB(database) {
const config = {
host: process.env.POSTGRES_HOST,
port: process.env.POSTGRES_PORT,
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
};
try {
await pgtools.dropdb({ config }, database);
} catch (_) {
/* do nothing*/
}
return pgtools.createdb(config, database);
}
/**
* Start serving the newly created backend and make sure that all db migrations works correctly
*/
async function testBackendStart(appDir, isPostgres) {
if (isPostgres) {
print('Creating DBs');
await Promise.all(
[
'catalog',
'scaffolder',
'auth',
'identity',
'proxy',
'techdocs',
].map(name => createDB(`backstage_plugin_${name}`)),
);
print('Created DBs');
}
const child = spawnPiped(['yarn', 'workspace', 'backend', 'start'], {
cwd: appDir,
});
let stdout = '';
let stderr = '';
child.stdout.on('data', data => {
stdout = stdout + data.toString('utf8');
});
child.stderr.on('data', data => {
stderr = stderr + data.toString('utf8');
});
let successful = false;
try {
await waitFor(() => stdout.includes('Listening on ') || stderr !== '');
if (stderr !== '') {
// Skipping the whole block
throw new Error(stderr);
}
print('Try to fetch entities from the backend');
// Try fetch entities, should be ok
await fetch('http://localhost:7000/catalog/entities').then(res =>
res.json(),
);
print('Entities fetched successfully');
successful = true;
} catch (error) {
throw new Error(`Backend failed to startup: ${error}`);
} finally {
print('Stopping the child process');
// Kill entire process group, otherwise we'll end up with hanging serve processes
killTree(child.pid);
}
try {
await waitForExit(child);
} catch (error) {
if (!successful) {
throw new Error(`Backend failed to startup: ${stderr}`);
}
print('Backend startup test finished successfully');
}
}
process.on('unhandledRejection', handleError);
main(process.argv.slice(2)).catch(handleError);
+6
View File
@@ -76,6 +76,12 @@ function handleError(err) {
}
}
/**
* Waits for fn() to be true
* Checks every 100ms
* .cancel() is available
* @returns {Promise} Promise of resolution
*/
function waitFor(fn) {
return new Promise(resolve => {
const handle = setInterval(() => {
+4 -2
View File
@@ -42,7 +42,7 @@
"@sucrase/webpack-loader": "^2.0.0",
"@svgr/plugin-jsx": "4.3.x",
"@svgr/plugin-svgo": "4.3.x",
"@svgr/rollup": "4.3.x",
"@svgr/rollup": "5.4.x",
"@svgr/webpack": "4.3.x",
"@types/start-server-webpack-plugin": "^2.2.0",
"@types/webpack-env": "^1.15.2",
@@ -68,14 +68,16 @@
"jest-css-modules": "^2.1.0",
"jest-esm-transformer": "^1.0.0",
"mini-css-extract-plugin": "^0.9.0",
"node-fetch": "^2.6.0",
"ora": "^4.0.3",
"pgtools": "^0.3.0",
"raw-loader": "^4.0.1",
"react": "^16.0.0",
"react-dev-utils": "^10.2.1",
"react-hot-loader": "^4.12.21",
"recursive-readdir": "^2.2.2",
"replace-in-file": "^6.0.0",
"rollup": "2.10.x",
"rollup": "2.23.x",
"rollup-plugin-dts": "^1.4.6",
"rollup-plugin-esbuild": "^2.0.0",
"rollup-plugin-image-files": "^1.4.2",
+8 -1
View File
@@ -39,5 +39,12 @@ export const pre = async () => {
export const post = async () => {
// postpack isn't called by yarn right now, so it needs to be called manually
await fs.move(PKG_BACKUP_PATH, PKG_PATH, { overwrite: true });
try {
await fs.move(PKG_BACKUP_PATH, PKG_PATH, { overwrite: true });
} catch (error) {
console.warn(
`Failed to restore package.json during postpack, ${error}. ` +
'Your package will be fine but you may have ended up with some garbage in the repo.',
);
}
};
-11
View File
@@ -22,17 +22,6 @@ import { version } from './lib/version';
const main = (argv: string[]) => {
program.name('backstage-cli').version(version);
program
.command('create-app')
.description('Creates a new app in a new directory')
.option(
'--skip-install',
'Skip the install and builds steps after creating the app',
)
.action(
lazyAction(() => import('./commands/create-app/createApp'), 'default'),
);
program
.command('app:build')
.description('Build an app for a production release')
+1 -1
View File
@@ -92,7 +92,7 @@ export async function runCheck(cmd: string, ...args: string[]) {
}
export async function waitForExit(
child: ChildProcess & { exitCode?: number },
child: ChildProcess & { exitCode: number | null },
name?: string,
): Promise<void> {
if (typeof child.exitCode === 'number') {
@@ -1,6 +0,0 @@
app:
title: Scaffolded Backstage App
baseUrl: http://localhost:3000
organization:
name: Acme Corporation
@@ -1,3 +0,0 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
@@ -1,9 +0,0 @@
import { createPlugin } from '@backstage/core';
import WelcomePage from './components/WelcomePage';
export const plugin = createPlugin({
id: 'welcome',
register({ router }) {
router.registerRoute('/', WelcomePage);
},
});
@@ -1,8 +0,0 @@
{
"extends": "@backstage/cli/config/tsconfig.json",
"include": ["packages/*/src", "plugins/*/src", "plugins/*/dev"],
"exclude": ["**/node_modules"],
"compilerOptions": {
"outDir": "dist"
}
}
@@ -28,7 +28,7 @@
"@material-ui/lab": "4.0.0-alpha.45",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-use": "^14.2.0"
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^{{version}}",
@@ -36,7 +36,7 @@
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^25.2.2",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"jest-fetch-mock": "^3.0.3"
},
+1 -1
View File
@@ -36,7 +36,7 @@
"yup": "^0.29.1"
},
"devDependencies": {
"@types/jest": "^25.2.2",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"@types/yup": "^0.28.2"
},
+44 -7
View File
@@ -40,14 +40,15 @@ describe('readEnv', () => {
APP_CONFIG_numbers_a: '1',
APP_CONFIG_numbers_b: '2',
APP_CONFIG_numbers_c: 'false',
APP_CONFIG_numbers_d: undefined,
APP_CONFIG_numbers_d: 'abc',
APP_CONFIG_numbers_e: undefined,
APP_CONFIG_very_deep_nested_config_object: '{}',
}),
).toEqual([
{
data: {
foo: 'bar',
numbers: { a: 1, b: 2, c: false },
numbers: { a: 1, b: 2, c: false, d: 'abc' },
very: { deep: { nested: { config: { object: {} } } } },
},
context: 'env',
@@ -55,6 +56,37 @@ describe('readEnv', () => {
]);
});
it('should accept string values', () => {
expect(readEnv({ APP_CONFIG_foo: '"abc"', APP_CONFIG_bar: 'xyz' })).toEqual(
[
{
data: {
foo: 'abc',
bar: 'xyz',
},
context: 'env',
},
],
);
});
it('should accept complex objects', () => {
expect(
readEnv({
APP_CONFIG_foo: '{ "a": 123, "b": "123", "c": [] }',
APP_CONFIG_bar: '[123, "abc", {}]',
}),
).toEqual([
{
data: {
foo: { a: 123, b: '123', c: [] },
bar: [123, 'abc', {}],
},
context: 'env',
},
]);
});
it.each([
['APP_CONFIG__foo'],
['APP_CONFIG_foo_'],
@@ -68,12 +100,17 @@ describe('readEnv', () => {
);
});
it.each([['hello'], ['"hello'], ['{'], ['}'], ['123abc']])(
'should reject invalid value %p',
it.each([['hello'], ['"hello'], ['{'], ['}']])(
'should fallback to string when invalid json value %p',
value => {
expect(() => readEnv({ APP_CONFIG_foo: value })).toThrow(
/^Failed to parse JSON-serialized config value for key 'foo', SyntaxError: /,
);
expect(readEnv({ APP_CONFIG_foo: value })).toEqual([
{
data: {
foo: value,
},
context: 'env',
},
]);
},
);
+9 -1
View File
@@ -72,7 +72,7 @@ export function readEnv(env: {
);
}
try {
const parsedValue = JSON.parse(value);
const [, parsedValue] = safeJsonParse(value);
if (parsedValue === null) {
throw new Error('value may not be null');
}
@@ -89,3 +89,11 @@ export function readEnv(env: {
return data ? [{ data, context: 'env' }] : [];
}
function safeJsonParse(str: string): [Error | null, any] {
try {
return [null, JSON.parse(str)];
} catch (err) {
return [err, str];
}
}
+1 -1
View File
@@ -33,7 +33,7 @@
"lodash": "^4.17.15"
},
"devDependencies": {
"@types/jest": "^25.2.2",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0"
},
"files": [
+2 -2
View File
@@ -37,7 +37,7 @@
"prop-types": "^15.7.2",
"react": "^16.12.0",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^14.2.0",
"react-use": "^15.3.3",
"zen-observable": "^0.8.15"
},
"devDependencies": {
@@ -46,7 +46,7 @@
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^25.2.2",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"@types/zen-observable": "^0.8.0",
"jest-fetch-mock": "^3.0.3"
@@ -137,7 +137,7 @@ describe('RefreshingAuthSessionManager', () => {
expect(refreshSession).toBeCalledTimes(1);
});
it('should forward option to instantly show auth popup', async () => {
it('should forward option to instantly show auth popup and not attempt refresh', async () => {
const createSession = jest.fn();
const refreshSession = jest.fn().mockRejectedValue(new Error('NOPE'));
const manager = new RefreshingAuthSessionManager({
@@ -151,7 +151,7 @@ describe('RefreshingAuthSessionManager', () => {
scopes: new Set(),
instantPopup: true,
});
expect(refreshSession).toBeCalledTimes(1);
expect(refreshSession).toBeCalledTimes(0);
});
it('should remove session straight away', async () => {
@@ -90,7 +90,12 @@ export class RefreshingAuthSessionManager<T> implements SessionManager<T> {
// The user may still have a valid refresh token in their cookies. Attempt to
// initiate a fresh session through the backend using that refresh token.
if (!this.currentSession) {
//
// We skip this check if an instant login popup is requested, as we need to
// stay in a synchronous call stack from the user interaction. The downside
// is that that the user will sometimes be requested to log in even if they
// already had an existing session.
if (!this.currentSession && !options.instantPopup) {
try {
const newSession = await this.collapsedSessionRefresh();
this.currentSession = newSession;
+2 -2
View File
@@ -51,7 +51,7 @@
"react-router-dom": "6.0.0-beta.0",
"react-sparklines": "^1.7.0",
"react-syntax-highlighter": "^12.2.1",
"react-use": "^14.2.0"
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.16",
@@ -61,7 +61,7 @@
"@testing-library/user-event": "^12.0.7",
"@types/classnames": "^2.2.9",
"@types/google-protobuf": "^3.7.2",
"@types/jest": "^25.2.2",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"@types/react-helmet": "^5.0.15",
"@types/zen-observable": "^0.8.0",
@@ -24,6 +24,7 @@ import { useNavigate } from 'react-router';
interface IErrorPageProps {
status: string;
statusMessage: string;
additionalInfo?: string;
}
const useStyles = makeStyles<BackstageTheme>(theme => ({
@@ -38,7 +39,11 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
},
}));
export const ErrorPage = ({ status, statusMessage }: IErrorPageProps) => {
export const ErrorPage = ({
status,
statusMessage,
additionalInfo,
}: IErrorPageProps) => {
const classes = useStyles();
const navigate = useNavigate();
@@ -49,6 +54,9 @@ export const ErrorPage = ({ status, statusMessage }: IErrorPageProps) => {
<Typography variant="body1" className={classes.subtitle}>
ERROR {status}: {statusMessage}
</Typography>
<Typography variant="body1" className={classes.subtitle}>
{additionalInfo}
</Typography>
<Typography variant="h2" className={classes.title}>
Looks like someone dropped the mic!
</Typography>
@@ -85,6 +85,14 @@ export const App = () => (
</Page>
);
export const Documentation = () => (
<Page theme={pageTheme.documentation}>
<Header title="Documentation component page" type="documentation">
{labels}
</Header>
</Page>
);
export const Other = () => (
<Page theme={pageTheme.other}>
<Header title="Other/generic component page" type="other">
@@ -65,6 +65,11 @@ export const gradients: Record<string, Gradient> = {
waveColor: '#9BF0E1',
opacity: ['0.72', '0.0'],
},
pinkSea: {
colors: ['#C8077A', '#C2297D'],
waveColor: '#ea93c3',
opacity: ['0.8', '0.0'],
},
};
export const pageTheme: Record<string, PageTheme> = {
@@ -72,7 +77,7 @@ export const pageTheme: Record<string, PageTheme> = {
gradient: gradients.teal,
},
documentation: {
gradient: gradients.eveningSea,
gradient: gradients.pinkSea,
},
tool: {
gradient: gradients.purpleSky,
+14 -6
View File
@@ -129,19 +129,27 @@ const recentlyViewedIntroText =
export const SidebarIntro: FC = () => {
const { isOpen } = useContext(SidebarContext);
const [
{ starredItemsDismissed, recentlyViewedItemsDismissed },
setDismissedIntro,
] = useLocalStorage<SidebarIntroLocalStorage>(SIDEBAR_INTRO_LOCAL_STORAGE, {
const defaultValue = {
starredItemsDismissed: false,
recentlyViewedItemsDismissed: false,
});
};
const [dismissedIntro, setDismissedIntro] = useLocalStorage<
SidebarIntroLocalStorage
>(SIDEBAR_INTRO_LOCAL_STORAGE);
const { starredItemsDismissed, recentlyViewedItemsDismissed } =
dismissedIntro ?? {};
const dismissStarred = () => {
setDismissedIntro(state => ({ ...state, starredItemsDismissed: true }));
setDismissedIntro(state => ({
...defaultValue,
...state,
starredItemsDismissed: true,
}));
};
const dismissRecentlyViewed = () => {
setDismissedIntro(state => ({
...defaultValue,
...state,
recentlyViewedItemsDismissed: true,
}));
+3 -2
View File
@@ -17,13 +17,14 @@
export * from './Content';
export * from './ContentHeader';
export * from './ErrorBoundary';
export * from './ErrorPage';
export * from './Header';
export * from './HeaderLabel';
export * from './HeaderTabs';
export * from './HomepageTimer';
export * from './InfoCard';
export * from './ItemCard';
export * from './Page';
export * from './Sidebar';
export * from './SignInPage';
export * from './TabbedCard';
export * from './HeaderTabs';
export * from './ItemCard';
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
ignorePatterns: ['templates/**'],
rules: {
'no-console': 0,
},
};
+17
View File
@@ -0,0 +1,17 @@
# @backstage/create-app
This package provides a CLI for creating apps.
You can use the flag `--skip-install` to skip the install.
## Installation
With `npx`:
```sh
$ npx @backstage/create-app
```
## Documentation
- [Backstage Readme](https://github.com/spotify/backstage/blob/master/README.md)
- [Backstage Documentation](https://github.com/spotify/backstage/blob/master/docs/README.md)
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env node
/*
* 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.
*/
const path = require('path');
// Figure out whether we're running inside the backstage repo or as an installed dependency
const isLocal = require('fs').existsSync(path.resolve(__dirname, '../src'));
if (!isLocal || process.env.BACKSTAGE_E2E_CLI_TEST) {
require('..');
} else {
require('ts-node').register({
transpileOnly: true,
compilerOptions: {
module: 'CommonJS',
},
});
require('../src');
}
+49
View File
@@ -0,0 +1,49 @@
{
"name": "@backstage/create-app",
"description": "Create app package for Backstage",
"version": "0.1.1-alpha.16",
"private": false,
"publishConfig": {
"access": "public"
},
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/spotify/backstage",
"directory": "packages/create-app"
},
"keywords": [
"backstage"
],
"license": "Apache-2.0",
"main": "dist/index.cjs.js",
"bin": {
"backstage-create-app": "bin/backstage-create-app"
},
"scripts": {
"build": "backstage-cli build --outputs cjs",
"lint": "backstage-cli lint"
},
"dependencies": {
"commander": "^4.1.1",
"chalk": "^4.0.0",
"fs-extra": "^9.0.0",
"handlebars": "^4.7.3",
"inquirer": "^7.0.4",
"ora": "^4.0.3",
"recursive-readdir": "^2.2.2"
},
"devDependencies": {
"@types/fs-extra": "^9.0.1",
"@types/react-dev-utils": "^9.0.4",
"@types/inquirer": "^6.5.0",
"@types/recursive-readdir": "^2.2.0",
"@types/ora": "^3.2.0",
"ts-node": "^8.6.2"
},
"files": [
"bin",
"dist",
"templates"
]
}
@@ -22,9 +22,9 @@ import inquirer, { Answers, Question } from 'inquirer';
import { exec as execCb } from 'child_process';
import { resolve as resolvePath } from 'path';
import os from 'os';
import { Task, templatingTask } from '../../lib/tasks';
import { paths } from '../../lib/paths';
import { version } from '../../lib/version';
import { Task, templatingTask } from './lib/tasks';
import { paths } from './lib/paths';
import { version } from './lib/version';
const exec = promisify(execCb);
@@ -104,8 +104,17 @@ export default async (cmd: Command): Promise<void> => {
return true;
},
},
{
type: 'list',
name: 'dbType',
message: chalk.blue('Select database for the backend [required]'),
// @ts-ignore
choices: ['PostgreSQL', 'SQLite'],
},
];
const answers: Answers = await inquirer.prompt(questions);
answers.dbTypePG = answers.dbType === 'PostgreSQL';
answers.dbTypeSqlite = answers.dbType === 'SQLite';
const templateDir = paths.resolveOwn('templates/default-app');
const tempDir = resolvePath(os.tmpdir(), answers.name);
+49
View File
@@ -0,0 +1,49 @@
/*
* 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 program from 'commander';
import chalk from 'chalk';
import { exitWithError } from './lib/errors';
import { version } from './lib/version';
import createApp from './createApp';
const main = (argv: string[]) => {
program.name('backstage-create-app').version(version);
program
.description('Creates a new app in a new directory')
.option(
'--skip-install',
'Skip the install and builds steps after creating the app',
)
.action(createApp)
if (!process.argv.slice(2).length) {
program.outputHelp(chalk.yellow);
}
program.parse(argv);
};
process.on('unhandledRejection', rejection => {
if (rejection instanceof Error) {
exitWithError(rejection);
} else {
exitWithError(new Error(`Unknown rejection: '${rejection}'`));
}
});
main(process.argv);
+46
View File
@@ -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 chalk from 'chalk';
export class CustomError extends Error {
get name(): string {
return this.constructor.name;
}
}
export class ExitCodeError extends CustomError {
readonly code: number;
constructor(code: number, command?: string) {
if (command) {
super(`Command '${command}' exited with code ${code}`);
} else {
super(`Child exited with code ${code}`);
}
this.code = code;
}
}
export function exitWithError(error: Error): never {
if (error instanceof ExitCodeError) {
process.stderr.write(`\n${chalk.red(error.message)}\n\n`);
process.exit(error.code);
} else {
process.stderr.write(`\n${chalk.red(`${error}`)}\n\n`);
process.exit(1);
}
}
+150
View File
@@ -0,0 +1,150 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs-extra';
import { dirname, resolve as resolvePath } from 'path';
export type ResolveFunc = (...paths: string[]) => string;
// Common paths and resolve functions used by the cli.
// Currently assumes it is being executed within a monorepo.
export type Paths = {
// Root dir of the cli itself, containing package.json
ownDir: string;
// Monorepo root dir of the cli itself. Only accessible when running inside Backstage repo.
ownRoot: string;
// The location of the app that the cli is being executed in
targetDir: string;
// The monorepo root package of the app that the cli is being executed in.
targetRoot: string;
// Resolve a path relative to own repo
resolveOwn: ResolveFunc;
// Resolve a path relative to own monorepo root. Only accessible when running inside Backstage repo.
resolveOwnRoot: ResolveFunc;
// Resolve a path relative to the app
resolveTarget: ResolveFunc;
// Resolve a path relative to the app repo root
resolveTargetRoot: ResolveFunc;
};
// Looks for a package.json that has name: "root" to identify the root of the monorepo
export function findRootPath(topPath: string): string {
let path = topPath;
// Some sanity check to avoid infinite loop
for (let i = 0; i < 1000; i++) {
const packagePath = resolvePath(path, 'package.json');
const exists = fs.pathExistsSync(packagePath);
if (exists) {
try {
const data = fs.readJsonSync(packagePath);
if (data.name === 'root' || data.name.includes('backstage-e2e')) {
return path;
}
} catch (error) {
throw new Error(
`Failed to parse package.json file while searching for root, ${error}`,
);
}
}
const newPath = dirname(path);
if (newPath === path) {
throw new Error(
`No package.json with name "root" found as a parent of ${topPath}`,
);
}
path = newPath;
}
throw new Error(
`Iteration limit reached when searching for root package.json at ${topPath}`,
);
}
// Finds the root of the cli package itself
export function findOwnDir() {
// Known relative locations of package in dist/dev
const pathDist = '..';
const pathDev = '../..';
// Check the closest dir first
const pkgInDist = resolvePath(__dirname, pathDist, 'package.json');
const isDist = fs.pathExistsSync(pkgInDist);
const path = isDist ? pathDist : pathDev;
return resolvePath(__dirname, path);
}
// Finds the root of the monorepo that the cli exists in. Only accessible when running inside Backstage repo.
export function findOwnRootPath(ownDir: string) {
const isLocal = fs.pathExistsSync(resolvePath(ownDir, 'src'));
if (!isLocal) {
throw new Error(
'Tried to access monorepo package root dir outside of Backstage repository',
);
}
return resolvePath(ownDir, '../..');
}
export function findPaths(): Paths {
const ownDir = findOwnDir();
const targetDir = fs.realpathSync(process.cwd());
// Lazy load this as it will throw an error if we're not inside the Backstage repo.
let ownRoot = '';
const getOwnRoot = () => {
if (!ownRoot) {
ownRoot = findOwnRootPath(ownDir);
}
return ownRoot;
};
// We're not always running in a monorepo, so we lazy init this to only crash commands
// that require a monorepo when we're not in one.
let targetRoot = '';
const getTargetRoot = () => {
if (!targetRoot) {
targetRoot = findRootPath(targetDir);
}
return targetRoot;
};
return {
ownDir,
get ownRoot() {
return getOwnRoot();
},
targetDir,
get targetRoot() {
return getTargetRoot();
},
resolveOwn: (...paths) => resolvePath(ownDir, ...paths),
resolveOwnRoot: (...paths) => resolvePath(getOwnRoot(), ...paths),
resolveTarget: (...paths) => resolvePath(targetDir, ...paths),
resolveTargetRoot: (...paths) => resolvePath(getTargetRoot(), ...paths),
};
}
export const paths = findPaths();
+105
View File
@@ -0,0 +1,105 @@
/*
* 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 chalk from 'chalk';
import fs from 'fs-extra';
import handlebars from 'handlebars';
import ora from 'ora';
import { basename, dirname } from 'path';
import recursive from 'recursive-readdir';
const TASK_NAME_MAX_LENGTH = 14;
export class Task {
static log(name: string = '') {
process.stdout.write(`${chalk.green(name)}\n`);
}
static error(message: string = '') {
process.stdout.write(`\n${chalk.red(message)}\n\n`);
}
static section(name: string) {
const title = chalk.green(`${name}:`);
process.stdout.write(`\n ${title}\n`);
}
static exit(code: number = 0) {
process.exit(code);
}
static async forItem(
task: string,
item: string,
taskFunc: () => Promise<void>,
): Promise<void> {
const paddedTask = chalk.green(task.padEnd(TASK_NAME_MAX_LENGTH));
const spinner = ora({
prefixText: chalk.green(` ${paddedTask}${chalk.cyan(item)}`),
spinner: 'arc',
color: 'green',
}).start();
try {
await taskFunc();
spinner.succeed();
} catch (error) {
spinner.fail();
throw error;
}
}
}
export async function templatingTask(
templateDir: string,
destinationDir: string,
context: any,
) {
const files = await recursive(templateDir).catch(error => {
throw new Error(`Failed to read template directory: ${error.message}`);
});
for (const file of files) {
const destinationFile = file.replace(templateDir, destinationDir);
await fs.ensureDir(dirname(destinationFile));
if (file.endsWith('.hbs')) {
await Task.forItem('templating', basename(file), async () => {
const destination = destinationFile.replace(/\.hbs$/, '');
const template = await fs.readFile(file);
const compiled = handlebars.compile(template.toString());
const contents = compiled({ name: basename(destination), ...context });
await fs.writeFile(destination, contents).catch(error => {
throw new Error(
`Failed to create file: ${destination}: ${error.message}`,
);
});
});
} else {
await Task.forItem('copying', basename(file), async () => {
await fs.copyFile(file, destinationFile).catch(error => {
const destination = destinationFile;
throw new Error(
`Failed to copy file to ${destination} : ${error.message}`,
);
});
});
}
}
}
+26
View File
@@ -0,0 +1,26 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs-extra';
import { paths } from './paths';
export function findVersion() {
const pkgContent = fs.readFileSync(paths.resolveOwn('package.json'), 'utf8');
return JSON.parse(pkgContent).version;
}
export const version = findVersion();
export const isDev = fs.pathExistsSync(paths.resolveOwn('src'));
@@ -0,0 +1,3 @@
module.exports = {
root: true,
};
@@ -0,0 +1,22 @@
app:
title: Scaffolded Backstage App
baseUrl: http://localhost:3000
organization:
name: Acme Corporation
backend:
baseUrl: http://localhost:7000
listen: 0.0.0.0:7000
cors:
origin: http://localhost:3000
methods: [GET, POST, PUT, DELETE]
credentials: true
proxy:
'/test':
target: 'https://example.com'
changeOrigin: true
techdocs:
storageUrl: https://techdocs-mock-sites.storage.googleapis.com
@@ -16,13 +16,13 @@
"react-dom": "^16.13.1",
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^14.2.0"
"react-use": "^15.3.3"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^25.2.2",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"@types/react-dom": "^16.9.8",
"cross-env": "^7.0.0",

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
};
@@ -0,0 +1,9 @@
FROM node:12
WORKDIR /usr/src/app
COPY . .
RUN yarn install --frozen-lockfile --production
CMD ["node", "packages/backend"]
@@ -0,0 +1,68 @@
# example-backend
This package is an EXAMPLE of a Backstage backend.
The main purpose of this package is to provide a test bed for Backstage plugins
that have a backend part. Feel free to experiment locally or within your fork
by adding dependencies and routes to this backend, to try things out.
Our goal is to eventually amend the create-app flow of the CLI, such that a
production ready version of a backend skeleton is made alongside the frontend
app. Until then, feel free to experiment here!
## Development
To run the example backend, first go to the project root and run
```bash
yarn install
yarn tsc
yarn build
```
You should only need to do this once.
After that, go to the `packages/backend` directory and run
```bash
AUTH_GOOGLE_CLIENT_ID=x AUTH_GOOGLE_CLIENT_SECRET=x \
AUTH_GITHUB_CLIENT_ID=x AUTH_GITHUB_CLIENT_SECRET=x \
AUTH_OAUTH2_CLIENT_ID=x AUTH_OAUTH2_CLIENT_SECRET=x \
AUTH_OAUTH2_AUTH_URL=x AUTH_OAUTH2_TOKEN_URL=x \
LOG_LEVEL=debug \
yarn start
```
Substitute `x` for actual values, or leave them as
dummy values just to try out the backend without using the auth or sentry features.
The backend starts up on port 7000 per default.
## Populating The Catalog
If you want to use the catalog functionality, you need to add so called locations
to the backend. These are places where the backend can find some entity descriptor
data to consume and serve.
To get started, you can issue the following after starting the backend, from inside
the `plugins/catalog-backend` directory:
```bash
yarn mock-data
```
You should then start seeing data on `localhost:7000/catalog/entities`.
The catalog currently runs in-memory only, so feel free to try it out, but it will
need to be re-populated on next startup.
## Authentication
We chose [Passport](http://www.passportjs.org/) as authentication platform due to its comprehensive set of supported authentication [strategies](http://www.passportjs.org/packages/).
Read more about the [auth-backend](https://github.com/spotify/backstage/blob/master/plugins/auth-backend/README.md) and [how to add a new provider](https://github.com/spotify/backstage/blob/master/docs/auth/add-auth-provider.md)
## Documentation
- [Backstage Readme](https://github.com/spotify/backstage/blob/master/README.md)
- [Backstage Documentation](https://github.com/spotify/backstage/blob/master/docs/README.md)
@@ -0,0 +1,51 @@
{
"name": "backend",
"version": "0.0.0",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"private": true,
"engines": {
"node": "12"
},
"scripts": {
"build": "backstage-cli backend:build",
"build-image": "backstage-cli backend:build-image example-backend",
"start": "backstage-cli backend:dev",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"clean": "backstage-cli clean",
"migrate:create": "knex migrate:make -x ts"
},
"dependencies": {
"@backstage/backend-common": "^{{version}}",
"@backstage/catalog-model": "^{{version}}",
"@backstage/config": "^0.1.1-alpha.13",
"@backstage/config-loader": "^0.1.1-alpha.13",
"@backstage/plugin-auth-backend": "^{{version}}",
"@backstage/plugin-catalog-backend": "^{{version}}",
"@backstage/plugin-identity-backend": "^{{version}}",
"@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",
"express": "^4.17.1",
"knex": "^0.21.1",
{{#if dbTypePG}}
"pg": "^8.3.0",
{{/if}}
{{#if dbTypeSqlite}}
"sqlite3": "^4.2.0",
{{/if}}
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.15",
"@types/dockerode": "^2.5.32",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",
"@types/helmet": "^0.0.47"
}
}
@@ -0,0 +1,24 @@
/*
* 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 { PluginEnvironment } from './types';
describe('test', () => {
it('unbreaks the test runner', () => {
const unbreaker = {} as PluginEnvironment;
expect(unbreaker).toBeTruthy();
});
});
@@ -0,0 +1,111 @@
/*
* 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.
*/
/*
* Hi!
*
* Note that this is an EXAMPLE Backstage backend. Please check the README.
*
* Happy hacking!
*/
import {
createServiceBuilder,
getRootLogger,
useHotMemoize,
} from '@backstage/backend-common';
import { ConfigReader, AppConfig } from '@backstage/config';
import { loadConfig } from '@backstage/config-loader';
{{#if dbTypePG}}
import knex, { PgConnectionConfig } from 'knex';
{{/if}}
{{#if dbTypeSqlite}}
import knex from 'knex';
{{/if}}
import auth from './plugins/auth';
import catalog from './plugins/catalog';
import identity from './plugins/identity';
import scaffolder from './plugins/scaffolder';
import proxy from './plugins/proxy';
import techdocs from './plugins/techdocs';
import { PluginEnvironment } from './types';
function makeCreateEnv(loadedConfigs: AppConfig[]) {
const config = ConfigReader.fromConfigs(loadedConfigs);
return (plugin: string): PluginEnvironment => {
const logger = getRootLogger().child({ type: 'plugin', plugin });
{{#if dbTypePG}}
const knexConfig = {
client: 'pg',
useNullAsDefault: true,
connection: {
port: process.env.POSTGRES_PORT,
host: process.env.POSTGRES_HOST,
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
database: `backstage_plugin_${plugin}`,
} as PgConnectionConfig,
};
{{/if}}
{{#if dbTypeSqlite}}
const knexConfig = {
client: 'sqlite3',
connection: ':memory:',
useNullAsDefault: true,
};
{{/if}}
const database = knex(knexConfig);
database.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
resource.run('PRAGMA foreign_keys = ON', () => {});
});
return { logger, database, config };
};
}
async function main() {
const configs = await loadConfig();
const configReader = ConfigReader.fromConfigs(configs);
const createEnv = makeCreateEnv(configs);
const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
const authEnv = useHotMemoize(module, () => createEnv('auth'));
const identityEnv = useHotMemoize(module, () => createEnv('identity'));
const proxyEnv = useHotMemoize(module, () => createEnv('proxy'));
const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
const service = createServiceBuilder(module)
.loadConfig(configReader)
.addRouter('/catalog', await catalog(catalogEnv))
.addRouter('/scaffolder', await scaffolder(scaffolderEnv))
.addRouter('/auth', await auth(authEnv))
.addRouter('/identity', await identity(identityEnv))
.addRouter('/techdocs', await techdocs(techdocsEnv))
.addRouter('/proxy', await proxy(proxyEnv));
await service.start().catch(err => {
console.log(err);
process.exit(1);
});
}
module.hot?.accept();
main().catch(error => {
console.error(`Backend failed to start up, ${error}`);
process.exit(1);
});
@@ -0,0 +1,26 @@
/*
* 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 { createRouter } from '@backstage/plugin-auth-backend';
import { PluginEnvironment } from '../types';
export default async function createPlugin({
logger,
database,
config,
}: PluginEnvironment) {
return await createRouter({ logger, config, database });
}
@@ -0,0 +1,56 @@
/*
* 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 {
createRouter,
DatabaseEntitiesCatalog,
DatabaseLocationsCatalog,
DatabaseManager,
HigherOrderOperations,
LocationReaders,
runPeriodically,
} from '@backstage/plugin-catalog-backend';
import { PluginEnvironment } from '../types';
import { useHotCleanup } from '@backstage/backend-common';
export default async function createPlugin({
logger,
database,
}: PluginEnvironment) {
const locationReader = new LocationReaders(logger);
const db = await DatabaseManager.createDatabase(database, { logger });
const entitiesCatalog = new DatabaseEntitiesCatalog(db);
const locationsCatalog = new DatabaseLocationsCatalog(db);
const higherOrderOperation = new HigherOrderOperations(
entitiesCatalog,
locationsCatalog,
locationReader,
logger,
);
useHotCleanup(
module,
runPeriodically(() => higherOrderOperation.refreshAllLocations(), 10000),
);
return await createRouter({
entitiesCatalog,
locationsCatalog,
higherOrderOperation,
logger,
});
}
@@ -0,0 +1,22 @@
/*
* 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 { createRouter } from '@backstage/plugin-identity-backend';
import { PluginEnvironment } from '../types';
export default async function createPlugin({ logger }: PluginEnvironment) {
return await createRouter({ logger });
}
@@ -0,0 +1,25 @@
/*
* 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.
*/
// @ts-ignore
import { createRouter } from '@backstage/plugin-proxy-backend';
import { PluginEnvironment } from '../types';
export default async function createPlugin({
logger,
config,
}: PluginEnvironment) {
return await createRouter({ logger, config });
}
@@ -0,0 +1,56 @@
/*
* 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 {
CookieCutter,
createRouter,
FilePreparer,
GithubPreparer,
Preparers,
GithubPublisher,
CreateReactAppTemplater,
Templaters,
} from '@backstage/plugin-scaffolder-backend';
import { Octokit } from '@octokit/rest';
import type { PluginEnvironment } from '../types';
import Docker from 'dockerode';
export default async function createPlugin({ logger }: PluginEnvironment) {
const cookiecutterTemplater = new CookieCutter();
const craTemplater = new CreateReactAppTemplater();
const templaters = new Templaters();
templaters.register('cookiecutter', cookiecutterTemplater);
templaters.register('cra', craTemplater);
const filePreparer = new FilePreparer();
const githubPreparer = new GithubPreparer();
const preparers = new Preparers();
preparers.register('file', filePreparer);
preparers.register('github', githubPreparer);
const githubClient = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN });
const publisher = new GithubPublisher({ client: githubClient });
const dockerClient = new Docker();
return await createRouter({
preparers,
templaters,
publisher,
logger,
dockerClient,
});
}
@@ -0,0 +1,22 @@
/*
* 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 { createRouter } from '@backstage/plugin-techdocs-backend';
import { PluginEnvironment } from '../types';
export default async function createPlugin({ logger }: PluginEnvironment) {
return await createRouter({ logger });
}
@@ -0,0 +1,25 @@
/*
* 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 Knex from 'knex';
import { Logger } from 'winston';
import { Config } from '@backstage/config';
export type PluginEnvironment = {
logger: Logger;
database: Knex;
config: Config;
};
@@ -25,7 +25,7 @@
"@material-ui/icons": "^4.9.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-use": "^14.2.0",
"react-use": "^15.3.3",
"react-router-dom": "6.0.0-beta.0"
},
"devDependencies": {

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