Solved merge conflicts

This commit is contained in:
ebarrios
2020-09-21 10:09:21 +02:00
116 changed files with 3509 additions and 1149 deletions
+2
View File
@@ -3,6 +3,8 @@ name: E2E Test Linux
on:
pull_request:
paths-ignore:
- 'contrib/**'
- 'docs/**'
- 'microsite/**'
jobs:
+8 -2
View File
@@ -6,13 +6,15 @@ If you encounter issues while upgrading to a newer version, don't hesitate to re
## Next Release
> Collect changes for the next release below
## v0.1.1-alpha.22
### @backstage/core
- Introduced initial version of an inverted app/plugin relationship, where plugins export components for apps to use, instead registering themselves directly into the app. This enables more fine-grained control of plugin features, and also composition of plugins such as catalog pages with additional cards and tabs. This breaks the use of `RouteRef`s, and there will be more changes related to this in the future, but this change lays the initial foundation. See `packages/app` and followup PRs for how to update plugins for this change. [#2076](https://github.com/spotify/backstage/pull/2076)
- Switch to an automatic dependency injection mechanism for all Utility APIs, allowing plugins to ship default implementations of their APIs. See [https://backstage.io/docs/api/utility-apis](https://backstage.io/docs/api/utility-apis). [#2285](https://github.com/spotify/backstage/pull/2285)
> Collect changes for the next release below
### @backstage/cli
- Change `backstage-cli backend:build-image` to forward all args to `docker image build`, instead of just tag. Also add `--build` flag for building all dependent packages before packaging the workspace for the docker build. [#2299](https://github.com/spotify/backstage/pull/2299)
@@ -21,6 +23,10 @@ If you encounter issues while upgrading to a newer version, don't hesitate to re
- Change root `tsc` output dir to `dist-types`, in order to allow for standalone plugin repos. [#2278](https://github.com/spotify/backstage/pull/2278)
### @backstage/catalog-backend
- We have simplified the way that GitHub ingestion works. The `catalog.processors.githubApi` key is deprecated, in favor of `catalog.processors.github`. At the same time, the location type `github/api` is likewise deprecated, in favor of `github`. This location type now serves both raw HTTP reads and APIv3 reads, depending on how you configure it. It also supports having several providers at once - for example, both public GitHub and an internal GitHub Enterprise, with different keys. If you still use the `catalog.processors.githubApi` config key, things will work but you will get a deprecation warning at startup. In a later release, support for the old key will go away entirely. See the [configuration section in the docs](https://backstage.io/docs/features/software-catalog/configuration) for more details.
## v0.1.1-alpha.21
- Added many more frontend plugins to the template along with the sidebar. [#1942](https://github.com/spotify/backstage/pull/1942), [#2084](https://github.com/spotify/backstage/pull/2084)
+9 -5
View File
@@ -35,6 +35,8 @@ organization:
techdocs:
storageUrl: http://localhost:7000/techdocs/static/docs
requestUrl: http://localhost:7000/techdocs/docs
generators:
techdocs: 'docker'
sentry:
organization: spotify
@@ -58,21 +60,23 @@ catalog:
- allow: [Component, API, Group, Template, Location]
processors:
github:
privateToken:
$secret:
env: GITHUB_PRIVATE_TOKEN
githubApi:
providers:
- target: https://github.com
token:
$secret:
env: GITHUB_PRIVATE_TOKEN
# Example for how to add your GitHub Enterprise instance:
#### Example for how to add your GitHub Enterprise instance using the API:
# - target: https://ghe.example.net
# apiBaseUrl: https://ghe.example.net/api/v3
# token:
# $secret:
# env: GHE_PRIVATE_TOKEN
#### Example for how to add your GitHub Enterprise instance using raw HTTP fetches (token is optional):
# - target: https://ghe.example.net
# rawBaseUrl: https://ghe.example.net/raw
# token:
# $secret:
# env: GHE_PRIVATE_TOKEN
bitbucketApi:
username:
$secret:
+9
View File
@@ -0,0 +1,9 @@
# Backstage Contrib
This directory contains various community contributions related to Backstage.
Unless otherwise specified, all content in this hierarchy fall under the same
[licensing terms](../LICENSE) as in the rest of the repository, and come with
no guarantees of functionality or fitness of purpose. That being said, we
really appreciate contributions in here and encourage them being kept up to
date.
@@ -0,0 +1,23 @@
FROM node:12 AS build
RUN mkdir /app
COPY . /app
WORKDIR /app
RUN yarn install
RUN yarn workspace example-app build
# Contruct backstage-frontend image
FROM nginx:mainline
RUN apt-get update && apt-get -y install jq && rm -rf /var/lib/apt/lists/*
# Copy from build stage
COPY --from=build /app/packages/app/dist /usr/share/nginx/html
COPY docker/default.conf.template /etc/nginx/conf.d/default.conf.template
COPY docker/run.sh /usr/local/bin/run.sh
CMD run.sh
ENV PORT 80
@@ -0,0 +1,19 @@
# Standalone Dockerfile for frontend
This directory contains the resources which will help you build backstage without any requirements
other than docker itself. It uses a multi-stage Dockerfile to build and ship backstage.
## Usage
You can simply run the following command to build backstage.
```
# Make sure you are in the root directory of backstage then run
docker build -t backstage-frontend -f ./contrib/docker/multi-stage-frontend/Dockerfile .
```
After a successful build, You can simply run backstage frontend with the following command.
```
docker run -it --rm -p 3080:80 backstage-frontend
```
@@ -0,0 +1 @@
# Basic Kubernetes example with Helm
@@ -4,6 +4,67 @@ title: Catalog Configuration
description: Documentation on Software Catalog Configuration
---
## Processors
The catalog makes use of so called processors to perform all kinds of ingestion
tasks, such as reading raw entity data from a remote source, parsing it,
transforming it, and validating it. These processors are configured under the
`catalog.processors` key.
### Processor: github
The `github` processor is responsible for fetching entity data from files on
GitHub or GitHub Enterprise. The configuration for this processor lives under
`catalog.processors.github`. Example:
```yaml
catalog:
processors:
github:
providers:
- target: https://github.com
token:
$secret:
env: GITHUB_PRIVATE_TOKEN
- target: https://ghe.example.net
apiBaseUrl: https://ghe.example.net/api/v3
rawBaseUrl: https://ghe.example.net/raw
token:
$secret:
env: GHE_PRIVATE_TOKEN
```
The main subkey is `providers`, where you can list the various GitHub compatible
providers you want to be able to fetch data from. Each entry is a structure with
up to four elements:
- `target` (required): The string prefix of the location target that you want to
match on, with no trailing slash. For GitHub, it should be exactly
`https://github.com`.
- `token` (optional): An authentication token as expected by GitHub. If
supplied, it will be passed along with all calls to this provider, both API
and raw. If it is not supplied, anonymous access will be used.
- `apiBaseUrl` (optional): If you want to communicate using the APIv3 method
with this provider, specify the base URL for its endpoint here, with no
trailing slash. Specifically when the target is github, you can leave it out
to be inferred automatically. For a GitHub Enterprise installation, it is
commonly at `https://api.<host>` or `https://<host>/api/v3`.
- `rawBaseUrl` (optional): If you want to communicate using the raw HTTP method
with this provider, specify the base URL for its endpoint here, with no
trailing slash. Specifically when the target is public GitHub, you can leave
it out to be inferred automatically. For a GitHub Enterprise installation, it
is commonly at `https://api.<host>` or `https://<host>/api/v3`.
You need to supply either `apiBaseUrl` or `rawBaseUrl` or both (except for
public GitHub, for which we can infer them). The `apiBaseUrl` will always be
preferred over the other if a `token` is given, otherwise `rawBaseUrl` will be
preferred.
If you do not supply a public GitHub provider, one will be added automatically,
silently at startup for convenience. So you only have to list it if you want to
supply a token for it - and if you do, you can also leave out the `apiBaseUrl`
and `rawBaseUrl` fields.
## Static Location Configuration
To enable declarative catalog setups, it is possible to add locations to the
+24
View File
@@ -71,6 +71,30 @@ building and publishing of your documentation, you want to change the
`requestUrl` to point to your storage. In this case `storageUrl` is not
required.
### Disable Docker in Docker situation (Optional)
The TechDocs backend plugin runs a docker container with mkdocs to generate the
frontend of the docs from source files (Markdown). If you are deploying
Backstage using Docker, this will mean that your Backstage Docker container will
try to run another Docker container for TechDocs backend.
To avoid this problem, we have a configuration available. You can set a value in
your `app-config.yaml` that tells the techdocs generator if it should run the
`local` mkdocs or run it from `docker`. This defaults to running as `docker` if
no config is provided.
```yaml
techdocs:
generators:
techdocs: local
```
Setting `generators.techdocs` to `local` means you will have to make sure your
environment is compatible with techdocs. You will have to install the
`mkdocs-techdocs-container` and 'mkdocs' package from pip, as well as graphviz
and plantuml from your package manager. This has only been tested with python
3.7 and python 3.8.
## Run Backstage locally
Change folder to `<backstage-project-root>/packages/backend` and run the
+1 -1
View File
@@ -41,7 +41,7 @@ From the terminal:
「wds」: Project is running at http://localhost:3000/
```
Once the app compiles, a browser window should have popped with your stand alone
Once the app compiles, a browser window should have popped with your stand-alone
application loaded at `localhost:3000`. This could take a couple minutes.
```zsh
+1 -1
View File
@@ -2,5 +2,5 @@
"packages": ["packages/*", "plugins/*"],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.1.1-alpha.20"
"version": "0.1.1-alpha.22"
}
+1
View File
@@ -40,6 +40,7 @@
"ids": [
"features/software-catalog/software-catalog-overview",
"features/software-catalog/installation",
"features/software-catalog/configuration",
"features/software-catalog/system-model",
"features/software-catalog/descriptor-format",
"features/software-catalog/well-known-annotations",
+2 -1
View File
@@ -28,10 +28,11 @@ nav:
- Features:
- Software Catalog:
- Overview: 'features/software-catalog/index.md'
- Installation: 'features/software-catalog/installation.md'
- Configuration: 'features/software-catalog/configuration.md'
- System model: 'features/software-catalog/system-model.md'
- YAML File Format: 'features/software-catalog/descriptor-format.md'
- Well-known Annotations: 'features/software-catalog/well-known-annotations.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'
+25 -25
View File
@@ -1,33 +1,33 @@
{
"name": "example-app",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": true,
"bundled": true,
"dependencies": {
"@backstage/catalog-model": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/plugin-api-docs": "^0.1.1-alpha.21",
"@backstage/plugin-catalog": "^0.1.1-alpha.21",
"@backstage/plugin-circleci": "^0.1.1-alpha.21",
"@backstage/plugin-cloudbuild": "^0.1.1-alpha.21",
"@backstage/plugin-explore": "^0.1.1-alpha.21",
"@backstage/plugin-gcp-projects": "^0.1.1-alpha.21",
"@backstage/plugin-github-actions": "^0.1.1-alpha.21",
"@backstage/plugin-gitops-profiles": "^0.1.1-alpha.21",
"@backstage/plugin-graphiql": "^0.1.1-alpha.21",
"@backstage/plugin-jenkins": "^0.1.1-alpha.21",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.21",
"@backstage/plugin-newrelic": "^0.1.1-alpha.21",
"@backstage/plugin-register-component": "^0.1.1-alpha.21",
"@backstage/plugin-rollbar": "^0.1.1-alpha.21",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.21",
"@backstage/plugin-sentry": "^0.1.1-alpha.21",
"@backstage/plugin-tech-radar": "^0.1.1-alpha.21",
"@backstage/plugin-techdocs": "^0.1.1-alpha.21",
"@backstage/plugin-welcome": "^0.1.1-alpha.21",
"@backstage/test-utils": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.22",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/plugin-api-docs": "^0.1.1-alpha.22",
"@backstage/plugin-catalog": "^0.1.1-alpha.22",
"@backstage/plugin-circleci": "^0.1.1-alpha.22",
"@backstage/plugin-cloudbuild": "^0.1.1-alpha.22",
"@backstage/plugin-explore": "^0.1.1-alpha.22",
"@backstage/plugin-gcp-projects": "^0.1.1-alpha.22",
"@backstage/plugin-github-actions": "^0.1.1-alpha.22",
"@backstage/plugin-gitops-profiles": "^0.1.1-alpha.22",
"@backstage/plugin-graphiql": "^0.1.1-alpha.22",
"@backstage/plugin-jenkins": "^0.1.1-alpha.22",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.22",
"@backstage/plugin-newrelic": "^0.1.1-alpha.22",
"@backstage/plugin-register-component": "^0.1.1-alpha.22",
"@backstage/plugin-rollbar": "^0.1.1-alpha.22",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.22",
"@backstage/plugin-sentry": "^0.1.1-alpha.22",
"@backstage/plugin-tech-radar": "^0.1.1-alpha.22",
"@backstage/plugin-techdocs": "^0.1.1-alpha.22",
"@backstage/plugin-welcome": "^0.1.1-alpha.22",
"@backstage/test-utils": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@octokit/rest": "^18.0.0",
+7 -7
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -29,9 +29,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/cli-common": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/config-loader": "^0.1.1-alpha.21",
"@backstage/cli-common": "^0.1.1-alpha.22",
"@backstage/config": "^0.1.1-alpha.22",
"@backstage/config-loader": "^0.1.1-alpha.22",
"@types/cors": "^2.8.6",
"@types/express": "^4.17.6",
"compression": "^1.7.4",
@@ -42,12 +42,12 @@
"helmet": "^4.0.0",
"knex": "^0.21.1",
"lodash": "^4.17.15",
"logform": "^2.1.1",
"morgan": "^1.10.0",
"prom-client": "^12.0.0",
"selfsigned": "^1.10.7",
"stoppable": "^1.1.0",
"winston": "^3.2.1",
"logform": "^2.1.1"
"winston": "^3.2.1"
},
"peerDependencies": {
"pg-connection-string": "^2.3.0"
@@ -58,7 +58,7 @@
}
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@types/compression": "^1.7.0",
"@types/http-errors": "^1.6.3",
"@types/morgan": "^1.9.0",
+16 -16
View File
@@ -1,6 +1,6 @@
{
"name": "example-backend",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"private": true,
@@ -18,23 +18,23 @@
"migrate:create": "knex migrate:make -x ts"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/plugin-app-backend": "^0.1.1-alpha.21",
"@backstage/plugin-auth-backend": "^0.1.1-alpha.21",
"@backstage/plugin-catalog-backend": "^0.1.1-alpha.21",
"@backstage/plugin-graphql-backend": "^0.1.1-alpha.21",
"@backstage/plugin-identity-backend": "^0.1.1-alpha.21",
"@backstage/plugin-proxy-backend": "^0.1.1-alpha.21",
"@backstage/plugin-rollbar-backend": "^0.1.1-alpha.21",
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.21",
"@backstage/plugin-sentry-backend": "^0.1.1-alpha.21",
"@backstage/plugin-techdocs-backend": "^0.1.1-alpha.21",
"@backstage/backend-common": "^0.1.1-alpha.22",
"@backstage/catalog-model": "^0.1.1-alpha.22",
"@backstage/config": "^0.1.1-alpha.22",
"@backstage/plugin-app-backend": "^0.1.1-alpha.22",
"@backstage/plugin-auth-backend": "^0.1.1-alpha.22",
"@backstage/plugin-catalog-backend": "^0.1.1-alpha.22",
"@backstage/plugin-graphql-backend": "^0.1.1-alpha.22",
"@backstage/plugin-identity-backend": "^0.1.1-alpha.22",
"@backstage/plugin-proxy-backend": "^0.1.1-alpha.22",
"@backstage/plugin-rollbar-backend": "^0.1.1-alpha.22",
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.22",
"@backstage/plugin-sentry-backend": "^0.1.1-alpha.22",
"@backstage/plugin-techdocs-backend": "^0.1.1-alpha.22",
"@gitbeaker/node": "^23.5.0",
"@octokit/rest": "^18.0.0",
"dockerode": "^3.2.0",
"example-app": "^0.1.1-alpha.21",
"example-app": "^0.1.1-alpha.22",
"express": "^4.17.1",
"knex": "^0.21.1",
"pg": "^8.3.0",
@@ -43,7 +43,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@types/dockerode": "^2.5.32",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",
-1
View File
@@ -64,7 +64,6 @@ async function main() {
const configs = await loadBackendConfig();
const configReader = ConfigReader.fromConfigs(configs);
const createEnv = makeCreateEnv(configs);
const healthcheckEnv = useHotMemoize(module, () => createEnv('healthcheck'));
const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
+6 -2
View File
@@ -30,10 +30,14 @@
*/
import { createRouter } from '@backstage/plugin-graphql-backend';
import type { PluginEnvironment } from '../types';
import { PluginEnvironment } from '../types';
export default async function createPlugin({ logger }: PluginEnvironment) {
export default async function createPlugin({
logger,
config,
}: PluginEnvironment) {
return await createRouter({
logger,
config,
});
}
+1 -1
View File
@@ -31,7 +31,7 @@ export default async function createPlugin({
config,
}: PluginEnvironment) {
const generators = new Generators();
const techdocsGenerator = new TechdocsGenerator(logger);
const techdocsGenerator = new TechdocsGenerator(logger, config);
generators.register('techdocs', techdocsGenerator);
const preparers = new Preparers();
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/catalog-model",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,7 +20,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.22",
"@types/json-schema": "^7.0.5",
"@types/yup": "^0.28.2",
"json-schema": "^0.2.5",
@@ -29,7 +29,7 @@
"yup": "^0.29.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@types/express": "^4.17.6",
"@types/jest": "^26.0.7",
"@types/lodash": "^4.14.151",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli-common",
"description": "Common functionality used by cli, backend, and create-app",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"main": "src/index.ts",
"types": "src/index.ts",
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"publishConfig": {
"access": "public"
@@ -28,9 +28,9 @@
"backstage-cli": "bin/backstage-cli"
},
"dependencies": {
"@backstage/cli-common": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/config-loader": "^0.1.1-alpha.21",
"@backstage/cli-common": "^0.1.1-alpha.22",
"@backstage/config": "^0.1.1-alpha.22",
"@backstage/config-loader": "^0.1.1-alpha.22",
"@hot-loader/react-dom": "^16.13.0",
"@lerna/package-graph": "^3.18.5",
"@lerna/project": "^3.18.0",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/config-loader",
"description": "Config loading functionality used by Backstage backend, and CLI",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"publishConfig": {
"access": "public",
@@ -30,7 +30,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.22",
"fs-extra": "^9.0.0",
"yaml": "^1.9.2",
"yup": "^0.29.1"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/config",
"description": "Config API used by Backstage core, backend, and CLI",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"publishConfig": {
"access": "public",
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-api",
"description": "Internal Core API used by Backstage plugins and apps",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,8 +29,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@types/react": "^16.9",
@@ -41,8 +41,8 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/test-utils-core": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/test-utils-core": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core",
"description": "Core API used by Backstage plugins and apps",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,9 +29,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/core-api": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.22",
"@backstage/core-api": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -54,8 +54,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/test-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/test-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/create-app",
"description": "Create app package for Backstage",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"publishConfig": {
"access": "public"
@@ -27,7 +27,7 @@
"start": "nodemon --"
},
"dependencies": {
"@backstage/cli-common": "^0.1.1-alpha.21",
"@backstage/cli-common": "^0.1.1-alpha.22",
"chalk": "^4.0.0",
"commander": "^6.1.0",
"fs-extra": "^9.0.0",
@@ -46,6 +46,8 @@ proxy:
techdocs:
storageUrl: http://localhost:7000/techdocs/static/docs
requestUrl: http://localhost:7000/techdocs/docs
generators:
techdocs: 'docker'
lighthouse:
baseUrl: http://localhost:3003
@@ -65,10 +67,6 @@ catalog:
- allow: [Component, API, Group, Template, Location]
processors:
github:
privateToken:
$secret:
env: GITHUB_PRIVATE_TOKEN
githubApi:
providers:
- target: https://github.com
token:
@@ -15,7 +15,7 @@ export default async function createPlugin({
config,
}: PluginEnvironment) {
const generators = new Generators();
const techdocsGenerator = new TechdocsGenerator(logger);
const techdocsGenerator = new TechdocsGenerator(logger, config);
generators.register('techdocs', techdocsGenerator);
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/dev-utils",
"description": "Utilities for developing Backstage plugins.",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,10 +29,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/test-utils": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/test-utils": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@testing-library/jest-dom": "^5.10.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "docgen",
"description": "Tool for generating API Documentation for itself",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": true,
"homepage": "https://backstage.io",
"repository": {
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "e2e-test",
"description": "E2E test for verifying Backstage packages",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": true,
"homepage": "https://backstage.io",
"repository": {
@@ -21,7 +21,7 @@
"test:e2e": "yarn start"
},
"devDependencies": {
"@backstage/cli-common": "^0.1.1-alpha.21",
"@backstage/cli-common": "^0.1.1-alpha.22",
"@types/fs-extra": "^9.0.1",
"@types/node": "^13.7.2",
"fs-extra": "^9.0.0",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "storybook",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"description": "Storybook build for core package",
"private": true,
"scripts": {
@@ -14,7 +14,7 @@
]
},
"dependencies": {
"@backstage/theme": "^0.1.1-alpha.21"
"@backstage/theme": "^0.1.1-alpha.22"
},
"devDependencies": {
"@storybook/addon-actions": "^6.0.21",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@techdocs/cli",
"description": "CLI for running TechDocs locally.",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"publishConfig": {
"access": "public"
@@ -44,7 +44,7 @@
"ext": "ts"
},
"dependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"commander": "^6.1.0",
"fs-extra": "^9.0.1",
"http-proxy": "^1.18.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/test-utils-core",
"description": "Utilities to test Backstage core",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"publishConfig": {
"access": "public",
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/test-utils",
"description": "Utilities to test Backstage plugins and apps.",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,10 +29,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/core-api": "^0.1.1-alpha.21",
"@backstage/test-utils-core": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/core-api": "^0.1.1-alpha.22",
"@backstage/test-utils-core": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/theme",
"description": "material-ui theme for use with Backstage.",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"publishConfig": {
"access": "public",
@@ -31,7 +31,7 @@
"@material-ui/core": "^4.11.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21"
"@backstage/cli": "^0.1.1-alpha.22"
},
"files": [
"dist"
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-api-docs",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,10 +20,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/plugin-catalog": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.22",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/plugin-catalog": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@kyma-project/asyncapi-react": "^0.11.0",
"@material-icons/font": "^1.0.2",
"@material-ui/core": "^4.11.0",
@@ -39,9 +39,9 @@
"swagger-ui-react": "^3.31.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/dev-utils": "^0.1.1-alpha.21",
"@backstage/test-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/dev-utils": "^0.1.1-alpha.22",
"@backstage/test-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-app-backend",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,8 +20,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.21",
"@backstage/config-loader": "^0.1.1-alpha.21",
"@backstage/backend-common": "^0.1.1-alpha.22",
"@backstage/config-loader": "^0.1.1-alpha.22",
"@types/express": "^4.17.6",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
@@ -30,7 +30,7 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@types/supertest": "^2.0.8",
"msw": "^0.19.5",
"supertest": "^4.0.2"
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-auth-backend",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,8 +20,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/backend-common": "^0.1.1-alpha.22",
"@backstage/config": "^0.1.1-alpha.22",
"@types/express": "^4.17.6",
"compression": "^1.7.4",
"cookie-parser": "^1.4.5",
@@ -49,7 +49,7 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@types/body-parser": "^1.19.0",
"@types/cookie-parser": "^1.4.2",
"@types/jwt-decode": "2.2.1",
+7 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog-backend",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,13 +20,14 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/backend-common": "^0.1.1-alpha.22",
"@backstage/catalog-model": "^0.1.1-alpha.22",
"@backstage/config": "^0.1.1-alpha.22",
"@types/express": "^4.17.6",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"fs-extra": "^9.0.0",
"git-url-parse": "^11.2.0",
"knex": "^0.21.1",
"lodash": "^4.17.15",
"morgan": "^1.10.0",
@@ -39,7 +40,8 @@
"yup": "^0.29.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@types/git-url-parse": "^9.0.0",
"@types/lodash": "^4.14.151",
"@types/node-fetch": "^2.5.7",
"@types/supertest": "^2.0.8",
@@ -343,7 +343,14 @@ export class CommonDatabase implements Database {
entityName?: string,
message?: string,
): Promise<void> {
return this.database<DatabaseLocationUpdateLogEvent>(
// Remove log entries older than a day
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - 1);
await this.database<DatabaseLocationUpdateLogEvent>('location_update_log')
.where('created_at', '<', cutoff.toISOString())
.del();
await this.database<DatabaseLocationUpdateLogEvent>(
'location_update_log',
).insert({
status,
@@ -27,7 +27,6 @@ import { AnnotateLocationEntityProcessor } from './processors/AnnotateLocationEn
import { EntityPolicyProcessor } from './processors/EntityPolicyProcessor';
import { FileReaderProcessor } from './processors/FileReaderProcessor';
import { GithubReaderProcessor } from './processors/GithubReaderProcessor';
import { GithubApiReaderProcessor } from './processors/GithubApiReaderProcessor';
import { GitlabApiReaderProcessor } from './processors/GitlabApiReaderProcessor';
import { GitlabReaderProcessor } from './processors/GitlabReaderProcessor';
import { BitbucketApiReaderProcessor } from './processors/BitbucketApiReaderProcessor';
@@ -67,18 +66,19 @@ export class LocationReaders implements LocationReader {
private readonly rulesEnforcer: CatalogRulesEnforcer;
static defaultProcessors(options: {
logger: Logger;
config?: Config;
entityPolicy?: EntityPolicy;
}): LocationProcessor[] {
const {
logger,
config = new ConfigReader({}, 'missing-config'),
entityPolicy = new EntityPolicies(),
} = options;
return [
StaticLocationProcessor.fromConfig(config),
new FileReaderProcessor(),
new GithubReaderProcessor(config),
GithubApiReaderProcessor.fromConfig(config),
GithubReaderProcessor.fromConfig(config, logger),
new GitlabApiReaderProcessor(config),
new GitlabReaderProcessor(),
new BitbucketApiReaderProcessor(config),
@@ -94,7 +94,7 @@ export class LocationReaders implements LocationReader {
constructor({
logger = getVoidLogger(),
config,
processors = LocationReaders.defaultProcessors({ config }),
processors = LocationReaders.defaultProcessors({ logger, config }),
}: Options) {
this.logger = logger;
this.processors = processors;
@@ -1,162 +0,0 @@
/*
* 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 } from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import {
getRawUrl,
getRequestOptions,
GithubApiReaderProcessor,
ProviderConfig,
readConfig,
} from './GithubApiReaderProcessor';
describe('GithubApiReaderProcessor', () => {
describe('getRequestOptions', () => {
it('sets the correct API version', () => {
const config: ProviderConfig = { target: '', apiBaseUrl: '' };
expect((getRequestOptions(config).headers as any).Accept).toEqual(
'application/vnd.github.v3.raw',
);
});
it('inserts a token when needed', () => {
const withToken: ProviderConfig = {
target: '',
apiBaseUrl: '',
token: 'A',
};
const withoutToken: ProviderConfig = {
target: '',
apiBaseUrl: '',
};
expect(
(getRequestOptions(withToken).headers as any).Authorization,
).toEqual('token A');
expect(
(getRequestOptions(withoutToken).headers as any).Authorization,
).toBeUndefined();
});
});
describe('getRawUrl', () => {
it('rejects targets that do not look like URLs', () => {
const config: ProviderConfig = { target: '', apiBaseUrl: '' };
expect(() => getRawUrl('a/b', config)).toThrow(/Incorrect URL: a\/b/);
});
it('passes through the happy path', () => {
const config: ProviderConfig = {
target: 'https://github.com',
apiBaseUrl: 'https://api.github.com',
};
expect(
getRawUrl(
'https://github.com/a/b/blob/branchname/path/to/c.yaml',
config,
),
).toEqual(
new URL(
'https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname',
),
);
});
});
describe('readConfig', () => {
function config(
providers: { target: string; apiBaseUrl?: string; token?: string }[],
) {
return ConfigReader.fromConfigs([
{
context: '',
data: {
catalog: { processors: { githubApi: { providers } } },
},
},
]);
}
it('adds a default GitHub entry when missing', () => {
const output = readConfig(config([]));
expect(output).toEqual([
{ target: 'https://github.com', apiBaseUrl: 'https://api.github.com' },
]);
});
it('injects the correct GitHub API base URL when missing', () => {
const output = readConfig(config([{ target: 'https://github.com' }]));
expect(output).toEqual([
{ target: 'https://github.com', apiBaseUrl: 'https://api.github.com' },
]);
});
it('rejects custom targets with no API base URL', () => {
expect(() =>
readConfig(config([{ target: 'https://ghe.company.com' }])),
).toThrow(
'Provider at https://ghe.company.com must configure an explicit apiBaseUrl',
);
});
it('rejects funky configs', () => {
expect(() => readConfig(config([{ target: 7 } as any]))).toThrow(
/target/,
);
expect(() => readConfig(config([{ noTarget: '7' } as any]))).toThrow(
/target/,
);
expect(() =>
readConfig(
config([{ target: 'https://github.com', apiBaseUrl: 7 } as any]),
),
).toThrow(/apiBaseUrl/);
expect(() =>
readConfig(config([{ target: 'https://github.com', token: 7 } as any])),
).toThrow(/token/);
});
});
describe('implementation', () => {
it('rejects unknown types', async () => {
const processor = new GithubApiReaderProcessor([
{ target: 'https://github.com', apiBaseUrl: 'https://api.github.com' },
]);
const location: LocationSpec = {
type: 'not-github/api',
target: 'https://github.com',
};
await expect(
processor.readLocation(location, false, () => {}),
).resolves.toBeFalsy();
});
it('rejects unknown targets', async () => {
const processor = new GithubApiReaderProcessor([
{ target: 'https://github.com', apiBaseUrl: 'https://api.github.com' },
]);
const location: LocationSpec = {
type: 'github/api',
target: 'https://not.github.com/apa',
};
await expect(
processor.readLocation(location, false, () => {}),
).rejects.toThrow(
/There is no GitHub provider that matches https:\/\/not.github.com\/apa/,
);
});
});
});
@@ -1,192 +0,0 @@
/*
* 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 } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import fetch, { HeadersInit, RequestInit } from 'node-fetch';
import * as result from './results';
import { LocationProcessor, LocationProcessorEmit } from './types';
/**
* The configuration parameters for a single GitHub API provider.
*/
export type ProviderConfig = {
/**
* The prefix of the target that this matches on, e.g. "https://github.com",
* with no trailing slash.
*/
target: string;
/**
* The base URL of the API of this provider, e.g. "https://api.github.com",
* with no trailing slash.
*/
apiBaseUrl: string;
/**
* The authorization token to use for requests to this provider.
*
* If no token is specified, anonymous API access is used.
*/
token?: string;
};
export function getRequestOptions(provider: ProviderConfig): RequestInit {
const headers: HeadersInit = {
Accept: 'application/vnd.github.v3.raw',
};
if (provider.token) {
headers.Authorization = `token ${provider.token}`;
}
return {
headers,
};
}
// Converts for example
// from: https://github.com/a/b/blob/branchname/path/to/c.yaml
// to: https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname
export function getRawUrl(target: string, provider: ProviderConfig): URL {
try {
const oldPath = new URL(target).pathname.split('/');
const [, userOrOrg, repoName, blobOrRaw, ref, ...restOfPath] = oldPath;
if (
!userOrOrg ||
!repoName ||
(blobOrRaw !== 'blob' && blobOrRaw !== 'raw') ||
!restOfPath.join('/').match(/\.ya?ml$/)
) {
throw new Error('Wrong URL or Invalid file path');
}
// Transform to API path
const newPath = [
'repos',
userOrOrg,
repoName,
'contents',
...restOfPath,
].join('/');
return new URL(`${provider.apiBaseUrl}/${newPath}?ref=${ref}`);
} catch (e) {
throw new Error(`Incorrect URL: ${target}, ${e}`);
}
}
export function readConfig(configRoot: Config): ProviderConfig[] {
const providers: ProviderConfig[] = [];
// In a previous version of the configuration, we only supported github,
// and the "privateToken" key held the token to use for it. The new
// configuration method is to use the "providers" key instead.
const config = configRoot.getOptionalConfig('catalog.processors.githubApi');
const providerConfigs = config?.getOptionalConfigArray('providers') ?? [];
const legacyToken = config?.getOptionalString('privateToken');
// First read all the explicit providers
for (const providerConfig of providerConfigs) {
const target = providerConfig.getString('target').replace(/\/+$/, '');
let apiBaseUrl = providerConfig.getOptionalString('apiBaseUrl');
const token = providerConfig.getOptionalString('token');
if (apiBaseUrl) {
apiBaseUrl = apiBaseUrl.replace(/\/+$/, '');
} else if (target === 'https://github.com') {
apiBaseUrl = 'https://api.github.com';
} else {
throw new Error(
`Provider at ${target} must configure an explicit apiBaseUrl`,
);
}
providers.push({ target, apiBaseUrl, token });
}
// If no explicit github.com provider was added, put one in the list as
// a convenience
if (!providers.some(p => p.target === 'https://github.com')) {
providers.push({
target: 'https://github.com',
apiBaseUrl: 'https://api.github.com',
token: legacyToken,
});
}
return providers;
}
/**
* A processor that adds the ability to read files from GitHub v3 APIs, such as
* the one exposed by GitHub itself.
*/
export class GithubApiReaderProcessor implements LocationProcessor {
private providers: ProviderConfig[];
static fromConfig(config: Config) {
return new GithubApiReaderProcessor(readConfig(config));
}
constructor(providers: ProviderConfig[]) {
this.providers = providers;
}
async readLocation(
location: LocationSpec,
optional: boolean,
emit: LocationProcessorEmit,
): Promise<boolean> {
if (location.type !== 'github/api') {
return false;
}
const provider = this.providers.find(p =>
location.target.startsWith(`${p.target}/`),
);
if (!provider) {
throw new Error(
`There is no GitHub provider that matches ${location.target}. Please add a configuration entry for it under catalog.github.processors.githubApi.`,
);
}
try {
const url = getRawUrl(location.target, provider);
const options = getRequestOptions(provider);
const response = await fetch(url.toString(), options);
if (response.ok) {
const data = await response.buffer();
emit(result.data(location, data));
} else {
const message = `${location.target} could not be read as ${url}, ${response.status} ${response.statusText}`;
if (response.status === 404) {
if (!optional) {
emit(result.notFoundError(location, message));
}
} else {
emit(result.generalError(location, message));
}
}
} catch (e) {
const message = `Unable to read ${location.type} ${location.target}, ${e}`;
emit(result.generalError(location, message));
}
return true;
}
}
@@ -0,0 +1,269 @@
/*
* 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 { getVoidLogger } from '@backstage/backend-common';
import { LocationSpec } from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import {
getApiRequestOptions,
getApiUrl,
getRawRequestOptions,
getRawUrl,
GithubReaderProcessor,
ProviderConfig,
readConfig,
} from './GithubReaderProcessor';
describe('GithubReaderProcessor', () => {
describe('getApiRequestOptions', () => {
it('sets the correct API version', () => {
const config: ProviderConfig = { target: '', apiBaseUrl: '' };
expect((getApiRequestOptions(config).headers as any).Accept).toEqual(
'application/vnd.github.v3.raw',
);
});
it('inserts a token when needed', () => {
const withToken: ProviderConfig = {
target: '',
apiBaseUrl: '',
token: 'A',
};
const withoutToken: ProviderConfig = {
target: '',
apiBaseUrl: '',
};
expect(
(getApiRequestOptions(withToken).headers as any).Authorization,
).toEqual('token A');
expect(
(getApiRequestOptions(withoutToken).headers as any).Authorization,
).toBeUndefined();
});
});
describe('getRawRequestOptions', () => {
it('inserts a token when needed', () => {
const withToken: ProviderConfig = {
target: '',
rawBaseUrl: '',
token: 'A',
};
const withoutToken: ProviderConfig = {
target: '',
rawBaseUrl: '',
};
expect(
(getRawRequestOptions(withToken).headers as any).Authorization,
).toEqual('token A');
expect(
(getRawRequestOptions(withoutToken).headers as any).Authorization,
).toBeUndefined();
});
});
describe('getApiUrl', () => {
it('rejects targets that do not look like URLs', () => {
const config: ProviderConfig = { target: '', apiBaseUrl: '' };
expect(() => getApiUrl('a/b', config)).toThrow(/Incorrect URL: a\/b/);
});
it('happy path for github', () => {
const config: ProviderConfig = {
target: 'https://github.com',
apiBaseUrl: 'https://api.github.com',
};
expect(
getApiUrl(
'https://github.com/a/b/blob/branchname/path/to/c.yaml',
config,
),
).toEqual(
new URL(
'https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname',
),
);
expect(
getApiUrl(
'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml',
config,
),
).toEqual(
new URL(
'https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname',
),
);
});
it('happy path for ghe', () => {
const config: ProviderConfig = {
target: 'https://ghe.mycompany.net',
apiBaseUrl: 'https://ghe.mycompany.net/api/v3',
};
expect(
getApiUrl(
'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml',
config,
),
).toEqual(
new URL(
'https://ghe.mycompany.net/api/v3/repos/a/b/contents/path/to/c.yaml?ref=branchname',
),
);
});
});
describe('getRawUrl', () => {
it('rejects targets that do not look like URLs', () => {
const config: ProviderConfig = { target: '', apiBaseUrl: '' };
expect(() => getRawUrl('a/b', config)).toThrow(/Incorrect URL: a\/b/);
});
it('happy path for github', () => {
const config: ProviderConfig = {
target: 'https://github.com',
rawBaseUrl: 'https://raw.githubusercontent.com',
};
expect(
getRawUrl(
'https://github.com/a/b/blob/branchname/path/to/c.yaml',
config,
),
).toEqual(
new URL(
'https://raw.githubusercontent.com/a/b/branchname/path/to/c.yaml',
),
);
});
it('happy path for ghe', () => {
const config: ProviderConfig = {
target: 'https://ghe.mycompany.net',
rawBaseUrl: 'https://ghe.mycompany.net/raw',
};
expect(
getRawUrl(
'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml',
config,
),
).toEqual(
new URL('https://ghe.mycompany.net/raw/a/b/branchname/path/to/c.yaml'),
);
});
});
describe('readConfig', () => {
function config(
providers: { target: string; apiBaseUrl?: string; token?: string }[],
) {
return ConfigReader.fromConfigs([
{
context: '',
data: {
catalog: { processors: { github: { providers } } },
},
},
]);
}
it('adds a default GitHub entry when missing', () => {
const output = readConfig(config([]), getVoidLogger());
expect(output).toEqual([
{
target: 'https://github.com',
apiBaseUrl: 'https://api.github.com',
rawBaseUrl: 'https://raw.githubusercontent.com',
},
]);
});
it('injects the correct GitHub API base URL when missing', () => {
const output = readConfig(
config([{ target: 'https://github.com' }]),
getVoidLogger(),
);
expect(output).toEqual([
{
target: 'https://github.com',
apiBaseUrl: 'https://api.github.com',
rawBaseUrl: 'https://raw.githubusercontent.com',
},
]);
});
it('rejects custom targets with no base URLs', () => {
expect(() =>
readConfig(
config([{ target: 'https://ghe.company.com' }]),
getVoidLogger(),
),
).toThrow(
'Provider at https://ghe.company.com must configure an explicit apiBaseUrl or rawBaseUrl',
);
});
it('rejects funky configs', () => {
expect(() =>
readConfig(config([{ target: 7 } as any]), getVoidLogger()),
).toThrow(/target/);
expect(() =>
readConfig(config([{ noTarget: '7' } as any]), getVoidLogger()),
).toThrow(/target/);
expect(() =>
readConfig(
config([{ target: 'https://github.com', apiBaseUrl: 7 } as any]),
getVoidLogger(),
),
).toThrow(/apiBaseUrl/);
expect(() =>
readConfig(
config([{ target: 'https://github.com', token: 7 } as any]),
getVoidLogger(),
),
).toThrow(/token/);
});
});
describe('implementation', () => {
it('rejects unknown types', async () => {
const processor = new GithubReaderProcessor([
{ target: 'https://github.com', apiBaseUrl: 'https://api.github.com' },
]);
const location: LocationSpec = {
type: 'not-github/api',
target: 'https://github.com',
};
await expect(
processor.readLocation(location, false, () => {}),
).resolves.toBeFalsy();
});
it('rejects unknown targets', async () => {
const processor = new GithubReaderProcessor([
{ target: 'https://github.com', apiBaseUrl: 'https://api.github.com' },
]);
const location: LocationSpec = {
type: 'github/api',
target: 'https://not.github.com/apa',
};
await expect(
processor.readLocation(location, false, () => {}),
).rejects.toThrow(
/There is no GitHub provider that matches https:\/\/not.github.com\/apa/,
);
});
});
});
@@ -15,33 +15,207 @@
*/
import { LocationSpec } from '@backstage/catalog-model';
import fetch, { RequestInit, HeadersInit } from 'node-fetch';
import { Config } from '@backstage/config';
import parseGitUri from 'git-url-parse';
import fetch, { HeadersInit, RequestInit } from 'node-fetch';
import { Logger } from 'winston';
import * as result from './results';
import { LocationProcessor, LocationProcessorEmit } from './types';
import { Config } from '@backstage/config';
export class GithubReaderProcessor implements LocationProcessor {
private privateToken: string;
/**
* The configuration parameters for a single GitHub API provider.
*/
export type ProviderConfig = {
/**
* The prefix of the target that this matches on, e.g. "https://github.com",
* with no trailing slash.
*/
target: string;
constructor(config?: Config) {
this.privateToken =
config?.getOptionalString('catalog.processors.github.privateToken') ?? '';
/**
* The base URL of the API of this provider, e.g. "https://api.github.com",
* with no trailing slash.
*
* May be omitted specifically for GitHub; then it will be deduced.
*
* The API will always be preferred if both its base URL and a token are
* present.
*/
apiBaseUrl?: string;
/**
* The base URL of the raw fetch endpoint of this provider, e.g.
* "https://raw.githubusercontent.com", with no trailing slash.
*
* May be omitted specifically for GitHub; then it will be deduced.
*
* The API will always be preferred if both its base URL and a token are
* present.
*/
rawBaseUrl?: string;
/**
* The authorization token to use for requests to this provider.
*
* If no token is specified, anonymous access is used.
*/
token?: string;
};
export function getApiRequestOptions(provider: ProviderConfig): RequestInit {
const headers: HeadersInit = {
Accept: 'application/vnd.github.v3.raw',
};
if (provider.token) {
headers.Authorization = `token ${provider.token}`;
}
getRequestOptions(): RequestInit {
const headers: HeadersInit = {
Accept: 'application/vnd.github.v3.raw',
};
return {
headers,
};
}
if (this.privateToken !== '') {
headers.Authorization = `token ${this.privateToken}`;
export function getRawRequestOptions(provider: ProviderConfig): RequestInit {
const headers: HeadersInit = {};
if (provider.token) {
headers.Authorization = `token ${provider.token}`;
}
return {
headers,
};
}
// Converts for example
// from: https://github.com/a/b/blob/branchname/path/to/c.yaml
// to: https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname
export function getApiUrl(target: string, provider: ProviderConfig): URL {
try {
const { owner, name, ref, filepathtype, filepath } = parseGitUri(target);
if (
!owner ||
!name ||
!ref ||
(filepathtype !== 'blob' && filepathtype !== 'raw') ||
!filepath?.match(/\.ya?ml$/)
) {
throw new Error('Wrong URL or invalid file path');
}
const requestOptions: RequestInit = {
headers,
};
const pathWithoutSlash = filepath.replace(/^\//, '');
return new URL(
`${provider.apiBaseUrl}/repos/${owner}/${name}/contents/${pathWithoutSlash}?ref=${ref}`,
);
} catch (e) {
throw new Error(`Incorrect URL: ${target}, ${e}`);
}
}
return requestOptions;
// Converts for example
// from: https://github.com/a/b/blob/branchname/c.yaml
// to: https://raw.githubusercontent.com/a/b/branchname/c.yaml
export function getRawUrl(target: string, provider: ProviderConfig): URL {
try {
const { owner, name, ref, filepathtype, filepath } = parseGitUri(target);
if (
!owner ||
!name ||
!ref ||
(filepathtype !== 'blob' && filepathtype !== 'raw') ||
!filepath?.match(/\.ya?ml$/)
) {
throw new Error('Wrong URL or invalid file path');
}
const pathWithoutSlash = filepath.replace(/^\//, '');
return new URL(
`${provider.rawBaseUrl}/${owner}/${name}/${ref}/${pathWithoutSlash}`,
);
} catch (e) {
throw new Error(`Incorrect URL: ${target}, ${e}`);
}
}
export function readConfig(config: Config, logger: Logger): ProviderConfig[] {
const providers: ProviderConfig[] = [];
// TODO(freben): Deprecate the old config root entirely in a later release
if (config.has('catalog.processors.githubApi')) {
logger.warn(
'The catalog.processors.githubApi configuration key has been deprecated, please use catalog.processors.github instead',
);
}
// In a previous version of the configuration, we only supported github,
// and the "privateToken" key held the token to use for it. The new
// configuration method is to use the "providers" key instead.
const providerConfigs =
config.getOptionalConfigArray('catalog.processors.github.providers') ??
config.getOptionalConfigArray('catalog.processors.githubApi.providers') ??
[];
const legacyToken =
config.getOptionalString('catalog.processors.github.privateToken') ??
config.getOptionalString('catalog.processors.githubApi.privateToken');
// First read all the explicit providers
for (const providerConfig of providerConfigs) {
const target = providerConfig.getString('target').replace(/\/+$/, '');
let apiBaseUrl = providerConfig.getOptionalString('apiBaseUrl');
let rawBaseUrl = providerConfig.getOptionalString('rawBaseUrl');
const token = providerConfig.getOptionalString('token');
if (apiBaseUrl) {
apiBaseUrl = apiBaseUrl.replace(/\/+$/, '');
} else if (target === 'https://github.com') {
apiBaseUrl = 'https://api.github.com';
}
if (rawBaseUrl) {
rawBaseUrl = rawBaseUrl.replace(/\/+$/, '');
} else if (target === 'https://github.com') {
rawBaseUrl = 'https://raw.githubusercontent.com';
}
if (!apiBaseUrl && !rawBaseUrl) {
throw new Error(
`Provider at ${target} must configure an explicit apiBaseUrl or rawBaseUrl`,
);
}
providers.push({ target, apiBaseUrl, rawBaseUrl, token });
}
// If no explicit github.com provider was added, put one in the list as
// a convenience
if (!providers.some(p => p.target === 'https://github.com')) {
providers.push({
target: 'https://github.com',
apiBaseUrl: 'https://api.github.com',
rawBaseUrl: 'https://raw.githubusercontent.com',
token: legacyToken,
});
}
return providers;
}
/**
* A processor that adds the ability to read files from GitHub v3 APIs, such as
* the one exposed by GitHub itself.
*/
export class GithubReaderProcessor implements LocationProcessor {
private providers: ProviderConfig[];
static fromConfig(config: Config, logger: Logger) {
return new GithubReaderProcessor(readConfig(config, logger));
}
constructor(providers: ProviderConfig[]) {
this.providers = providers;
}
async readLocation(
@@ -49,16 +223,30 @@ export class GithubReaderProcessor implements LocationProcessor {
optional: boolean,
emit: LocationProcessorEmit,
): Promise<boolean> {
if (location.type !== 'github') {
// The github/api type is for backward compatibility
if (location.type !== 'github' && location.type !== 'github/api') {
return false;
}
try {
const url = this.buildRawUrl(location.target);
const provider = this.providers.find(p =>
location.target.startsWith(`${p.target}/`),
);
if (!provider) {
throw new Error(
`There is no GitHub provider that matches ${location.target}. Please add a configuration entry for it under catalog.processors.github.providers.`,
);
}
// TODO(freben): Should "hard" errors thrown by this line be treated as
// notFound instead of fatal?
const response = await fetch(url.toString(), this.getRequestOptions());
try {
const useApi =
provider.apiBaseUrl && (provider.token || !provider.rawBaseUrl);
const url = useApi
? getApiUrl(location.target, provider)
: getRawUrl(location.target, provider);
const options = useApi
? getApiRequestOptions(provider)
: getRawRequestOptions(provider);
const response = await fetch(url.toString(), options);
if (response.ok) {
const data = await response.buffer();
@@ -80,41 +268,4 @@ export class GithubReaderProcessor implements LocationProcessor {
return true;
}
// Converts
// from: https://github.com/a/b/blob/master/c.yaml
// to: https://raw.githubusercontent.com/a/b/master/c.yaml
private buildRawUrl(target: string): URL {
try {
const url = new URL(target);
const [
empty,
userOrOrg,
repoName,
blobKeyword,
...restOfPath
] = url.pathname.split('/');
if (
url.hostname !== 'github.com' ||
empty !== '' ||
userOrOrg === '' ||
repoName === '' ||
blobKeyword !== 'blob' ||
!restOfPath.join('/').match(/\.yaml$/)
) {
throw new Error('Wrong GitHub URL');
}
// Removing the "blob" part
url.pathname = [empty, userOrOrg, repoName, ...restOfPath].join('/');
url.hostname = 'raw.githubusercontent.com';
url.protocol = 'https';
return url;
} catch (e) {
throw new Error(`Incorrect url: ${target}, ${e}`);
}
}
}
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
};
+11
View File
@@ -0,0 +1,11 @@
# Catalog GraphQL Plugin
## Getting Started
This is the Catalog GraphQL plugin.
It provides the `catalog` part of the GraphQL schema.
To register it with the GraphQL backend, be sure to follow the [Getting Started](../graphql/README.md#getting-started) guide of the GraphQL plugin.
<!-- TODO: Need to make the GraphQL plugin compatible with non forked repos >
+15
View File
@@ -0,0 +1,15 @@
overwrite: true
generates:
./src/graphql/types.ts:
schema: ./src/schema.js
plugins:
- typescript
- typescript-resolvers
hooks:
afterOneFileWrite:
- eslint --fix
config:
contextType: '@graphql-modules/core#ModuleContext'
allowParentTypeOverride: true
useIndexSignature: true
defaultMapper: Partial<{T}>
+49
View File
@@ -0,0 +1,49 @@
{
"name": "@backstage/plugin-catalog-graphql",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"scripts": {
"start": "backstage-cli backend:dev",
"build": "backstage-cli backend:build",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"generate:types": "graphql-codegen",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.1.1-alpha.22",
"@backstage/backend-common": "^0.1.1-alpha.22",
"@backstage/config": "^0.1.1-alpha.22",
"@graphql-modules/core": "^0.7.17",
"apollo-server": "^2.16.1",
"graphql": "^15.3.0",
"graphql-tag": "^2.11.0",
"graphql-type-json": "^0.3.2",
"node-fetch": "^2.6.0",
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.22",
"@types/express": "^4.17.7",
"@types/supertest": "^2.0.8",
"@graphql-codegen/cli": "^1.17.7",
"@graphql-codegen/typescript": "^1.17.7",
"@graphql-codegen/typescript-resolvers": "^1.17.7",
"ts-node": "^8.10.2",
"eslint-plugin-graphql": "^4.0.0",
"msw": "^0.19.5",
"supertest": "^4.0.2"
},
"files": [
"dist"
]
}
@@ -0,0 +1,305 @@
/*
* 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 { createModule } from './module';
import { execute } from 'graphql';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { ConfigReader } from '@backstage/config';
import { ReaderEntity } from '../service/client';
import { createLogger } from 'winston';
import { gql } from 'apollo-server';
describe('Catalog Module', () => {
const worker = setupServer();
const mockCatalogBaseUrl = 'http://im.mock';
const mockConfig = ConfigReader.fromConfigs([
{
context: '',
data: {
backend: {
baseUrl: mockCatalogBaseUrl,
},
},
},
]);
beforeAll(() => worker.listen());
afterAll(() => worker.close());
afterEach(() => worker.resetHandlers());
describe('Default Entity', () => {
beforeEach(() => {
const mockResponse: ReaderEntity[] = [
{
apiVersion: 'something',
kind: 'Component',
metadata: {
annotations: {},
etag: '123',
generation: 1,
labels: {},
name: 'Ben',
namespace: 'Blames',
uid: '123',
},
spec: {
type: 'thing',
lifecycle: 'something',
owner: 'auser',
},
},
];
worker.use(
rest.get(`${mockCatalogBaseUrl}/catalog/entities`, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockResponse)),
),
);
});
it('should call the catalog client when requesting entities', async () => {
const { schema } = await createModule({
config: mockConfig,
logger: createLogger(),
});
const result = await execute({
schema,
document: gql`
query {
catalog {
list {
kind
apiVersion
metadata {
name
}
}
}
}
`,
});
const [catalogItem] = result.data?.catalog.list;
expect(catalogItem.kind).toBe('Component');
expect(catalogItem.apiVersion).toBe('something');
expect(catalogItem.metadata.name).toBe('Ben');
});
it('Defaults to empty annotations when none are provided', async () => {
const mockResponse: ReaderEntity[] = [
{
apiVersion: 'something',
kind: 'Component',
metadata: {
annotations: null as any,
etag: '123',
generation: 1,
labels: {},
name: 'Ben',
namespace: 'Blames',
uid: '123',
},
spec: {
type: 'thing',
lifecycle: 'something',
owner: 'auser',
},
},
];
worker.use(
rest.get(`${mockCatalogBaseUrl}/catalog/entities`, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockResponse)),
),
);
const { schema } = await createModule({
config: mockConfig,
logger: createLogger(),
});
const result = await execute({
schema,
document: gql`
query {
catalog {
list {
metadata {
annotations
}
}
}
}
`,
});
const [catalogItem] = result.data?.catalog.list;
expect(catalogItem.metadata.annotations).toEqual({});
});
it('Defaults to empty labels when none are provided', async () => {
const mockResponse: ReaderEntity[] = [
{
apiVersion: 'something',
kind: 'Component',
metadata: {
annotations: {},
etag: '123',
generation: 1,
labels: null as any,
name: 'Ben',
namespace: 'Blames',
uid: '123',
},
spec: {
type: 'thing',
lifecycle: 'something',
owner: 'auser',
},
},
];
worker.use(
rest.get(`${mockCatalogBaseUrl}/catalog/entities`, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockResponse)),
),
);
const { schema } = await createModule({
config: mockConfig,
logger: createLogger(),
});
const result = await execute({
schema,
document: gql`
query {
catalog {
list {
metadata {
labels
}
}
}
}
`,
});
const [catalogItem] = result.data?.catalog.list;
expect(catalogItem.metadata.labels).toEqual({});
});
it('Returns the correct record from the annotation dictionary', async () => {
const mockResponse: ReaderEntity[] = [
{
apiVersion: 'something',
kind: 'Component',
metadata: {
annotations: { lob: 'bloben' },
etag: '123',
generation: 1,
labels: {},
name: 'Ben',
namespace: 'Blames',
uid: '123',
},
spec: {
type: 'thing',
lifecycle: 'something',
owner: 'auser',
},
},
];
worker.use(
rest.get(`${mockCatalogBaseUrl}/catalog/entities`, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockResponse)),
),
);
const { schema } = await createModule({
config: mockConfig,
logger: createLogger(),
});
const result = await execute({
schema,
document: gql`
query {
catalog {
list {
metadata {
test: annotation(name: "lob")
}
}
}
}
`,
});
const [catalogItem] = result.data?.catalog.list;
expect(catalogItem.metadata.test).toEqual('bloben');
});
it('Returns the correct record from the labels dictionary', async () => {
const mockResponse: ReaderEntity[] = [
{
apiVersion: 'something',
kind: 'Component',
metadata: {
annotations: {},
etag: '123',
generation: 1,
labels: { lob2: 'bloben' },
name: 'Ben',
namespace: 'Blames',
uid: '123',
},
spec: {
type: 'thing',
lifecycle: 'something',
owner: 'auser',
},
},
];
worker.use(
rest.get(`${mockCatalogBaseUrl}/catalog/entities`, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockResponse)),
),
);
const { schema } = await createModule({
config: mockConfig,
logger: createLogger(),
});
const result = await execute({
schema,
document: gql`
query {
catalog {
list {
metadata {
test: label(name: "lob2")
}
}
}
}
`,
});
const [catalogItem] = result.data?.catalog.list;
expect(catalogItem.metadata.test).toEqual('bloben');
});
});
});
@@ -0,0 +1,110 @@
/*
* 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 { Logger } from 'winston';
import { makeExecutableSchema } from 'apollo-server';
import { GraphQLModule } from '@graphql-modules/core';
import { Resolvers, CatalogQuery } from './types';
import { Config } from '@backstage/config';
import { CatalogClient } from '../service/client';
import GraphQLJSON, { GraphQLJSONObject } from 'graphql-type-json';
import { Entity } from '@backstage/catalog-model';
export interface ModuleOptions {
logger: Logger;
config: Config;
}
export async function createModule(
options: ModuleOptions,
): Promise<GraphQLModule> {
const { default: typeDefs } = require('../schema');
const catalogClient = new CatalogClient(
options.config.getString('backend.baseUrl'),
);
const resolvers: Resolvers = {
JSON: GraphQLJSON,
JSONObject: GraphQLJSONObject,
DefaultEntitySpec: {
raw: rootValue => {
const { entity } = rootValue as { entity: Entity };
return entity.spec ?? null;
},
},
Query: {
catalog: () => ({} as CatalogQuery),
},
CatalogQuery: {
list: async () => {
return await catalogClient.list();
},
},
CatalogEntity: {
metadata: entity => ({ ...entity.metadata!, entity }),
spec: entity => ({ ...entity.spec!, entity }),
},
EntityMetadata: {
__resolveType: rootValue => {
const {
entity: { kind },
} = rootValue as { entity: Entity };
switch (kind) {
case 'Component':
return 'ComponentMetadata';
case 'Template':
return 'TemplateMetadata';
default:
return 'DefaultEntityMetadata';
}
},
annotation: (e, { name }) => e.annotations?.[name] ?? null,
labels: e => e.labels ?? {},
annotations: e => e.annotations ?? {},
label: (e, { name }) => e.labels?.[name] ?? null,
},
EntitySpec: {
__resolveType: rootValue => {
const {
entity: { kind },
} = rootValue as { entity: Entity };
switch (kind) {
case 'Component':
return 'ComponentEntitySpec';
case 'Template':
return 'TemplateEntitySpec';
default:
return 'DefaultEntitySpec';
}
},
},
};
const schema = makeExecutableSchema({
typeDefs,
resolvers,
inheritResolversFromInterfaces: true,
});
const module = new GraphQLModule({
extraSchemas: [schema],
logger: options.logger as any,
});
return module;
}
@@ -0,0 +1,587 @@
/*
* 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 {
GraphQLResolveInfo,
GraphQLScalarType,
GraphQLScalarTypeConfig,
} from 'graphql';
import { ModuleContext } from '@graphql-modules/core';
export type Maybe<T> = T | null;
export type Exact<T extends { [key: string]: unknown }> = {
[K in keyof T]: T[K];
};
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export type RequireFields<T, K extends keyof T> = {
[X in Exclude<keyof T, K>]?: T[X];
} &
{ [P in K]-?: NonNullable<T[P]> };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
JSON: any;
JSONObject: any;
};
export type EntityMetadata = {
name: Scalars['String'];
annotations: Scalars['JSONObject'];
annotation?: Maybe<Scalars['JSON']>;
labels: Scalars['JSONObject'];
label?: Maybe<Scalars['JSON']>;
uid: Scalars['String'];
etag: Scalars['String'];
generation: Scalars['Int'];
};
export type EntityMetadataAnnotationArgs = {
name: Scalars['String'];
};
export type EntityMetadataLabelArgs = {
name: Scalars['String'];
};
export type DefaultEntityMetadata = EntityMetadata & {
__typename?: 'DefaultEntityMetadata';
name: Scalars['String'];
annotations: Scalars['JSONObject'];
annotation?: Maybe<Scalars['JSON']>;
labels: Scalars['JSONObject'];
label?: Maybe<Scalars['JSON']>;
uid: Scalars['String'];
etag: Scalars['String'];
generation: Scalars['Int'];
};
export type DefaultEntityMetadataAnnotationArgs = {
name: Scalars['String'];
};
export type DefaultEntityMetadataLabelArgs = {
name: Scalars['String'];
};
export type ComponentMetadata = EntityMetadata & {
__typename?: 'ComponentMetadata';
name: Scalars['String'];
annotations: Scalars['JSONObject'];
annotation?: Maybe<Scalars['JSON']>;
labels: Scalars['JSONObject'];
label?: Maybe<Scalars['JSON']>;
uid: Scalars['String'];
etag: Scalars['String'];
generation: Scalars['Int'];
relationships?: Maybe<Scalars['String']>;
};
export type ComponentMetadataAnnotationArgs = {
name: Scalars['String'];
};
export type ComponentMetadataLabelArgs = {
name: Scalars['String'];
};
export type TemplateMetadata = EntityMetadata & {
__typename?: 'TemplateMetadata';
name: Scalars['String'];
annotations: Scalars['JSONObject'];
annotation?: Maybe<Scalars['JSON']>;
labels: Scalars['JSONObject'];
label?: Maybe<Scalars['JSON']>;
uid: Scalars['String'];
etag: Scalars['String'];
generation: Scalars['Int'];
updatedBy?: Maybe<Scalars['String']>;
};
export type TemplateMetadataAnnotationArgs = {
name: Scalars['String'];
};
export type TemplateMetadataLabelArgs = {
name: Scalars['String'];
};
export type TemplateEntitySpec = {
__typename?: 'TemplateEntitySpec';
type: Scalars['String'];
path?: Maybe<Scalars['String']>;
schema: Scalars['JSONObject'];
templater: Scalars['String'];
};
export type ComponentEntitySpec = {
__typename?: 'ComponentEntitySpec';
title: Scalars['String'];
lifecycle: Scalars['String'];
owner: Scalars['String'];
};
export type LocationEntitySpec = {
__typename?: 'LocationEntitySpec';
type: Scalars['String'];
target?: Maybe<Scalars['String']>;
targets: Array<Scalars['String']>;
};
export type DefaultEntitySpec = {
__typename?: 'DefaultEntitySpec';
raw?: Maybe<Scalars['JSONObject']>;
};
export type EntitySpec =
| DefaultEntitySpec
| TemplateEntitySpec
| LocationEntitySpec
| ComponentEntitySpec;
export type CatalogEntity = {
__typename?: 'CatalogEntity';
apiVersion: Scalars['String'];
kind: Scalars['String'];
metadata?: Maybe<EntityMetadata>;
spec: EntitySpec;
};
export type CatalogQuery = {
__typename?: 'CatalogQuery';
list: Array<CatalogEntity>;
};
export type Query = {
__typename?: 'Query';
catalog: CatalogQuery;
};
export type WithIndex<TObject> = TObject & Record<string, any>;
export type ResolversObject<TObject> = WithIndex<TObject>;
export type ResolverTypeWrapper<T> = Promise<T> | T;
export type LegacyStitchingResolver<TResult, TParent, TContext, TArgs> = {
fragment: string;
resolve: ResolverFn<TResult, TParent, TContext, TArgs>;
};
export type NewStitchingResolver<TResult, TParent, TContext, TArgs> = {
selectionSet: string;
resolve: ResolverFn<TResult, TParent, TContext, TArgs>;
};
export type StitchingResolver<TResult, TParent, TContext, TArgs> =
| LegacyStitchingResolver<TResult, TParent, TContext, TArgs>
| NewStitchingResolver<TResult, TParent, TContext, TArgs>;
export type Resolver<TResult, TParent = {}, TContext = {}, TArgs = {}> =
| ResolverFn<TResult, TParent, TContext, TArgs>
| StitchingResolver<TResult, TParent, TContext, TArgs>;
export type ResolverFn<TResult, TParent, TContext, TArgs> = (
parent: TParent,
args: TArgs,
context: TContext,
info: GraphQLResolveInfo,
) => Promise<TResult> | TResult;
export type SubscriptionSubscribeFn<TResult, TParent, TContext, TArgs> = (
parent: TParent,
args: TArgs,
context: TContext,
info: GraphQLResolveInfo,
) => AsyncIterator<TResult> | Promise<AsyncIterator<TResult>>;
export type SubscriptionResolveFn<TResult, TParent, TContext, TArgs> = (
parent: TParent,
args: TArgs,
context: TContext,
info: GraphQLResolveInfo,
) => TResult | Promise<TResult>;
export interface SubscriptionSubscriberObject<
TResult,
TKey extends string,
TParent,
TContext,
TArgs
> {
subscribe: SubscriptionSubscribeFn<
{ [key in TKey]: TResult },
TParent,
TContext,
TArgs
>;
resolve?: SubscriptionResolveFn<
TResult,
{ [key in TKey]: TResult },
TContext,
TArgs
>;
}
export interface SubscriptionResolverObject<TResult, TParent, TContext, TArgs> {
subscribe: SubscriptionSubscribeFn<any, TParent, TContext, TArgs>;
resolve: SubscriptionResolveFn<TResult, any, TContext, TArgs>;
}
export type SubscriptionObject<
TResult,
TKey extends string,
TParent,
TContext,
TArgs
> =
| SubscriptionSubscriberObject<TResult, TKey, TParent, TContext, TArgs>
| SubscriptionResolverObject<TResult, TParent, TContext, TArgs>;
export type SubscriptionResolver<
TResult,
TKey extends string,
TParent = {},
TContext = {},
TArgs = {}
> =
| ((
...args: any[]
) => SubscriptionObject<TResult, TKey, TParent, TContext, TArgs>)
| SubscriptionObject<TResult, TKey, TParent, TContext, TArgs>;
export type TypeResolveFn<TTypes, TParent = {}, TContext = {}> = (
parent: TParent,
context: TContext,
info: GraphQLResolveInfo,
) => Maybe<TTypes> | Promise<Maybe<TTypes>>;
export type IsTypeOfResolverFn<T = {}> = (
obj: T,
info: GraphQLResolveInfo,
) => boolean | Promise<boolean>;
export type NextResolverFn<T> = () => Promise<T>;
export type DirectiveResolverFn<
TResult = {},
TParent = {},
TContext = {},
TArgs = {}
> = (
next: NextResolverFn<TResult>,
parent: TParent,
args: TArgs,
context: TContext,
info: GraphQLResolveInfo,
) => TResult | Promise<TResult>;
/** Mapping between all available schema types and the resolvers types */
export type ResolversTypes = ResolversObject<{
JSON: ResolverTypeWrapper<Partial<Scalars['JSON']>>;
JSONObject: ResolverTypeWrapper<Partial<Scalars['JSONObject']>>;
EntityMetadata:
| ResolversTypes['DefaultEntityMetadata']
| ResolversTypes['ComponentMetadata']
| ResolversTypes['TemplateMetadata'];
String: ResolverTypeWrapper<Partial<Scalars['String']>>;
Int: ResolverTypeWrapper<Partial<Scalars['Int']>>;
DefaultEntityMetadata: ResolverTypeWrapper<Partial<DefaultEntityMetadata>>;
ComponentMetadata: ResolverTypeWrapper<Partial<ComponentMetadata>>;
TemplateMetadata: ResolverTypeWrapper<Partial<TemplateMetadata>>;
TemplateEntitySpec: ResolverTypeWrapper<Partial<TemplateEntitySpec>>;
ComponentEntitySpec: ResolverTypeWrapper<Partial<ComponentEntitySpec>>;
LocationEntitySpec: ResolverTypeWrapper<Partial<LocationEntitySpec>>;
DefaultEntitySpec: ResolverTypeWrapper<Partial<DefaultEntitySpec>>;
EntitySpec: Partial<
| ResolversTypes['DefaultEntitySpec']
| ResolversTypes['TemplateEntitySpec']
| ResolversTypes['LocationEntitySpec']
| ResolversTypes['ComponentEntitySpec']
>;
CatalogEntity: ResolverTypeWrapper<
Partial<
Omit<CatalogEntity, 'spec'> & { spec: ResolversTypes['EntitySpec'] }
>
>;
CatalogQuery: ResolverTypeWrapper<Partial<CatalogQuery>>;
Query: ResolverTypeWrapper<{}>;
Boolean: ResolverTypeWrapper<Partial<Scalars['Boolean']>>;
}>;
/** Mapping between all available schema types and the resolvers parents */
export type ResolversParentTypes = ResolversObject<{
JSON: Partial<Scalars['JSON']>;
JSONObject: Partial<Scalars['JSONObject']>;
EntityMetadata:
| ResolversParentTypes['DefaultEntityMetadata']
| ResolversParentTypes['ComponentMetadata']
| ResolversParentTypes['TemplateMetadata'];
String: Partial<Scalars['String']>;
Int: Partial<Scalars['Int']>;
DefaultEntityMetadata: Partial<DefaultEntityMetadata>;
ComponentMetadata: Partial<ComponentMetadata>;
TemplateMetadata: Partial<TemplateMetadata>;
TemplateEntitySpec: Partial<TemplateEntitySpec>;
ComponentEntitySpec: Partial<ComponentEntitySpec>;
LocationEntitySpec: Partial<LocationEntitySpec>;
DefaultEntitySpec: Partial<DefaultEntitySpec>;
EntitySpec: Partial<
| ResolversParentTypes['DefaultEntitySpec']
| ResolversParentTypes['TemplateEntitySpec']
| ResolversParentTypes['LocationEntitySpec']
| ResolversParentTypes['ComponentEntitySpec']
>;
CatalogEntity: Partial<
Omit<CatalogEntity, 'spec'> & { spec: ResolversParentTypes['EntitySpec'] }
>;
CatalogQuery: Partial<CatalogQuery>;
Query: {};
Boolean: Partial<Scalars['Boolean']>;
}>;
export interface JsonScalarConfig
extends GraphQLScalarTypeConfig<ResolversTypes['JSON'], any> {
name: 'JSON';
}
export interface JsonObjectScalarConfig
extends GraphQLScalarTypeConfig<ResolversTypes['JSONObject'], any> {
name: 'JSONObject';
}
export type EntityMetadataResolvers<
ContextType = ModuleContext,
ParentType = ResolversParentTypes['EntityMetadata']
> = ResolversObject<{
__resolveType: TypeResolveFn<
'DefaultEntityMetadata' | 'ComponentMetadata' | 'TemplateMetadata',
ParentType,
ContextType
>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
annotations?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
annotation?: Resolver<
Maybe<ResolversTypes['JSON']>,
ParentType,
ContextType,
RequireFields<EntityMetadataAnnotationArgs, 'name'>
>;
labels?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
label?: Resolver<
Maybe<ResolversTypes['JSON']>,
ParentType,
ContextType,
RequireFields<EntityMetadataLabelArgs, 'name'>
>;
uid?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
etag?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
generation?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
}>;
export type DefaultEntityMetadataResolvers<
ContextType = ModuleContext,
ParentType = ResolversParentTypes['DefaultEntityMetadata']
> = ResolversObject<{
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
annotations?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
annotation?: Resolver<
Maybe<ResolversTypes['JSON']>,
ParentType,
ContextType,
RequireFields<DefaultEntityMetadataAnnotationArgs, 'name'>
>;
labels?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
label?: Resolver<
Maybe<ResolversTypes['JSON']>,
ParentType,
ContextType,
RequireFields<DefaultEntityMetadataLabelArgs, 'name'>
>;
uid?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
etag?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
generation?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
}>;
export type ComponentMetadataResolvers<
ContextType = ModuleContext,
ParentType = ResolversParentTypes['ComponentMetadata']
> = ResolversObject<{
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
annotations?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
annotation?: Resolver<
Maybe<ResolversTypes['JSON']>,
ParentType,
ContextType,
RequireFields<ComponentMetadataAnnotationArgs, 'name'>
>;
labels?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
label?: Resolver<
Maybe<ResolversTypes['JSON']>,
ParentType,
ContextType,
RequireFields<ComponentMetadataLabelArgs, 'name'>
>;
uid?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
etag?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
generation?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
relationships?: Resolver<
Maybe<ResolversTypes['String']>,
ParentType,
ContextType
>;
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
}>;
export type TemplateMetadataResolvers<
ContextType = ModuleContext,
ParentType = ResolversParentTypes['TemplateMetadata']
> = ResolversObject<{
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
annotations?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
annotation?: Resolver<
Maybe<ResolversTypes['JSON']>,
ParentType,
ContextType,
RequireFields<TemplateMetadataAnnotationArgs, 'name'>
>;
labels?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
label?: Resolver<
Maybe<ResolversTypes['JSON']>,
ParentType,
ContextType,
RequireFields<TemplateMetadataLabelArgs, 'name'>
>;
uid?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
etag?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
generation?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
updatedBy?: Resolver<
Maybe<ResolversTypes['String']>,
ParentType,
ContextType
>;
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
}>;
export type TemplateEntitySpecResolvers<
ContextType = ModuleContext,
ParentType = ResolversParentTypes['TemplateEntitySpec']
> = ResolversObject<{
type?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
path?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
schema?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
templater?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
}>;
export type ComponentEntitySpecResolvers<
ContextType = ModuleContext,
ParentType = ResolversParentTypes['ComponentEntitySpec']
> = ResolversObject<{
title?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
lifecycle?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
owner?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
}>;
export type LocationEntitySpecResolvers<
ContextType = ModuleContext,
ParentType = ResolversParentTypes['LocationEntitySpec']
> = ResolversObject<{
type?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
target?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
targets?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
}>;
export type DefaultEntitySpecResolvers<
ContextType = ModuleContext,
ParentType = ResolversParentTypes['DefaultEntitySpec']
> = ResolversObject<{
raw?: Resolver<Maybe<ResolversTypes['JSONObject']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
}>;
export type EntitySpecResolvers<
ContextType = ModuleContext,
ParentType = ResolversParentTypes['EntitySpec']
> = ResolversObject<{
__resolveType: TypeResolveFn<
| 'DefaultEntitySpec'
| 'TemplateEntitySpec'
| 'LocationEntitySpec'
| 'ComponentEntitySpec',
ParentType,
ContextType
>;
}>;
export type CatalogEntityResolvers<
ContextType = ModuleContext,
ParentType = ResolversParentTypes['CatalogEntity']
> = ResolversObject<{
apiVersion?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
kind?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
metadata?: Resolver<
Maybe<ResolversTypes['EntityMetadata']>,
ParentType,
ContextType
>;
spec?: Resolver<ResolversTypes['EntitySpec'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
}>;
export type CatalogQueryResolvers<
ContextType = ModuleContext,
ParentType = ResolversParentTypes['CatalogQuery']
> = ResolversObject<{
list?: Resolver<
Array<ResolversTypes['CatalogEntity']>,
ParentType,
ContextType
>;
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
}>;
export type QueryResolvers<
ContextType = ModuleContext,
ParentType = ResolversParentTypes['Query']
> = ResolversObject<{
catalog?: Resolver<ResolversTypes['CatalogQuery'], ParentType, ContextType>;
}>;
export type Resolvers<ContextType = ModuleContext> = ResolversObject<{
JSON?: GraphQLScalarType;
JSONObject?: GraphQLScalarType;
EntityMetadata?: EntityMetadataResolvers<ContextType>;
DefaultEntityMetadata?: DefaultEntityMetadataResolvers<ContextType>;
ComponentMetadata?: ComponentMetadataResolvers<ContextType>;
TemplateMetadata?: TemplateMetadataResolvers<ContextType>;
TemplateEntitySpec?: TemplateEntitySpecResolvers<ContextType>;
ComponentEntitySpec?: ComponentEntitySpecResolvers<ContextType>;
LocationEntitySpec?: LocationEntitySpecResolvers<ContextType>;
DefaultEntitySpec?: DefaultEntitySpecResolvers<ContextType>;
EntitySpec?: EntitySpecResolvers<ContextType>;
CatalogEntity?: CatalogEntityResolvers<ContextType>;
CatalogQuery?: CatalogQueryResolvers<ContextType>;
Query?: QueryResolvers<ContextType>;
}>;
/**
* @deprecated
* Use "Resolvers" root object instead. If you wish to get "IResolvers", add "typesPrefix: I" to your config.
*/
export type IResolvers<ContextType = ModuleContext> = Resolvers<ContextType>;
@@ -14,20 +14,4 @@
* limitations under the License.
*/
import { getRootLogger } from '@backstage/backend-common';
import yn from 'yn';
import { startStandaloneServer } from './service/standaloneServer';
const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000;
const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
const logger = getRootLogger();
startStandaloneServer({ port, enableCors, logger }).catch(err => {
logger.error(err);
process.exit(1);
});
process.on('SIGINT', () => {
logger.info('CTRL+C pressed; exiting.');
process.exit(0);
});
export * from './graphql/module';
+88
View File
@@ -0,0 +1,88 @@
/*
* 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 metadataFields = /* GraphQL */ `
name: String!
annotations: JSONObject!
annotation(name: String!): JSON
labels: JSONObject!
label(name: String!): JSON
uid: String!
etag: String!
generation: Int!
`;
const schema = /* GraphQL */ `
scalar JSON
scalar JSONObject
interface EntityMetadata {
${metadataFields}
}
type DefaultEntityMetadata implements EntityMetadata {
${metadataFields}
}
type ComponentMetadata implements EntityMetadata {
${metadataFields}
# mock field to prove extensions working
relationships: String
}
type TemplateMetadata implements EntityMetadata {
${metadataFields}
# mock field to prove extensions working
updatedBy: String
}
# TODO: move this definition into plugin-scaffolder-graphql
type TemplateEntitySpec {
type: String!
path: String
schema: JSONObject!
templater: String!
}
type ComponentEntitySpec {
type: String!
lifecycle: String!
owner: String!
}
type DefaultEntitySpec {
raw: JSONObject
}
union EntitySpec = DefaultEntitySpec | TemplateEntitySpec | ComponentEntitySpec
type CatalogEntity {
apiVersion: String!
kind: String!
metadata: EntityMetadata
spec: EntitySpec!
}
type CatalogQuery {
list: [CatalogEntity!]!
}
type Query {
catalog: CatalogQuery!
}
`;
export default schema;
@@ -0,0 +1,59 @@
/*
* 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 { CatalogClient } from './client';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
describe('Catalog GraphQL Module', () => {
const worker = setupServer();
beforeAll(() => worker.listen());
afterAll(() => worker.close());
afterEach(() => worker.resetHandlers());
const baseUrl = 'http://localhost:1234';
it('will return the entities', async () => {
const expectedResponse = [{ id: 'something' }];
worker.use(
rest.get(`${baseUrl}/catalog/entities`, (_, res, ctx) =>
res(ctx.status(200), ctx.json(expectedResponse)),
),
);
const client = new CatalogClient(baseUrl);
const response = await client.list();
expect(response).toEqual(expectedResponse);
});
it('throws an error with the text', async () => {
const expectedResponse = 'something broke';
worker.use(
rest.get(`${baseUrl}/catalog/entities`, (_, res, ctx) =>
res(ctx.status(500), ctx.text(expectedResponse)),
),
);
const client = new CatalogClient(baseUrl);
await expect(() => client.list()).rejects.toThrow(expectedResponse);
});
});
@@ -0,0 +1,45 @@
/*
* 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 { Entity, EntityMeta } from '@backstage/catalog-model';
import fetch from 'node-fetch';
import { JsonObject } from '@backstage/config';
export interface ReaderEntityMeta extends EntityMeta {
uid: string;
etag: string;
generation: number;
namespace: string;
annotations: Record<string, string>;
labels: Record<string, string>;
}
export interface ReaderEntity extends Entity {
metadata: JsonObject & ReaderEntityMeta;
}
export class CatalogClient {
constructor(private baseUrl: string) {}
async list(): Promise<ReaderEntity[]> {
const res = await fetch(`${this.baseUrl}/catalog/entities`);
if (!res.ok) {
// todo(blam): need some better way to handle errors here
// experiment with throwing the input errors etc and having graphql versions of that
throw new Error(await res.text());
}
const entities: ReaderEntity[] = await res.json();
return entities;
}
}
@@ -13,21 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getRootLogger } from '@backstage/backend-common';
import yn from 'yn';
import { startStandaloneServer } from './service/standaloneServer';
const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000;
const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
const logger = getRootLogger();
startStandaloneServer({ port, enableCors, logger }).catch(err => {
logger.error(err);
process.exit(1);
});
process.on('SIGINT', () => {
logger.info('CTRL+C pressed; exiting.');
process.exit(0);
});
export {};
+11 -11
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,14 +21,15 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.21",
"@backstage/plugin-techdocs": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.22",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.22",
"@backstage/plugin-techdocs": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"@types/react": "^16.9",
"classnames": "^2.2.6",
"moment": "^2.26.0",
"react": "^16.13.1",
@@ -37,13 +38,12 @@
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^15.3.3",
"swr": "^0.3.0",
"@types/react": "^16.9"
"swr": "^0.3.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/dev-utils": "^0.1.1-alpha.21",
"@backstage/test-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/dev-utils": "^0.1.1-alpha.22",
"@backstage/test-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/react-hooks": "^3.3.0",
@@ -47,7 +47,7 @@ export const EntityFilterGroupsProvider = ({
function useProvideEntityFilters(): FilterGroupsContext {
const catalogApi = useApi(catalogApiRef);
const [{ value: entities, error }, doReload] = useAsyncFn(() =>
catalogApi.getEntities(),
catalogApi.getEntities({ kind: 'Component' }),
);
const filterGroups = useRef<{
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-circleci",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,10 +21,10 @@
"postpack": "backstage-cli postpack"
},
"dependencies": {
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.21",
"@backstage/plugin-catalog": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.22",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/plugin-catalog": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -38,8 +38,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/dev-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/dev-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-cloudbuild",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,11 +21,11 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/core-api": "^0.1.1-alpha.21",
"@backstage/plugin-catalog": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.22",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/core-api": "^0.1.1-alpha.22",
"@backstage/plugin-catalog": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -40,8 +40,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/dev-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/dev-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-explore",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,21 +21,21 @@
"start": "backstage-cli plugin:serve"
},
"dependencies": {
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"classnames": "^2.2.6",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-use": "^15.3.3",
"react-router": "6.0.0-beta.0"
"react-router": "6.0.0-beta.0",
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/dev-utils": "^0.1.1-alpha.21",
"@backstage/test-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/dev-utils": "^0.1.1-alpha.22",
"@backstage/test-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-gcp-projects",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,8 +21,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -32,8 +32,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/dev-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/dev-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
-124
View File
@@ -1,124 +0,0 @@
/*
* 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 { GCPApi } from './GCPApi';
import { Project, Operation, Status } from './types';
const BaseURL =
'https://content-cloudresourcemanager.googleapis.com/v1/projects';
export class GCPClient implements GCPApi {
async listProjects({ token }: { token: string }): Promise<Project[]> {
const response = await fetch(BaseURL, {
headers: new Headers({
Accept: '*/*',
Authorization: `Bearer ${token}`,
}),
});
if (!response.ok) {
return [
{
name: 'Error',
projectNumber: 'Response status is not OK',
projectId: 'Error',
lifecycleState: 'error',
createTime: 'Error',
},
];
}
const data = await response.json();
return data.projects;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getProject(
projectId: string,
token: Promise<string>,
): Promise<Project> {
const url = `${BaseURL}/${projectId}`;
const response = await fetch(url, {
headers: new Headers({
Authorization: `Bearer ${await token}`,
}),
});
const dataBlank: Project = {
name: 'Error',
projectNumber: `Response status is ${response.status}`,
projectId: 'Error',
lifecycleState: 'error',
createTime: 'Error',
};
if (!response.ok) {
return dataBlank;
}
const data = await response.json();
const newData: Project = data;
return newData;
}
async createProject(
projectName: string,
projectId: string,
token: string,
): Promise<Operation> {
const status: Status = {
code: 0,
message: '',
details: [],
};
const op: Operation = {
name: '',
metadata: '',
done: true,
error: status,
response: '',
};
const newProject: Project = {
name: projectName,
projectId: projectId,
};
const body = JSON.stringify(newProject);
const response = await fetch(BaseURL, {
headers: new Headers({
Accept: '*/*',
Authorization: `Bearer ${token}`,
}),
body: body,
method: 'POST',
});
if (!response.ok) {
status.code = response.status;
return op;
}
const data = await response.json();
return data;
}
}
@@ -17,18 +17,16 @@
import { createApiRef } from '@backstage/core';
import { Project, Operation } from './types';
export const GCPApiRef = createApiRef<GCPApi>({
export const gcpApiRef = createApiRef<GcpApi>({
id: 'plugin.gcpprojects.service',
description: 'Used by the GCP Projects plugin to make requests',
});
export type GCPApi = {
listProjects: ({ token }: { token: string }) => Promise<Project[]>;
getProject: (projectId: string, token: Promise<string>) => Promise<Project>;
createProject: (
projectName: string,
projectId: string,
owner: string,
token: string,
) => Promise<Operation>;
export type GcpApi = {
listProjects(): Promise<Project[]>;
getProject(projectId: string): Promise<Project>;
createProject(options: {
projectId: string;
projectName: string;
}): Promise<Operation>;
};
+98
View File
@@ -0,0 +1,98 @@
/*
* 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 { OAuthApi } from '@backstage/core';
import { GcpApi } from './GcpApi';
import { Operation, Project } from './types';
const BASE_URL =
'https://content-cloudresourcemanager.googleapis.com/v1/projects';
export class GcpClient implements GcpApi {
constructor(private readonly googleAuthApi: OAuthApi) {}
async listProjects(): Promise<Project[]> {
const response = await fetch(BASE_URL, {
headers: {
Accept: '*/*',
Authorization: `Bearer ${await this.getToken()}`,
},
});
if (!response.ok) {
throw new Error(
`List request failed to ${BASE_URL} with ${response.status} ${response.statusText}`,
);
}
const { projects } = await response.json();
return projects;
}
async getProject(projectId: string): Promise<Project> {
const url = `${BASE_URL}/${projectId}`;
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${await this.getToken()}`,
},
});
if (!response.ok) {
throw new Error(
`Get request failed to ${url} with ${response.status} ${response.statusText}`,
);
}
return await response.json();
}
async createProject(options: {
projectId: string;
projectName: string;
}): Promise<Operation> {
const newProject: Project = {
name: options.projectName,
projectId: options.projectId,
};
const response = await fetch(BASE_URL, {
method: 'POST',
headers: {
Accept: '*/*',
Authorization: `Bearer ${await this.getToken()}`,
},
body: JSON.stringify(newProject),
});
if (!response.ok) {
throw new Error(
`Create request failed to ${BASE_URL} with ${response.status} ${response.statusText}`,
);
}
return await response.json();
}
async getToken(): Promise<string> {
// NOTE(freben): There's a .read-only variant of this scope that we could
// use for readonly operations, but that means we would ask the user for a
// second auth during creation and I decided to keep the wider scope for
// all ops for now
return this.googleAuthApi.getAccessToken(
'https://www.googleapis.com/auth/cloud-platform',
);
}
}
+2 -2
View File
@@ -14,6 +14,6 @@
* limitations under the License.
*/
export * from './GCPApi';
export * from './GCPClient';
export * from './GcpApi';
export * from './GcpClient';
export * from './types';
@@ -14,24 +14,23 @@
* limitations under the License.
*/
import React, { FC, useState } from 'react';
import { Grid, Button, TextField } from '@material-ui/core';
import {
InfoCard,
Content,
ContentHeader,
Header,
HeaderLabel,
InfoCard,
Page,
pageTheme,
SimpleStepper,
SimpleStepperStep,
StructuredMetadataTable,
HeaderLabel,
Page,
Header,
pageTheme,
SupportButton,
} from '@backstage/core';
import { Button, Grid, TextField } from '@material-ui/core';
import React, { useState } from 'react';
export const Project: FC<{}> = () => {
export const Project = () => {
const [projectName, setProjectName] = useState('');
const [projectId, setProjectId] = useState('');
const [disabled, setDisabled] = useState(true);
@@ -14,6 +14,17 @@
* limitations under the License.
*/
import {
Content,
ContentHeader,
Header,
HeaderLabel,
Page,
pageTheme,
SupportButton,
useApi,
WarningPanel,
} from '@backstage/core';
import {
Button,
ButtonGroup,
@@ -27,20 +38,9 @@ import {
Theme,
Typography,
} from '@material-ui/core';
import {
useApi,
googleAuthApiRef,
HeaderLabel,
Page,
Header,
pageTheme,
SupportButton,
Content,
ContentHeader,
} from '@backstage/core';
import React from 'react';
import { useAsync } from 'react-use';
import { GCPApiRef } from '../../api';
import { gcpApiRef } from '../../api';
const useStyles = makeStyles<Theme>(theme => ({
root: {
@@ -56,34 +56,27 @@ const useStyles = makeStyles<Theme>(theme => ({
}));
const DetailsPage = () => {
const api = useApi(GCPApiRef);
const googleApi = useApi(googleAuthApiRef);
const token = googleApi.getAccessToken(
'https://www.googleapis.com/auth/cloud-platform.read-only',
);
const api = useApi(gcpApiRef);
const classes = useStyles();
const status = useAsync(
() =>
const { loading, error, value: details } = useAsync(
async () =>
api.getProject(
decodeURIComponent(location.search.split('projectId=')[1]),
token,
),
[location.search],
);
if (status.loading) {
if (loading) {
return <LinearProgress />;
} else if (status.error) {
} else if (error) {
return (
<Typography variant="h6" color="error">
Failed to load build, {status.error.message}
</Typography>
<WarningPanel title="Failed to load project">
{error.toString()}
</WarningPanel>
);
}
const details = status.value;
return (
<Table component={Paper} className={classes.table}>
<Table>
@@ -17,18 +17,19 @@
// NEEDS WORK
import {
Link,
useApi,
googleAuthApiRef,
HeaderLabel,
Page,
Header,
pageTheme,
SupportButton,
Content,
ContentHeader,
Header,
HeaderLabel,
Link,
Page,
pageTheme,
SupportButton,
useApi,
WarningPanel,
} from '@backstage/core';
import {
Button,
LinearProgress,
Paper,
Table,
@@ -38,11 +39,10 @@ import {
TableRow,
Tooltip,
Typography,
Button,
} from '@material-ui/core';
import React from 'react';
import { useAsync } from 'react-use';
import { GCPApiRef, Project } from '../../api';
import { gcpApiRef, Project } from '../../api';
const LongText = ({ text, max }: { text: string; max: number }) => {
if (text.length < max) {
@@ -63,27 +63,17 @@ const labels = (
);
const PageContents = () => {
const api = useApi(GCPApiRef);
const googleApi = useApi(googleAuthApiRef);
const api = useApi(gcpApiRef);
const { loading, error, value } = useAsync(async () => {
const token = await googleApi.getAccessToken(
'https://www.googleapis.com/auth/cloud-platform.read-only',
);
const projects = api.listProjects({ token });
return projects;
});
const { loading, error, value } = useAsync(() => api.listProjects());
if (loading) {
return <LinearProgress />;
}
if (error) {
} else if (error) {
return (
<Typography variant="h2" color="error">
{error.message}{' '}
</Typography>
<WarningPanel title="Failed to load projects">
{error.toString()}
</WarningPanel>
);
}
+18 -9
View File
@@ -15,34 +15,43 @@
*/
import {
createApiFactory,
createPlugin,
createRouteRef,
createApiFactory,
googleAuthApiRef,
} from '@backstage/core';
import { ProjectListPage } from './components/ProjectListPage';
import { ProjectDetailsPage } from './components/ProjectDetailsPage';
import { gcpApiRef, GcpClient } from './api';
import { NewProjectPage } from './components/NewProjectPage';
import { GCPApiRef, GCPClient } from './api';
import { ProjectDetailsPage } from './components/ProjectDetailsPage';
import { ProjectListPage } from './components/ProjectListPage';
export const rootRouteRef = createRouteRef({
path: '/gcp-projects',
title: 'GCP Projects',
});
export const ProjectRouteRef = createRouteRef({
export const projectRouteRef = createRouteRef({
path: '/gcp-projects/project',
title: 'GCP Project Page',
});
export const NewProjectRouteRef = createRouteRef({
export const newProjectRouteRef = createRouteRef({
path: '/gcp-projects/new',
title: 'GCP Project Page',
});
export const plugin = createPlugin({
id: 'gcp-projects',
apis: [createApiFactory(GCPApiRef, new GCPClient())],
apis: [
createApiFactory({
api: gcpApiRef,
deps: { googleAuthApi: googleAuthApiRef },
factory({ googleAuthApi }) {
return new GcpClient(googleAuthApi);
},
}),
],
register({ router }) {
router.addRoute(rootRouteRef, ProjectListPage);
router.addRoute(ProjectRouteRef, ProjectDetailsPage);
router.addRoute(NewProjectRouteRef, NewProjectPage);
router.addRoute(projectRouteRef, ProjectDetailsPage);
router.addRoute(newProjectRouteRef, NewProjectPage);
},
});
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-github-actions",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,11 +21,11 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/core-api": "^0.1.1-alpha.21",
"@backstage/plugin-catalog": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.22",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/core-api": "^0.1.1-alpha.22",
"@backstage/plugin-catalog": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -40,8 +40,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/dev-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/dev-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-gitops-profiles",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,8 +21,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -32,8 +32,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/dev-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/dev-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-graphiql",
"description": "Backstage plugin for browsing GraphQL APIs",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"publishConfig": {
"access": "public",
@@ -31,8 +31,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -43,9 +43,9 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/dev-utils": "^0.1.1-alpha.21",
"@backstage/test-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/dev-utils": "^0.1.1-alpha.22",
"@backstage/test-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+3 -3
View File
@@ -2,9 +2,9 @@
## Getting Started
This backend plugin can be started in a standalone mode from directly in this package
with `yarn start`. However, it will have limited functionality and that process is
most convenient when developing the plugin itself.
This is the GraphQL Backend plugin.
It is responsible for merging different `graphql-plugins` together to provide the end schema.
To run it within the backend do:
+11 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-graphql-backend",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -19,19 +19,24 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.22",
"@backstage/plugin-catalog-graphql": "^0.1.1-alpha.22",
"@graphql-modules/core": "^0.7.17",
"@backstage/backend-common": "^0.1.1-alpha.22",
"@types/express": "^4.17.6",
"apollo-server": "^2.16.0",
"apollo-server-express": "^2.16.0",
"apollo-server": "^2.16.1",
"apollo-server-express": "^2.16.1",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"graphql": "^15.3.0",
"whatwg-fetch": "^3.4.0",
"helmet": "^4.0.0",
"node-fetch": "^2.6.0",
"reflect-metadata": "^0.1.13",
"winston": "^3.2.1",
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@types/supertest": "^2.0.8",
"eslint-plugin-graphql": "^4.0.0",
"msw": "^0.20.5",
-11
View File
@@ -1,11 +0,0 @@
type CatalogEntity {
id: String
}
type CatalogQuery {
list: [CatalogEntity!]!
}
type Query {
catalog: CatalogQuery!
}
-2
View File
@@ -14,6 +14,4 @@
* limitations under the License.
*/
require('whatwg-fetch');
export * from './service/router';
+18 -3
View File
@@ -13,10 +13,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import './router';
import { createRouter } from './router';
import supertest from 'supertest';
import { ConfigReader } from '@backstage/config';
import { createLogger } from 'winston';
import express from 'express';
describe('Router', () => {
it('should pass the test', () => {
expect(true).toBeTruthy();
describe('/health', () => {
it('should return ok', async () => {
const config = ConfigReader.fromConfigs([
{ data: { backend: { baseUrl: 'lol' } }, context: 'something' },
]);
const router = await createRouter({ config, logger: createLogger() });
const app = express().use(router);
const { body } = await supertest(app).get('/health');
expect(body).toEqual({ status: 'ok' });
});
});
});
+32 -4
View File
@@ -19,7 +19,11 @@ import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import fs from 'fs';
import { GraphQLModule } from '@graphql-modules/core';
import { ApolloServer } from 'apollo-server-express';
import { createModule as createCatalogModule } from '@backstage/plugin-catalog-graphql';
import { Config } from '@backstage/config';
import helmet from 'helmet';
const schemaPath = resolvePackagePath(
'@backstage/plugin-graphql-backend',
@@ -28,6 +32,7 @@ const schemaPath = resolvePackagePath(
export interface RouterOptions {
logger: Logger;
config: Config;
}
export async function createRouter(
@@ -35,16 +40,39 @@ export async function createRouter(
): Promise<express.Router> {
const typeDefs = await fs.promises.readFile(schemaPath, 'utf-8');
const server = new ApolloServer({ typeDefs, logger: options.logger });
const router = Router();
const catalogModule = await createCatalogModule(options);
const apolloMiddleware = server.getMiddleware({ path: '/' });
router.use(apolloMiddleware);
const { schema } = new GraphQLModule({
imports: [catalogModule],
typeDefs,
});
const server = new ApolloServer({
schema,
logger: options.logger,
introspection: true,
playground: process.env.NODE_ENV === 'development',
});
const router = Router();
router.get('/health', (_, response) => {
response.send({ status: 'ok' });
});
const apolloMiddleware = server.getMiddleware({ path: '/' });
if (process.env.NODE_ENV === 'development')
router.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'", "'unsafe-inline'", 'http://*'],
},
}),
);
router.use(apolloMiddleware);
router.use(errorHandler());
return router;
@@ -1,62 +0,0 @@
/*
* 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.
*/
/*
* 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 { createServiceBuilder } from '@backstage/backend-common';
import { Server } from 'http';
import { Logger } from 'winston';
import { createRouter } from './router';
export interface ServerOptions {
port: number;
enableCors: boolean;
logger: Logger;
}
export async function startStandaloneServer(
options: ServerOptions,
): Promise<Server> {
const logger = options.logger.child({ service: 'graphql-backend' });
logger.debug('Starting application server...');
const router = await createRouter({
logger,
});
const service = createServiceBuilder(module)
.enableCors({ origin: 'http://localhost:3000' })
.addRouter('/graphql', router);
return await service.start().catch(err => {
logger.error(err);
process.exit(1);
});
}
module.hot?.accept();
-1
View File
@@ -13,6 +13,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
require('whatwg-fetch');
export {};
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-identity-backend",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,7 +20,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.21",
"@backstage/backend-common": "^0.1.1-alpha.22",
"@types/express": "^4.17.6",
"compression": "^1.7.4",
"cors": "^2.8.5",
@@ -33,7 +33,7 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"jest-fetch-mock": "^3.0.3"
},
"files": [
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-jenkins",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,10 +21,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/plugin-catalog": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.22",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/plugin-catalog": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -36,8 +36,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/dev-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/dev-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-lighthouse",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,9 +21,9 @@
"start": "backstage-cli plugin:serve"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.22",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -34,9 +34,9 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/dev-utils": "^0.1.1-alpha.21",
"@backstage/test-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/dev-utils": "^0.1.1-alpha.22",
"@backstage/test-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-newrelic",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,8 +21,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@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": "^0.1.1-alpha.21",
"@backstage/dev-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/dev-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",

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