diff --git a/CHANGELOG.md b/CHANGELOG.md
index 25360f3e1e..d360dd995d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,10 @@ If you encounter issues while upgrading to a newer version, don't hesitate to re
- Renamed `SessionStateApi` to `SessionApi` and `logout` to `signOut`. Custom implementations of the `SingInPage` app-component will need to rename their `logout` function. The different auth provider items for the `UserSettingsMenu` have been consolidated into a single `ProviderSettingsItem`, meaning you need to replace existing usages of `OAuthProviderSettings` and `OIDCProviderSettings`. [#2555](https://github.com/spotify/backstage/pull/2555).
+### @backstage/auth-backend
+
+- The default mount path of backend plugins was changed to `/api/:pluginId`, and as part of that it was needed to enable configuration of the base path of the auth backend, so that it can construct redirect URLs correctly. The default base path is `/api/auth`, but you need to set this to `/auth` if you want to keep using the old path. Note that you will also need to reconfigure any allowed redirect URLs to include `/api` if you switch to the new recommended pattern. [#2562](https://github.com/spotify/backstage/pull/2562)
+
## v0.1.1-alpha.22
### @backstage/core
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a6f7e5e5e1..d3c2780967 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -32,6 +32,10 @@ If you start developing a plugin that you aim to release as open source, we sugg
You can also use this process if you have an idea for a good plugin but you hope that someone else will pick up the work.
+## Adding Non-code Contributions
+
+Since there is such a large landscape of possible development, build, and deployment environments, we welcome community contributions in these areas in the [`/contrib`](https://github.com/spotify/backstage/tree/master/contrib) folder of the project. This is an excellent place to put things that help out the community at large, but which may not fit within the scope of the core product to support natively. Here, you will find Helm charts, alternative Docker images, and much more.
+
## Write Documentation
The current documentation is very limited. Help us make the `/docs` folder come alive.
diff --git a/README.md b/README.md
index 9ef09aee06..e05c1eb16e 100644
--- a/README.md
+++ b/README.md
@@ -26,6 +26,8 @@ Out of the box, Backstage includes:
For more information go to [backstage.io](https://backstage.io) or join our [Discord chatroom](https://discord.gg/EBHEGzX).
+🎉 Backstage is a CNCF Sandbox project. Read the announcement [here](https://backstage.io/blog/2020/09/23/backstage-cncf-sandbox).
+
## Project roadmap
A detailed project roadmap, including already delivered milestones, is available [here](https://backstage.io/docs/overview/roadmap).
@@ -51,11 +53,9 @@ Check out [the documentation](https://backstage.io/docs/getting-started) on how
- [Code of Conduct](CODE_OF_CONDUCT.md) - This is how we roll
- [Adopters](ADOPTERS.md) - Companies already using Backstage
- [Blog](https://backstage.io/blog/) - Announcements and updates
-- [Newsletter](https://mailchi.mp/spotify/backstage-community)
+- [Newsletter](https://mailchi.mp/spotify/backstage-community) - Subscribe to our email newsletter
- Give us a star ⭐️ - If you are using Backstage or think it is an interesting project, we would love a star ❤️
-Or, if you are an open source developer and are interested in joining our team, please reach out to [foss-opportunities@spotify.com ](mailto:foss-opportunities@spotify.com)
-
## License
Copyright 2020 Spotify AB.
diff --git a/app-config.yaml b/app-config.yaml
index 60ecb813f7..01925c21dd 100644
--- a/app-config.yaml
+++ b/app-config.yaml
@@ -33,8 +33,8 @@ organization:
name: Spotify
techdocs:
- storageUrl: http://localhost:7000/techdocs/static/docs
- requestUrl: http://localhost:7000/techdocs/docs
+ storageUrl: http://localhost:7000/api/techdocs/static/docs
+ requestUrl: http://localhost:7000/api/techdocs/docs
generators:
techdocs: 'docker'
@@ -55,6 +55,10 @@ newrelic:
lighthouse:
baseUrl: http://localhost:3003
+kubernetes:
+ clusterLocatorMethod: 'configMultiTenant'
+ clusters: []
+
catalog:
rules:
- allow: [Component, API, Group, Template, Location]
diff --git a/docs/auth/add-auth-provider.md b/docs/auth/add-auth-provider.md
index c5f7645a49..56586cbfc6 100644
--- a/docs/auth/add-auth-provider.md
+++ b/docs/auth/add-auth-provider.md
@@ -229,9 +229,10 @@ name.
### Test the new provider
-You can `curl -i localhost:7000/auth/providerA/start` and which should provide a
-`302` redirect with a `Location` header. Paste the url from that header into a
-web browser and you should be able to trigger the authorization flow.
+You can `curl -i localhost:7000/api/auth/providerA/start` and which should
+provide a `302` redirect with a `Location` header. Paste the url from that
+header into a web browser and you should be able to trigger the authorization
+flow.
---
diff --git a/docs/features/software-templates/adding-templates.md b/docs/features/software-templates/adding-templates.md
index fad5c68e41..59019e754e 100644
--- a/docs/features/software-templates/adding-templates.md
+++ b/docs/features/software-templates/adding-templates.md
@@ -88,7 +88,7 @@ running:
```sh
curl \
--location \
- --request POST 'localhost:7000/catalog/locations' \
+ --request POST 'localhost:7000/api/catalog/locations' \
--header 'Content-Type: application/json' \
--data-raw "{\"type\": \"file\", \"target\": \"${YOUR PATH HERE}/template.yaml\"}"
```
@@ -98,7 +98,7 @@ If loading from a Git location, you can run the following
```sh
curl \
--location \
- --request POST 'localhost:7000/catalog/locations' \
+ --request POST 'localhost:7000/api/catalog/locations' \
--header 'Content-Type: application/json' \
--data-raw "{\"type\": \"github\", \"target\": \"https://${YOUR GITHUB REPO}blob/master/${PATH TO FOLDER}/template.yaml\"}"
```
diff --git a/docs/features/techdocs/getting-started.md b/docs/features/techdocs/getting-started.md
index b9d215fd8a..03494564c3 100644
--- a/docs/features/techdocs/getting-started.md
+++ b/docs/features/techdocs/getting-started.md
@@ -57,8 +57,8 @@ The default storage and request URLs:
```yaml
techdocs:
- storageUrl: http://localhost:7000/techdocs/static/docs
- requestUrl: http://localhost:7000/techdocs/docs
+ storageUrl: http://localhost:7000/api/techdocs/static/docs
+ requestUrl: http://localhost:7000/api/techdocs/docs
```
If you want `techdocs-backend` to manage building and publishing, you want
diff --git a/docs/openapi/definitions/auth.yaml b/docs/openapi/definitions/auth.yaml
index eb96d434c2..cb736fd2a9 100644
--- a/docs/openapi/definitions/auth.yaml
+++ b/docs/openapi/definitions/auth.yaml
@@ -6,10 +6,10 @@ info:
**Provided by `@backstage/auth-backend`.**
- The purpose of the Auth APIs in Backstage are to identify the user, and to provide a way for plugins
+ The purpose of the Auth APIs in Backstage are to identify the user, and to provide a way for plugins
to request access to 3rd party services on behalf of that user.
- The API is supplied with a list of providers - such as `Google` or `Github` - and will add the endpoints
+ The API is supplied with a list of providers - such as `Google` or `Github` - and will add the endpoints
described below to each of those providers.
Read more about [User Authentication and Authorization in Backstage](https://github.com/spotify/backstage/blob/master/docs/auth/overview.md).
@@ -21,7 +21,7 @@ externalDocs:
description: Backstage official documentation
url: https://github.com/spotify/backstage/blob/master/docs/README.md
servers:
- - url: http://localhost:7000/auth/
+ - url: http://localhost:7000/api/auth/
tags:
- name: provider
description: List of endpoints per provider
diff --git a/docs/overview/architecture-overview.md b/docs/overview/architecture-overview.md
index f2c6dad61f..8edf3e9e4c 100644
--- a/docs/overview/architecture-overview.md
+++ b/docs/overview/architecture-overview.md
@@ -4,6 +4,23 @@ title: Architecture overview
description: Documentation on Architecture overview
---
+## Terminology
+
+Backstage is constructed out of three parts. We separate Backstage in this way
+because we see three groups of contributors that work with Backstage in three
+different ways.
+
+- Core - Base functionality built by core developers in the open source project.
+- App - The app is an instance of a Backstage app that is deployed and tweaked.
+ The app ties together core functionality with additional plugins. The app is
+ built and maintained by app developers, usually a productivity team within a
+ company.
+- Plugins - Additional functionality to make your Backstage app useful for your
+ company. Plugins can be specific to a company or open sourced and reusable. At
+ Spotify we have over 100 plugins built by over 50 different teams. It has been
+ very powerful to get contributions from various infrastructure teams added
+ into a single unified developer experience.
+
## Overview
The following diagram shows how Backstage might look when deployed inside a
diff --git a/docs/overview/architecture-terminology.md b/docs/overview/architecture-terminology.md
deleted file mode 100644
index 092ca7fb01..0000000000
--- a/docs/overview/architecture-terminology.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-id: architecture-terminology
-title: Architecture terminology
-description: Documentation on Architecture terminology
----
-
-Backstage is constructed out of three parts. We separate Backstage in this way
-because we see three groups of contributors that work with Backstage in three
-different ways.
-
-- Core - Base functionality built by core devs in the open source project.
-- App - The app is an instance of a Backstage app that is deployed and tweaked.
- The app ties together core functionality with additional plugins. The app is
- built and maintained by app developers, usually a productivity team within a
- company.
-- Plugins - Additional functionality to make your Backstage app useful for your
- company. Plugins can be specific to a company or open sourced and reusable. At
- Spotify we have over 100 plugins built by over 50 different teams. It has been
- very powerful to get contributions from various infrastructure teams added
- into a single unified developer experience.
diff --git a/docs/overview/background.md b/docs/overview/background.md
index 486260bac3..aca46614c2 100644
--- a/docs/overview/background.md
+++ b/docs/overview/background.md
@@ -1,7 +1,8 @@
---
id: background
title: The Spotify Story
-description: Documentation on Background and Story behind making of Backstage
+description: Backstage was born out of necessity at Spotify. We found that as we grew, our
+infrastructure was becoming more fragmented, our engineers less productive.
---
Backstage was born out of necessity at Spotify. We found that as we grew, our
@@ -29,3 +30,9 @@ building a new microservice using an automated template in Backstage. Create,
maintain, and find the documentation for all that software in Backstage.
One place for everything. Accessible to everyone.
+
+Backstage was originally built by Spotify and then donated to the CNCF.
+Backstage is currently in the Sandbox phase. Read the announcement
+[here](https://backstage.io/blog/2020/09/23/backstage-cncf-sandbox).
+
+
diff --git a/docs/overview/support.md b/docs/overview/support.md
index 6e00c17fab..12bc7cfaf9 100644
--- a/docs/overview/support.md
+++ b/docs/overview/support.md
@@ -13,10 +13,7 @@ description: Support and Community Details and Links
- [FAQ](../FAQ.md) - Frequently Asked Questions
- [Code of Conduct](../../CODE_OF_CONDUCT.md) - This is how we roll
- [Blog](https://backstage.io/blog/) - Announcements and updates
-- [Newsletter](https://mailchi.mp/spotify/backstage-community)
+- [Newsletter](https://mailchi.mp/spotify/backstage-community) - Subscribe to
+ our email newsletter
- Give us a star ⭐️ - If you are using Backstage or think it is an interesting
project, we would love a star ❤️
-
-Or, if you are an open source developer and are interested in joining our team,
-please reach out to
-[foss-opportunities@spotify.com ](mailto:foss-opportunities@spotify.com)
diff --git a/docs/overview/what-is-backstage.md b/docs/overview/what-is-backstage.md
index 0d16943c9b..3d41c714ee 100644
--- a/docs/overview/what-is-backstage.md
+++ b/docs/overview/what-is-backstage.md
@@ -1,7 +1,7 @@
---
id: what-is-backstage
title: What is Backstage?
-description: Backsatge is an open platform for building developer portals.
+description: Backstage is an open platform for building developer portals.
Powered by a centralized service catalog, Backstage restores order to your microservices and infrastructure
---
@@ -33,7 +33,14 @@ Out of the box, Backstage includes:
[open source plugins](https://github.com/spotify/backstage/tree/master/plugins)
that further expand Backstage’s customizability and functionality
-### Benefits
+## Backstage and the CNCF
+
+Backstage is a CNCF Sandbox project. Read the announcement
+[here](https://backstage.io/blog/2020/09/23/backstage-cncf-sandbox).
+
+
+
+## Benefits
- For _engineering managers_, it allows you to maintain standards and best
practices across the organization, and can help you manage your whole tech
diff --git a/docs/tutorials/quickstart-app-auth.md b/docs/tutorials/quickstart-app-auth.md
index 82f9cab537..00494c7768 100644
--- a/docs/tutorials/quickstart-app-auth.md
+++ b/docs/tutorials/quickstart-app-auth.md
@@ -115,7 +115,7 @@ export AUTH_GITHUB_CLIENT_SECRET=xxx
> Log into http://github.com
> Navigate to (Settings > Developer Settings > OAuth Apps > New OAuth App)[https://github.com/settings/applications/new]
> Set Homepage URL = http://localhost:3000
-> Set Callback URL = http://localhost:7000/auth/github
+> Set Callback URL = http://localhost:7000/api/auth/github
> Click [Register application]
> On the next page, copy and paste your new Client ID and Client Secret to the environment variables above, `AUTH_GITHUB_CLIENT_ID` & `AUTH_GITHUB_CLIENT_SECRET`
> Don't forget to `source` that profile file again if necessary.
diff --git a/microsite/core/Footer.js b/microsite/core/Footer.js
index 761862c1fc..8e3c4c086d 100644
--- a/microsite/core/Footer.js
+++ b/microsite/core/Footer.js
@@ -51,6 +51,7 @@ class Footer extends React.Component {
Subscribe to our newsletter
+ CNCF Sandbox
More
diff --git a/microsite/sidebars.json b/microsite/sidebars.json
index 8fa4935a38..e9993b2e8b 100644
--- a/microsite/sidebars.json
+++ b/microsite/sidebars.json
@@ -3,7 +3,6 @@
"Overview": [
"overview/what-is-backstage",
"overview/architecture-overview",
- "overview/architecture-terminology",
"overview/roadmap",
"overview/vision",
"overview/background",
diff --git a/mkdocs.yml b/mkdocs.yml
index ba607f8a6e..f85044822e 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -5,7 +5,6 @@ nav:
- Overview:
- What is Backstage?: 'overview/what-is-backstage.md'
- Backstage architecture: 'overview/architecture-overview.md'
- - Architecture and terminology: 'overview/architecture-terminology.md'
- Roadmap: 'overview/roadmap.md'
- Vision: 'overview/vision.md'
- The Spotify story: 'overview/background.md'
diff --git a/packages/app/src/App.test.tsx b/packages/app/src/App.test.tsx
index cddd093855..40c49416bf 100644
--- a/packages/app/src/App.test.tsx
+++ b/packages/app/src/App.test.tsx
@@ -31,7 +31,7 @@ describe('App', () => {
baseUrl: 'http://localhost:3003',
},
techdocs: {
- storageUrl: 'http://localhost:7000/techdocs/static/docs',
+ storageUrl: 'http://localhost:7000/api/techdocs/static/docs',
},
},
context: 'test',
diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts
index 7f57f6f970..581a591152 100644
--- a/packages/app/src/apis.ts
+++ b/packages/app/src/apis.ts
@@ -16,11 +16,8 @@
import {
errorApiRef,
- discoveryApiRef,
- UrlPatternDiscovery,
githubAuthApiRef,
createApiFactory,
- configApiRef,
} from '@backstage/core';
import {
@@ -38,15 +35,6 @@ import {
} from '@roadiehq/backstage-plugin-github-pull-requests';
export const apis = [
- // TODO(Rugvip): migrate to use /api
- createApiFactory({
- api: discoveryApiRef,
- deps: { configApi: configApiRef },
- factory: ({ configApi }) =>
- UrlPatternDiscovery.compile(
- `${configApi.getString('backend.baseUrl')}/{{ pluginId }}`,
- ),
- }),
createApiFactory({
api: graphQlBrowseApiRef,
deps: { errorApi: errorApiRef, githubAuthApi: githubAuthApiRef },
diff --git a/packages/backend/package.json b/packages/backend/package.json
index e1b8665b57..5bfaf2ee46 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -37,6 +37,7 @@
"dockerode": "^3.2.0",
"example-app": "^0.1.1-alpha.23",
"express": "^4.17.1",
+ "express-promise-router": "^3.0.3",
"knex": "^0.21.1",
"pg": "^8.3.0",
"pg-connection-string": "^2.3.0",
diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
index 7c52175d76..9d4a738ecf 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -22,12 +22,14 @@
* Happy hacking!
*/
+import Router from 'express-promise-router';
import {
createDatabaseClient,
createServiceBuilder,
loadBackendConfig,
getRootLogger,
useHotMemoize,
+ notFoundHandler,
} from '@backstage/backend-common';
import { ConfigReader, AppConfig } from '@backstage/config';
import healthcheck from './plugins/healthcheck';
@@ -78,19 +80,23 @@ async function main() {
const graphqlEnv = useHotMemoize(module, () => createEnv('graphql'));
const appEnv = useHotMemoize(module, () => createEnv('app'));
+ const apiRouter = Router();
+ apiRouter.use('/catalog', await catalog(catalogEnv));
+ apiRouter.use('/rollbar', await rollbar(rollbarEnv));
+ apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv));
+ apiRouter.use('/sentry', await sentry(sentryEnv));
+ apiRouter.use('/auth', await auth(authEnv, '/api/auth'));
+ apiRouter.use('/identity', await identity(identityEnv));
+ apiRouter.use('/techdocs', await techdocs(techdocsEnv));
+ apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv));
+ apiRouter.use('/proxy', await proxy(proxyEnv, '/api/proxy'));
+ apiRouter.use('/graphql', await graphql(graphqlEnv));
+ apiRouter.use(notFoundHandler());
+
const service = createServiceBuilder(module)
.loadConfig(configReader)
.addRouter('', await healthcheck(healthcheckEnv))
- .addRouter('/catalog', await catalog(catalogEnv))
- .addRouter('/rollbar', await rollbar(rollbarEnv))
- .addRouter('/scaffolder', await scaffolder(scaffolderEnv))
- .addRouter('/sentry', await sentry(sentryEnv))
- .addRouter('/auth', await auth(authEnv))
- .addRouter('/identity', await identity(identityEnv))
- .addRouter('/techdocs', await techdocs(techdocsEnv))
- .addRouter('/kubernetes', await kubernetes(kubernetesEnv))
- .addRouter('/proxy', await proxy(proxyEnv, '/proxy'))
- .addRouter('/graphql', await graphql(graphqlEnv))
+ .addRouter('/api', apiRouter)
.addRouter('', await app(appEnv));
await service.start().catch(err => {
diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts
index b24dd6ca6b..60baaff6b7 100644
--- a/packages/backend/src/plugins/auth.ts
+++ b/packages/backend/src/plugins/auth.ts
@@ -17,10 +17,9 @@
import { createRouter } from '@backstage/plugin-auth-backend';
import { PluginEnvironment } from '../types';
-export default async function createPlugin({
- logger,
- database,
- config,
-}: PluginEnvironment) {
- return await createRouter({ logger, config, database });
+export default async function createPlugin(
+ { logger, database, config }: PluginEnvironment,
+ basePath: string,
+) {
+ return await createRouter({ logger, config, database, basePath });
}
diff --git a/packages/backend/src/plugins/kubernetes.ts b/packages/backend/src/plugins/kubernetes.ts
index 42b7722ff6..7906765533 100644
--- a/packages/backend/src/plugins/kubernetes.ts
+++ b/packages/backend/src/plugins/kubernetes.ts
@@ -17,6 +17,9 @@
import { createRouter } from '@backstage/plugin-kubernetes-backend';
import { PluginEnvironment } from '../types';
-export default async function createPlugin({ logger }: PluginEnvironment) {
- return await createRouter({ logger });
+export default async function createPlugin({
+ logger,
+ config,
+}: PluginEnvironment) {
+ return await createRouter({ logger, config });
}
diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts
index 247812b6fd..cbaf956bdb 100644
--- a/packages/cli/src/commands/create-plugin/createPlugin.ts
+++ b/packages/cli/src/commands/create-plugin/createPlugin.ts
@@ -231,7 +231,11 @@ export default async (cmd: Command) => {
const privatePackage = cmd.private === false ? false : true;
const isMonoRepo = await fs.pathExists(paths.resolveTargetRoot('lerna.json'));
const appPackage = paths.resolveTargetRoot('packages/app');
- const templateDir = paths.resolveOwn('templates/default-plugin');
+ const templateDir = paths.resolveOwn(
+ cmd.backend
+ ? 'templates/default-backend-plugin'
+ : 'templates/default-plugin',
+ );
const tempDir = resolvePath(os.tmpdir(), answers.id);
const pluginDir = isMonoRepo
? paths.resolveTargetRoot('plugins', answers.id)
@@ -252,6 +256,7 @@ export default async (cmd: Command) => {
await createTemporaryPluginFolder(tempDir);
Task.section('Preparing files');
+
await templatingTask(templateDir, tempDir, {
...answers,
version,
@@ -267,7 +272,7 @@ export default async (cmd: Command) => {
Task.section('Building the plugin');
await buildPlugin(pluginDir);
- if (await fs.pathExists(appPackage)) {
+ if ((await fs.pathExists(appPackage)) && !cmd.backend) {
Task.section('Adding plugin as dependency in app');
await addPluginDependencyToApp(paths.targetRoot, name, version);
diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts
index f1828cef84..fea250cd55 100644
--- a/packages/cli/src/commands/index.ts
+++ b/packages/cli/src/commands/index.ts
@@ -61,6 +61,10 @@ export function registerCommands(program: CommanderStatic) {
program
.command('create-plugin')
+ .option(
+ '--backend',
+ 'Create plugin with the backend dependencies as default',
+ )
.description('Creates a new plugin in the current repository')
.option('--scope
', 'NPM scope')
.option('--npm-registry ', 'NPM registry URL')
diff --git a/packages/cli/templates/default-backend-plugin/.eslintrc.js b/packages/cli/templates/default-backend-plugin/.eslintrc.js
new file mode 100644
index 0000000000..16a033dbc6
--- /dev/null
+++ b/packages/cli/templates/default-backend-plugin/.eslintrc.js
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: [require.resolve('@backstage/cli/config/eslint.backend')],
+};
diff --git a/packages/cli/templates/default-backend-plugin/README.md.hbs b/packages/cli/templates/default-backend-plugin/README.md.hbs
new file mode 100644
index 0000000000..5c34b40360
--- /dev/null
+++ b/packages/cli/templates/default-backend-plugin/README.md.hbs
@@ -0,0 +1,14 @@
+# {{id}}
+
+Welcome to the {{id}} backend plugin!
+
+_This plugin was created through the Backstage CLI_
+
+## Getting started
+
+Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn
+start` in the root directory, and then navigating to [/{{id}}](http://localhost:3000/{{id}}).
+
+You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
+This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
+It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory.
diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs
new file mode 100644
index 0000000000..40e612609a
--- /dev/null
+++ b/packages/cli/templates/default-backend-plugin/package.json.hbs
@@ -0,0 +1,44 @@
+{
+ "name": "{{name}}",
+ "version": "{{version}}",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ {{#if privatePackage}} "private": {{privatePackage}},
+ {{/if}}
+ "publishConfig": {
+ {{#if npmRegistry}} "registry": "{{npmRegistry}}",
+ {{/if}}
+ "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",
+ "clean": "backstage-cli clean"
+ },
+ "dependencies": {
+ "@backstage/backend-common": "^{{version}}",
+ "@backstage/config": "^{{version}}",
+ "@types/express": "^4.17.6",
+ "express": "^4.17.1",
+ "express-promise-router": "^3.0.3",
+ "winston": "^3.2.1",
+ "node-fetch": "^2.6.1",
+ "yn": "^4.0.0"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^{{version}}",
+ "@types/supertest": "^2.0.8",
+ "supertest": "^4.0.2",
+ "msw": "^0.20.5"
+ },
+ "files": [
+ "dist"
+ ]
+ }
diff --git a/packages/cli/templates/default-backend-plugin/src/index.ts b/packages/cli/templates/default-backend-plugin/src/index.ts
new file mode 100644
index 0000000000..7612c392a2
--- /dev/null
+++ b/packages/cli/templates/default-backend-plugin/src/index.ts
@@ -0,0 +1,17 @@
+/*
+ * 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.
+ */
+
+export * from './service/router';
diff --git a/packages/cli/templates/default-backend-plugin/src/run.ts.hbs b/packages/cli/templates/default-backend-plugin/src/run.ts.hbs
new file mode 100644
index 0000000000..b96989e4b8
--- /dev/null
+++ b/packages/cli/templates/default-backend-plugin/src/run.ts.hbs
@@ -0,0 +1,33 @@
+/*
+ * 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 { 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);
+});
diff --git a/packages/cli/templates/default-backend-plugin/src/service/router.test.ts b/packages/cli/templates/default-backend-plugin/src/service/router.test.ts
new file mode 100644
index 0000000000..0aaeafa379
--- /dev/null
+++ b/packages/cli/templates/default-backend-plugin/src/service/router.test.ts
@@ -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 { getVoidLogger } from '@backstage/backend-common';
+import express from 'express';
+import request from 'supertest';
+
+import { createRouter } from './router';
+
+describe('createRouter', () => {
+ let app: express.Express;
+
+ beforeAll(async () => {
+ const router = await createRouter({
+ logger: getVoidLogger(),
+ });
+ app = express().use(router);
+ });
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ describe('GET /health', () => {
+ it('returns ok', async () => {
+ const response = await request(app).get('/health');
+
+ expect(response.status).toEqual(200);
+ expect(response.body).toEqual({ status: 'ok' });
+ });
+ });
+});
diff --git a/packages/cli/templates/default-backend-plugin/src/service/router.ts b/packages/cli/templates/default-backend-plugin/src/service/router.ts
new file mode 100644
index 0000000000..3ea8219365
--- /dev/null
+++ b/packages/cli/templates/default-backend-plugin/src/service/router.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 { errorHandler } from '@backstage/backend-common';
+import express from 'express';
+import Router from 'express-promise-router';
+import { Logger } from 'winston';
+
+export interface RouterOptions {
+ logger: Logger;
+}
+
+export async function createRouter(
+ options: RouterOptions,
+): Promise {
+ const { logger } = options;
+
+ const router = Router();
+ router.use(express.json());
+
+ router.get('/health', (_, response) => {
+ logger.info('PONG!');
+ response.send({ status: 'ok' });
+ });
+ router.use(errorHandler());
+ return router;
+}
diff --git a/packages/cli/templates/default-backend-plugin/src/service/standaloneServer.ts.hbs b/packages/cli/templates/default-backend-plugin/src/service/standaloneServer.ts.hbs
new file mode 100644
index 0000000000..6e38965246
--- /dev/null
+++ b/packages/cli/templates/default-backend-plugin/src/service/standaloneServer.ts.hbs
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { 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 {
+ const logger = options.logger.child({ service: '{{id}}-backend' });
+ logger.debug('Starting application server...');
+ const router = await createRouter({
+ logger,
+ });
+
+ const service = createServiceBuilder(module)
+ .enableCors({ origin: 'http://localhost:3000' })
+ .addRouter('/{{id}}', router);
+
+ return await service.start().catch(err => {
+ logger.error(err);
+ process.exit(1);
+ });
+}
+
+module.hot?.accept();
diff --git a/packages/cli/templates/default-backend-plugin/src/setupTests.ts b/packages/cli/templates/default-backend-plugin/src/setupTests.ts
new file mode 100644
index 0000000000..a5907fd52f
--- /dev/null
+++ b/packages/cli/templates/default-backend-plugin/src/setupTests.ts
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+export {};
+global.fetch = require('node-fetch');
diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs
index 52cffeb621..e024884547 100644
--- a/packages/cli/templates/default-plugin/package.json.hbs
+++ b/packages/cli/templates/default-plugin/package.json.hbs
@@ -41,7 +41,8 @@
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
- "jest-fetch-mock": "^3.0.3"
+ "msw": "^0.20.5",
+ "node-fetch": "^2.6.1"
},
"files": [
"dist"
diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs
index 37759313ee..1069790218 100644
--- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs
+++ b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs
@@ -1,18 +1,35 @@
import React from 'react';
import { render } from '@testing-library/react';
-import mockFetch from 'jest-fetch-mock';
import ExampleComponent from './ExampleComponent';
import { ThemeProvider } from '@material-ui/core';
import { lightTheme } from '@backstage/theme';
+import { rest } from 'msw';
+import { setupServer } from 'msw/node';
+
describe('ExampleComponent', () => {
+ const server = setupServer();
+ // Enable API mocking before tests.
+ beforeAll(() => server.listen())
+
+ // Reset any runtime request handlers we may add during the tests.
+ afterEach(() => server.resetHandlers())
+
+ // Disable API mocking after the tests are done.
+ afterAll(() => server.close())
+
+ // setup mock response
+ beforeEach(() => {
+ server.use(rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))))
+ })
+
it('should render', () => {
- mockFetch.mockResponse(() => new Promise(() => {}));
const rendered = render(
,
- );
- expect(rendered.getByText('Welcome to {{ id }}!')).toBeInTheDocument();
+ );
+ expect(rendered.getByText('Welcome to {{ id }}!')).toBeInTheDocument();
});
});
+
\ No newline at end of file
diff --git a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs
index cc61c215cd..c584289077 100644
--- a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs
+++ b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs
@@ -1,11 +1,25 @@
import React from 'react';
import { render } from '@testing-library/react';
-import mockFetch from 'jest-fetch-mock';
import ExampleFetchComponent from './ExampleFetchComponent';
+import { rest } from 'msw';
+import { setupServer } from 'msw/node';
describe('ExampleFetchComponent', () => {
+ const server = setupServer();
+ // Enable API mocking before tests.
+ beforeAll(() => server.listen())
+
+ // Reset any runtime request handlers we may add during the tests.
+ afterEach(() => server.resetHandlers())
+
+ // Disable API mocking after the tests are done.
+ afterAll(() => server.close())
+
+ // setup mock response
+ beforeEach(() => {
+ server.use(rest.get('https://randomuser.me/*', (_, res, ctx) => res(ctx.status(200), ctx.delay(2000), ctx.json({}))))
+ })
it('should render', async () => {
- mockFetch.mockResponse(() => new Promise(() => {}));
const rendered = render();
expect(await rendered.findByTestId('progress')).toBeInTheDocument();
});
diff --git a/packages/cli/templates/default-plugin/src/setupTests.ts b/packages/cli/templates/default-plugin/src/setupTests.ts
index 3fa8703ac4..cc559f672e 100644
--- a/packages/cli/templates/default-plugin/src/setupTests.ts
+++ b/packages/cli/templates/default-plugin/src/setupTests.ts
@@ -1,3 +1,2 @@
import '@testing-library/jest-dom';
-
-require('jest-fetch-mock').enableMocks();
+global.fetch = require('node-fetch');
diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs
index a7d7aa8010..92c517bf93 100644
--- a/packages/create-app/templates/default-app/app-config.yaml.hbs
+++ b/packages/create-app/templates/default-app/app-config.yaml.hbs
@@ -44,8 +44,8 @@ proxy:
changeOrigin: true
techdocs:
- storageUrl: http://localhost:7000/techdocs/static/docs
- requestUrl: http://localhost:7000/techdocs/docs
+ storageUrl: http://localhost:7000/api/techdocs/static/docs
+ requestUrl: http://localhost:7000/api/techdocs/docs
generators:
techdocs: 'docker'
diff --git a/packages/create-app/templates/default-app/packages/app/src/App.test.tsx b/packages/create-app/templates/default-app/packages/app/src/App.test.tsx
index e88c3b47dd..21a2eaa703 100644
--- a/packages/create-app/templates/default-app/packages/app/src/App.test.tsx
+++ b/packages/create-app/templates/default-app/packages/app/src/App.test.tsx
@@ -12,7 +12,7 @@ describe('App', () => {
app: { title: 'Test' },
backend: { baseUrl: 'http://localhost:7000' },
techdocs: {
- storageUrl: 'http://localhost:7000/techdocs/static/docs',
+ storageUrl: 'http://localhost:7000/api/techdocs/static/docs',
},
},
context: 'test',
diff --git a/packages/create-app/templates/default-app/packages/backend/package.json.hbs b/packages/create-app/templates/default-app/packages/backend/package.json.hbs
index adde81e830..b70f36650e 100644
--- a/packages/create-app/templates/default-app/packages/backend/package.json.hbs
+++ b/packages/create-app/templates/default-app/packages/backend/package.json.hbs
@@ -31,6 +31,7 @@
"@gitbeaker/node": "^23.5.0",
"dockerode": "^3.2.0",
"express": "^4.17.1",
+ "express-promise-router": "^3.0.3",
"knex": "^0.21.1",
{{#if dbTypePG}}
"pg": "^8.3.0",
diff --git a/packages/create-app/templates/default-app/packages/backend/src/index.ts b/packages/create-app/templates/default-app/packages/backend/src/index.ts
index 6a014727e9..457b95a7a9 100644
--- a/packages/create-app/templates/default-app/packages/backend/src/index.ts
+++ b/packages/create-app/templates/default-app/packages/backend/src/index.ts
@@ -6,12 +6,14 @@
* Happy hacking!
*/
+import Router from 'express-promise-router';
import {
createDatabaseClient,
createServiceBuilder,
loadBackendConfig,
getRootLogger,
useHotMemoize,
+ notFoundHandler,
} from '@backstage/backend-common';
import { ConfigReader, AppConfig } from '@backstage/config';
import auth from './plugins/auth';
@@ -51,14 +53,18 @@ async function main() {
const proxyEnv = useHotMemoize(module, () => createEnv('proxy'));
const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
+ const apiRouter = Router();
+ apiRouter.use('/catalog', await catalog(catalogEnv))
+ apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv))
+ apiRouter.use('/auth', await auth(authEnv))
+ apiRouter.use('/identity', await identity(identityEnv))
+ apiRouter.use('/techdocs', await techdocs(techdocsEnv))
+ apiRouter.use('/proxy', await proxy(proxyEnv, '/api/proxy'))
+ apiRouter.use(notFoundHandler());
+
const service = createServiceBuilder(module)
.loadConfig(configReader)
- .addRouter('/catalog', await catalog(catalogEnv))
- .addRouter('/scaffolder', await scaffolder(scaffolderEnv))
- .addRouter('/auth', await auth(authEnv))
- .addRouter('/identity', await identity(identityEnv))
- .addRouter('/techdocs', await techdocs(techdocsEnv))
- .addRouter('/proxy', await proxy(proxyEnv, '/proxy'));
+ .addRouter('/api', apiRouter)
await service.start().catch(err => {
console.log(err);
diff --git a/packages/e2e-test/src/e2e-test.ts b/packages/e2e-test/src/e2e-test.ts
index 969a77cb2a..c7f6137ff1 100644
--- a/packages/e2e-test/src/e2e-test.ts
+++ b/packages/e2e-test/src/e2e-test.ts
@@ -55,6 +55,9 @@ async function main() {
print('Creating a Backstage Plugin');
const pluginName = await createPlugin('test-plugin', appDir);
+ print('Creating a Backstage Backend Plugin');
+ await createPlugin('test-backend-plugin', appDir, ['--backend']);
+
print('Starting the app');
await testAppServe(pluginName, appDir);
@@ -238,8 +241,12 @@ async function overrideModuleResolutions(appDir: string, workspaceDir: string) {
/**
* Uses create-plugin command to create a new plugin in the app
*/
-async function createPlugin(pluginName: string, appDir: string) {
- const child = spawnPiped(['yarn', 'create-plugin'], {
+async function createPlugin(
+ pluginName: string,
+ appDir: string,
+ options: string[] = [],
+) {
+ const child = spawnPiped(['yarn', 'create-plugin', ...options], {
cwd: appDir,
});
@@ -384,7 +391,7 @@ async function testBackendStart(appDir: string, isPostgres: boolean) {
print('Try to fetch entities from the backend');
// Try fetch entities, should be ok
- await fetch('http://localhost:7000/catalog/entities').then(res =>
+ await fetch('http://localhost:7000/api/catalog/entities').then(res =>
res.json(),
);
print('Entities fetched successfully');
diff --git a/packages/techdocs-container/techdocs-core/README.md b/packages/techdocs-container/techdocs-core/README.md
index 49b9fce4e4..7f34722e81 100644
--- a/packages/techdocs-container/techdocs-core/README.md
+++ b/packages/techdocs-container/techdocs-core/README.md
@@ -82,6 +82,10 @@ Extensions:
## Changelog
+### 0.0.9
+
+- Change development status to 3 - Alpha
+
### 0.0.8
- Superfences and Codehilite doesn't work very well together (squidfunk/mkdocs-material#1604) so therefore the codehilite extension is replaced by pymdownx.highlight
diff --git a/packages/techdocs-container/techdocs-core/setup.py b/packages/techdocs-container/techdocs-core/setup.py
index e5a8206150..4385bc2e38 100644
--- a/packages/techdocs-container/techdocs-core/setup.py
+++ b/packages/techdocs-container/techdocs-core/setup.py
@@ -14,13 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License.
"""
from setuptools import setup, find_packages
+from os import path
+# read the contents of the README file in the current directory
+this_dir = path.abspath(path.dirname(__file__))
+with open(path.join(this_dir, "README.md"), encoding="utf-8") as file:
+ long_description = file.read()
setup(
name="mkdocs-techdocs-core",
- version="0.0.8",
+ version="0.0.9",
description="A Mkdocs package that contains TechDocs defaults",
- long_description="",
+ long_description=long_description,
+ long_description_content_type="text/markdown",
keywords="mkdocs",
url="https://github.com/spotify/backstage",
author="TechDocs Core",
@@ -37,7 +43,7 @@ setup(
"pymdown-extensions==7.1",
],
classifiers=[
- "Development Status :: 1 - Planning",
+ "Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Intended Audience :: Information Technology",
"License :: OSI Approved :: Apache Software License",
diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts
index 73d6c13859..72a1df716f 100644
--- a/packages/theme/src/themes.ts
+++ b/packages/theme/src/themes.ts
@@ -80,12 +80,12 @@ export const darkTheme = createTheme({
default: '#333333',
},
status: {
- ok: '#1DB954',
- warning: '#FF9800',
- error: '#E22134',
- running: '#2E77D0',
- pending: '#FFED51',
- aborted: '#757575',
+ ok: '#71CF88',
+ warning: '#FFB84D',
+ error: '#F84C55',
+ running: '#3488E3',
+ pending: '#FEF071',
+ aborted: '#9E9E9E',
},
bursts: {
fontColor: '#FEFEFE',
@@ -103,7 +103,7 @@ export const darkTheme = createTheme({
},
border: '#E6E6E6',
textContrast: '#FFFFFF',
- textVerySubtle: '#DDD',
+ textVerySubtle: '#ececec',
textSubtle: '#EEEEEE',
highlight: '#FFFBCC',
errorBackground: '#FFEBEE',
diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json
index bc384bf796..b9e37d6d7a 100644
--- a/plugins/api-docs/package.json
+++ b/plugins/api-docs/package.json
@@ -49,7 +49,9 @@
"@types/node": "^12.0.0",
"@types/react": "^16.9",
"@types/swagger-ui-react": "^3.23.3",
- "jest-fetch-mock": "^3.0.3"
+ "jest-fetch-mock": "^3.0.3",
+ "msw": "^0.20.5",
+ "node-fetch": "^2.6.1"
},
"files": [
"dist"
diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json
index 8d7fb67171..d403580f86 100644
--- a/plugins/app-backend/package.json
+++ b/plugins/app-backend/package.json
@@ -32,7 +32,7 @@
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.23",
"@types/supertest": "^2.0.8",
- "msw": "^0.19.5",
+ "msw": "^0.20.5",
"supertest": "^4.0.2"
},
"files": [
diff --git a/plugins/auth-backend/README.md b/plugins/auth-backend/README.md
index 9af90df3f2..355fcc1536 100644
--- a/plugins/auth-backend/README.md
+++ b/plugins/auth-backend/README.md
@@ -34,8 +34,8 @@ Follow this link, [Create new OAuth App](https://github.com/settings/application
1. Set Application Name to `backstage-dev` or something along those lines.
1. You can set the Homepage URL to whatever you want to.
1. The Authorization Callback URL should match the redirect URI set in Backstage.
- 1. Set this to `http://localhost:7000/auth/github` for local development.
- 1. Set this to `http://{APP_FQDN}:{APP_BACKEND_PORT}/auth/github` for non-local deployments.
+ 1. Set this to `http://localhost:7000/api/auth/github` for local development.
+ 1. Set this to `http://{APP_FQDN}:{APP_BACKEND_PORT}/api/auth/github` for non-local deployments.
```bash
export AUTH_GITHUB_CLIENT_ID=x
@@ -78,14 +78,14 @@ export AUTH_AUTH0_CLIENT_SECRET=x
#### Creating an Azure AD App Registration
-An Azure AD App Registration is required to be able to sign in using Azure AD and the Microsoft Graph API.
+An Azure AD App Registration is required to be able to sign in using Azure AD and the Microsoft Graph API.
Click [here](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps) to create a new one.
- Click on the `New Registration` button.
- Give the app a name. e.g. `backstage-dev`
- Select `Accounts in this organizational directory only` under supported account types.
- Enter the callback URL for your backstage backend instance:
- - For local development, this is likely `http://localhost:7000/auth/microsoft/handler/frame`
+ - For local development, this is likely `http://localhost:7000/api/auth/microsoft/handler/frame`
- For non-local deployments, this will be `https://{APP_FQDN}:{APP_BACKEND_PORT}/auth/microsoft/handler/frame`
- Click `Register`.
diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json
index d1c563229c..8a9bfadca1 100644
--- a/plugins/auth-backend/package.json
+++ b/plugins/auth-backend/package.json
@@ -58,7 +58,8 @@
"@types/passport-google-oauth20": "^2.0.3",
"@types/passport-microsoft": "^0.0.0",
"@types/passport-saml": "^1.1.2",
- "jest-fetch-mock": "^3.0.3"
+ "jest-fetch-mock": "^3.0.3",
+ "msw": "^0.20.5"
},
"files": [
"dist",
diff --git a/plugins/auth-backend/scripts/start-saml-idp.sh b/plugins/auth-backend/scripts/start-saml-idp.sh
index 33217f7978..b312a4b85e 100755
--- a/plugins/auth-backend/scripts/start-saml-idp.sh
+++ b/plugins/auth-backend/scripts/start-saml-idp.sh
@@ -18,4 +18,4 @@ fi
echo "Downloading and starting SAML-IdP"
export NPM_CONFIG_REGISTRY=https://registry.npmjs.org
-exec npx saml-idp --acsUrl "http://localhost:7000/auth/saml/handler/frame" --audience "http://localhost:7000" --port 7001
+exec npx saml-idp --acsUrl "http://localhost:7000/api/auth/saml/handler/frame" --audience "http://localhost:7000" --port 7001
diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts
index 19a74d47c5..e70e687879 100644
--- a/plugins/auth-backend/src/service/router.ts
+++ b/plugins/auth-backend/src/service/router.ts
@@ -28,6 +28,7 @@ export interface RouterOptions {
logger: Logger;
database: Knex;
config: Config;
+ basePath?: string;
}
export async function createRouter(
@@ -38,7 +39,8 @@ export async function createRouter(
const appUrl = options.config.getString('app.baseUrl');
const backendUrl = options.config.getString('backend.baseUrl');
- const authUrl = `${backendUrl}/auth`;
+ // TODO(Rugvip): Replace with service discovery of external URL
+ const authUrl = backendUrl + (options.basePath ?? '/api/auth');
const keyDurationSeconds = 3600;
diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json
index 5d767c4f85..8597f3aa3c 100644
--- a/plugins/catalog-graphql/package.json
+++ b/plugins/catalog-graphql/package.json
@@ -39,7 +39,7 @@
"@types/express": "^4.17.7",
"@types/supertest": "^2.0.8",
"eslint-plugin-graphql": "^4.0.0",
- "msw": "^0.19.5",
+ "msw": "^0.20.5",
"supertest": "^4.0.2",
"ts-node": "^8.10.2"
},
diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json
index ea6f2ae44d..d3241ec10d 100644
--- a/plugins/catalog/package.json
+++ b/plugins/catalog/package.json
@@ -53,7 +53,8 @@
"jest-fetch-mock": "^3.0.3",
"msw": "^0.20.5",
"react-test-renderer": "^16.13.1",
- "whatwg-fetch": "^3.4.0"
+ "whatwg-fetch": "^3.4.0",
+ "node-fetch": "^2.6.1"
},
"files": [
"dist"
diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json
index ebff4b9eba..a661c2562f 100644
--- a/plugins/circleci/package.json
+++ b/plugins/circleci/package.json
@@ -46,7 +46,9 @@
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"@types/react-lazylog": "^4.5.0",
- "jest-fetch-mock": "^3.0.3"
+ "jest-fetch-mock": "^3.0.3",
+ "msw": "^0.20.5",
+ "node-fetch": "^2.6.1"
},
"files": [
"dist"
diff --git a/plugins/explore/package.json b/plugins/explore/package.json
index 1a498f1dc2..2f25b4f137 100644
--- a/plugins/explore/package.json
+++ b/plugins/explore/package.json
@@ -41,7 +41,9 @@
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
- "jest-fetch-mock": "^3.0.3"
+ "jest-fetch-mock": "^3.0.3",
+ "msw": "^0.20.5",
+ "node-fetch": "^2.6.1"
},
"files": [
"dist"
diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json
index 603fcf5a31..406b2428df 100644
--- a/plugins/gcp-projects/package.json
+++ b/plugins/gcp-projects/package.json
@@ -4,7 +4,6 @@
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
- "private": true,
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
@@ -39,7 +38,9 @@
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
- "jest-fetch-mock": "^3.0.3"
+ "jest-fetch-mock": "^3.0.3",
+ "msw": "^0.20.5",
+ "node-fetch": "^2.6.1"
},
"files": [
"dist"
diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json
index f7000407ac..5d85d8b3fa 100644
--- a/plugins/github-actions/package.json
+++ b/plugins/github-actions/package.json
@@ -47,7 +47,9 @@
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
- "jest-fetch-mock": "^3.0.3"
+ "jest-fetch-mock": "^3.0.3",
+ "msw": "^0.20.5",
+ "node-fetch": "^2.6.1"
},
"files": [
"dist"
diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json
index 1168b05c6d..a58ba6576a 100644
--- a/plugins/gitops-profiles/package.json
+++ b/plugins/gitops-profiles/package.json
@@ -39,7 +39,9 @@
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
- "jest-fetch-mock": "^3.0.3"
+ "jest-fetch-mock": "^3.0.3",
+ "msw": "^0.20.5",
+ "node-fetch": "^2.6.1"
},
"files": [
"dist"
diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json
index 3042e6cd57..1db4bba978 100644
--- a/plugins/graphiql/package.json
+++ b/plugins/graphiql/package.json
@@ -53,7 +53,9 @@
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"jest-fetch-mock": "^3.0.3",
- "react-router-dom": "6.0.0-beta.0"
+ "react-router-dom": "6.0.0-beta.0",
+ "msw": "^0.20.5",
+ "node-fetch": "^2.6.1"
},
"files": [
"dist"
diff --git a/plugins/identity-backend/package.json b/plugins/identity-backend/package.json
index fb9bffc2e2..63ecb65d1e 100644
--- a/plugins/identity-backend/package.json
+++ b/plugins/identity-backend/package.json
@@ -34,7 +34,8 @@
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.23",
- "jest-fetch-mock": "^3.0.3"
+ "jest-fetch-mock": "^3.0.3",
+ "msw": "^0.20.5"
},
"files": [
"dist"
diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json
index e777dd2119..84e8a47664 100644
--- a/plugins/jenkins/package.json
+++ b/plugins/jenkins/package.json
@@ -44,7 +44,9 @@
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"@types/testing-library__jest-dom": "^5.9.1",
- "jest-fetch-mock": "^3.0.3"
+ "jest-fetch-mock": "^3.0.3",
+ "msw": "^0.20.5",
+ "node-fetch": "^2.6.1"
},
"files": [
"dist"
diff --git a/plugins/kubernetes-backend/examples/dice-roller/README.md b/plugins/kubernetes-backend/examples/dice-roller/README.md
new file mode 100644
index 0000000000..b64e2df37f
--- /dev/null
+++ b/plugins/kubernetes-backend/examples/dice-roller/README.md
@@ -0,0 +1,42 @@
+# Dice roller
+
+An app to roll dice (it doesn't actually do that).
+
+# Viewing in local Minikube running Backstage locally
+
+## Prerequisites
+
+- kubectl installed
+- Minikube installed
+- jq installed
+- Backstage locally built and ready to run
+
+## Steps
+
+1. Start minikube
+2. Get the Kubernetes master base url `kubectl cluster-info`
+3. Apply manifests `kubectl apply -f dice-roller-manifests.yaml`
+4. Get service account token (see below)
+5. Start Backstage UI and backend
+6. Register existing component in Backstage
+ - https://github.com/mclarke47/dice-roller/blob/master/catalog-info.yaml
+
+Update `app-config.yaml` as follows.
+
+```yaml
+---
+kubernetes:
+ clusterLocatorMethod: 'configMultiTenant'
+ clusters:
+ - url:
+ name: minikube
+ serviceAccountToken:
+```
+
+### Getting the service account token
+
+```
+kubectl get secret DICE_ROLLER_TOKEN_NAME -o=json | jq -r '.data["token"]' | base64 --decode | pbcopy
+```
+
+Paste into `app-config.yaml` `kubernetes.clusters[].serviceAccountToken`
diff --git a/plugins/kubernetes-backend/examples/dice-roller/catalog-info.yaml b/plugins/kubernetes-backend/examples/dice-roller/catalog-info.yaml
new file mode 100644
index 0000000000..a944232224
--- /dev/null
+++ b/plugins/kubernetes-backend/examples/dice-roller/catalog-info.yaml
@@ -0,0 +1,13 @@
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ name: dice-roller
+ description: It rolls dice
+ tags:
+ - go
+ annotations:
+ 'backstage.io/kubernetes-id': dice-roller
+spec:
+ type: service
+ lifecycle: production
+ owner: guest
diff --git a/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml b/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml
new file mode 100644
index 0000000000..ef448c1688
--- /dev/null
+++ b/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml
@@ -0,0 +1,79 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: dice-roller
+ labels:
+ 'backstage.io/kubernetes-id': dice-roller
+spec:
+ selector:
+ matchLabels:
+ app: dice-roller
+ replicas: 2
+ template:
+ metadata:
+ labels:
+ app: dice-roller
+ 'backstage.io/kubernetes-id': dice-roller
+ spec:
+ containers:
+ - name: nginx
+ image: nginx:1.14.2
+ ports:
+ - containerPort: 80
+
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: dice-roller
+ namespace: default
+ labels:
+ 'backstage.io/kubernetes-id': dice-roller
+data:
+ foo: bar
+
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: dice-roller
+ labels:
+ 'backstage.io/kubernetes-id': dice-roller
+type: Opaque
+data:
+ username: YWRtaW4=
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: dice-roller
+ labels:
+ 'backstage.io/kubernetes-id': dice-roller
+spec:
+ selector:
+ app: dice-roller
+ ports:
+ - protocol: TCP
+ port: 80
+ targetPort: 9376
+
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: dice-roller
+automountServiceAccountToken: false
+
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: sa-admin
+roleRef:
+ kind: ClusterRole
+ name: cluster-admin
+ apiGroup: rbac.authorization.k8s.io
+subjects:
+ - kind: ServiceAccount
+ name: dice-roller
+ namespace: default
diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json
index 406d9c8d7f..e1f7a39592 100644
--- a/plugins/kubernetes-backend/package.json
+++ b/plugins/kubernetes-backend/package.json
@@ -21,7 +21,9 @@
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.23",
+ "@backstage/config": "^0.1.1-alpha.23",
"@types/express": "^4.17.6",
+ "@kubernetes/client-node": "^0.12.1",
"compression": "^1.7.4",
"cors": "^2.8.5",
"express": "^4.17.1",
@@ -29,12 +31,14 @@
"fs-extra": "^9.0.0",
"helmet": "^4.0.0",
"morgan": "^1.10.0",
+ "stream-buffers": "^3.0.2",
"winston": "^3.2.1",
"yn": "^4.0.0"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
- "jest-fetch-mock": "^3.0.3"
+ "jest-fetch-mock": "^3.0.3",
+ "supertest": "^4.0.2",
+ "@backstage/cli": "^0.1.1-alpha.23"
},
"files": [
"dist"
diff --git a/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.test.ts
new file mode 100644
index 0000000000..34788d16cb
--- /dev/null
+++ b/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.test.ts
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '@backstage/backend-common';
+import { MultiTenantConfigClusterLocator } from './MultiTenantConfigClusterLocator';
+import { ConfigReader, Config } from '@backstage/config';
+
+describe('MultiTenantConfigClusterLocator', () => {
+ it('empty clusters returns empty cluster details', async () => {
+ const config: Config = new ConfigReader(
+ {
+ clusters: [],
+ },
+ 'ctx',
+ );
+
+ const sut = MultiTenantConfigClusterLocator.fromConfig(
+ config.getConfigArray('clusters'),
+ );
+
+ const result = await sut.getClusterByServiceId('ignored');
+
+ expect(result).toStrictEqual([]);
+ });
+
+ it('one clusters returns one cluster details', async () => {
+ const config: Config = new ConfigReader(
+ {
+ clusters: [
+ {
+ name: 'cluster1',
+ url: 'http://localhost:8080',
+ },
+ ],
+ },
+ 'ctx',
+ );
+
+ const sut = MultiTenantConfigClusterLocator.fromConfig(
+ config.getConfigArray('clusters'),
+ );
+
+ const result = await sut.getClusterByServiceId('ignored');
+
+ expect(result).toStrictEqual([
+ {
+ name: 'cluster1',
+ serviceAccountToken: undefined,
+ url: 'http://localhost:8080',
+ },
+ ]);
+ });
+
+ it('two clusters returns two cluster details', async () => {
+ const config: Config = new ConfigReader(
+ {
+ clusters: [
+ {
+ name: 'cluster1',
+ serviceAccountToken: undefined,
+ url: 'http://localhost:8080',
+ },
+ {
+ name: 'cluster2',
+ serviceAccountToken: undefined,
+ url: 'http://localhost:8081',
+ },
+ ],
+ },
+ 'ctx',
+ );
+
+ const sut = MultiTenantConfigClusterLocator.fromConfig(
+ config.getConfigArray('clusters'),
+ );
+
+ const result = await sut.getClusterByServiceId('ignored');
+
+ expect(result).toStrictEqual([
+ {
+ name: 'cluster1',
+ serviceAccountToken: undefined,
+ url: 'http://localhost:8080',
+ },
+ {
+ name: 'cluster2',
+ serviceAccountToken: undefined,
+ url: 'http://localhost:8081',
+ },
+ ]);
+ });
+});
diff --git a/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.ts
new file mode 100644
index 0000000000..3d9e2dd8b4
--- /dev/null
+++ b/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.ts
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Config } from '@backstage/config';
+import { ClusterDetails, KubernetesClusterLocator } from '..';
+
+// This cluster locator assumes that every service is located on every cluster
+// Therefore it will always return all clusters in an app configuration file
+export class MultiTenantConfigClusterLocator
+ implements KubernetesClusterLocator {
+ private readonly clusterDetails: ClusterDetails[];
+
+ constructor(clusterDetails: ClusterDetails[]) {
+ this.clusterDetails = clusterDetails;
+ }
+
+ static fromConfig(config: Config[]): MultiTenantConfigClusterLocator {
+ return new MultiTenantConfigClusterLocator(
+ config.map(c => {
+ return {
+ name: c.getString('name'),
+ url: c.getString('url'),
+ serviceAccountToken: c.getOptionalString('serviceAccountToken'),
+ };
+ }),
+ );
+ }
+
+ // As this implementation always returns all clusters serviceId is ignored here
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ async getClusterByServiceId(_serviceId: string): Promise {
+ return this.clusterDetails;
+ }
+}
diff --git a/plugins/kubernetes-backend/src/cluster-locator/types.ts b/plugins/kubernetes-backend/src/cluster-locator/types.ts
new file mode 100644
index 0000000000..9b793da558
--- /dev/null
+++ b/plugins/kubernetes-backend/src/cluster-locator/types.ts
@@ -0,0 +1,17 @@
+/*
+ * 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.
+ */
+
+export type ClusterLocatorMethod = 'configMultiTenant' | 'http';
diff --git a/plugins/kubernetes-backend/src/index.ts b/plugins/kubernetes-backend/src/index.ts
index 7612c392a2..96f070ff1e 100644
--- a/plugins/kubernetes-backend/src/index.ts
+++ b/plugins/kubernetes-backend/src/index.ts
@@ -15,3 +15,4 @@
*/
export * from './service/router';
+export * from './types/types';
diff --git a/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts
new file mode 100644
index 0000000000..a3dc67716a
--- /dev/null
+++ b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts
@@ -0,0 +1,136 @@
+/*
+ * 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 '@backstage/backend-common';
+import { KubernetesClientProvider } from './KubernetesClientProvider';
+
+describe('KubernetesClientProvider', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('can get core client by cluster details', async () => {
+ const sut = new KubernetesClientProvider();
+
+ const mockGetKubeConfig = jest.fn(sut.getKubeConfig.bind({}));
+
+ sut.getKubeConfig = mockGetKubeConfig;
+
+ const result = sut.getCoreClientByClusterDetails({
+ name: 'cluster-name',
+ url: 'http://localhost:9999',
+ serviceAccountToken: 'TOKEN',
+ });
+
+ expect(result.basePath).toBe('http://localhost:9999');
+ // These fields aren't on the type but are there
+ const auth = (result as any).authentications.default;
+ expect(auth.users[0].token).toBe('TOKEN');
+ expect(auth.clusters[0].name).toBe('cluster-name');
+
+ expect(mockGetKubeConfig.mock.calls.length).toBe(1);
+ });
+
+ it('can get cached core client by cluster details', async () => {
+ const sut = new KubernetesClientProvider();
+
+ const mockGetKubeConfig = jest.fn(sut.getKubeConfig.bind({}));
+
+ sut.getKubeConfig = mockGetKubeConfig;
+
+ const result1 = sut.getCoreClientByClusterDetails({
+ name: 'cluster-name',
+ url: 'http://localhost:9999',
+ serviceAccountToken: 'TOKEN',
+ });
+
+ const result2 = sut.getCoreClientByClusterDetails({
+ name: 'cluster-name',
+ url: 'http://localhost:9999',
+ serviceAccountToken: 'TOKEN',
+ });
+
+ expect(result1.basePath).toBe('http://localhost:9999');
+ // These fields aren't on the type but are there
+ const auth1 = (result1 as any).authentications.default;
+ expect(auth1.users[0].token).toBe('TOKEN');
+ expect(auth1.clusters[0].name).toBe('cluster-name');
+
+ expect(result2.basePath).toBe('http://localhost:9999');
+ // These fields aren't on the type but are there
+ const auth2 = (result2 as any).authentications.default;
+ expect(auth2.users[0].token).toBe('TOKEN');
+ expect(auth2.clusters[0].name).toBe('cluster-name');
+
+ expect(mockGetKubeConfig.mock.calls.length).toBe(1);
+ });
+
+ it('can get apps client by cluster details', async () => {
+ const sut = new KubernetesClientProvider();
+
+ const mockGetKubeConfig = jest.fn(sut.getKubeConfig.bind({}));
+
+ sut.getKubeConfig = mockGetKubeConfig;
+
+ const result = sut.getAppsClientByClusterDetails({
+ name: 'cluster-name',
+ url: 'http://localhost:9999',
+ serviceAccountToken: 'TOKEN',
+ });
+
+ expect(result.basePath).toBe('http://localhost:9999');
+ // These fields aren't on the type but are there
+ const auth = (result as any).authentications.default;
+ expect(auth.users[0].token).toBe('TOKEN');
+ expect(auth.clusters[0].name).toBe('cluster-name');
+
+ expect(mockGetKubeConfig.mock.calls.length).toBe(1);
+ });
+
+ it('can get cached apps client by cluster details', async () => {
+ const sut = new KubernetesClientProvider();
+
+ const mockGetKubeConfig = jest.fn(sut.getKubeConfig.bind({}));
+
+ sut.getKubeConfig = mockGetKubeConfig;
+
+ const result1 = sut.getAppsClientByClusterDetails({
+ name: 'cluster-name',
+ url: 'http://localhost:9999',
+ serviceAccountToken: 'TOKEN',
+ });
+
+ const result2 = sut.getAppsClientByClusterDetails({
+ name: 'cluster-name',
+ url: 'http://localhost:9999',
+ serviceAccountToken: 'TOKEN',
+ });
+
+ expect(result1.basePath).toBe('http://localhost:9999');
+ // These fields aren't on the type but are there
+ const auth1 = (result1 as any).authentications.default;
+ expect(auth1.users[0].token).toBe('TOKEN');
+ expect(auth1.clusters[0].name).toBe('cluster-name');
+
+ expect(result2.basePath).toBe('http://localhost:9999');
+ // These fields aren't on the type but are there
+ const auth2 = (result2 as any).authentications.default;
+ expect(auth2.users[0].token).toBe('TOKEN');
+ expect(auth2.clusters[0].name).toBe('cluster-name');
+
+ expect(mockGetKubeConfig.mock.calls.length).toBe(1);
+ });
+});
diff --git a/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts
new file mode 100644
index 0000000000..c3986e3ff7
--- /dev/null
+++ b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts
@@ -0,0 +1,96 @@
+/*
+ * 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 { ClusterDetails } from '..';
+import { AppsV1Api, CoreV1Api, KubeConfig } from '@kubernetes/client-node';
+
+export class KubernetesClientProvider {
+ private readonly coreClientMap: {
+ [key: string]: CoreV1Api;
+ };
+
+ private readonly appsClientMap: {
+ [key: string]: AppsV1Api;
+ };
+
+ constructor() {
+ this.coreClientMap = {};
+ this.appsClientMap = {};
+ }
+
+ // visible for testing
+ getKubeConfig(clusterDetails: ClusterDetails) {
+ const cluster = {
+ name: clusterDetails.name,
+ server: clusterDetails.url,
+ // TODO configure this
+ skipTLSVerify: true,
+ };
+
+ // TODO configure
+ const user = {
+ name: 'service-account',
+ token: clusterDetails.serviceAccountToken,
+ };
+
+ const context = {
+ name: `${clusterDetails.name}`,
+ user: user.name,
+ cluster: cluster.name,
+ };
+
+ const kc = new KubeConfig();
+ kc.loadFromOptions({
+ clusters: [cluster],
+ users: [user],
+ contexts: [context],
+ currentContext: context.name,
+ });
+ return kc;
+ }
+
+ getCoreClientByClusterDetails(clusterDetails: ClusterDetails) {
+ const clientMapKey = clusterDetails.name;
+
+ if (this.coreClientMap.hasOwnProperty(clientMapKey)) {
+ return this.coreClientMap[clientMapKey];
+ }
+
+ const kc = this.getKubeConfig(clusterDetails);
+
+ const client = kc.makeApiClient(CoreV1Api);
+
+ this.coreClientMap[clientMapKey] = client;
+
+ return client;
+ }
+
+ getAppsClientByClusterDetails(clusterDetails: ClusterDetails) {
+ const clientMapKey = clusterDetails.name;
+
+ if (this.appsClientMap.hasOwnProperty(clientMapKey)) {
+ return this.appsClientMap[clientMapKey];
+ }
+
+ const kc = this.getKubeConfig(clusterDetails);
+
+ const client = kc.makeApiClient(AppsV1Api);
+
+ this.appsClientMap[clientMapKey] = client;
+
+ return client;
+ }
+}
diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts
new file mode 100644
index 0000000000..359e7c5806
--- /dev/null
+++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts
@@ -0,0 +1,108 @@
+/*
+ * 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 { KubernetesClientBasedFetcher } from './KubernetesFetcher';
+
+describe('KubernetesClientProvider', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('should return pods, services', async () => {
+ const clientMock: any = {
+ listPodForAllNamespaces: jest.fn(),
+ listServiceForAllNamespaces: jest.fn(),
+ };
+
+ const kubernetesClientProvider: any = {
+ getCoreClientByClusterDetails: jest.fn(() => clientMock),
+ getAppsClientByClusterDetails: jest.fn(() => clientMock),
+ };
+
+ const sut = new KubernetesClientBasedFetcher({
+ kubernetesClientProvider,
+ logger: getVoidLogger(),
+ });
+
+ clientMock.listPodForAllNamespaces.mockResolvedValueOnce({
+ body: {
+ items: [
+ {
+ metadata: {
+ name: 'pod-name',
+ },
+ },
+ ],
+ },
+ });
+
+ clientMock.listServiceForAllNamespaces.mockResolvedValueOnce({
+ body: {
+ items: [
+ {
+ metadata: {
+ name: 'service-name',
+ },
+ },
+ ],
+ },
+ });
+
+ const result = await sut.fetchObjectsByServiceId(
+ 'some-service',
+ {
+ name: 'cluster1',
+ url: 'http://localhost:9999',
+ serviceAccountToken: undefined,
+ },
+ new Set(['pods', 'services']),
+ );
+
+ expect(result).toStrictEqual([
+ {
+ type: 'pods',
+ resources: [
+ {
+ metadata: {
+ name: 'pod-name',
+ },
+ },
+ ],
+ },
+ {
+ type: 'services',
+ resources: [
+ {
+ metadata: {
+ name: 'service-name',
+ },
+ },
+ ],
+ },
+ ]);
+
+ expect(clientMock.listPodForAllNamespaces.mock.calls.length).toBe(1);
+ expect(clientMock.listServiceForAllNamespaces.mock.calls.length).toBe(1);
+
+ expect(
+ kubernetesClientProvider.getAppsClientByClusterDetails.mock.calls.length,
+ ).toBe(2);
+ expect(
+ kubernetesClientProvider.getCoreClientByClusterDetails.mock.calls.length,
+ ).toBe(2);
+ });
+});
diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts
new file mode 100644
index 0000000000..f6d3edb5e9
--- /dev/null
+++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts
@@ -0,0 +1,212 @@
+/*
+ * 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 {
+ AppsV1Api,
+ CoreV1Api,
+ V1ConfigMap,
+ V1Deployment,
+ V1Pod,
+ V1ReplicaSet,
+ V1Secret,
+} from '@kubernetes/client-node';
+import { KubernetesClientProvider } from './KubernetesClientProvider';
+import { V1Service } from '@kubernetes/client-node/dist/gen/model/v1Service';
+import { Logger } from 'winston';
+import {
+ KubernetesFetcher,
+ ClusterDetails,
+ KubernetesObjectTypes,
+ FetchResponse,
+} from '..';
+
+export interface Clients {
+ core: CoreV1Api;
+ apps: AppsV1Api;
+}
+
+export interface KubernetesClientBasedFetcherOptions {
+ kubernetesClientProvider: KubernetesClientProvider;
+ logger: Logger;
+}
+
+export class KubernetesClientBasedFetcher implements KubernetesFetcher {
+ private readonly kubernetesClientProvider: KubernetesClientProvider;
+ private readonly logger: Logger;
+
+ constructor({
+ kubernetesClientProvider,
+ logger,
+ }: KubernetesClientBasedFetcherOptions) {
+ this.kubernetesClientProvider = kubernetesClientProvider;
+ this.logger = logger;
+ }
+
+ fetchObjectsByServiceId(
+ serviceId: string,
+ clusterDetails: ClusterDetails,
+ objectTypesToFetch: Set,
+ ): Promise {
+ return Promise.all(
+ Array.from(objectTypesToFetch).map(type => {
+ return this.fetchByObjectType(serviceId, clusterDetails, type);
+ }),
+ );
+ }
+
+ private fetchByObjectType(
+ serviceId: string,
+ clusterDetails: ClusterDetails,
+ type: KubernetesObjectTypes,
+ ): Promise {
+ switch (type) {
+ case 'pods':
+ return this.fetchPodsByServiceId(serviceId, clusterDetails).then(r => ({
+ type: type,
+ resources: r,
+ }));
+ case 'configmaps':
+ return this.fetchConfigMapsByServiceId(
+ serviceId,
+ clusterDetails,
+ ).then(r => ({ type: type, resources: r }));
+ case 'deployments':
+ return this.fetchDeploymentsByServiceId(
+ serviceId,
+ clusterDetails,
+ ).then(r => ({ type: type, resources: r }));
+ case 'replicasets':
+ return this.fetchReplicaSetsByServiceId(
+ serviceId,
+ clusterDetails,
+ ).then(r => ({ type: type, resources: r }));
+ case 'secrets':
+ return this.fetchSecretsByServiceId(
+ serviceId,
+ clusterDetails,
+ ).then(r => ({ type: type, resources: r }));
+ case 'services':
+ return this.fetchServicesByServiceId(
+ serviceId,
+ clusterDetails,
+ ).then(r => ({ type: type, resources: r }));
+ default:
+ // unrecognised type
+ throw new Error(`unrecognised type=${type}`);
+ }
+ }
+
+ private singleClusterFetch(
+ clusterDetails: ClusterDetails,
+ fn: (client: Clients) => Promise<{ body: { items: Array } }>,
+ ): Promise> {
+ const core = this.kubernetesClientProvider.getCoreClientByClusterDetails(
+ clusterDetails,
+ );
+ const apps = this.kubernetesClientProvider.getAppsClientByClusterDetails(
+ clusterDetails,
+ );
+
+ this.logger.debug(`calling cluster=${clusterDetails.name}`);
+ return fn({ core, apps }).then(result => {
+ return result.body.items;
+ });
+ }
+
+ private fetchServicesByServiceId(
+ serviceId: string,
+ clusterDetails: ClusterDetails,
+ ): Promise> {
+ return this.singleClusterFetch(clusterDetails, ({ core }) =>
+ core.listServiceForAllNamespaces(
+ false,
+ '',
+ '',
+ `backstage.io/kubernetes-id=${serviceId}`,
+ ),
+ );
+ }
+
+ private fetchPodsByServiceId(
+ serviceId: string,
+ clusterDetails: ClusterDetails,
+ ): Promise> {
+ return this.singleClusterFetch(clusterDetails, ({ core }) =>
+ core.listPodForAllNamespaces(
+ false,
+ '',
+ '',
+ `backstage.io/kubernetes-id=${serviceId}`,
+ ),
+ );
+ }
+
+ private fetchConfigMapsByServiceId(
+ serviceId: string,
+ clusterDetails: ClusterDetails,
+ ): Promise> {
+ return this.singleClusterFetch(clusterDetails, ({ core }) =>
+ core.listConfigMapForAllNamespaces(
+ false,
+ '',
+ '',
+ `backstage.io/kubernetes-id=${serviceId}`,
+ ),
+ );
+ }
+
+ private fetchSecretsByServiceId(
+ serviceId: string,
+ clusterDetails: ClusterDetails,
+ ): Promise> {
+ return this.singleClusterFetch(clusterDetails, ({ core }) =>
+ core.listSecretForAllNamespaces(
+ false,
+ '',
+ '',
+ `backstage.io/kubernetes-id=${serviceId}`,
+ ),
+ );
+ }
+
+ private fetchDeploymentsByServiceId(
+ serviceId: string,
+ clusterDetails: ClusterDetails,
+ ): Promise> {
+ return this.singleClusterFetch(clusterDetails, ({ apps }) =>
+ apps.listDeploymentForAllNamespaces(
+ false,
+ '',
+ '',
+ `backstage.io/kubernetes-id=${serviceId}`,
+ ),
+ );
+ }
+
+ private fetchReplicaSetsByServiceId(
+ serviceId: string,
+ clusterDetails: ClusterDetails,
+ ): Promise> {
+ return this.singleClusterFetch(clusterDetails, ({ apps }) =>
+ apps.listReplicaSetForAllNamespaces(
+ false,
+ '',
+ '',
+ `backstage.io/kubernetes-id=${serviceId}`,
+ ),
+ );
+ }
+}
diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts
new file mode 100644
index 0000000000..3f8081571a
--- /dev/null
+++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts
@@ -0,0 +1,242 @@
+/*
+ * 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 { handleGetKubernetesObjectsByServiceId } from './getKubernetesObjectsByServiceIdHandler';
+import { getVoidLogger } from '@backstage/backend-common';
+import { ClusterDetails } from '..';
+
+const TEST_SERVICE_ID = 'my-service';
+
+const fetchObjectsByServiceId = jest.fn();
+
+const getClusterByServiceId = jest.fn();
+
+const mockFetch = (mock: jest.Mock) => {
+ mock.mockImplementation((serviceId: string, clusterDetails: ClusterDetails) =>
+ Promise.resolve([
+ {
+ type: 'pods',
+ resources: [
+ {
+ metadata: {
+ name: `my-pods-${serviceId}-${clusterDetails.name}`,
+ },
+ },
+ ],
+ },
+ {
+ type: 'configmaps',
+ resources: [
+ {
+ metadata: {
+ name: `my-configmaps-${serviceId}-${clusterDetails.name}`,
+ },
+ },
+ ],
+ },
+ {
+ type: 'services',
+ resources: [
+ {
+ metadata: {
+ name: `my-services-${serviceId}-${clusterDetails.name}`,
+ },
+ },
+ ],
+ },
+ ]),
+ );
+};
+
+describe('handleGetKubernetesObjectsByServiceId', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('retrieve objects for one cluster', async () => {
+ getClusterByServiceId.mockImplementation(() =>
+ Promise.resolve([
+ {
+ name: 'test-cluster',
+ },
+ ]),
+ );
+
+ mockFetch(fetchObjectsByServiceId);
+
+ const result = await handleGetKubernetesObjectsByServiceId(
+ TEST_SERVICE_ID,
+ {
+ fetchObjectsByServiceId,
+ },
+ {
+ getClusterByServiceId,
+ },
+ getVoidLogger(),
+ );
+
+ expect(getClusterByServiceId.mock.calls.length).toBe(1);
+ expect(fetchObjectsByServiceId.mock.calls.length).toBe(1);
+ expect(result).toStrictEqual({
+ items: [
+ {
+ cluster: {
+ name: 'test-cluster',
+ },
+ resources: [
+ {
+ resources: [
+ {
+ metadata: {
+ name: 'my-pods-my-service-test-cluster',
+ },
+ },
+ ],
+ type: 'pods',
+ },
+ {
+ resources: [
+ {
+ metadata: {
+ name: 'my-configmaps-my-service-test-cluster',
+ },
+ },
+ ],
+ type: 'configmaps',
+ },
+ {
+ resources: [
+ {
+ metadata: {
+ name: 'my-services-my-service-test-cluster',
+ },
+ },
+ ],
+ type: 'services',
+ },
+ ],
+ },
+ ],
+ });
+ });
+
+ it('retrieve objects for two clusters', async () => {
+ getClusterByServiceId.mockImplementation(() =>
+ Promise.resolve([
+ {
+ name: 'test-cluster',
+ },
+ {
+ name: 'other-cluster',
+ },
+ ]),
+ );
+
+ mockFetch(fetchObjectsByServiceId);
+
+ const result = await handleGetKubernetesObjectsByServiceId(
+ TEST_SERVICE_ID,
+ {
+ fetchObjectsByServiceId,
+ },
+ {
+ getClusterByServiceId,
+ },
+ getVoidLogger(),
+ );
+
+ expect(getClusterByServiceId.mock.calls.length).toBe(1);
+ expect(fetchObjectsByServiceId.mock.calls.length).toBe(2);
+ expect(result).toStrictEqual({
+ items: [
+ {
+ cluster: {
+ name: 'test-cluster',
+ },
+ resources: [
+ {
+ resources: [
+ {
+ metadata: {
+ name: 'my-pods-my-service-test-cluster',
+ },
+ },
+ ],
+ type: 'pods',
+ },
+ {
+ resources: [
+ {
+ metadata: {
+ name: 'my-configmaps-my-service-test-cluster',
+ },
+ },
+ ],
+ type: 'configmaps',
+ },
+ {
+ resources: [
+ {
+ metadata: {
+ name: 'my-services-my-service-test-cluster',
+ },
+ },
+ ],
+ type: 'services',
+ },
+ ],
+ },
+ {
+ cluster: {
+ name: 'other-cluster',
+ },
+ resources: [
+ {
+ resources: [
+ {
+ metadata: {
+ name: 'my-pods-my-service-other-cluster',
+ },
+ },
+ ],
+ type: 'pods',
+ },
+ {
+ resources: [
+ {
+ metadata: {
+ name: 'my-configmaps-my-service-other-cluster',
+ },
+ },
+ ],
+ type: 'configmaps',
+ },
+ {
+ resources: [
+ {
+ metadata: {
+ name: 'my-services-my-service-other-cluster',
+ },
+ },
+ ],
+ type: 'services',
+ },
+ ],
+ },
+ ],
+ });
+ });
+});
diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts
new file mode 100644
index 0000000000..52968d4c20
--- /dev/null
+++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts
@@ -0,0 +1,69 @@
+/*
+ * 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 {
+ KubernetesClusterLocator,
+ KubernetesFetcher,
+ KubernetesObjectTypes,
+ ObjectsByServiceIdResponse,
+} from '..';
+
+export type GetKubernetesObjectsByServiceIdHandler = (
+ serviceId: string,
+ fetcher: KubernetesFetcher,
+ clusterLocator: KubernetesClusterLocator,
+ logger: Logger,
+ objectsToFetch?: Set,
+) => Promise;
+
+const DEFAULT_OBJECTS = new Set([
+ 'pods',
+ 'services',
+ 'configmaps',
+ 'secrets',
+ 'deployments',
+ 'replicasets',
+]);
+
+export const handleGetKubernetesObjectsByServiceId: GetKubernetesObjectsByServiceIdHandler = async (
+ serviceId,
+ fetcher,
+ clusterLocator,
+ logger,
+ objectsToFetch = DEFAULT_OBJECTS,
+) => {
+ const clusterDetails = await clusterLocator.getClusterByServiceId(serviceId);
+
+ logger.info(
+ `serviceId=${serviceId} clusterDetails=${clusterDetails.map(c => c.name)}`,
+ );
+
+ return Promise.all(
+ clusterDetails.map(cd => {
+ return fetcher
+ .fetchObjectsByServiceId(serviceId, cd, objectsToFetch)
+ .then(result => {
+ return {
+ cluster: {
+ name: cd.name,
+ },
+ resources: result,
+ };
+ });
+ }),
+ ).then(r => ({ items: r }));
+};
diff --git a/plugins/kubernetes-backend/src/service/router.test.ts b/plugins/kubernetes-backend/src/service/router.test.ts
new file mode 100644
index 0000000000..7731fe9abb
--- /dev/null
+++ b/plugins/kubernetes-backend/src/service/router.test.ts
@@ -0,0 +1,87 @@
+/*
+ * 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 express from 'express';
+import request from 'supertest';
+import { makeRouter } from './router';
+import {
+ KubernetesClusterLocator,
+ KubernetesFetcher,
+ ObjectsByServiceIdResponse,
+} from '..';
+
+describe('router', () => {
+ let app: express.Express;
+ let kubernetesFetcher: jest.Mocked;
+ let kubernetesClusterLocator: jest.Mocked;
+ let handleGetByServiceId: jest.Mock>;
+
+ beforeAll(async () => {
+ kubernetesFetcher = {
+ fetchObjectsByServiceId: jest.fn(),
+ };
+
+ kubernetesClusterLocator = {
+ getClusterByServiceId: jest.fn(),
+ };
+
+ handleGetByServiceId = jest.fn();
+
+ const router = makeRouter(
+ getVoidLogger(),
+ kubernetesFetcher,
+ kubernetesClusterLocator,
+ handleGetByServiceId as any,
+ );
+ app = express().use(router);
+ });
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ describe('GET /services/:serviceId', () => {
+ it('happy path: lists kubernetes objects', async () => {
+ const result = {
+ clusterOne: {
+ pods: [
+ {
+ metadata: {
+ name: 'pod1',
+ },
+ },
+ ],
+ },
+ } as any;
+ handleGetByServiceId.mockReturnValueOnce(Promise.resolve(result));
+
+ const response = await request(app).get('/services/test-service');
+
+ expect(response.status).toEqual(200);
+ expect(response.body).toEqual(result);
+ });
+
+ it('internal error: lists kubernetes objects', async () => {
+ handleGetByServiceId.mockRejectedValue(Error('some internal error'));
+
+ const response = await request(app).get('/services/test-service');
+
+ expect(response.status).toEqual(500);
+ expect(response.body).toEqual({ error: 'some internal error' });
+ });
+ });
+});
diff --git a/plugins/kubernetes-backend/src/service/router.ts b/plugins/kubernetes-backend/src/service/router.ts
index b200896887..d038c1ed7d 100644
--- a/plugins/kubernetes-backend/src/service/router.ts
+++ b/plugins/kubernetes-backend/src/service/router.ts
@@ -17,19 +17,65 @@
import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
+import { Config } from '@backstage/config';
+import { ClusterLocatorMethod } from '../cluster-locator/types';
+import { MultiTenantConfigClusterLocator } from '../cluster-locator/MultiTenantConfigClusterLocator';
+import { KubernetesClientBasedFetcher } from './KubernetesFetcher';
+import { KubernetesClientProvider } from './KubernetesClientProvider';
+import {
+ GetKubernetesObjectsByServiceIdHandler,
+ handleGetKubernetesObjectsByServiceId,
+} from './getKubernetesObjectsByServiceIdHandler';
+import { KubernetesClusterLocator, KubernetesFetcher } from '..';
export interface RouterOptions {
logger: Logger;
+ config: Config;
}
-const makeRouter = (logger: Logger): express.Router => {
+const getClusterLocator = (config: Config): KubernetesClusterLocator => {
+ const clusterLocatorMethod = config.getString(
+ 'kubernetes.clusterLocatorMethod',
+ ) as ClusterLocatorMethod;
+
+ switch (clusterLocatorMethod) {
+ case 'configMultiTenant':
+ return MultiTenantConfigClusterLocator.fromConfig(
+ config.getConfigArray('kubernetes.clusters'),
+ );
+ case 'http':
+ throw new Error('not implemented');
+ default:
+ throw new Error(
+ `Unsupported kubernetes.clusterLocatorMethod "${clusterLocatorMethod}"`,
+ );
+ }
+};
+
+export const makeRouter = (
+ logger: Logger,
+ fetcher: KubernetesFetcher,
+ clusterLocator: KubernetesClusterLocator,
+ handleGetByServiceId: GetKubernetesObjectsByServiceIdHandler,
+): express.Router => {
const router = Router();
router.use(express.json());
+ // TODO error handling
router.get('/services/:serviceId', async (req, res) => {
const serviceId = req.params.serviceId;
- logger.info(`HERE ${serviceId}`);
- res.send({ serviceId });
+
+ try {
+ const response = await handleGetByServiceId(
+ serviceId,
+ fetcher,
+ clusterLocator,
+ logger,
+ );
+ res.send(response);
+ } catch (e) {
+ res.status(500).send({ error: e.message });
+ }
});
return router;
@@ -41,5 +87,18 @@ export async function createRouter(
const logger = options.logger;
logger.info('Initializing Kubernetes backend');
- return makeRouter(logger);
+
+ const clusterLocator = getClusterLocator(options.config);
+
+ const fetcher = new KubernetesClientBasedFetcher({
+ kubernetesClientProvider: new KubernetesClientProvider(),
+ logger,
+ });
+
+ return makeRouter(
+ logger,
+ fetcher,
+ clusterLocator,
+ handleGetKubernetesObjectsByServiceId,
+ );
}
diff --git a/plugins/kubernetes-backend/src/service/standaloneApplication.ts b/plugins/kubernetes-backend/src/service/standaloneApplication.ts
index 5eea4fb8f5..e23e87a7a2 100644
--- a/plugins/kubernetes-backend/src/service/standaloneApplication.ts
+++ b/plugins/kubernetes-backend/src/service/standaloneApplication.ts
@@ -25,6 +25,7 @@ import express from 'express';
import helmet from 'helmet';
import { Logger } from 'winston';
import { createRouter } from './router';
+import { ConfigReader } from '@backstage/config';
export interface ApplicationOptions {
enableCors: boolean;
@@ -35,6 +36,7 @@ export async function createStandaloneApplication(
options: ApplicationOptions,
): Promise {
const { enableCors, logger } = options;
+ const config = ConfigReader.fromConfigs([]);
const app = express();
app.use(helmet());
@@ -44,7 +46,7 @@ export async function createStandaloneApplication(
app.use(compression());
app.use(express.json());
app.use(requestLoggingHandler());
- app.use('/', await createRouter({ logger }));
+ app.use('/', await createRouter({ logger, config }));
app.use(notFoundHandler());
app.use(errorHandler());
diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts
new file mode 100644
index 0000000000..2c69fd67a0
--- /dev/null
+++ b/plugins/kubernetes-backend/src/types/types.ts
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ V1ConfigMap,
+ V1Deployment,
+ V1Pod,
+ V1ReplicaSet,
+ V1Secret,
+ V1Service,
+} from '@kubernetes/client-node';
+
+export interface ClusterDetails {
+ name: string;
+ url: string;
+ // TODO this will eventually be configured by the auth translation work
+ serviceAccountToken: string | undefined;
+}
+
+export interface ClusterObjects {
+ cluster: { name: string };
+ resources: FetchResponse[];
+}
+
+export interface ObjectsByServiceIdResponse {
+ items: ClusterObjects[];
+}
+
+export type FetchResponse =
+ | PodFetchResponse
+ | ServiceFetchResponse
+ | ConfigMapFetchResponse
+ | SecretFetchResponse
+ | DeploymentFetchResponse
+ | ReplicaSetsFetchResponse;
+
+// TODO fairly sure there's a easier way to do this
+
+export type KubernetesObjectTypes =
+ | 'pods'
+ | 'services'
+ | 'configmaps'
+ | 'secrets'
+ | 'deployments'
+ | 'replicasets';
+
+export interface PodFetchResponse {
+ type: 'pods';
+ resources: Array;
+}
+
+export interface ServiceFetchResponse {
+ type: 'services';
+ resources: Array;
+}
+
+export interface ConfigMapFetchResponse {
+ type: 'configmaps';
+ resources: Array;
+}
+
+export interface SecretFetchResponse {
+ type: 'secrets';
+ resources: Array;
+}
+
+export interface DeploymentFetchResponse {
+ type: 'deployments';
+ resources: Array;
+}
+
+export interface ReplicaSetsFetchResponse {
+ type: 'replicasets';
+ resources: Array;
+}
+
+// Fetches information from a kubernetes cluster using the cluster details object
+// to target a specific cluster
+export interface KubernetesFetcher {
+ fetchObjectsByServiceId(
+ serviceId: string,
+ clusterDetails: ClusterDetails,
+ objectTypesToFetch: Set,
+ ): Promise;
+}
+
+// Used to locate which cluster(s) a service is running on
+export interface KubernetesClusterLocator {
+ getClusterByServiceId(serviceId: string): Promise;
+}
diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json
index d9311deb85..d1f3d5e7ba 100644
--- a/plugins/kubernetes/package.json
+++ b/plugins/kubernetes/package.json
@@ -22,6 +22,7 @@
"dependencies": {
"@backstage/catalog-model": "^0.1.1-alpha.23",
"@backstage/core": "^0.1.1-alpha.23",
+ "@backstage/plugin-kubernetes-backend": "^0.1.1-alpha.23",
"@backstage/theme": "^0.1.1-alpha.23",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
@@ -39,7 +40,9 @@
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
- "jest-fetch-mock": "^3.0.3"
+ "jest-fetch-mock": "^3.0.3",
+ "msw": "^0.20.5",
+ "node-fetch": "^2.6.1"
},
"files": [
"dist"
diff --git a/plugins/kubernetes/src/Router.tsx b/plugins/kubernetes/src/Router.tsx
index 994788bf72..41f534fbef 100644
--- a/plugins/kubernetes/src/Router.tsx
+++ b/plugins/kubernetes/src/Router.tsx
@@ -22,7 +22,7 @@ import { rootCatalogKubernetesRouteRef } from './plugin';
import { KubernetesContent } from './components/KubernetesContent';
import { WarningPanel } from '@backstage/core';
-const KUBERNETES_ANNOTATION = 'backstage.io/kubernetes';
+const KUBERNETES_ANNOTATION = 'backstage.io/kubernetes-id';
export const Router = ({ entity }: { entity: Entity }) => {
const kubernetesAnnotationValue =
diff --git a/plugins/kubernetes/src/api/KubernetesBackendClient.ts b/plugins/kubernetes/src/api/KubernetesBackendClient.ts
index fed3cd2ed0..cbb3b03a38 100644
--- a/plugins/kubernetes/src/api/KubernetesBackendClient.ts
+++ b/plugins/kubernetes/src/api/KubernetesBackendClient.ts
@@ -16,6 +16,7 @@
import { DiscoveryApi } from '@backstage/core';
import { KubernetesApi } from './types';
+import { ObjectsByServiceIdResponse } from '@backstage/plugin-kubernetes-backend';
export class KubernetesBackendClient implements KubernetesApi {
private readonly discoveryApi: DiscoveryApi;
@@ -37,7 +38,9 @@ export class KubernetesBackendClient implements KubernetesApi {
return await response.json();
}
- async getObjectsByServiceId(serviceId: String): Promise<{}> {
+ async getObjectsByServiceId(
+ serviceId: String,
+ ): Promise {
return await this.getRequired(`/services/${serviceId}`);
}
}
diff --git a/plugins/kubernetes/src/api/types.ts b/plugins/kubernetes/src/api/types.ts
index 2ad6e567f9..5ec3cda6f8 100644
--- a/plugins/kubernetes/src/api/types.ts
+++ b/plugins/kubernetes/src/api/types.ts
@@ -15,6 +15,7 @@
*/
import { createApiRef } from '@backstage/core';
+import { ObjectsByServiceIdResponse } from '@backstage/plugin-kubernetes-backend';
export const kubernetesApiRef = createApiRef({
id: 'plugin.kubernetes.service',
@@ -23,5 +24,5 @@ export const kubernetesApiRef = createApiRef({
});
export interface KubernetesApi {
- getObjectsByServiceId(serviceId: String): Promise<{}>;
+ getObjectsByServiceId(serviceId: String): Promise;
}
diff --git a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx
index d79454a5d1..bbcef6a0d0 100644
--- a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx
+++ b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx
@@ -15,25 +15,19 @@
*/
import React, { FC, useEffect, useState } from 'react';
-import { Typography, Grid } from '@material-ui/core';
-import {
- InfoCard,
- Page,
- pageTheme,
- Content,
- ContentHeader,
- useApi,
-} from '@backstage/core';
+import { Grid } from '@material-ui/core';
+import { InfoCard, Page, pageTheme, Content, useApi } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { kubernetesApiRef } from '../../api/types';
+import { ObjectsByServiceIdResponse } from '@backstage/plugin-kubernetes-backend';
// TODO this is a temporary component used to construct the Kubernetes plugin boilerplate
export const KubernetesContent: FC<{ entity: Entity }> = ({ entity }) => {
const kubernetesApi = useApi(kubernetesApiRef);
- const [kubernetesObjects, setKubernetesObjects] = useState<{} | undefined>(
- undefined,
- );
+ const [kubernetesObjects, setKubernetesObjects] = useState<
+ ObjectsByServiceIdResponse | undefined
+ >(undefined);
const [error, setError] = useState(undefined);
useEffect(() => {
@@ -50,21 +44,28 @@ export const KubernetesContent: FC<{ entity: Entity }> = ({ entity }) => {
return (
-
-
-
-
- {kubernetesObjects === undefined && loading....
}
- {error !== undefined && {error}
}
- {kubernetesObjects !== undefined && (
-
- backend response: {JSON.stringify(kubernetesObjects)}
-
- )}
-
-
-
+ {kubernetesObjects === undefined && loading....
}
+ {error !== undefined && {error}
}
+ {kubernetesObjects !== undefined && (
+
+ {kubernetesObjects.items.map((item, i) => (
+
+
+ {item.resources.map((fr, j) => (
+
+
+ {fr.type}:{' '}
+ {(fr.resources as any)
+ .map((v: any) => v.metadata.name)
+ .join(' ')}
+
+ ))}
+
+
+ ))}
+
+ )}
diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json
index 622fec9d0c..6ca1495a05 100644
--- a/plugins/lighthouse/package.json
+++ b/plugins/lighthouse/package.json
@@ -42,7 +42,9 @@
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
- "jest-fetch-mock": "^3.0.3"
+ "jest-fetch-mock": "^3.0.3",
+ "msw": "^0.20.5",
+ "node-fetch": "^2.6.1"
},
"files": [
"dist"
diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json
index da95e0e580..38a5c437a4 100644
--- a/plugins/newrelic/package.json
+++ b/plugins/newrelic/package.json
@@ -38,7 +38,9 @@
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
- "jest-fetch-mock": "^3.0.3"
+ "jest-fetch-mock": "^3.0.3",
+ "msw": "^0.20.5",
+ "node-fetch": "^2.6.1"
},
"files": [
"dist"
diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json
index 5e801b42a9..9e3ef7e307 100644
--- a/plugins/register-component/package.json
+++ b/plugins/register-component/package.json
@@ -43,7 +43,9 @@
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
- "jest-fetch-mock": "^3.0.3"
+ "jest-fetch-mock": "^3.0.3",
+ "msw": "^0.20.5",
+ "node-fetch": "^2.6.1"
},
"files": [
"dist"
diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json
index ef386e50ea..18f13c3969 100644
--- a/plugins/rollbar/package.json
+++ b/plugins/rollbar/package.json
@@ -47,7 +47,9 @@
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"@types/react": "^16.9",
- "jest-fetch-mock": "^3.0.3"
+ "jest-fetch-mock": "^3.0.3",
+ "msw": "^0.20.5",
+ "node-fetch": "^2.6.1"
},
"files": [
"dist"
diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json
index 26cf58c040..2abd3feb8f 100644
--- a/plugins/scaffolder/package.json
+++ b/plugins/scaffolder/package.json
@@ -49,7 +49,9 @@
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
- "jest-fetch-mock": "^3.0.3"
+ "jest-fetch-mock": "^3.0.3",
+ "msw": "^0.20.5",
+ "node-fetch": "^2.6.1"
},
"files": [
"dist"
diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json
index bddeff243c..143a2dfc12 100644
--- a/plugins/sentry/package.json
+++ b/plugins/sentry/package.json
@@ -43,7 +43,9 @@
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
- "jest-fetch-mock": "^3.0.3"
+ "jest-fetch-mock": "^3.0.3",
+ "msw": "^0.20.5",
+ "node-fetch": "^2.6.1"
},
"files": [
"dist"
diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json
index 7b0bf4eb3a..c9950cdc95 100644
--- a/plugins/tech-radar/package.json
+++ b/plugins/tech-radar/package.json
@@ -45,7 +45,9 @@
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"@types/react": "^16.9",
- "jest-fetch-mock": "^3.0.3"
+ "jest-fetch-mock": "^3.0.3",
+ "msw": "^0.20.5",
+ "node-fetch": "^2.6.1"
},
"files": [
"dist"
diff --git a/plugins/techdocs-backend/README.md b/plugins/techdocs-backend/README.md
index 2185c3b3e4..b7eca2c5cb 100644
--- a/plugins/techdocs-backend/README.md
+++ b/plugins/techdocs-backend/README.md
@@ -18,11 +18,11 @@ yarn start
## What techdocs-backend does
-This plugin is the backend part of the techdocs plugin. It provides building and serving of your docs without having to use another service and hosting provider. To use it set your techdocs storageUrl in your `app-config.yml` to `http://localhost:7000/techdocs/static/docs`.
+This plugin is the backend part of the techdocs plugin. It provides building and serving of your docs without having to use another service and hosting provider. To use it set your techdocs storageUrl in your `app-config.yml` to `http://localhost:7000/api/techdocs/static/docs`.
```yaml
techdocs:
- storageUrl: http://localhost:7000/techdocs/static/docs
+ storageUrl: http://localhost:7000/api/techdocs/static/docs
```
## Extending techdocs-backend
diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts
index e0a066c168..14820da4e9 100644
--- a/plugins/techdocs-backend/src/service/router.ts
+++ b/plugins/techdocs-backend/src/service/router.ts
@@ -63,7 +63,7 @@ export async function createRouter({
const entity = (await (
await fetch(
- `${baseUrl}/catalog/entities/by-name/${kind}/${namespace}/${name}`,
+ `${baseUrl}/api/catalog/entities/by-name/${kind}/${namespace}/${name}`,
)
).json()) as Entity;
diff --git a/plugins/techdocs-backend/src/techdocs/stages/publish/local.ts b/plugins/techdocs-backend/src/techdocs/stages/publish/local.ts
index e33c6fa17d..f328443965 100644
--- a/plugins/techdocs-backend/src/techdocs/stages/publish/local.ts
+++ b/plugins/techdocs-backend/src/techdocs/stages/publish/local.ts
@@ -64,7 +64,7 @@ export class LocalPublish implements PublisherBase {
}
resolve({
- remoteUrl: `http://localhost:7000/techdocs/static/docs/${entity.metadata.name}`,
+ remoteUrl: `http://localhost:7000/api/techdocs/static/docs/${entity.metadata.name}`,
});
});
});
diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json
index 180a1b153a..e376202a41 100644
--- a/plugins/techdocs/package.json
+++ b/plugins/techdocs/package.json
@@ -48,7 +48,9 @@
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"canvas": "^2.6.1",
- "jest-fetch-mock": "^3.0.3"
+ "jest-fetch-mock": "^3.0.3",
+ "msw": "^0.20.5",
+ "node-fetch": "^2.6.1"
},
"files": [
"dist"
diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json
index 4dca91e503..d6c3a907b7 100644
--- a/plugins/welcome/package.json
+++ b/plugins/welcome/package.json
@@ -39,7 +39,9 @@
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
- "jest-fetch-mock": "^3.0.3"
+ "jest-fetch-mock": "^3.0.3",
+ "msw": "^0.20.5",
+ "node-fetch": "^2.6.1"
},
"files": [
"dist"
diff --git a/yarn.lock b/yarn.lock
index 33a1720a14..46402a9815 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2441,6 +2441,34 @@
resolved "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796"
integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==
+"@kubernetes/client-node@^0.12.1":
+ version "0.12.2"
+ resolved "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.12.2.tgz#8728684bd57d1cbcbe14fe742e79be7021403eea"
+ integrity sha512-J0UwyFl1Iv/IZ6WMP7LaizBEoKPnqwtc8tIO2q/X+EuDT7eGpPPAMHXSEOC/EI9JGIf0FaJEcDHhB/Dio/mKhw==
+ dependencies:
+ "@types/js-yaml" "^3.12.1"
+ "@types/node" "^10.12.0"
+ "@types/request" "^2.47.1"
+ "@types/stream-buffers" "^3.0.3"
+ "@types/tar" "^4.0.3"
+ "@types/underscore" "^1.8.9"
+ "@types/ws" "^6.0.1"
+ byline "^5.0.0"
+ execa "1.0.0"
+ isomorphic-ws "^4.0.1"
+ js-yaml "^3.13.1"
+ jsonpath-plus "^0.19.0"
+ openid-client "^4.1.1"
+ request "^2.88.0"
+ rfc4648 "^1.3.0"
+ shelljs "^0.8.2"
+ stream-buffers "^3.0.2"
+ tar "^6.0.2"
+ tmp-promise "^3.0.2"
+ tslib "^1.9.3"
+ underscore "^1.9.1"
+ ws "^7.3.1"
+
"@kyleshockey/object-assign-deep@^0.4.2":
version "0.4.2"
resolved "https://registry.npmjs.org/@kyleshockey/object-assign-deep/-/object-assign-deep-0.4.2.tgz#84900f0eefc372798f4751b5262830b8208922ec"
@@ -3490,7 +3518,7 @@
dependencies:
"@types/node" ">= 8"
-"@open-draft/until@^1.0.0", "@open-draft/until@^1.0.3":
+"@open-draft/until@^1.0.3":
version "1.0.3"
resolved "https://registry.npmjs.org/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca"
integrity sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==
@@ -4638,6 +4666,11 @@
"@types/node" "*"
"@types/responselike" "*"
+"@types/caseless@*":
+ version "0.12.2"
+ resolved "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8"
+ integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==
+
"@types/classnames@^2.2.9":
version "2.2.10"
resolved "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.10.tgz#cc658ca319b6355399efc1f5b9e818f1a24bf999"
@@ -4710,11 +4743,6 @@
dependencies:
"@types/express" "*"
-"@types/cookie@^0.3.3":
- version "0.3.3"
- resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803"
- integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==
-
"@types/cookie@^0.4.0":
version "0.4.0"
resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz#14f854c0f93d326e39da6e3b6f34f7d37513d108"
@@ -5000,7 +5028,7 @@
resolved "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.6.tgz#f1a1cb35aff47bc5cfb05cb0c441ca91e914c26f"
integrity sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw==
-"@types/js-yaml@^3.12.5":
+"@types/js-yaml@^3.12.1", "@types/js-yaml@^3.12.5":
version "3.12.5"
resolved "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.5.tgz#136d5e6a57a931e1cce6f9d8126aa98a9c92a6bb"
integrity sha512-JCcp6J0GV66Y4ZMDAQCXot4xprYB+Zfd3meK9+INSJeVZwJmHAW30BBEEkPzXswMXuiyReUGOP3GxrADc9wPww==
@@ -5144,6 +5172,11 @@
resolved "https://registry.npmjs.org/@types/node/-/node-10.17.28.tgz#0e36d718a29355ee51cec83b42d921299200f6d9"
integrity sha512-dzjES1Egb4c1a89C7lKwQh8pwjYmlOAG9dW1pBgxEk57tMrLnssOfEthz8kdkNaBd7lIqQx7APm5+mZ619IiCQ==
+"@types/node@^10.12.0":
+ version "10.17.35"
+ resolved "https://registry.npmjs.org/@types/node/-/node-10.17.35.tgz#58058f29b870e6ae57b20e4f6e928f02b7129f56"
+ integrity sha512-gXx7jAWpMddu0f7a+L+txMplp3FnHl53OhQIF9puXKq3hDGY/GjH+MF04oWnV/adPSCrbtHumDCFwzq2VhltWA==
+
"@types/node@^12.0.0":
version "12.12.53"
resolved "https://registry.npmjs.org/@types/node/-/node-12.12.53.tgz#be0d375933c3d15ef2380dafb3b0350ea7021129"
@@ -5392,6 +5425,16 @@
resolved "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.28.tgz#6bda7db8653fa62643f5ee69e9f69c11a392e3a6"
integrity sha1-a9p9uGU/piZD9e5p6facEaOS46Y=
+"@types/request@^2.47.1":
+ version "2.48.5"
+ resolved "https://registry.npmjs.org/@types/request/-/request-2.48.5.tgz#019b8536b402069f6d11bee1b2c03e7f232937a0"
+ integrity sha512-/LO7xRVnL3DxJ1WkPGDQrp4VTV1reX9RkC85mJ+Qzykj2Bdw+mG15aAfDahc76HtknjzE16SX/Yddn6MxVbmGQ==
+ dependencies:
+ "@types/caseless" "*"
+ "@types/node" "*"
+ "@types/tough-cookie" "*"
+ form-data "^2.5.0"
+
"@types/resolve@1.17.1":
version "1.17.1"
resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
@@ -5477,6 +5520,13 @@
dependencies:
"@types/node" "*"
+"@types/stream-buffers@^3.0.3":
+ version "3.0.3"
+ resolved "https://registry.npmjs.org/@types/stream-buffers/-/stream-buffers-3.0.3.tgz#34e565bf64e3e4bdeee23fd4aa58d4636014a02b"
+ integrity sha512-NeFeX7YfFZDYsCfbuaOmFQ0OjSmHreKBpp7MQ4alWQBHeh2USLsj7qyMyn9t82kjqIX516CR/5SRHnARduRtbQ==
+ dependencies:
+ "@types/node" "*"
+
"@types/styled-jsx@^2.2.8":
version "2.2.8"
resolved "https://registry.npmjs.org/@types/styled-jsx/-/styled-jsx-2.2.8.tgz#b50d13d8a3c34036282d65194554cf186bab7234"
@@ -5555,6 +5605,11 @@
dependencies:
"@types/node" "*"
+"@types/tough-cookie@*":
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz#fef1904e4668b6e5ecee60c52cc6a078ffa6697d"
+ integrity sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==
+
"@types/uglify-js@*":
version "3.0.4"
resolved "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082"
@@ -5562,6 +5617,11 @@
dependencies:
source-map "^0.6.1"
+"@types/underscore@^1.8.9":
+ version "1.10.23"
+ resolved "https://registry.npmjs.org/@types/underscore/-/underscore-1.10.23.tgz#cc672e8864000d288e1e39c609fd9cab84391ff3"
+ integrity sha512-vX1NPekXhrLquFWskH2thcvFAha187F/lM6xYOoEMZWwJ/6alSk0/ttmGP/YRqcqtCv0TMbZjYAdZyHAEcuU4g==
+
"@types/uuid@^8.0.0":
version "8.0.0"
resolved "https://registry.npmjs.org/@types/uuid/-/uuid-8.0.0.tgz#165aae4819ad2174a17476dbe66feebd549556c0"
@@ -5623,6 +5683,13 @@
resolved "https://registry.npmjs.org/@types/whatwg-streams/-/whatwg-streams-0.0.7.tgz#28bfe73dc850562296367249c4b32a50db81e9d3"
integrity sha512-6sDiSEP6DWcY2ZolsJ2s39ZmsoGQ7KVwBDI3sESQsEm9P2dHTcqnDIHRZFRNtLCzWp7hCFGqYbw5GyfpQnJ01A==
+"@types/ws@^6.0.1":
+ version "6.0.4"
+ resolved "https://registry.npmjs.org/@types/ws/-/ws-6.0.4.tgz#7797707c8acce8f76d8c34b370d4645b70421ff1"
+ integrity sha512-PpPrX7SZW9re6+Ha8ojZG4Se8AZXgf0GK6zmfqEuCsY49LFDNXO3SByp44X3dFEqtB73lkCDAdUazhAjVPiNwg==
+ dependencies:
+ "@types/node" "*"
+
"@types/ws@^7.0.0":
version "7.2.6"
resolved "https://registry.npmjs.org/@types/ws/-/ws-7.2.6.tgz#516cbfb818310f87b43940460e065eb912a4178d"
@@ -7161,7 +7228,7 @@ base64-js@^1.0.2, base64-js@^1.2.0:
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
-base64url@3.x.x:
+base64url@3.x.x, base64url@^3.0.1:
version "3.0.1"
resolved "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d"
integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==
@@ -10635,6 +10702,19 @@ exec-sh@^0.3.2:
resolved "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5"
integrity sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==
+execa@1.0.0, execa@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
+ integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
+ dependencies:
+ cross-spawn "^6.0.0"
+ get-stream "^4.0.0"
+ is-stream "^1.1.0"
+ npm-run-path "^2.0.0"
+ p-finally "^1.0.0"
+ signal-exit "^3.0.0"
+ strip-eof "^1.0.0"
+
execa@3.4.0:
version "3.4.0"
resolved "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz#c08ed4550ef65d858fac269ffc8572446f37eb89"
@@ -10664,19 +10744,6 @@ execa@^0.7.0:
signal-exit "^3.0.0"
strip-eof "^1.0.0"
-execa@^1.0.0:
- version "1.0.0"
- resolved "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
- integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
- dependencies:
- cross-spawn "^6.0.0"
- get-stream "^4.0.0"
- is-stream "^1.1.0"
- npm-run-path "^2.0.0"
- p-finally "^1.0.0"
- signal-exit "^3.0.0"
- strip-eof "^1.0.0"
-
execa@^4.0.0, execa@^4.0.1:
version "4.0.2"
resolved "https://registry.npmjs.org/execa/-/execa-4.0.2.tgz#ad87fb7b2d9d564f70d2b62d511bee41d5cbb240"
@@ -11342,7 +11409,7 @@ fork-ts-checker-webpack-plugin@^4.0.5, fork-ts-checker-webpack-plugin@^4.1.4:
tapable "^1.0.0"
worker-rpc "^0.1.0"
-form-data@^2.3.1, form-data@^2.3.2:
+form-data@^2.3.1, form-data@^2.3.2, form-data@^2.5.0:
version "2.5.1"
resolved "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4"
integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==
@@ -11987,6 +12054,23 @@ got@^11.5.2:
p-cancelable "^2.0.0"
responselike "^2.0.0"
+got@^11.6.2:
+ version "11.7.0"
+ resolved "https://registry.npmjs.org/got/-/got-11.7.0.tgz#a386360305571a74548872e674932b4ef70d3b24"
+ integrity sha512-7en2XwH2MEqOsrK0xaKhbWibBoZqy+f1RSUoIeF1BLcnf+pyQdDsljWMfmOh+QKJwuvDIiKx38GtPh5wFdGGjg==
+ dependencies:
+ "@sindresorhus/is" "^3.1.1"
+ "@szmarczak/http-timer" "^4.0.5"
+ "@types/cacheable-request" "^6.0.1"
+ "@types/responselike" "^1.0.0"
+ cacheable-lookup "^5.0.3"
+ cacheable-request "^7.0.1"
+ decompress-response "^6.0.0"
+ http2-wrapper "^1.0.0-beta.5.2"
+ lowercase-keys "^2.0.0"
+ p-cancelable "^2.0.0"
+ responselike "^2.0.0"
+
got@^9.6.0:
version "9.6.0"
resolved "https://registry.npmjs.org/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85"
@@ -12154,7 +12238,7 @@ graphql-upload@^8.0.2:
http-errors "^1.7.3"
object-path "^0.11.4"
-graphql@15.3.0, graphql@^15.0.0, graphql@^15.3.0:
+graphql@15.3.0, graphql@^15.3.0:
version "15.3.0"
resolved "https://registry.npmjs.org/graphql/-/graphql-15.3.0.tgz#3ad2b0caab0d110e3be4a5a9b2aa281e362b5278"
integrity sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w==
@@ -12330,7 +12414,7 @@ he@^1.1.0, he@^1.2.0:
resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
-headers-utils@^1.1.9, headers-utils@^1.2.0:
+headers-utils@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/headers-utils/-/headers-utils-1.2.0.tgz#5e10d1bc9d2bccf789547afca5b991a3167241e8"
integrity sha512-4/BMXcWrJErw7JpM87gF8MNEXcIMLzepYZjNRv/P9ctgupl2Ywa3u1PgHtNhSRq84bHH9Ndlkdy7bSi+bZ9I9A==
@@ -13609,6 +13693,11 @@ isomorphic-form-data@~2.0.0:
dependencies:
form-data "^2.3.2"
+isomorphic-ws@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc"
+ integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==
+
isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
@@ -14115,6 +14204,13 @@ jose@^1.27.1:
dependencies:
"@panva/asn1.js" "^1.0.0"
+jose@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/jose/-/jose-2.0.2.tgz#fb22385b80c658cc7a0cae05b7086c04c6be49f4"
+ integrity sha512-yD93lsiMA1go/qxSY/vXWBodmIZJIxeB7QhFi8z1yQ3KUwKENqI9UA8VCHlQ5h3x1zWuWZjoY87ByQzkQbIrQg==
+ dependencies:
+ "@panva/asn1.js" "^1.0.0"
+
js-cookie@^2.2.1:
version "2.2.1"
resolved "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
@@ -14407,6 +14503,11 @@ jsonparse@^1.2.0:
resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=
+jsonpath-plus@^0.19.0:
+ version "0.19.0"
+ resolved "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-0.19.0.tgz#b901e57607055933dc9a8bef0cc25160ee9dd64c"
+ integrity sha512-GSVwsrzW9LsA5lzsqe4CkuZ9wp+kxBb2GwNniaWzI2YFn5Ig42rSW8ZxVpWXaAfakXNrx5pgY5AbQq7kzX29kg==
+
jsonpointer@^4.0.1:
version "4.0.1"
resolved "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
@@ -15302,7 +15403,7 @@ make-dir@^3.0.0, make-dir@^3.0.2:
dependencies:
semver "^6.0.0"
-make-error@1.x, make-error@^1.1.1:
+make-error@1.x, make-error@^1.1.1, make-error@^1.3.6:
version "1.3.6"
resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
@@ -15939,22 +16040,6 @@ ms@^2.0.0, ms@^2.1.1:
resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
-msw@^0.19.5:
- version "0.19.5"
- resolved "https://registry.npmjs.org/msw/-/msw-0.19.5.tgz#7d2a1a852ccf1644d3db6735d69fff6777aac33f"
- integrity sha512-J5eQ++gDVZoHPC8gVXtWcakLjgmPipvFj/sEnlRV/WViXuiq2CamSqO3Wbh6H8bAmj+k2vUWCfcVT1HjMdKB2Q==
- dependencies:
- "@open-draft/until" "^1.0.0"
- "@types/cookie" "^0.3.3"
- chalk "^4.0.0"
- cookie "^0.4.1"
- graphql "^15.0.0"
- headers-utils "^1.1.9"
- node-match-path "^0.4.2"
- node-request-interceptor "^0.2.5"
- statuses "^2.0.0"
- yargs "^15.3.1"
-
msw@^0.20.5:
version "0.20.5"
resolved "https://registry.npmjs.org/msw/-/msw-0.20.5.tgz#b6141080c0d8b17c451d9ca36c28cc47b4ac487a"
@@ -16131,7 +16216,7 @@ node-fetch@^1.0.1:
encoding "^0.1.11"
is-stream "^1.0.1"
-node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0:
+node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0, node-fetch@^2.6.1:
version "2.6.1"
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
@@ -16214,7 +16299,7 @@ node-libs-browser@^2.2.1:
util "^0.11.0"
vm-browserify "^1.0.1"
-node-match-path@^0.4.2, node-match-path@^0.4.4:
+node-match-path@^0.4.4:
version "0.4.4"
resolved "https://registry.npmjs.org/node-match-path/-/node-match-path-0.4.4.tgz#516a10926093c0cc6f237d020685b593b19baebb"
integrity sha512-pBq9gp7TG0r0VXuy/oeZmQsjBSnYQo7G886Ly/B3azRwZuEtHCY155dzmfoKWcDPGgyfIGD8WKVC7h3+6y7yTg==
@@ -16273,14 +16358,6 @@ node-releases@^1.1.52, node-releases@^1.1.58:
resolved "https://registry.npmjs.org/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084"
integrity sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA==
-node-request-interceptor@^0.2.5:
- version "0.2.6"
- resolved "https://registry.npmjs.org/node-request-interceptor/-/node-request-interceptor-0.2.6.tgz#541278d7033bb6a8befb5dd793f83428cf6446a2"
- integrity sha512-aJW1tPSM7nzuZFRe+C/KSz22GJO3CVFMxHHmMGX8Z+tjP7TCIVbzeckLFVfJG68BdVgrdOOP7Ejc57ag820eyA==
- dependencies:
- debug "^4.1.1"
- headers-utils "^1.2.0"
-
node-request-interceptor@^0.3.5:
version "0.3.5"
resolved "https://registry.npmjs.org/node-request-interceptor/-/node-request-interceptor-0.3.5.tgz#4b26159617829c9a70643012c0fdc3ae4c78ae43"
@@ -16529,6 +16606,11 @@ object-copy@^0.1.0:
define-property "^0.2.5"
kind-of "^3.0.3"
+object-hash@^2.0.1:
+ version "2.0.3"
+ resolved "https://registry.npmjs.org/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea"
+ integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg==
+
object-inspect@^1.7.0:
version "1.7.0"
resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
@@ -16644,6 +16726,11 @@ octokit-pagination-methods@^1.1.0:
resolved "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4"
integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==
+oidc-token-hash@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.0.tgz#acdfb1f4310f58e64d5d74a4e8671a426986e888"
+ integrity sha512-8Yr4CZSv+Tn8ZkN3iN2i2w2G92mUKClp4z7EGUfdsERiYSbj7P4i/NHm72ft+aUdsiFx9UdIPSTwbyzQ6C4URg==
+
omggif@1.0.7:
version "1.0.7"
resolved "https://registry.npmjs.org/omggif/-/omggif-1.0.7.tgz#59d2eecb0263de84635b3feb887c0c9973f1e49d"
@@ -16712,6 +16799,20 @@ opencollective-postinstall@^2.0.2:
resolved "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89"
integrity sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==
+openid-client@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.npmjs.org/openid-client/-/openid-client-4.1.1.tgz#3e8a25584c4292e9b9b03e60358f5549fb85197a"
+ integrity sha512-/qch3I3v8UtO0A7wVgyXJJjGX/knR8bv06DQpLuKQqLG5u4AHcgusGuVKPKAcneLZvHKbKovF2+3e2ngXyuudA==
+ dependencies:
+ base64url "^3.0.1"
+ got "^11.6.2"
+ jose "^2.0.2"
+ lru-cache "^6.0.0"
+ make-error "^1.3.6"
+ object-hash "^2.0.1"
+ oidc-token-hash "^5.0.0"
+ p-any "^3.0.0"
+
opn@^5.5.0:
version "5.5.0"
resolved "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc"
@@ -16817,6 +16918,14 @@ overlayscrollbars@^1.10.2:
resolved "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-1.13.0.tgz#1edb436328133b94877b558f77966d5497ca36a7"
integrity sha512-p8oHrMeRAKxXDMPI/EBNITj/zTVHKNnAnM59Im+xnoZUlV07FyTg46wom2286jJlXGGfcPFG/ba5NUiCwWNd4w==
+p-any@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/p-any/-/p-any-3.0.0.tgz#79847aeed70b5d3a10ea625296c0c3d2e90a87b9"
+ integrity sha512-5rqbqfsRWNb0sukt0awwgJMlaep+8jV45S15SKKB34z4UuzjcofIfnriCBhWjZP2jbVtjt9yRl7buB6RlKsu9w==
+ dependencies:
+ p-cancelable "^2.0.0"
+ p-some "^5.0.0"
+
p-cancelable@^1.0.0:
version "1.1.0"
resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
@@ -16963,6 +17072,14 @@ p-retry@^3.0.1:
dependencies:
retry "^0.12.0"
+p-some@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.npmjs.org/p-some/-/p-some-5.0.0.tgz#8b730c74b4fe5169d7264a240ad010b6ebc686a4"
+ integrity sha512-Js5XZxo6vHjB9NOYAzWDYAIyyiPvva0DWESAIWIK7uhSpGsyg5FwUPxipU/SOQx5x9EqhOh545d1jo6cVkitig==
+ dependencies:
+ aggregate-error "^3.0.0"
+ p-cancelable "^2.0.0"
+
p-timeout@^3.1.0:
version "3.2.0"
resolved "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe"
@@ -19793,6 +19910,11 @@ reusify@^1.0.4:
resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+rfc4648@^1.3.0:
+ version "1.4.0"
+ resolved "https://registry.npmjs.org/rfc4648/-/rfc4648-1.4.0.tgz#c75b2856ad2e2d588b6ddb985d556f1f7f2a2abd"
+ integrity sha512-3qIzGhHlMHA6PoT6+cdPKZ+ZqtxkIvg8DZGKA5z6PQ33/uuhoJ+Ws/D/J9rXW6gXodgH8QYlz2UCl+sdUDmNIg==
+
rgb-regex@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1"
@@ -20332,7 +20454,7 @@ shell-quote@1.7.2:
resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2"
integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==
-shelljs@^0.8.3:
+shelljs@^0.8.2, shelljs@^0.8.3:
version "0.8.4"
resolved "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2"
integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==
@@ -20896,6 +21018,11 @@ stream-browserify@^2.0.1:
inherits "~2.0.1"
readable-stream "^2.0.2"
+stream-buffers@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.2.tgz#5249005a8d5c2d00b3a32e6e0a6ea209dc4f3521"
+ integrity sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ==
+
stream-combiner@~0.0.4:
version "0.0.4"
resolved "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14"
@@ -21760,6 +21887,13 @@ tinycolor2@^1.4.1:
resolved "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8"
integrity sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=
+tmp-promise@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.2.tgz#6e933782abff8b00c3119d63589ca1fb9caaa62a"
+ integrity sha512-OyCLAKU1HzBjL6Ev3gxUeraJNlbNingmi8IrHHEsYH8LTmEuhvYfqvhn2F/je+mjf4N58UmZ96OMEy1JanSCpA==
+ dependencies:
+ tmp "^0.2.0"
+
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@@ -21767,6 +21901,13 @@ tmp@^0.0.33:
dependencies:
os-tmpdir "~1.0.2"
+tmp@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
+ integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==
+ dependencies:
+ rimraf "^3.0.0"
+
tmp@~0.1.0:
version "0.1.0"
resolved "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877"
@@ -22219,6 +22360,11 @@ undefsafe@^2.0.2:
dependencies:
debug "^2.2.0"
+underscore@^1.9.1:
+ version "1.11.0"
+ resolved "https://registry.npmjs.org/underscore/-/underscore-1.11.0.tgz#dd7c23a195db34267186044649870ff1bab5929e"
+ integrity sha512-xY96SsN3NA461qIRKZ/+qox37YXPtSBswMGfiNptr+wrt6ds4HaMw23TP612fEyGekRE6LNRiLYr/aqbHXNedw==
+
unfetch@^4.1.0:
version "4.1.0"
resolved "https://registry.npmjs.org/unfetch/-/unfetch-4.1.0.tgz#6ec2dd0de887e58a4dee83a050ded80ffc4137db"
@@ -23272,6 +23418,11 @@ ws@^7.2.3:
resolved "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd"
integrity sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==
+ws@^7.3.1:
+ version "7.3.1"
+ resolved "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8"
+ integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==
+
x-is-string@^0.1.0:
version "0.1.0"
resolved "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz#474b50865af3a49a9c4657f05acd145458f77d82"