This commit is contained in:
Tim Hansen
2020-09-24 14:45:32 -06:00
98 changed files with 2193 additions and 232 deletions
+4
View File
@@ -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
+4
View File
@@ -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.
+3 -3
View File
@@ -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.
+6 -2
View File
@@ -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]
+4 -3
View File
@@ -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.
---
@@ -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\"}"
```
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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
+17
View File
@@ -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
-20
View File
@@ -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.
+8 -1
View File
@@ -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).
<img src="https://backstage.io/img/cncf-white.svg" width="400" />
+2 -5
View File
@@ -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)
+9 -2
View File
@@ -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 Backstages 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).
<img src="https://backstage.io/img/cncf-white.svg" width="400" />
## Benefits
- For _engineering managers_, it allows you to maintain standards and best
practices across the organization, and can help you manage your whole tech
+1 -1
View File
@@ -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.
+1
View File
@@ -51,6 +51,7 @@ class Footer extends React.Component {
<a href="https://mailchi.mp/spotify/backstage-community">
Subscribe to our newsletter
</a>
<a href="https://www.cncf.io/sandbox-projects/">CNCF Sandbox</a>
</div>
<div>
<h5>More</h5>
-1
View File
@@ -3,7 +3,6 @@
"Overview": [
"overview/what-is-backstage",
"overview/architecture-overview",
"overview/architecture-terminology",
"overview/roadmap",
"overview/vision",
"overview/background",
-1
View File
@@ -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'
+1 -1
View File
@@ -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',
-12
View File
@@ -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 },
+1
View File
@@ -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",
+16 -10
View File
@@ -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 => {
+5 -6
View File
@@ -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 });
}
+5 -2
View File
@@ -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 });
}
@@ -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);
+4
View File
@@ -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 <scope>', 'NPM scope')
.option('--npm-registry <URL>', 'NPM registry URL')
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
};
@@ -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.
@@ -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"
]
}
@@ -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';
@@ -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);
});
@@ -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' });
});
});
});
@@ -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<express.Router> {
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;
}
@@ -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<Server> {
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();
@@ -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');
@@ -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"
@@ -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(
<ThemeProvider theme={lightTheme}>
<ExampleComponent />
</ThemeProvider>,
);
expect(rendered.getByText('Welcome to {{ id }}!')).toBeInTheDocument();
);
expect(rendered.getByText('Welcome to {{ id }}!')).toBeInTheDocument();
});
});
@@ -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(<ExampleFetchComponent />);
expect(await rendered.findByTestId('progress')).toBeInTheDocument();
});
@@ -1,3 +1,2 @@
import '@testing-library/jest-dom';
require('jest-fetch-mock').enableMocks();
global.fetch = require('node-fetch');
@@ -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'
@@ -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',
@@ -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",
@@ -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);
+10 -3
View File
@@ -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');
@@ -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
@@ -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",
+7 -7
View File
@@ -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',
+3 -1
View File
@@ -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"
+1 -1
View File
@@ -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": [
+4 -4
View File
@@ -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`.
+2 -1
View File
@@ -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",
@@ -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
+3 -1
View File
@@ -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;
+1 -1
View File
@@ -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"
},
+2 -1
View File
@@ -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"
+3 -1
View File
@@ -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"
+3 -1
View File
@@ -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"
+3 -2
View File
@@ -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"
+3 -1
View File
@@ -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"
+3 -1
View File
@@ -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"
+3 -1
View File
@@ -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"
+2 -1
View File
@@ -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"
+3 -1
View File
@@ -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"
@@ -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: <KUBERNETES MASTER BASE URL FROM STEP 2>
name: minikube
serviceAccountToken: <TOKEN FROM STEP 4>
```
### 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`
@@ -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
@@ -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
+6 -2
View File
@@ -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"
@@ -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',
},
]);
});
});
@@ -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<ClusterDetails[]> {
return this.clusterDetails;
}
}
@@ -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';
+1
View File
@@ -15,3 +15,4 @@
*/
export * from './service/router';
export * from './types/types';
@@ -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);
});
});
@@ -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;
}
}
@@ -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);
});
});
@@ -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<KubernetesObjectTypes>,
): Promise<FetchResponse[]> {
return Promise.all(
Array.from(objectTypesToFetch).map(type => {
return this.fetchByObjectType(serviceId, clusterDetails, type);
}),
);
}
private fetchByObjectType(
serviceId: string,
clusterDetails: ClusterDetails,
type: KubernetesObjectTypes,
): Promise<FetchResponse> {
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<T>(
clusterDetails: ClusterDetails,
fn: (client: Clients) => Promise<{ body: { items: Array<T> } }>,
): Promise<Array<T>> {
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<Array<V1Service>> {
return this.singleClusterFetch<V1Service>(clusterDetails, ({ core }) =>
core.listServiceForAllNamespaces(
false,
'',
'',
`backstage.io/kubernetes-id=${serviceId}`,
),
);
}
private fetchPodsByServiceId(
serviceId: string,
clusterDetails: ClusterDetails,
): Promise<Array<V1Pod>> {
return this.singleClusterFetch<V1Pod>(clusterDetails, ({ core }) =>
core.listPodForAllNamespaces(
false,
'',
'',
`backstage.io/kubernetes-id=${serviceId}`,
),
);
}
private fetchConfigMapsByServiceId(
serviceId: string,
clusterDetails: ClusterDetails,
): Promise<Array<V1ConfigMap>> {
return this.singleClusterFetch<V1Pod>(clusterDetails, ({ core }) =>
core.listConfigMapForAllNamespaces(
false,
'',
'',
`backstage.io/kubernetes-id=${serviceId}`,
),
);
}
private fetchSecretsByServiceId(
serviceId: string,
clusterDetails: ClusterDetails,
): Promise<Array<V1Secret>> {
return this.singleClusterFetch<V1Secret>(clusterDetails, ({ core }) =>
core.listSecretForAllNamespaces(
false,
'',
'',
`backstage.io/kubernetes-id=${serviceId}`,
),
);
}
private fetchDeploymentsByServiceId(
serviceId: string,
clusterDetails: ClusterDetails,
): Promise<Array<V1Deployment>> {
return this.singleClusterFetch<V1Deployment>(clusterDetails, ({ apps }) =>
apps.listDeploymentForAllNamespaces(
false,
'',
'',
`backstage.io/kubernetes-id=${serviceId}`,
),
);
}
private fetchReplicaSetsByServiceId(
serviceId: string,
clusterDetails: ClusterDetails,
): Promise<Array<V1ReplicaSet>> {
return this.singleClusterFetch<V1ReplicaSet>(clusterDetails, ({ apps }) =>
apps.listReplicaSetForAllNamespaces(
false,
'',
'',
`backstage.io/kubernetes-id=${serviceId}`,
),
);
}
}
@@ -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',
},
],
},
],
});
});
});
@@ -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<KubernetesObjectTypes>,
) => Promise<ObjectsByServiceIdResponse>;
const DEFAULT_OBJECTS = new Set<KubernetesObjectTypes>([
'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 }));
};
@@ -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<KubernetesFetcher>;
let kubernetesClusterLocator: jest.Mocked<KubernetesClusterLocator>;
let handleGetByServiceId: jest.Mock<Promise<ObjectsByServiceIdResponse>>;
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' });
});
});
});
@@ -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,
);
}
@@ -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<express.Application> {
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());
@@ -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<V1Pod>;
}
export interface ServiceFetchResponse {
type: 'services';
resources: Array<V1Service>;
}
export interface ConfigMapFetchResponse {
type: 'configmaps';
resources: Array<V1ConfigMap>;
}
export interface SecretFetchResponse {
type: 'secrets';
resources: Array<V1Secret>;
}
export interface DeploymentFetchResponse {
type: 'deployments';
resources: Array<V1Deployment>;
}
export interface ReplicaSetsFetchResponse {
type: 'replicasets';
resources: Array<V1ReplicaSet>;
}
// 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<KubernetesObjectTypes>,
): Promise<FetchResponse[]>;
}
// Used to locate which cluster(s) a service is running on
export interface KubernetesClusterLocator {
getClusterByServiceId(serviceId: string): Promise<ClusterDetails[]>;
}
+4 -1
View File
@@ -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"
+1 -1
View File
@@ -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 =
@@ -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<ObjectsByServiceIdResponse> {
return await this.getRequired(`/services/${serviceId}`);
}
}
+2 -1
View File
@@ -15,6 +15,7 @@
*/
import { createApiRef } from '@backstage/core';
import { ObjectsByServiceIdResponse } from '@backstage/plugin-kubernetes-backend';
export const kubernetesApiRef = createApiRef<KubernetesApi>({
id: 'plugin.kubernetes.service',
@@ -23,5 +24,5 @@ export const kubernetesApiRef = createApiRef<KubernetesApi>({
});
export interface KubernetesApi {
getObjectsByServiceId(serviceId: String): Promise<{}>;
getObjectsByServiceId(serviceId: String): Promise<ObjectsByServiceIdResponse>;
}
@@ -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<string | undefined>(undefined);
useEffect(() => {
@@ -50,21 +44,28 @@ export const KubernetesContent: FC<{ entity: Entity }> = ({ entity }) => {
return (
<Page theme={pageTheme.tool}>
<Content>
<ContentHeader title="This is where you would see your kubernetes objects" />
<Grid container spacing={3} direction="column">
<Grid item>
<InfoCard title="This is where you would see your kubernetes objects">
<Typography variant="body1">
{kubernetesObjects === undefined && <div>loading....</div>}
{error !== undefined && <div>{error}</div>}
{kubernetesObjects !== undefined && (
<div>
backend response: {JSON.stringify(kubernetesObjects)}
</div>
)}
</Typography>
</InfoCard>
</Grid>
{kubernetesObjects === undefined && <div>loading....</div>}
{error !== undefined && <div>{error}</div>}
{kubernetesObjects !== undefined && (
<div>
{kubernetesObjects.items.map((item, i) => (
<Grid item key={i}>
<InfoCard key={item.cluster.name} title={item.cluster.name}>
{item.resources.map((fr, j) => (
<div key={j}>
<br />
{fr.type}:{' '}
{(fr.resources as any)
.map((v: any) => v.metadata.name)
.join(' ')}
</div>
))}
</InfoCard>
</Grid>
))}
</div>
)}
</Grid>
</Content>
</Page>
+3 -1
View File
@@ -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"
+3 -1
View File
@@ -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"
+3 -1
View File
@@ -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"
+3 -1
View File
@@ -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"
+3 -1
View File
@@ -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"
+3 -1
View File
@@ -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"
+3 -1
View File
@@ -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"
+2 -2
View File
@@ -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
@@ -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;
@@ -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}`,
});
});
});
+3 -1
View File
@@ -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"
+3 -1
View File
@@ -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"
+203 -52
View File
@@ -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"